वेब गेम्स के लिए स्ट्रीमिंग एसेट लोडिंग
Players 100MB डाउनलोड का इंतज़ार नहीं करेंगे। एसेट्स को धीरे-धीरे लोड करें ताकि वे खेल सकें जबकि बाकी सब background में लोड होता रहे।
1) लोडिंग रणनीति
एसेट्स को tiers में बाँटें:
- Critical — पहली स्क्रीन दिखाने के लिए ज़रूरी (< 1MB)
- Gameplay — खेलने के लिए ज़रूरी (< 10MB)
- Enhancement — होना अच्छा है (background में लोड करें)
2) बेसिक एसेट लोडर
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) प्रोग्रेस के साथ लोडिंग
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
}ध्यान दें: जब server response को compress करता है (gzip या brotli, जो ज़्यादातर CDN डिफ़ॉल्ट रूप से करते हैं), तो Content-Length compressed साइज़ होता है जबकि आप जो chunks पढ़ते हैं वे पहले से decompressed होते हैं। इससे received / total 100% से आगे निकल जाता है। compressed responses पर सटीक byte-level प्रोग्रेस बार के लिए, या तो एसेट को uncompressed सर्व करें, अपना खुद का uncompressed-size header भेजें, या XMLHttpRequest के onprogress event का इस्तेमाल करें, जो असली ट्रांसफर प्रोग्रेस बताता है।
4) समग्र प्रोग्रेस के साथ बैच लोडिंग
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) प्रायोरिटी क्यू लोडर
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) Levels के लिए lazy loading
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) बड़ी फ़ाइलें स्ट्रीम करना
बड़ी फ़ाइलों (3D models, audio) के लिए, स्ट्रीम करें और टुकड़ों में प्रोसेस करें:
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) लोड किए गए एसेट्स को कैश करना
स्थायी कैशिंग के लिए IndexedDB के साथ जोड़ें:
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) लोडिंग स्क्रीन पैटर्न
class LoadingScreen {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.progress = 0
this.message = 'Loading...'
}
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) सर्वोत्तम तरीके
- तुरंत कुछ दिखाएँ — एक static image भी चलेगी
- जो दिख रहा है उसे पहले लोड करें — मौजूदा view की textures
- Placeholders का इस्तेमाल करें — low-res images जो बाद में अपग्रेड हो जाएँ
- अगला level prefetch करें — जब player अभी भी खेल रहा हो
- विफलताओं को शालीनता से संभालें — retry logic, fallbacks
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)))
}
}
}संबंधित
- तेज़ी से लोड होने वाला वेब गेम शिप करें
- गेम कैशिंग के लिए service workers
- IndexedDB गेम सेव्स
- गेम लॉजिक के लिए Web Workers — मुख्य thread से बाहर एसेट्स को decode और प्रोसेस करें
- मुफ़्त गेम एसेट्स कहाँ मिलेंगे — स्ट्रीम करने के लिए 3D models, textures, और audio के स्रोत
बाहरी संसाधन
- MDN: Fetch API — streaming responses और ReadableStream
- MDN: ReadableStream — स्ट्रीम किए गए data chunks को प्रोसेस करना
- glTF 2.0 specification — 3D एसेट्स स्ट्रीम करने का मानक फ़ॉर्मेट