DEMIENGINEDOCUMENTATION Everything is free
Lua scripting · Create

on_create through on_destroy, with a full worked script.

Script lifecycle

A Lua script is a small module of functions the engine calls at known moments — when its entity is created, every frame, when physics ticks, when it is destroyed. This page explains what a script module is, who self is, and what each hook is for.

What a script module is

A script is a Lua file under your project's scripts/ folder, referenced from a scene as script://…. The file returns a table of hook functions. The engine creates one instance of that table per entity that uses the script, then calls the hooks at the right times.

local Debug = require("demi.debug")

local SkyforgeRune = {}

function SkyforgeRune:on_create()
    Debug.log("rune created: " .. self.id)
end

return SkyforgeRune

The returned table is the script definition. on_create is one hook on it; use on_start for lookups that need the whole scene. The engine creates an instance per entity. Module locals and upvalues are shared by those instances, so put per-entity mutable state on self. Nothing runs until a scene or prefab attaches the script to an entity.

self and self.id

Inside every hook, self is the script instance. self.id is the entity this instance belongs to. Use self to store per-entity state (counters, flags, targets) and self.id with the entity and transform APIs.

local Transform3D = require("demi.transform3d")

function SkyforgeRune:on_create()
    self.pulses = 0            -- per-entity state
end

function SkyforgeRune:on_update(dt)
    self.pulses = self.pulses + 1
    Transform3D.add_position(self.id, 0, dt * 0.2, 0)  -- float upward
end
  • self — the script instance table (hooks plus whatever fields you set).
  • self.id — entity handle for this instance; pass it to Entity, Transform3D, Animation, and friends.

Two runes in the same scene mean two instances, each with its own self.pulses. Never put per-entity state in a local outside the returned table and expect it to split per entity.

The hooks

HookWhen it runsArguments
on_createOnce, when the instance is built and its properties are in placeNone
on_startOnce, after every instance in the scene has been createdSetup that needs the whole scene: cross-entity lookups, timers, subscriptions. Most scripts do their setup here.
on_update(dt)Every rendered framedt: scaled seconds since the last frame
on_fixed_update(fixed_dt)Every fixed simulation tickfixed_dt: fixed seconds for this tick
on_destroyOnce, when the entity is being removedNone
on_ui_event(payload)When a HUD node this script owns fires an interaction eventOne payload table (node ID, event name)
on_ui_value_changed(payload)When a HUD value (slider, toggle) changesOne payload table (node ID, new value)
on_ui_submit(payload)When a form-style HUD submit happensOne payload table

Only define the hooks you need. A rendering-only script can be just on_update. Physics movement belongs in on_fixed_update so it stays in step with the simulation; visual smoothing belongs in on_update.

A full Skyforge example

local Transform3D = require("demi.transform3d")
local Input = require("demi.input")
local Time = require("demi.time")

local SkyforgeHoverRune = {}

function SkyforgeHoverRune:on_create()
    self.base_y = select(2, Transform3D.get_position(self.id))
    self.t = 0
end

function SkyforgeHoverRune:on_start()
    -- safe place to look up other entities: every instance exists now
end

function SkyforgeHoverRune:on_update(dt)
    self.t = self.t + dt
    local x, _, z = Transform3D.get_position(self.id)
    local y = self.base_y + math.sin(self.t * 2) * 0.15
    Transform3D.set_position(self.id, x, y, z)
end

function SkyforgeHoverRune:on_destroy()
    self.t = nil
end

return SkyforgeHoverRune

on_create reads the starting y and zeroes a timer. on_start is empty here but is the first safe moment to resolve other entities by name. on_update receives dt in scaled seconds (pause and time_scale affect it), computes a bobbing height with math.sin, and writes position back — all three coordinates, because set_position replaces the whole local position. on_destroy clears state; it runs before the entity goes away, so self.id is still valid inside it.

When a hook takes an argument, assign exactly that many names: function S:on_update(dt) takes one, function S:on_fixed_update(fixed_dt) takes one. For the UI hooks the argument is a single payload table — read payload.node and payload.event rather than expecting multiple returns.

Attaching a script to an entity

A scene entity carries a LuaScript component that names the module and its property overrides:

{
  "id": "skyforge_rune",
  "components": {
    "LuaScript": {
      "module": "script://scripts/hover_rune.lua",
      "properties": {
        "speed": 8.5
      }
    }
  }
}
  • module — the script:// URI of the Lua file. It must return a hook table.
  • properties — values installed on the instance before on_create runs, so the first hook already sees them.

Invalid property overrides reject scene activation — the load fails with a clear error instead of starting with half a script. If you reload a script during development and the new version does not parse, the previous instance keeps running. Property declaration is covered in Script properties.

Next steps