Loading scenes and instantiating prefabs at runtime.
Scene & prefab API
Scenes are the big chunks of your game — a Skyforge level, a town, a boss arena. Prefabs are reusable object templates you place many times. This page covers loading scenes without stalling the game and instantiating prefabs from scripts.
What scenes and prefabs are
A scene is one authored document (*.scene.json) with a set of entities. Games switch scenes to move between areas. A prefab is a saved entity tree (a weapon, a crate stack, an enemy kit) you can instantiate as many times as you need. Scenes reference prefabs; prefabs do not load scenes.
Scene URIs look like scene://game/skyforge_outpost. Prefab URIs look like prefab://props/skyforge_rune. Both are stable IDs, never file paths.
Two-phase scene loading
Loading a big level from disk in one step would freeze the frame. DemiEngine splits it in two: prepare (load in the background, show a progress number) and activate (swap it in at a frame boundary so the swap never tears a frame in half).
local Scene = require("demi.scene")
Scene.prepare("scene://game/skyforge_outpost") -- starts background load
function Game:on_update()
local p = Scene.progress() -- 0.0 … 1.0
if Scene.is_prepared() then
Scene.activate() -- swap at the next frame boundary
end
end
Scene.prepare(uri, additive?) begins loading. Pass true as the second argument to load additively on top of the current scene instead of replacing it. Scene.progress() returns a number from 0 to 1. Scene.is_prepared() returns a boolean that is true when the load finished and the scene is waiting to go live. Scene.activate() commits the swap at the next frame boundary and returns true when a prepared scene was activated.
Scene calls
| Call | Takes | Returns |
|---|---|---|
Scene.load(uri) | Scene URI | true when the immediate load was accepted. For progress UI prefer prepare/activate. |
Scene.reload() | Nothing | true when the current scene reload was accepted. |
Scene.prepare(uri, additive?) | Scene URI, optional boolean | true when the background load was accepted. |
Scene.cancel() | Nothing | true when a pending prepare was aborted. |
Scene.progress() | Nothing | Number 0…1 for the pending prepare. |
Scene.is_prepared() | Nothing | Boolean, true when ready to activate. |
Scene.activate() | Nothing | true when the prepared scene was committed at the frame boundary. |
Scene.unload(uri) | Scene URI | true when the scene was removed (useful for additive layers). |
Scene.set_persistent(id, on) | Entity handle, boolean | true when the entity was marked to survive scene switches. |
Scene.active() | Nothing | URI string of the live scene. |
Scene.error() | Nothing | Error message string for the last load; empty when the last load succeeded. |
Always check Scene.error() after a load attempt. It is an empty string on success and a human-readable message on failure — a missing document, a bad reference, a schema mismatch.
Scene events
Other scripts often need to know when the world changed. These engine events fire through Events:
scene_loaded— a scene finished loading and is live.scene_unloading— a scene is about to go away (last chance to read its entities).scene_unloaded— a scene is gone.active_scene_changed— the active scene URI changed (including afteractivate).
local Events = require("demi.events")
local token = Events.subscribe("scene_loaded", function(payload)
-- payload.scene: URI string of the scene that loaded
end)
Events.subscribe(name, handler) registers the handler and returns an integer token. The handler receives one payload table; for scene_loaded that table has a scene field with the URI string. Store the token if you plan to Events.unsubscribe(token) later.
Instantiating prefabs
A prefab instance is a whole entity tree. The call returns its instance ID; entities inside it are addressed as strings of the form "instanceId/localId", where localId is an authored entity ID inside the prefab.
local Prefab = require("demi.prefab")
local instance = Prefab.instantiate("prefab://props/skyforge_rune", {
id = "rune_a", -- required stable instance ID
position = { 3, 0, 8 }, -- optional Vec3 world position
overrides = {
body = { name = "Lit Rune" } -- keyed by prefab-local entity ID
},
pooled = true, -- optional instance pooling
})
-- instance: "rune_a", or nil on failure
if instance then
local glow = instance .. "/glow" -- expanded entity ID
end
| Option | Type | Meaning |
|---|---|---|
id | string | Required stable instance ID for this copy. Entity IDs read as "<id>/<localId>". |
position | {x,y,z} | World position for the root (3-number array). |
overrides | table | Per-entity field tables keyed by prefab-local ID ({ body = { name = "Elite" } }): objects merge, arrays replace, null removes a field. |
pooled | boolean | Reuse released instances instead of rebuilding the tree. |
Prefab.instantiate(prefabUri, options) returns the instance ID, or nil if the prefab is unknown or the options are invalid. The returned ID is an ownership handle, not necessarily an entity in the expanded tree. Look up contents by their "instanceId/localId" IDs.
local Prefab = require("demi.prefab")
local Entity = require("demi.entity")
Prefab.release(instance) -- return a pooled instance (or destroy a non-pooled one)
print(Prefab.pooled_count("prefab://props/skyforge_rune")) -- number of idle pooled instances
local spots = Prefab.placements(Entity.find("skyforge_altar"))
-- spots: array of placement tables for instances under that ancestor (possibly empty)
Prefab.set_template_cache_capacity(8) -- how many prepared templates to keep warm
Prefab.release(instance)— returns a pooled instance to the pool, or queues destruction of a non-pooled one. It also accepts any entity ID belonging to the instance. Returnstruewhen released.Prefab.pooled_count(prefabUri)→ number of idle instances currently pooled for that prefab (may be0).Prefab.placements(ancestor?)→ array of authored placement records (id,prefab,root, world-spaceposition/rotation/scale,preserve) under the given ancestor, sorted by ID. It reads markers and never spawns anything; empty array when there are none, nevernil.Prefab.set_template_cache_capacity(n)— keeps up tondecoded templates ready for fast instantiation (0disables the cache). Returnstruewhen the count was accepted.
Pooling matters for projectiles and pickups: instantiate once, release when done, instantiate again with the same ID to reuse the tree. Reuse resets components from authored prefab data before enabling the entities.
Next steps
- Entity API — handles, queries, and parenting.
- Asset groups & streaming — preloading a level's assets before
activate. - Events & timers — reacting to scene changes.