DEMIENGINEDOCUMENTATION Everything is free
Lua API reference · Create

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

CallTakesReturns
Scene.load(uri)Scene URItrue when the immediate load was accepted. For progress UI prefer prepare/activate.
Scene.reload()Nothingtrue when the current scene reload was accepted.
Scene.prepare(uri, additive?)Scene URI, optional booleantrue when the background load was accepted.
Scene.cancel()Nothingtrue when a pending prepare was aborted.
Scene.progress()NothingNumber 0…1 for the pending prepare.
Scene.is_prepared()NothingBoolean, true when ready to activate.
Scene.activate()Nothingtrue when the prepared scene was committed at the frame boundary.
Scene.unload(uri)Scene URItrue when the scene was removed (useful for additive layers).
Scene.set_persistent(id, on)Entity handle, booleantrue when the entity was marked to survive scene switches.
Scene.active()NothingURI string of the live scene.
Scene.error()NothingError 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 after activate).
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
OptionTypeMeaning
idstringRequired stable instance ID for this copy. Entity IDs read as "<id>/<localId>".
position{x,y,z}World position for the root (3-number array).
overridestablePer-entity field tables keyed by prefab-local ID ({ body = { name = "Elite" } }): objects merge, arrays replace, null removes a field.
pooledbooleanReuse 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. Returns true when released.
  • Prefab.pooled_count(prefabUri) → number of idle instances currently pooled for that prefab (may be 0).
  • Prefab.placements(ancestor?) → array of authored placement records (id, prefab, root, world-space position/rotation/scale, preserve) under the given ancestor, sorted by ID. It reads markers and never spawns anything; empty array when there are none, never nil.
  • Prefab.set_template_cache_capacity(n) — keeps up to n decoded templates ready for fast instantiation (0 disables the cache). Returns true when 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