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). eventsfire attimein seconds inside the state — listen foranimation_event.blend_spacesare 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.layersstore secondary states, weights, and a bonemask, but the current 3D renderer does not compose layer poses.transitionsswitch when aconditionmatches (greater/lesswithparameterandthreshold,trigger, orfinished).blend_durationtracks progress; the renderer does not yet cross-fade poses.pause_policyispause(freeze while the game is paused) orcontinue.
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).