Builtin Library
README.md
scene.shared.loader
Luau-side scene loader. Reads scene.json v6, dispatches to engine
FFIs, runs the sibling entrypoint.luau.
Public surface
local loader = require("@builtin::assetTypes.scene.shared.loader")
loader.load(ref, opts?)
-- ref: AssetRef<scene> | identity string | VFS path
-- opts.layer: layer name (default "main")
-- Returns: the Scene proxy (with player_config + camera_config attached).
loader.unload(layerName)
-- Wrapper around layers.unload(name); symmetric to M.load.
-- Cannot unload "main".
Behavior
- Resolve
refto a VFS path; append/scene.jsonif needed. vfs.read+Json.decode(frommodules.json) → body table.- Version check — refuse anything but
version: 6. - Attach
body.player+body.camerato the Scene proxy asplayer_config/camera_config(consumed by player_spawner + camera_spawner). - Activate the target layer via
layers.find(layerName)(the loader threads the layer throughentity.spawnopts), spawnbody.entitiesin two passes (shells + transforms first, parents + components second), then restore the previous active layer. - Apply
body.lightingvialights.setup+lights.addPointLight. Field name mapping:directional→sun,clear_color→clearColor(both engine API names and v6 field names are accepted). - Auto-discover sibling
entrypoint.luauviavfs.exists; if present, compile withloadstring+ run.
Performance
~5 ms for a 1000-entity scene (≈5k FFI calls × ~1 µs each). Acceptable for a load-time operation. If a particular call class becomes hot, batch primitives can be added in a narrow follow-up.
Why pure Luau
Per § 10 of the unification integration design, the scene format is
data + dispatch — no Rust-only primitives needed. The existing ECS
FFI (entity.spawn, entity(id).component.add, etc.) covers
everything. Keeping the loader in Luau means iteration speed,
auditability, and hot-reload.
FFI corrections vs. the original plan
The plan was drafted against assumed API names. The actual engine FFIs differ:
| Plan assumed | Actual engine API |
|---|---|
json.parse(s) | require("..json").decode(s) (modules.json) |
json.encode(t) | require("..json").encode(t) |
entity.spawn(n, { layer }).id | layer activation via layers.find(layer) + entity.spawn(n, opts).id |
entity.setParent(id, pid) | entity(id).setParent(pid) |
unloadLayer(name) | layers.unload(name) |
lights.setup({ directional }) | lights.setup({ sun }) |
lights.setup({ clear_color }) | lights.setup({ clearColor }) |
lights.addPointLight(table) | lights.addPointLight(name, x, y, z, opts) |
The loader accepts both the v6 field names and the engine API names for the lighting fields so that round-tripping through the scene saver and back works without a conversion step.