Lua ScriptingIntermediate

Animation playback API

Play, blend, stop, speed up, pause, and reverse clips, drive locomotion automatically, and use the clips the default character ships with.

Discover available clips

local names = gameObject.getAnimationNames()
for index, name in ipairs(names) do
    print(index .. ": " .. name)
end

Clip names are case-sensitive. They come from the model file, and from any external animations attached to the object.

External animations

A character or mesh can borrow clips from other files. In its inspector, click Add Animation, choose an FBX or glTF animation asset, and give it a Clip Name; scripts then play it by that name, exactly like a clip inside the model. An external clip is downloaded the first time a script plays it, not when the scene loads, so that first playAnimation call returns false and the clip starts a moment later. getAnimationNames() lists external clips before they have loaded. To have a clip ready before its first play, start loading it with preloadAnimations (see "Load clips ahead of time and move the playhead" below).

What plays before a script does

A model with animations starts playing one as soon as it loads. Current Animation in the inspector (animationClipName) chooses which. Left at None, the model's first clip with a non-zero duration plays, which skips the empty "Take 001" stack some Mixamo exports carry. Animation Speed (animationSpeed) and Loop Animation (loopAnimation) apply to that clip. The first playAnimation or enableAutoAnimation from a script replaces it.

Root motion

A character's clips play in place. The engine removes the horizontal movement of the skeleton's root bone (the hips, on a Mixamo rig), pinning it to the model's rest pose, so the model stays centred on its physics capsule, which your script moves. Vertical movement is kept, so a death, crouch or roll clip lowers the body to the floor. To move the character across the ground during a clip, move it from the script.

This applies to the clips inside the model and to clips added with Add Animation. It applies to characters only: a mesh plays its clips' root motion as authored. The inspector's animation preview plays clips unfiltered, so it can show drift that the running game does not.

Play and stop

SignatureBehavior
playAnimation(name, loop?, blendTime?)Returns true when the clip started. Defaults: loop=true, blendTime=0.2 seconds. The first call for an external animation that has not loaded yet returns false and starts the clip once its file arrives.
stopAnimation(name?, fadeOutTime?)Stops one clip or every clip. Default fade-out is immediate.
isAnimationPlaying(name?)Checks one clip or any active clip.

Control playback speed and direction

setAnimationSpeed(speed, name?) accepts a playback multiplier. Use 1 for normal speed, 0.5 for half speed, 0 to pause, and a negative value to play backward.

Pass a clip name to change only that animation action. If you omit name, the speed is applied to the object's animation mixer and affects every animation on the object. The named form is usually the safer choice while blending locomotion clips.

local walkClip = "walk"

function init()
    gameObject.playAnimation(walkClip, true, 0.2)
end

function update(deltaTime)
    local playbackSpeed = 1.0
    if input.moveBackward then
        playbackSpeed = -1.0
    end

    -- Target only the walk clip so blends and idle remain forward.
    gameObject.setAnimationSpeed(playbackSpeed, walkClip)
end

playAnimation replaces whatever is playing: every other clip fades out over blendTime, or stops at once when blendTime is 0. Clips do not layer, so you cannot play an upper-body clip on top of a walk.

Do not restart every frame

playAnimation() resets the action to the beginning. Call it only when animation state changes; change speed per frame with setAnimationSpeed().

Reverse-playback limitation

Negative speed works directly for looping clips and reverses an already-playing action from its current playhead. A non-looping action started with playAnimation(name, false) begins at time zero, so it cannot start backward from the final frame with the current API.

Load clips ahead of time and move the playhead

An external clip loads the first time it is played or preloaded. Start the download early so the first play is not lost, and check that a clip has arrived before you rely on its timing.

