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
| Signature | Behavior |
|---|---|
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.
| Signature | Behavior |
|---|---|
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.
| Key | Default | When 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 |
walkThreshold | 0.1 | Speed in m/s at which idle becomes walk |
runThreshold | 4.0 | Speed in m/s at which walk becomes run |
turnThreshold | 0.5 | Yaw rate in rad/s above which a stationary character is turning |
blendTime | 0.2 | Crossfade into a looping state |
oneShotBlendTime | 0.12 | Crossfade into the short jump takeoff |
landBlendTime | 0.25 | Crossfade 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
| Field | Meaning |
|---|---|
clip | The clip's name, as playAnimation knows it. |
trackCount, boundTrackCount | How many tracks the clip has, and how many of them name a bone this model has. |
missingBones | Each 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. |
positionScale | Present 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. |
advice | One 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.
| Signature | Behavior |
|---|---|
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
onTriggerEnteras 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:
| Purpose | Clips |
|---|---|
| Locomotion | idle, walk, run, walk_backward, strafe_left, strafe_right, forward_left, forward_right, backward_left, backward_right |
| Turning on the spot | turn_left, turn_right |
| Air | jump, fall, land |
| Crouched | crouch_idle, crouch_walk |
| Combat | attack, hit_reaction, death, aim_idle, fire_idle |
| Emote | wave |
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.