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.

Source: module-kits:kits/world_streaming/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs world-streamingMCP tool: gripforge_docs { "topic": "world-streaming" }

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 tagged player, otherwise the last player.moved.
  • Wanted chunks are all those within radius chunks of the focus chunk (a square), nearest first (distance to the chunk centre, then z, then x). With world.ground, chunks entirely outside its bounds are skipped.
  • A loaded chunk farther than radius + 1 chunks is released at once (one chunk of hysteresis, so walking along a border does not thrash).
  • Entities tagged streamable carry the tag dormant while 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 #

SettingTypeDefaultMeaning
chunkSizenumber 16-51264Chunk edge in metres. The grid other kits build on; a mod does not change it.
radiusinteger 1-82Chunks kept loaded around the focus.
budgetMsnumber 0.5-164Loading 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() and pending() list [{ x, z }]; isLoaded(x, z); chunkOf(pos); around(pos) lists the wanted chunks nearest first; onChunk(fn) calls fn(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.loaded and world.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 (no world.chunk.loaded, its streamable entities 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 in pending().
  • A chunk never loads: a world.chunk.load tap cancels it, or it lies entirely outside world.ground.
  • Props pop in late: build on world.chunk.loaded or onChunk, 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.