SignatureBehavior
preloadAnimations(names?)Starts loading clips without playing them: one name, a list of names, or with no argument every clip the object has. Returns true when all of them had already loaded; poll isAnimationReady or areAnimationsLoaded for the rest.
isAnimationReady(name)True once the clip has loaded, so playing or seeking it takes effect at once.
areAnimationsLoaded()True once every clip getAnimationNames() lists has loaded.
seekAnimation(name, time)Moves a playing clip's playhead to time seconds, clamped to its length, and poses the model at once, so a bone read straight afterwards sees the new pose. Returns false when the clip has not loaded or is not playing: play it first. A finished one-shot seeked back before its end plays on from there.
getAnimationTime(name?)The clip's playhead in seconds. With no name, the playhead of the clip posing the model most. nil when that clip is not playing.
getAnimationDuration(name)The clip's length in seconds; nil until it has loaded.

Know when a one-shot clip ends

Define onAnimationFinished(name) in the object's script and it is called once, with the clip's name, when a clip played with loop=false reaches its end. It runs after the frame's animation update rather than in the middle of it, and only once init() has run. Looping clips never finish.

local ATTACK = "attack"
local swinging = false

function init()
    -- Start the download now, so the first swing is not lost.
    gameObject.preloadAnimations({ ATTACK })
end

function update(deltaTime)
    if input.primaryFire and not swinging and gameObject.isAnimationReady(ATTACK) then
        swinging = true
        gameObject.playAnimation(ATTACK, false, 0.1)
    end
end

function onAnimationFinished(name)
    if name == ATTACK then
        swinging = false
    end
end

Use the playhead, not a timer, to find a moment inside a clip: getAnimationTime(ATTACK) / getAnimationDuration(ATTACK) is how far through the swing the character is, and stays right if the clip's speed changes.

Automatic locomotion

enableAutoAnimation(config) picks a clip each frame from the character's velocity, ground contact, and crouch state, so a controller does not have to name clips at the right moment. Call updateAutoAnimation() every frame — nothing happens without it. disableAutoAnimation() stops it, and getAnimationState() returns the current state name.

function init()
    gameObject.enableAutoAnimation({
        idle = "idle",
        walk = "walk",
        run = "run",
        walkBackward = "walk_backward",
        strafeLeft = "strafe_left",
        strafeRight = "strafe_right",
        forwardLeft = "forward_left",
        forwardRight = "forward_right",
        backwardLeft = "backward_left",
        backwardRight = "backward_right",
        turnLeft = "turn_left",
        turnRight = "turn_right",
        jump = "jump",
        fall = "fall",
        land = "land",
        crouchIdle = "crouch_idle",
        crouchWalk = "crouch_walk"
    })
end

function update(deltaTime)
    -- move the character first, so the state machine reads this frame's motion
    gameObject.updateAutoAnimation()
end

Every key is optional, and every one defaults to the clip name shown above — so a model whose clips already use these names needs no config at all. enableAutoAnimation({}) is a complete call.

Animation in a multiplayer room

Scripts written this way keep working in a room. Every player's browser animates every character automatically from its movement — the same idle, walk, run, directional, turn, jump, fall, land and crouch states — so a character's control script does not animate anything itself there: enableAutoAnimation, disableAutoAnimation and setAnimationSpeed set the character's configuration for its own player's screen, and updateAutoAnimation, playAnimation, stopAnimation and the getters do nothing (getAnimationNames() returns an empty list and getAnimationState() returns nil).

Other players always see a character animated with the default clip names in the table below, whatever configuration its script passes, so give a multiplayer character a model that uses them. Play an explicit clip — an emote, a hit reaction — from a GUI or camera script; that takes the character over on that browser. See Build and test a multiplayer game.

KeyDefaultWhen it plays
idle"idle"On the ground, below the walk threshold
walk"walk"On the ground, above the walk threshold
run"run"On the ground, above the run threshold
jump"jump"Once, on leaving the ground
fall"fall"Looped, once the takeoff finishes
land"land"Once, on touching down
crouchIdle"crouch_idle"Crouched and still
crouchWalk"crouch_walk"Crouched and moving
walkBackward"walk_backward"Moving backwards, relative to the way it faces
strafeLeft"strafe_left"Sidestepping to the character's left
strafeRight"strafe_right"Sidestepping to the character's right
forwardLeft"forward_left"Travelling forward-left
forwardRight"forward_right"Travelling forward-right
backwardLeft"backward_left"Travelling backward-left
backwardRight"backward_right"Travelling backward-right
turnLeft"turn_left"Standing still and turning left
turnRight"turn_right"Standing still and turning right
walkThreshold0.1Speed in m/s at which idle becomes walk
runThreshold4.0Speed in m/s at which walk becomes run
turnThreshold0.5Yaw rate in rad/s above which a stationary character is turning
blendTime0.2Crossfade into a looping state
oneShotBlendTime0.12Crossfade into the short jump takeoff
landBlendTime0.25Crossfade into the longer land recovery

