Moud

Animation graphs#

An animation graph is a state machine of clips. Each state plays a clip, or a blend of clips picked by parameters, and transitions move from state to state when their conditions hold. A script only sets the parameters.

A graph is a .animgraph file, and you play it like a clip: point an Animation at it and load it. Build one in the Graph tab of the animation editor, or write the file by hand.

Playing a graph#

A body that idles, walks and runs by its speed, and jumps:

luau
--!strict
-- server
local world = game.world
local body = world:find("npc") :: Character
local graph = world:add("Animation", { name = "locomotion", animationId = "res://animations/hero.animgraph" }) :: Animation
local track = body.animator:loadAnimation(graph)
track:play()

game.stepped:connect(function()
    local v = body.velocity
    track:setParameter("speed", vec3(v.x, 0, v.z).magnitude)
    local state = body.humanoid.state
    track:setParameter("grounded", state ~= "jumping" and state ~= "falling")
end)

body.humanoid.jumping:connect(function(started)
    if started then track:trigger("jump") end
end)

local whoosh = body:add("Sound", { soundId = "res://sounds/jump.ogg" }) :: Sound
track.stateChanged:connect(function(state)
    if state == "jump" then whoosh:play() end
end)
  • setParameter sets a number or a boolean, trigger fires a trigger once, and getParameter reads one back, or its default.
  • Parameters are the track's attributes, so a server's values reach every player. A LocalScript sets them on a track it loaded itself.
  • track.state is the state it is in, and stateChanged fires with the name each time it enters one. Both sides run the graph, so each keeps its own state.

The rest of the track works as for a clip:

  • weight, speed, priority, mask, blend, fadeTime and retargetId do what they do on any track.
  • timePosition and length are those of the state it is in.
  • Markers and events fire from the clip that weighs the most in the state.

The .animgraph file#

The graph the script above plays:

json
{
  "version": 1,
  "entry": "move",
  "parameters": {
    "speed": { "type": "number", "default": 0 },
    "grounded": { "type": "bool", "default": true },
    "jump": { "type": "trigger" }
  },
  "states": {
    "move": {
      "blend": "1d",
      "parameter": "speed",
      "motions": [
        { "clip": "res://animations/idle.anim", "at": 0 },
        { "clip": "res://animations/walk.anim", "at": 4 },
        { "clip": "res://animations/run.anim", "at": 8 }
      ]
    },
    "jump": { "clip": "res://animations/jump.anim", "loop": false },
    "fall": { "clip": "res://animations/fall.anim" }
  },
  "transitions": [
    { "from": "move", "to": "jump", "when": [{ "parameter": "jump" }], "fade": 0.1 },
    { "from": "jump", "to": "fall", "fade": 0.2 },
    { "from": "any", "to": "move", "when": [{ "parameter": "grounded", "value": true }], "exit": 0.5, "fade": 0.2 }
  ]
}
Field
parameters optional name to type, number, bool or trigger, and a default
states state name to what it plays. A state cannot be called any
entry optional where the graph starts; the first state when left out
transitions optional tried in order each tick; the first that can go is taken

States#

Field
clip the .anim it plays, for a state of one clip
blend optional 1d or 2d for a blend space, clip when left out
parameter for 1d, the number parameter the blend reads
parameters for 2d, the two number parameters, [x, y]
motions each with a clip and an at: a number on a 1d line, [x, y] in a 2d space
speed optional how fast it plays, 1 when left out
speedParameter optional a number parameter the speed is multiplied by
loop optional true or false. Left out, a clip state loops when its clip does and a blend always loops
position optional where the editor draws it. The engine reads past it

Blend spaces#

  • A 1d blend mixes the two clips either side of the parameter's value, and holds the first or last past the ends.
  • A 2d blend weighs every clip by how close the point is to it, so the weights change smoothly as the point moves between them. A point on a clip plays that clip alone.

The clips of a blend are kept in step. They run through their loops together, at a speed between theirs by weight, so a walk and a run blended put their feet down at the same time.

Transitions#

Field
from the state it leaves, or any for every state but its own to
to the state it enters
when optional conditions that must all hold, see below
exit optional how much of the state must have played first, 1 being once through. Left out, it can go at any time
fade optional seconds the two states blend for, 0.2 when left out

A condition has a parameter, a test and a value. The test is >, <, >=, <=, == or !=, and == when left out. A trigger needs only its parameter, and the transition that takes it uses it up.

  • A transition with no when and no exit goes when its state ends, so it only fits a state that does not loop.
  • A trigger fired while no transition wants it waits until one does.