World streaming
Decide which square chunks of a big world are live around the player, nearest first under a fixed per-tick budget: settings, services, events, the world.chunk.load and world.chunk.unload hooks.
Streaming a large world in chunks (world.streaming) #
What it is for #
world.streaming is bookkeeping for a world larger than one block. It draws nothing and builds nothing: it decides which square chunks are live around the player and tells the kits that build (colliders, props, NPC spawns, terrain). The world is a grid of chunkSize metre squares on X and Z; chunk (x, z) covers [x*size, (x+1)*size) x [z*size, (z+1)*size).
- The focus is the vehicle the player rides, otherwise
character.control, otherwise the entity taggedplayer, otherwise the lastplayer.moved. - Wanted chunks are all those within
radiuschunks of the focus chunk (a square), nearest first (distance to the chunk centre, then z, then x). Withworld.ground, chunks entirely outside its bounds are skipped. - A loaded chunk farther than
radius + 1chunks is released at once (one chunk of hysteresis, so walking along a border does not thrash). - Entities tagged
streamablecarry the tagdormantwhile their chunk is not loaded, so AI and renderers can skip them.
Turn it on #
{ "id": "world.streaming", "config": { "chunkSize": 32, "radius": 1, "budgetMs": 2 } }
It requires world.ground and game.engine. world.terrain builds its terrain chunks on the events of this kit and uses the same chunkSize.
First example #
<!-- test: kits/game_engine/host/tests/world_streaming.rs::chunks_load_around_the_player_nearest_first --> With chunkSize: 32, radius: 1, budgetMs: 2 and a player at (8, 8), the first tick loads two chunks, the player's own and the nearest neighbour: loaded() returns [{ x: 0, z: -1 }, { x: 0, z: 0 }] and world.chunks.pending is 7. After four more ticks world.chunks.loaded is 9, isLoaded(1, 1) is true and isLoaded(2, 0) is false. A save and reload keeps the 9 chunks.
Settings and defaults #
| Setting | Type | Default | Meaning |
|---|---|---|---|
chunkSize | number 16-512 | 64 | Chunk edge in metres. The grid other kits build on; a mod does not change it. |
radius | integer 1-8 | 2 | Chunks kept loaded around the focus. |
budgetMs | number 0.5-16 | 4 | Loading budget per tick. |
budgetMs is not measured against the wall clock: it becomes a fixed number of loads per tick, max(1, floor(budgetMs)), one chunk being priced 1 ms. The default loads four chunks per tick, so the 25 chunks of radius 2 are live after 7 ticks, and a replay loads the same chunks on the same ticks on every machine. A scene of game.engine may set radius and budgetMs in layers["world.streaming"].
Services, events, variables #
world.streaming(service):loaded()andpending()list[{ x, z }];isLoaded(x, z);chunkOf(pos);around(pos)lists the wanted chunks nearest first;onChunk(fn)callsfn(chunk, loaded)on every load and release and returns an unsubscribe function;chunkSize().world.chunk.loaded(event):{ x, z }, the chunk is live; kits that build materialise it.world.chunk.unloaded(event):{ x, z }, the chunk was released (too far, or its scene was left).player.moved(listened): the last known position when no controlled entity is found.- Variables:
world.chunks.loadedandworld.chunks.pending.
Leaving a scene releases every chunk (world.chunk.unloaded) and forgets the focus; entering one starts the grid around its entry. The loaded chunks and the last focus are saved; loading restores them without replaying events.
Hooks and modding #
Both hooks are cancellable, with no mutable field, and asked once per change:
world.chunk.load{ x, z }: the chunk is about to load. A cancel leaves it unloaded (noworld.chunk.loaded, itsstreamableentities stay dormant) and it is asked again only after it left the wanted square and is wanted anew. The load it would have taken is spent for this tick.world.chunk.unload{ x, z }: the loaded chunk is about to be released. A cancel keeps it loaded; leaving a scene releases every chunk without asking.
{ "id": "keep_home", "hook": "world.chunk.unload", "match": { "x": 0, "z": 0 }, "cancel": true, "reason": "home" }
A refusal is kept until released and saved with the game. A mod may change radius and budgetMs.
Engines #
The same chunk order and per-tick budget apply on the web, the native player, Godot, Unity and Unreal. The hooks run on the web and in the Rust kit host (the native player, and Godot and Unity through the kit library). The controlled-vehicle focus and world.district materialisation (streamEntities) are web-only for now.
When it does not do what you expect #
- Chunks load slowly: raise
budgetMs; the rest waits inpending(). - A chunk never loads: a
world.chunk.loadtap cancels it, or it lies entirely outsideworld.ground. - Props pop in late: build on
world.chunk.loadedoronChunk, not on a timer.
Limits #
It does no rendering, no geometry and no asset loading. Chunk contents come from the kits that listen to its events.
Versions #
Kit 1.5.0; it provides world.streaming 1.0.0 and requires world.ground@^1 and game.engine@^1. The hooks and the held refusals date from 1.4.0 and 1.5.0.