DEMIENGINEDOCUMENTATION Everything is free
Game systems

In this guide

State machines, blend spaces, IK, buses, snapshots, and spatial voices.

Animation & audio

One animation state machine can select sprite or skeletal clips. Audio is a runtime API with buses, snapshots, streaming, and spatial voices.

Animation state machines

{
  "initial_state": "idle",
  "speed": 1.0,
  "root_motion": false,
  "pause_policy": "pause",
  "states": {
    "idle": { "model_clip_name": "Idle", "duration": 1.0, "loop": true },
    "run":  { "model_clip_name": "Run", "duration": 0.8, "loop": true,
              "events": [{ "time": 0.4, "name": "footstep" }] },
    "aim":  { "model_clip_name": "Aim", "duration": 1.0, "loop": true }
  },
  "parameters": { "speed": 0.0 },
  "blend_spaces": {
    "idle": { "parameter_x": "speed",
      "points": [{ "x": 0, "state": "idle" }, { "x": 1, "state": "run" }] }
  },
  "layers": {
    "upper": { "state": "aim", "weight": 0.5, "additive": true, "mask": ["spine", "arm_l"] }
  },
  "transitions": {
    "start_run": { "from": "idle", "to": "run", "condition": "greater",
                   "parameter": "speed", "threshold": 0.1, "blend_duration": 0.15 },
    "stop_run": { "from": "run", "to": "idle", "condition": "less",
                  "parameter": "speed", "threshold": 0.1 }
  }
}
  • A state plays a model clip (model_clip_name) or a sprite clip (sprite_clip).
  • events fire at time in seconds inside the state — listen for animation_event.
  • blend_spaces are keyed by the active state name and sample named states by parameter value. Current playback chooses the highest-weight state; it does not mix visual poses.
  • layers store secondary states, weights, and a bone mask, but the current 3D renderer does not compose layer poses.
  • transitions switch when a condition matches (greater/less with parameter and threshold, trigger, or finished). blend_duration tracks progress; the renderer does not yet cross-fade poses.
  • pause_policy is pause (freeze while the game is paused) or continue.
local Animation = require("demi.animation")

Animation.set_number(id, "speed", 0.8)     -- feed a parameter
Animation.set_bool(id, "grounded", true)
-- Animation.trigger(id, "jump")           -- if a trigger transition uses "jump"

print(Animation.state(id))                 -- current state name, or ""
Animation.play(id, "idle")                 -- force a state
Animation.set_speed(id, 1.25)              -- playback rate
local t = Animation.normalized_time(id)    -- 0…1 inside the current state
local transition = Animation.transition(id) -- {from, to, progress, active}
Animation.set_layer_weight(id, "upper", 0.3)
Animation.set_root_motion(id, true)

animation_event payloads carry entity_id, state, and name, so one handler can service footstep, hit, and reload timings.

Sprite sheets

For simple frame animation, SpriteAnimator2D is enough (see 2D & isometric). Model clips come from glTF/GLB animation names when settings.model_import.import_animations is enabled; settings.animations.clips is optional validation metadata. Root motion is off unless enabled and uses evenly spaced root_motion_track samples authored on a state.

Two-bone IK

local Animation = require("demi.animation")

local solved = Animation.solve_two_bone_2d({
    root = hip, target = foot, pole = knee,
    upper_length = 0.4, lower_length = 0.4,
})
if solved.reached then
    -- solved.joint          → knee position
    -- solved.end_position   → achieved end position
else
    -- target is out of range; the chain is stretched toward it
end

Use Animation.set_bone_segment / clear_bone_segments to drive procedural leg visuals (see procedural_spider_3d).

Audio buses

The default bus tree is master → music, sfx, voice, ui.

local Audio = require("demi.audio")

Audio.set_bus_volume("music", 0.6)     -- 0…1
print(Audio.get_bus_volume("music"))
Audio.set_bus_muted("ui", true)
Audio.set_bus_paused("voice", true)

Audio.define_snapshot("quiet", { music = 0.2, sfx = 0.5 })
Audio.transition_snapshot("quiet", 0.4)  -- fade over 0.4 seconds

A snapshot captures a set of bus levels you can jump or fade to — pause menus, underwater, and so on.

Playing sounds

local handle = Audio.play("asset://audio/boom", {
    bus = "sfx",
    loop = false,
    streaming = false,
    volume = 0.9,
    pitch = 1.0,
    pan = 0.0,
    spatial = "3d",            -- "none" | "2d" | "3d"
    x = 2, y = 0, z = 4,       -- world position for spatial playback
    min_distance = 1.5,
    max_distance = 30,
    attenuation = "linear",
    rolloff = 1.0,
    doppler = false,
    delay = 0.0,
    fade_in = 0.05,
    concurrency_group = "impacts",
    max_voices = 8,
    voice_stealing = "oldest", -- "reject" | "oldest" | "quietest"
    pause_with_game = true,
})
Audio.stop(handle)             -- silence and release the voice

Audio.play returns an integer voice handle, or 0 when playback cannot start. Pass a nonzero handle to Audio.stop. voice_stealing decides what happens when max_voices is reached: reject the new sound, steal the oldest, or steal the quietest. Audio.crossfade(old_handle, new_asset_id, seconds) starts a new asset and fades the old voice out. Current Lua and entity-source playback submit zero source/listener velocity, so the doppler flag alone does not create motion-based pitch changes.

Mark long music and ambience clips with settings.streaming on the AudioClip asset so they are not fully decoded into memory.

Entity-attached sources

{
  "AudioSource": {
    "clip": "asset://audio/engine_loop",
    "bus": "sfx",
    "loop": true,
    "spatial": "3d",
    "play_on_start": true
  },
  "AudioListener": {}
}
local Source = require("demi.audio.source")
Source.play(id)    -- start the source on this entity
Source.stop(id)    -- stop it

AudioSource keeps the sound attached to the entity’s transform. Place one AudioListener in the scene (usually the camera or player).