Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

readme.md

spatialStreaming

World content that becomes resident around a moving source and is released behind it, so the memory a world costs follows the view rather than the world's total size.

The shape of it

  • A cell is a square column on the ground plane, cellSize on a side.
  • A group is one root entity and its descendants, serialized as records. A group is atomic — a subtree is never half resident — and it belongs to the cell its root stands in.
  • A source is a position streaming happens around: an entity (a player, a camera) or a fixed point. Each carries its own radii, and a cell stands as long as any one of them holds it.
  • A layer is a name a group is filed under, and each layer is its own grid. A layer carries its own radii and can be switched off, so decorative content releases long before the content a player interacts with — which only works because the two can stand in the same square and still stream apart.
  • A proxy is what a cell is drawn as while its content is released: its own records, so a merged mesh, an impostor or a bounding silhouette are all expressible. It stands out to proxyRadius, which reaches well past the radius the content is bought at — and follows that radius unless a world names one of its own.

A cell within loadRadius of the nearest source is made resident, past unloadRadius it is released, and between the two it keeps whatever it already is. That band is the whole reason a source can stand on a boundary without the cell under it loading and releasing every frame.

Using it

Attach StreamingSource to whatever the world should stay resident near:

entity.find("Player").component.add("StreamingSource", {
    cellSize = 64, loadRadius = 120, unloadRadius = 180, budget = 32,
})

Then hand content to the store. capture takes entities that are already standing and leaves them standing — residency passes to the streaming rule:

local streaming = require("@builtin::systems.spatialStreaming.spatialStreaming")
streaming.capture(entity.findAll("rock_*"), { layer = "props" })
streaming.setLayer("props", { loadRadius = 60, unloadRadius = 90 })

add registers content that was never spawned. With store = "vfs" the records are written under the configured directory and only the cell's manifest line stays in memory until a source arrives, which is what lets a world hold more than it can spawn at once:

streaming.add({
    { name = "rock", position = { x = 900, y = 0, z = 40 },
      components = { Model = { model = "@builtin::meshes.rock" } } },
}, { store = "vfs" })

stats() reports what the store holds and how much of it is standing:

local s = streaming.stats()
-- s.bytesResident is the encoded size of the groups standing right now;
-- s.bytesStored is the size of everything the store knows about.

The far field

A released cell is drawn as its proxy out to proxyRadius. That radius and the band past it follow unloadRadius unless a world pins them, so content streamed over a wide radius gets a far field past it without naming one — and a pin is measured against the content radii, since a proxy radius inside unloadRadius names ground the content is still standing on. Passing 0 hands a pin back:

streaming.configure({ loadRadius = 600, unloadRadius = 900 })  -- far field follows
streaming.configure({ proxyRadius = 3000, proxyUnloadRadius = 3600 })  -- or is pinned
streaming.configure({ proxyRadius = 0 })  -- and follows again

deriveProxies builds the bounding-silhouette case from the content the store already holds — one box over a cell, or one over each group in it. Each record is measured as a box of its own scale placed through the whole of its parent's transform, so a group standing rotated and scaled is enclosed where it actually stands; a mesh larger than one unit at scale 1 is measured as the unit box it scales from, which is what padding covers:

streaming.deriveProxies({ granularity = "group", material = "@builtin::materials.default" })

setProxy takes any records instead, which is the path for an authored merged mesh or a billboard impostor:

streaming.setProxy("4:0", {
    { name = "ridge", position = { x = 288, y = 8, z = 32 },
      scale = { x = 64, y = 16, z = 64 },
      components = { Model = { model = "@builtin::meshes.cube" } } },
})

The two representations are exchanged in an order that overlaps them: a cell's proxy goes up before the first of its groups is released, and comes down once the cell's own content has landed. A cell inside the proxy radius is therefore never represented by nothing, which is what the transition would otherwise read as — a hole in the far field for as long as the budget takes to fill it — and a silhouette, which is opaque, is never left standing over the content it was covering for. A proxy whose entities left the scene by some other path is taken down whole on the next step and raised again from its records.

stats() prices the far field separately: proxiesResident, entitiesProxy and bytesProxy against the entitiesTotal / bytesStored they stand in for. proxies() is the per-cell view: records is what a stand-in is made of, entities how much of it is standing.

What a group carries

  • A group is placed by its root and streams whole. A group spanning several cells is resident whenever its root's cell is. A root that moves while it is standing is re-filed into the cell it moved into when it is released, so content that walks streams from where it ended up.
  • A capture takes local content. A replicated entity is refused, with a count in the log: residency is a local decision, and despawning a replicated entity would reach every peer.
  • A record carries what component_snapshot serializes — the authored script-component surface — plus the entity's name, local transform and attributes.
  • A group whose root gameplay destroyed is dropped: the store forgets it rather than resurrecting deleted content on the next visit.

Several sources

Each source carries its own loadRadius / unloadRadius. A cell is resident while any source wants it resident and is released once every source has let go of it, so a spectator streaming a thin shell cannot pull the ground out from under the player standing on it. cellSize and budget are world-wide: the grid has to be one grid for a cell key to mean anything, and one step belongs to the frame rather than to a source.

Ground

Ground streams on its own path, around the same sources: a TerrainStream (terrainSystem.package) stands a landscape many kilometres across in tiles that load, draw and collide around every entity carrying StreamingSource, so the player and every vehicle a peer simulates have collision under them wherever they go, and the content this store holds stands on ground that is there.