IndexedDB ile oyun durumunu kaydetme ve yükleme
IndexedDB, tarayıcıda kalıcı oyun verileri için en iyi seçenektir. Büyük miktarda veriyi işler, çevrimdışı çalışır ve ana iş parçacığını engellemez.
1) Neden localStorage yerine IndexedDB?
| Özellik | localStorage | IndexedDB |
|---|---|---|
| Depolama sınırı | ~5-10 MB | 50+ MB (çoğu zaman GB) |
| Veri türleri | Yalnızca dizeler | Nesneler, blob'lar, diziler |
| Asenkron | Hayır (engeller) | Evet |
| İndeksli sorgular | Hayır | Evet |
Oyunlar için IndexedDB neredeyse her zaman doğru seçimdir.
2) Veritabanı açma
function openGameDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyGame', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (event) => {
const db = event.target.result
// Create stores
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
}
})
}3) Oyun durumunu kaydetme
async function saveGame(slot, gameState) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const saveData = {
slot,
state: gameState,
timestamp: Date.now(),
}
const request = store.put(saveData)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}4) Oyun durumunu yükleme
async function loadGame(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.get(slot)
request.onsuccess = () => resolve(request.result?.state || null)
request.onerror = () => reject(request.error)
})
}5) Tüm kayıtları listeleme
async function listSaves() {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.getAll()
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}6) Bir kaydı silme
async function deleteSave(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const request = store.delete(slot)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}7) Eksiksiz bir GameStorage sınıfı
class GameStorage {
constructor(dbName = 'GameData', version = 1) {
this.dbName = dbName
this.version = version
this.db = null
}
async init() {
this.db = await this.openDB()
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
if (!db.objectStoreNames.contains('assets')) {
db.createObjectStore('assets', { keyPath: 'url' })
}
}
})
}
async save(slot, data) {
const tx = this.db.transaction('saves', 'readwrite')
tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
return tx.complete
}
async load(slot) {
const tx = this.db.transaction('saves', 'readonly')
const result = await this.promisify(tx.objectStore('saves').get(slot))
return result?.data || null
}
async setSetting(key, value) {
const tx = this.db.transaction('settings', 'readwrite')
tx.objectStore('settings').put({ key, value })
}
async getSetting(key, defaultValue = null) {
const tx = this.db.transaction('settings', 'readonly')
const result = await this.promisify(tx.objectStore('settings').get(key))
return result?.value ?? defaultValue
}
promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
}8) İkili verileri (dokular, sesler) depolama
IndexedDB, Blob'ları ve ArrayBuffer'ları destekler:
async function cacheAsset(url, blob) {
const tx = db.transaction('assets', 'readwrite')
tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}
async function getCachedAsset(url) {
const tx = db.transaction('assets', 'readonly')
const result = await promisify(tx.objectStore('assets').get(url))
return result?.blob || null
}9) Otomatik kayıt kalıbı
class AutoSave {
constructor(storage, interval = 60000) {
this.storage = storage
this.interval = interval
this.timer = null
this.dirty = false
}
markDirty() {
this.dirty = true
}
start(getState) {
this.timer = setInterval(async () => {
if (this.dirty) {
await this.storage.save('autosave', getState())
this.dirty = false
console.log('Autosaved')
}
}, this.interval)
}
stop() {
clearInterval(this.timer)
}
}10) Hata yönetimi ve geri dönüş seçenekleri
async function safeLoad(slot, defaultState) {
try {
const saved = await loadGame(slot)
if (saved) {
// Validate/migrate old saves if needed
return migrateSave(saved)
}
} catch (err) {
console.warn('Failed to load save:', err)
}
return defaultState
}
function migrateSave(save) {
// Handle old save formats
if (!save.version) {
save.version = 1
save.settings = save.settings || {}
}
return save
}11) Kayıtları otomatik silinmeye karşı koruma
IndexedDB verilerinin kalıcı olacağı garanti edilmez. Bir kaynak varsayılan olarak "mümkün olan en iyi" depolamayı kullanır ve disk dolduğunda ya da Safari/WebKit'te kullanıcı sitenizle uzun süre etkileşim kurmadığında tarayıcı bu verileri silebilir. Oyun kayıtları ise tam olarak kaybetmek istemeyeceğiniz verilerdir.
Tarayıcının açık bir kullanıcı işlemi olmadan verilerinizi temizlememesi için kalıcı depolama isteyin:
async function makeStoragePersistent() {
if (navigator.storage && navigator.storage.persist) {
const persisted = await navigator.storage.persist()
console.log(persisted ? 'Saves are protected from eviction' : 'Saves may be evicted under storage pressure')
return persisted
}
return false
}Tarayıcılar bu izni etkileşim sinyallerine (kullanıcının sitenizle ne kadar etkileşim kurduğu, sitenin PWA olarak yüklenip yüklenmediği) göre verir; bu nedenle isteğin her zaman başarılı olacağını varsaymayın. Büyük kayıtları veya önbelleğe alınmış varlıkları yazmadan önce ne kadar alanınız kaldığını da kontrol edebilirsiniz:
async function checkStorage() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate()
console.log(`Using ${usage} of ${quota} bytes`)
}
}Her iki API de güvenli bir bağlam (HTTPS veya localhost) gerektirir.
İlgili içerikler
- Çevrimdışı oyunlar için PWA
- Oyun önbelleğe alma için service worker'lar
- Hızlı yüklenen bir web oyunu yayınlama
- Akış tabanlı varlık yükleme — IndexedDB'yi varlık önbelleği olarak kullanma
- Web oyunları için analiz — oyuncu davranışını anlamak için kaydetme/yükleme kalıplarını izleme
Harici Kaynaklar
- MDN: IndexedDB API — eksiksiz API başvurusu
- MDN: IndexedDB'yi kullanma — adım adım kılavuz
- idb kütüphanesi — Promise tabanlı, küçük bir IndexedDB sarmalayıcısı