DEMIENGINEDOCUMENTATION Everything is free
Create

In this guide

Lifecycle hooks, explicit imports, typed properties, events, and timers.

Lua scripting

Gameplay rules live in Lua 5.4. A script is a module that returns a table of lifecycle functions and fields. Attach it to an entity with the LuaScript component and it receives engine callbacks every frame.

A complete script

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

local Player = {}

function Player:on_create()
    -- self.id is the entity this script is attached to.
    -- self.speed already holds the authored property value.
    self.jump_held = false
end

function Player:on_update(dt)
    -- dt is seconds since the previous rendered frame.
    local x, y = Input.vector("move")   -- -1 … 1 on each axis
    local px, py, pz = Transform3D.get_position(self.id)
    Transform3D.set_position(self.id,
        px + x * self.speed * dt,
        py,
        pz + y * self.speed * dt)
end

function Player:on_fixed_update(fixed_dt)
    -- fixed_dt is the project simulation step (default 1/60).
    -- Put physics and other deterministic logic here.
end

function Player:on_destroy()
    -- Release timers, subscriptions, and pooled entities.
end

return Player

Walkthrough: the file returns one table, Player. The engine instantiates it per entity and calls its methods. Inside every hook, self is that instance and self.id is the entity handle you pass to every other API. Properties written in the editor arrive as fields on self before on_create runs.

Lifecycle

HookWhen it runsTypical use
on_create()Entity is created; properties are already setCached handles, initial state, subscriptions
on_start()Scene activation finishedCross-entity lookups that need the whole scene
on_update(dt)Every rendered frameInput sampling, presentation, camera follow
on_fixed_update(fixed_dt)Every simulation stepMovement, combat resolution, anything that must stay deterministic
on_destroy()Entity or scene is torn downCleanup
on_ui_event(e)A UI node fires an eventButtons, drags, focus changes
on_ui_value_changed(e)Slider/toggle/text value changesSettings, form fields
on_ui_submit(e)Submit actionConfirm dialogs, text fields

on_update and on_fixed_update can both run zero or more times per frame. Sample input in on_update, apply it in on_fixed_update, so replays stay aligned with the fixed step.

Service imports

local Input = require("demi.input")
local Events = require("demi.events")
local Health = require("demi.gameplay.health")  -- from an installed package

Each require returns the service table. Import only what the script needs. Engine services ship with the engine; package modules come from installed packages.

Typed script properties

Annotations expose fields in the Inspector so designers can tune values without editing Lua:

---@demi_component
---@display_name Player Controller
---@category Gameplay
---@description Moves the player with the move action.
local Player = {}

---@demi_property
---@label Move Speed
---@range 0 20
Player.speed = 6.0

---@demi_property
---@options red, blue
Player.team = "red"

---@demi_property entity
---@label Spawn Point
Player.spawn_point = "player_spawn"  -- stable entity ID

return Player
AnnotationEffect
@demi_componentRegisters the script as a component. Display name defaults to the humanized filename; category defaults to Scripting.
@demi_propertyExposes the assignment below it as an editable property.
@label, @descriptionInspector text.
@range min maxSlider bounds for numbers.
@options a, b, cDropdown choices for strings.
@categoryInspector grouping.

Property types come from the Lua value: boolean, number, integer, string, array, object, vec2, vec3, color, plus asset and entity when annotated. Defaults install before on_create — read them there, do not reassign them.

Scene or prefab overrides:

"LuaScript": {
  "module": "script://scripts/player.lua",
  "properties": { "speed": 8.5, "team": "blue" }
}

An override that fails validation is rejected and the script does not activate. Under hot reload the previous valid instance keeps running.

For stricter constraints, set a property_schema table on the returned module:

Player.property_schema = {
    speed = { type = "number", default = 6.0, minimum = 0.0, maximum = 20.0 },
    team  = { type = "enum", values = { "red", "blue" }, default = "red" },
    model = { type = "asset", required = true },
}
return Player

Types: boolean, number, integer, string, asset, entity, enum, array, object, vec2, vec3, color. Invalid overrides reject activation the same way.

Events

local Events = require("demi.events")

local id = Events.subscribe("physics_collision_enter", function(e)
    if e.other_id == "lava" then
        Events.emit("player_died", { cause = "lava" })
    end
end)
-- later:
Events.unsubscribe(id)

Events.subscribe(event_name, handler) returns an integer token. The handler receives one payload table and runs immediately when the event fires. Events.emit sends a game-defined payload to every subscriber of that name.

Engine events you can subscribe to:

  • Physics — physics_collision_enter/stay/exit, physics_trigger_enter/stay/exit, physics_contact, and the 3D counterparts physics3d_collision_enter/stay/exit, physics3d_trigger_enter/stay/exit, physics3d_contact
  • Animation — animation_event, sprite_animation, animation_collision
  • Scene — scene_loaded, scene_unloading, scene_unloaded, active_scene_changed
  • UI — ui_event, ui_submit
  • App — application_focus, application_blur, application_suspend, application_resume
  • Data — data_asset_reloaded

The optional demi.gameplay.events package is a caller-owned queue with an explicit flush() step — useful when you want to handle events at a controlled point in the frame.

Timers and time

local Timer = require("demi.timer")
local Time = require("demi.time")

local handle = Timer.after(1.5, function()
    print("one-shot after 1.5 seconds")
end)
local repeat_handle = Timer.every(0.25, function() end)
Timer.delay(0.1, function() end)   -- runs next opportunity
Timer.cancel(handle)               -- stop a pending timer

print(Time.delta_time)             -- seconds, this frame
print(Time.fixed_time)             -- seconds, simulation clock
print(Time.frame_count)            -- integer frame index
Time.set_paused(true)              -- freezes update hooks
Time.set_scale(0.5)                -- slow motion

after, every, and delay return a handle you can pass to cancel. Always cancel in on_destroy if the timer outlives the entity otherwise.

Editor stubs

Generate LuaLS/EmmyLua stubs for autocomplete:

demi lua-stubs generate .demi/lua

Add .demi/lua to LuaLS workspace.library. The generated files describe signatures for the editor and are not loaded at runtime.

Hot reload behavior

  • Edits are applied at frame boundaries, only after the new script compiles and validates.
  • A compile error or bad property override prints diagnostics and leaves the running instance untouched.
  • Instance fields on self reset on reload. Keep durable state in entity fields, game data, or saves.

Next