The air is two states, not one

jump plays once on takeoff and hands over to a looping fall when it finishes; land plays once on touchdown and then gives way to a ground state. That is why a takeoff clip should be the push-off alone. A clip that includes its own crouch and wind-up will appear to start late, because the character has already left the ground by the time it plays — the physics jump is instant and the animation is not.

A landing is cut short the moment the player moves off, so a long recovery clip does not lock the character in place.

oneShotBlendTime is short enough not to swallow the 0.30s takeoff, while landBlendTime eases into the much longer 1.40s touchdown. Finished one-shots are fully stopped and uncached before the next state, so a clamped final pose cannot keep blending into later movement.

A turn clip animates the feet, not the heading

Your script owns which way the character faces — the state machine reaches for turn_left or turn_right precisely because it saw that heading change. So the bundled turn clips shuffle the feet and shift the weight without rotating the model: the rotation you see is the one your controller applied.

That matters if you supply your own. Mixamo's turn animations are real 90-degree pivots, and bundling one unchanged rotates the character twice — the model swings ahead of its facing and snaps back each time the clip loops. The bundled ones have that pivot taken out at build time. If a turn looks like it over-rotates, this is why.

Missing clips fall back rather than failing

A state whose clip the model does not carry drops to the nearest one it does: run and the sideways and diagonal states onto walk, the backward diagonals onto walk_backward and then walk, a turn onto the matching strafe, crouch onto the standing clips, fall onto jump, and land onto idle, which is where every chain ends. The directional and turning states go further: they are not chosen at all unless the model has that clip, so a model without walk_backward backs up with its ordinary walk and run clips. A model with only idle, walk, run and jump therefore animates everywhere — and with no fall clip to hand over to, its jump loops for the whole descent instead of playing once.

A clip that is missing with no usable fallback is reported once in the browser console, not once per frame.

Check that a clip fits the model

A clip drives a model by bone name. A clip made for a different skeleton, or one that animates bones the model leaves out (often fingers or toes), cannot move those bones. When a clip from a separate file loads, the tracks for bones the model lacks are dropped, and the browser console gets one line for that clip instead of dozens of PropertyBinding warnings.

getAnimationDiagnostics() returns one table per loaded clip:

for _, report in ipairs(gameObject.getAnimationDiagnostics()) do
    if report.status ~= "ok" then
        print(report.clip .. ": " .. (report.advice or report.status))
    end
end
FieldMeaning
clipThe clip's name, as playAnimation knows it.
trackCount, boundTrackCountHow many tracks the clip has, and how many of them name a bone this model has.
missingBonesEach bone the clip animates that the model lacks, listed once.
status"ok" when every track binds, "partial" when some do, "incompatible" when none do: the clip was made for a different skeleton.
positionScalePresent only when the clip's bone positions are far from the model's rest pose, as a ratio: about 100 usually means a clip in centimetres on a model in metres (or the reverse), which collapses or twists the pose.
adviceOne sentence saying what to do. Absent when status is "ok".

Clips that have not loaded yet are not listed, so preload them first. Retargeting a clip onto a different skeleton is not supported: for an incompatible clip, export the motion from the same rig as the model (for Mixamo, download it for this character). For a unit mismatch, re-export one of the two files so their units match.

In the editor, the animation preview in the inspector shows a Rig compatibility box for any clip that is not "ok", with how many of its tracks bind, which bones are missing, and the same advice.

Attach objects to bones

A character, or a mesh with a skeleton, can read its bones and hold other objects at them: a sword in a hand, a hat on a head.

