Scene JSON is not the only way to create actors. Spawn at runtime from session code or an entity when content is dynamic (pickups, VFX hosts, debug helpers).
Deep reference: Session services — layer membership. Related: First session services.
Layer membership
| API | Layer | Survives additive unload? |
|---|---|---|
| Scene-authored actor | Loading scene’s layer | No |
w.spawn with .layer = .loose | null (loose) | Yes (until replace unload / destroy) |
w.spawn with .layer = .{ .layer = key } | Named layer | No |
w.spawn with .layer = .{ .inherit = id } | Copied from parent | Depends |
In Play, the hierarchy Global group lists loose entities (layer_key == null).
Spawn from a descriptor
Game modules spawn through hi.world().spawn with a SpawnDesc. The host returns an ActorRef (EntityId is the same type — prefer ActorRef).
const hi = @import("hikari_game");
const cube_mesh = hi.AssetRef.must(.model, "asset://./models/cube");
const pickup_material = hi.AssetRef.must(.material, "asset://./materials/cube");
const pickup_script = hi.AssetRef.must(.script, "asset://./scripts/pickup");
const w = hi.world();
const id = try w.spawn(&.{
.archetype = "cube",
.id = "pickup_01",
.name = "Pickup",
.active = true,
.transform = .{ .position = .{ 0, 1, 0 } },
.components = .{
.physics = .{
.body_type = .dynamic,
.collider_shape = .sphere,
.collider_radius = 0.35,
.mass = 0.5,
},
.render = .{
.geometry = cube_mesh,
.material = pickup_material,
},
},
.script = .{
.mode = .override,
.asset = pickup_script,
.params_json = "{\"respawn_seconds\":5}",
},
.user_data_json = "{\"score\":100}",
.layer = .loose,
});
_ = id;Omit id (or pass "") to let the host generate a unique scene id. Prefer stable unique ids when you will look the actor up later — additive loads reject duplicates across layers.
The request can initialize parent, active state, transform, render, physics, camera, light, visual zone, audio, script parameters, and authored component fields. An override is accepted only when the archetype declares that capability. Asset references, JSON, component values, parent/layer handles, and the scene id are validated before the entity is published.
awake and start observe the complete initial state. A failed request is rolled back;
it does not leave a partially configured entity in the world. Script mode defaults to
the archetype script; use .none to suppress it or .override to replace it.
Kawa uses the same pipeline:
let pickup = World.spawn({
archetype = "cube",
id = "pickup_02",
position = [2, 1, 0],
user_data_json = "{\"score\":100}",
components = {
physics = { body_type = "dynamic", collider_shape = "sphere", collider_radius = 0.35, mass = 0.5 },
render = { mesh = "asset://./models/cube", material = "asset://./materials/cube" }
},
script = { asset = "asset://./scripts/pickup", params_json = "{\"respawn_seconds\":5}" },
layer = "loose"
});Kawa returns an actor ref or nil when validation or spawning fails. script: nil
suppresses the archetype default script. parent and inherit_layer accept actor refs;
layer accepts "loose" or a loaded layer key.
Open scenes/runtime_spawn_showcase.json for a side-by-side executable example. The
red group is spawned by spawn_showcase_zig_entity.zig; the blue group is spawned by
assets/scripts/spawn_showcase_kawa.kawa. Each path creates a parented configured prop, a dynamic
physics sphere, a runtime light, initial component data, and a configured child script.
scenes/runtime_spawn_showcase inherits the scene-placed anchor’s layer (inherit_layer / .layer = .{ .inherit = anchor }); step through the scene tour on the session HUD to reach it. Use .layer = .loose when the actor must survive additive unload.
Keep cross-scene policy on the subsystem; do not re-place a “manager” actor in every scene JSON.
Verify
- Spawn a loose cube from a button or
onSceneLoaded. - Additive-load then unload a layer — loose actor remains; layer actors disappear.
requestSceneLoad(replace) — loose actors are torn down with the world rebuild.