Streaming Assets
Stream textures, models, and audio on demand to reduce memory and load times.
Introduction
AAA games stream assets from disk/CDN as needed rather than loading everything at start. This guide explains how professional engines manage asset bundles, use reference counting, and prefetch — then builds an asset streaming system in Appley.
Advanced Guide 16 of 25
Screenshot
How Professional Games Do It
U
n
G
r
U
s
T
r
I
m
L
o
P
r
P
r
U
s
W
h
C
o
U
s
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Asset streaming system with reference counting and LRU
const MEMORY_BUDGET = 1024 * 1024 * 1024 // 1GB
loadedAssets = {} // id -> { asset, refCount, lastAccess, size }
currentMemory = 0
// Request an asset (increments ref count)
func requestAsset(assetId) {
if loadedAssets[assetId] != null {
loadedAssets[assetId].refCount += 1
loadedAssets[assetId].lastAccess = time.now()
return loadedAssets[assetId].asset
}
// Not loaded — check memory budget
assetSize = getAssetSize(assetId)
while currentMemory + assetSize > MEMORY_BUDGET {
evictLRU()
}
// Load async
loadAssetAsync(assetId)
return null // Returns null while loading
}
// Release an asset (decrements ref count)
func releaseAsset(assetId) {
if loadedAssets[assetId] == null return
loadedAssets[assetId].refCount -= 1
if loadedAssets[assetId].refCount <= 0 {
// Unload if no references and not recently used
// (Keep in memory briefly for potential reuse)
loadedAssets[assetId].refCount = 0
}
}
// Async asset loading
func loadAssetAsync(assetId) {
storage.load("assets/" + assetId).then((data) => {
asset = createAssetFromData(data)
size = getAssetSize(assetId)
loadedAssets[assetId] = {
asset: asset,
refCount: 1,
lastAccess: time.now(),
size: size
}
currentMemory += size
// Notify objects waiting for this asset
emit("assetLoaded", { id: assetId, asset: asset })
})
}
// LRU eviction — unload least recently used
func evictLRU() {
oldest = null
oldestKey = null
for key in loadedAssets {
entry = loadedAssets[key]
// Only evict assets with 0 references
if entry.refCount <= 0 {
if oldest == null || entry.lastAccess < oldest.lastAccess {
oldest = entry
oldestKey = key
}
}
}
if oldest != null {
oldest.asset.unload()
currentMemory -= oldest.size
loadedAssets[oldestKey] = null
print("Evicted asset: " + oldestKey + " (freed " + oldest.size + " bytes)")
}
}
// Prefetch assets for upcoming area
func prefetchArea(areaId) {
manifest = getAreaManifest(areaId)
for assetId in manifest.assets {
if loadedAssets[assetId] == null {
// Pre-load without incrementing refcount
loadAssetAsync(assetId)
loadedAssets[assetId].refCount = 0 // Available but unreferenced
}
}
}
// Sector-based asset streaming
on update(dt) {
// Determine player sector
sectorX = math.floor(player.position.x / 100)
sectorZ = math.floor(player.position.z / 100)
// Prefetch adjacent sectors
for dx = -1 to 1 {
for dz = -1 to 1 {
sectorId = (sectorX + dx) + "," + (sectorZ + dz)
if !prefetchedSectors.has(sectorId) {
prefetchArea(sectorId)
prefetchedSectors.add(sectorId)
}
}
}
// Release distant sector assets
for sectorId in prefetchedSectors {
parts = string.split(sectorId, ",")
sx = math.parse(parts[0])
sz = math.parse(parts[1])
dist = math.max(math.abs(sx - sectorX), math.abs(sz - sectorZ))
if dist > 2 {
releaseSectorAssets(sectorId)
prefetchedSectors.delete(sectorId)
}
}
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[Function: requestAsset]
--> [Branch: already loaded?]
+-- True --> [refCount += 1, update lastAccess] --> [Return asset]
--> [Check: memory budget]
--> [While: over budget --> evictLRU]
--> [Load: async from storage]
--> [Return: null (loading)]
[Function: releaseAsset]
--> [refCount -= 1]
--> [Branch: refCount <= 0]
+-- True --> [Mark: available for eviction]
[Function: evictLRU]
--> [Find: oldest asset with 0 references]
--> [Unload: GPU resource]
--> [Reduce: currentMemory]
[On Update]
--> [Calculate: player sector]
--> [Prefetch: 3x3 surrounding sectors]
--> [For Each: not prefetched]
--> [loadAssetAsync: all sector assets]
--> [Release: sectors beyond distance 2]
--> [releaseSectorAssets]Professional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Use reference counting for asset lifecycle. Load when refcount goes 0 to 1. Unload when it returns to 0. This prevents unloading assets still in use and avoids loading already-loaded assets.
- Prefetch adjacent sectors. When the player enters a sector, start loading the 3x3 surrounding sectors. By the time they arrive, assets are ready. This hides streaming behind gameplay.
- Use LRU eviction with a memory budget. Set a hard memory limit (e.g., 1GB). When exceeded, unload the least-recently-used unreferenced assets. This prevents out-of-memory crashes.
- Use GPU texture compression (ASTC, DXT). Compressed textures are 4-8x smaller on disk and in memory. Decompression happens on the GPU — no CPU cost. This is the single biggest memory saver.
Common Mistakes
Loading everything at startup
Loading all assets at start causes long load times and high memory. Stream assets on demand based on player location and needs.
No reference counting
Without refcounting, you either never unload (memory leak) or unload assets still in use (crash). Reference counting is essential for safe streaming.
