Skip to content

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:

  1. Kritik — İlk ekranı göstermek için gerekli (< 1 MB)
  2. Oynanış — Oynamak için gerekli (< 10 MB)
  3. İyileştirme — Olması faydalı olanlar (arka planda yüklenir)

2) Temel varlık yükleyici

js
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

js
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

js
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

js
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 later

6) Bölümler için tembel yükleme

js
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:

js
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 arrives

8) Yüklenen varlıkları önbelleğe alma

Kalıcı önbellekleme için IndexedDB ile birleştirin:

js
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ı

js
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
js
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

Harici kaynaklar