SignatureBehavior
getBoneNames()Lists the model's bones.
getBoneWorldPosition(name)The bone's world position as animated this frame, {x, y, z}, or nil when there is no such bone.
getBoneWorldRotation(name)The bone's world rotation as a quaternion, {x, y, z, w}, or nil.
attachToBone(object, boneName, offset?)Called on the character or mesh. Holds object (a handle or an id) at the bone every frame until it is released. Returns true when attached.
detachFromBone(object?)Releases one held object, or with no argument everything this object holds. Returns true if anything was released.
getBoneAttachments()Lists what this object holds, as { objectId = ..., bone = ... } tables.

Bone names match exactly first, then loosely, ignoring case, separators and Mixamo's mixamorig prefix, so "RightHand" finds mixamorig:RightHand. Any named node of the model, such as a Grip empty exported with it, works as a bone.

function init()
    local sword = gameObject.getVar("sword") -- an object_ref variable
    local attached = gameObject.attachToBone(sword, "RightHand", {
        position = { x = 0, y = 0.05, z = 0.25 }, -- metres along the bone's axes
        euler = { x = 90, y = 0, z = 0 },          -- degrees
    })
    if not attached then
        for _, name in ipairs(gameObject.getBoneNames()) do
            print(name)
        end
    end
end

The offset is optional. Its position is in metres along the bone's axes and ignores the rig's own scale (Mixamo bones carry a 0.01 scale). Give the rotation either as rotation = {x, y, z, w}, a quaternion, or as euler = {x, y, z} in degrees, applied X, then Y, then Z. Bone axes differ between rigs, so expect to tune the offset until the object sits right. A malformed offset is refused and reported as a script error.

The held object is placed after animation and character movement each frame, so it sits on the hand of the pose that is drawn, and it is in the hand from the moment of the call. What happens to its physics depends on its body:

  • A dynamic body becomes kinematic while held and is swept to the hand, so it pushes what it hits and gravity no longer pulls it away. Detaching makes it dynamic again with the hand's velocity, which is how a weapon is thrown.
  • A kinematic body, including any trigger volume, stays kinematic and is swept the same way, so a sensor blade reports onTriggerEnter as it sweeps through characters and bodies.
  • A static body is moved to the hand without pushing anything.
  • An object with no body is only drawn there.

attachToBone returns false for a missing object or bone, for the object itself, and for an object the character is a child of. Stopping the run releases everything.

Bone sockets are presentation-only in a multiplayer room

The room server never animates a skeleton, so in an authority or prediction script the bone, diagnostics, preload and playhead calls on this page do nothing: attachToBone, detachFromBone, seekAnimation, preloadAnimations and the readiness checks return false, getBoneNames, getBoneAttachments and getAnimationDiagnostics return empty lists, and the rest return nil. Attach a weapon from a camera, GUI or presentation script, which runs on each player's browser. What a bone holds there is decoration on each screen, so never decide a hit from a bone-held hitbox: send an action and resolve the hit on the room server. See Build and test a multiplayer game.

Clips on the default character

A character with no mesh asset of its own renders the bundled default model, which ships with twenty-four clips. They are the names in the table above plus a set for scripts to drive directly:

PurposeClips
Locomotionidle, walk, run, walk_backward, strafe_left, strafe_right, forward_left, forward_right, backward_left, backward_right
Turning on the spotturn_left, turn_right
Airjump, fall, land
Crouchedcrouch_idle, crouch_walk
Combatattack, hit_reaction, death, aim_idle, fire_idle
Emotewave

walk_forward is also present, as a second name for walk.

The first seventeen are reachable through enableAutoAnimation; the rest are played with playAnimation at the moment your script wants them. The character's Current Animation dropdown in the inspector lists whichever clips the loaded model actually has, which is the quickest way to see them without running anything.

The default character changed on 14–16 September 2026

It first replaced the old four-clip set — idle, run, walk, and tpose — with the full locomotion set, then added the four diagonal clips. Scripts that referenced tpose will no longer find it. Existing clip names were otherwise retained.