Web oyunları için akışlı varlık yükleme
Oyuncular 100 MB'lık bir indirmeyi beklemez. Varlıkları aşamalı olarak yükleyin; böylece geri kalanı arka planda yüklenirken oyuncular oynamaya başlayabilir.
1) Yükleme stratejisi
Varlıkları katmanlara ayırın:
- Kritik — İlk ekranı göstermek için gerekli (< 1 MB)
- Oynanış — Oynamak için gerekli (< 10 MB)
- İyileştirme — Olması faydalı olanlar (arka planda yüklenir)
2) Temel varlık yükleyici
class AssetLoader {
constructor() {
this.cache = new Map()
this.loading = new Map()
}
async loadImage(url) {
if (this.cache.has(url)) return this.cache.get(url)
if (this.loading.has(url)) return this.loading.get(url)
const promise = new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
this.cache.set(url, img)
this.loading.delete(url)
resolve(img)
}
img.onerror = reject
img.src = url
})
this.loading.set(url, promise)
return promise
}
async loadJSON(url) {
if (this.cache.has(url)) return this.cache.get(url)
const response = await fetch(url)
const data = await response.json()
this.cache.set(url, data)
return data
}
async loadAudio(url, audioCtx) {
if (this.cache.has(url)) return this.cache.get(url)
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const audioBuffer = await audioCtx.decodeAudioData(buffer)
this.cache.set(url, audioBuffer)
return audioBuffer
}
}3) İlerleme göstergesiyle yükleme
async function loadWithProgress(url, onProgress) {
const response = await fetch(url)
const contentLength = response.headers.get('Content-Length')
const total = parseInt(contentLength, 10)
const reader = response.body.getReader()
const chunks = []
let received = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
onProgress(received / total)
}
const blob = new Blob(chunks)
return blob
}Dikkat: Sunucu yanıtı sıkıştırdığında (çoğu CDN'in varsayılan olarak yaptığı gibi gzip veya brotli ile), Content-Length sıkıştırılmış boyutu belirtirken okuduğunuz parçaların sıkıştırması zaten açılmış olur. Bu nedenle received / total değeri %100'ü aşar. Sıkıştırılmış yanıtlarda bayt düzeyinde doğru bir ilerleme çubuğu elde etmek için varlığı sıkıştırmadan sunun, sıkıştırılmamış boyutu belirten kendi üstbilginizi gönderin veya gerçek aktarım ilerlemesini bildiren XMLHttpRequest'in onprogress olayını kullanın.
4) Genel ilerleme göstergesiyle toplu yükleme
async function loadAssets(manifest, onProgress) {
const total = manifest.length
let completed = 0
const results = {}
const promises = manifest.map(async (item) => {
const asset = await loadAsset(item.url, item.type)
results[item.name] = asset
completed++
onProgress(completed / total, item.name)
})
await Promise.all(promises)
return results
}
// Usage
const manifest = [
{ name: 'player', url: 'player.png', type: 'image' },
{ name: 'level1', url: 'level1.json', type: 'json' },
{ name: 'music', url: 'music.mp3', type: 'audio' },
]
const assets = await loadAssets(manifest, (progress, name) => {
console.log(`Loading: ${Math.round(progress * 100)}% (${name})`)
})5) Öncelik kuyruklu yükleyici
class PriorityLoader {
constructor(concurrency = 4) {
this.queue = []
this.active = 0
this.concurrency = concurrency
}
add(url, priority = 0) {
return new Promise((resolve, reject) => {
this.queue.push({ url, priority, resolve, reject })
this.queue.sort((a, b) => b.priority - a.priority)
this.process()
})
}
async process() {
if (this.active >= this.concurrency || this.queue.length === 0) return
this.active++
const { url, resolve, reject } = this.queue.shift()
try {
const response = await fetch(url)
const blob = await response.blob()
resolve(blob)
} catch (err) {
reject(err)
}
this.active--
this.process()
}
}
// Usage
const loader = new PriorityLoader()
loader.add('critical.png', 10) // Load first
loader.add('optional.png', 1) // Load later6) Bölümler için tembel yükleme
class LevelManager {
constructor(loader) {
this.loader = loader
this.levels = new Map()
}
async preload(levelId) {
if (this.levels.has(levelId)) return
const manifest = await this.loader.loadJSON(`levels/${levelId}/manifest.json`)
const assets = await this.loadLevelAssets(manifest)
this.levels.set(levelId, { manifest, assets })
}
async loadLevelAssets(manifest) {
// Load only what's needed for this level
const assets = {}
for (const texture of manifest.textures) {
assets[texture.name] = await this.loader.loadImage(texture.url)
}
return assets
}
unload(levelId) {
this.levels.delete(levelId)
// Assets can be garbage collected
}
}7) Büyük dosyaları akışla yükleme
Büyük dosyaları (3D modeller, sesler) akışla yükleyip aşamalı olarak işleyin:
async function streamLargeFile(url, onChunk) {
const response = await fetch(url)
const reader = response.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
onChunk(value)
}
}
// For audio: use Media Source Extensions
// For 3D: process mesh data as it arrives8) Yüklenen varlıkları önbelleğe alma
Kalıcı önbellekleme için IndexedDB ile birleştirin:
class CachedLoader {
constructor() {
this.memCache = new Map()
this.dbName = 'AssetCache'
}
async loadImage(url) {
// Check memory
if (this.memCache.has(url)) return this.memCache.get(url)
// Check IndexedDB
const cached = await this.getFromDB(url)
if (cached) {
const img = await this.blobToImage(cached)
this.memCache.set(url, img)
return img
}
// Fetch and cache
const response = await fetch(url)
const blob = await response.blob()
await this.saveToDB(url, blob)
const img = await this.blobToImage(blob)
this.memCache.set(url, img)
return img
}
blobToImage(blob) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
URL.revokeObjectURL(img.src)
resolve(img)
}
img.src = URL.createObjectURL(blob)
})
}
// IndexedDB methods...
}9) Yükleme ekranı kalıbı
class LoadingScreen {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.progress = 0
this.message = 'Yükleniyor...'
}
update(progress, message) {
this.progress = progress
this.message = message || this.message
this.render()
}
render() {
const { ctx, canvas } = this
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Progress bar
const barWidth = canvas.width * 0.6
const barHeight = 20
const x = (canvas.width - barWidth) / 2
const y = canvas.height / 2
ctx.fillStyle = '#333'
ctx.fillRect(x, y, barWidth, barHeight)
ctx.fillStyle = '#4ade80'
ctx.fillRect(x, y, barWidth * this.progress, barHeight)
// Text
ctx.fillStyle = '#fff'
ctx.font = '16px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(this.message, canvas.width / 2, y - 20)
ctx.fillText(`${Math.round(this.progress * 100)}%`, canvas.width / 2, y + 50)
}
}10) En iyi uygulamalar
- Hemen bir şey gösterin — statik bir görsel bile olabilir
- Önce görünür olanları yükleyin — mevcut görünümün dokuları
- Yer tutucular kullanın — sonradan yüksek çözünürlüklü sürümleriyle değiştirilen düşük çözünürlüklü görseller
- Sonraki bölümü önceden getirin — oyuncu hâlâ oynarken
- Hataları sorunsuz yönetin — yeniden deneme mantığı ve yedek seçenekler
async function loadWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url)
} catch (err) {
if (i === maxRetries - 1) throw err
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}İlgili içerikler
- Hızlı yüklenen bir web oyunu yayınlayın
- Oyun önbelleklemesi için service worker'lar
- IndexedDB ile oyun kayıtları
- Oyun mantığı için Web Worker'lar — varlıkların kodunu ana iş parçacığı dışında çözüp işleyin
- Ücretsiz oyun varlıkları nerede bulunur? — akışla yükleyebileceğiniz 3D modeller, dokular ve sesler için kaynaklar
Harici kaynaklar
- MDN: Fetch API — yanıtları akışla alma ve ReadableStream
- MDN: ReadableStream — akışla gelen veri parçalarını işleme
- glTF 2.0 teknik şartnamesi — 3D varlıkları akışla yüklemek için standart biçim