Roblox Character Animation: Building an Animator Pipeline With Priorities, Blending, and Animation Events

Does your character's attack animation still play cleanly when the player is sprinting, jumping, and connected at 180 ms of ping all at once? If you ship a combat, traversal, or social game on Roblox, it should.
Most animation bugs that reach live players do not come from the animation itself. They come from the pipeline around it — which object loaded the track, what priority it carries, how its weight fades against other tracks, and whether the timing cues inside it hold up once the track is replicated to 30 other clients.
This guide walks through that pipeline from the ground up. We cover the Animator instance, the seven-level priority stack, weight blending for locomotion, and animation events built on keyframe markers, with the replication rules that tie them together.
What Is The Animator, And Why Does It Matter?
Every animated rig on Roblox plays its tracks through an Animator instance. For player characters and humanoid NPCs, the Animator sits inside the Humanoid; for non-humanoid rigs such as doors, creatures, or vehicles, it sits inside an AnimationController.
The Animator is the object that loads and plays animations on a rig. Call Animator:LoadAnimation to get an AnimationTrack — Humanoid:LoadAnimation is deprecated and should not appear in new code.
That said, where the Animator came from matters as much as where it sits. The Animator is also the object responsible for replicating animation state, which is why the rules in the replication section below all trace back to it.
Here's the baseline pattern for loading a track on the local player's character:
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
local swingAnim = Instance.new("Animation")
swingAnim.AnimationId = "rbxassetid://0000000000"
local swingTrack = animator:LoadAnimation(swingAnim)
swingTrack.Priority = Enum.AnimationPriority.Action
swingTrack.Looped = false
Note that the script waits for the Animator rather than creating one. As we explain below, an Animator created by a LocalScript breaks replication entirely.
Load Once, Cache Forever
LoadAnimation returns a new AnimationTrack every time you call it, even for the same Animation object. Calling it inside an input handler, a combat loop, or a RunService callback creates a new track every time it fires, and those tracks accumulate on the Animator.
Roblox logs a warning once a single Animator passes 256 loaded tracks, and well before that point you will notice memory creep in the developer console. The fix is a per-character cache:
- Load on spawn. Load every track a character needs inside the CharacterAdded handler, once, and store each one in a table keyed by name.
- Reuse on input. Input handlers look the track up and call Play, never LoadAnimation.
- Discard on death. When the character is removed, drop the table so the old tracks are garbage-collected along with the rig.
This one habit also makes the rest of the pipeline easier to test. A cached track is a stable object you can inspect in Roblox unit tests rather than a fresh instance that exists only for one frame.
Preload Before The First Play
A track's Length property reads 0 until the animation asset has finished downloading. Any code that schedules work from Length — a cooldown, a hit window, a combo timer — will misfire on the first swing of a session if the asset is still streaming in.
Accordingly, pass your Animation objects to ContentProvider:PreloadAsync during the loading screen. Keep in mind that this adds to your join-time budget, so profile it alongside your other preloads using the techniques in our Roblox performance profiling guide.
How Do Animation Priorities Work?
When two playing tracks both animate the same joint, Roblox resolves the conflict by priority first and weight second. A higher-priority track at full weight takes the joint outright; tracks at the same priority blend according to their relative weights.
Priority decides which track controls a joint when two tracks animate it. Higher priority wins on shared joints, and joints the higher track leaves untouched keep playing the lower track underneath.
That second clause is the part teams miss. An Action-priority sword swing that keys only the arms and torso will layer cleanly over a Movement-priority run, because the legs are left to the run cycle.
The Enum.AnimationPriority stack has seven levels, from lowest to highest:
| Priority | Typical Use | Overrides |
|---|---|---|
| Core | Base poses, default fallback animations | Nothing |
| Idle | Standing idle, breathing loops | Core |
| Movement | Walk, run, swim, climb cycles | Core, Idle |
| Action | Attacks, tool use, emotes | Core, Idle, Movement |
| Action2 | Hit reactions that interrupt attacks | Everything through Action |
| Action3 | Stuns, knockdowns, grabs | Everything through Action2 |
| Action4 | Cinematic finishers, cutscene overrides | Everything below it |
The priority is authored into the animation asset in the Animation Editor, but AnimationTrack.Priority can override it at runtime. We recommend setting it in code every time, as in the sample above, so the priority lives in version control instead of inside an uploaded asset nobody can diff.
Plan The Stack Before You Animate
Priority conflicts are far cheaper to settle in a spreadsheet than in a playtest. Before the animator opens the editor, agree on which gameplay states belong to which level.
A workable allocation for a typical combat game looks like this:
- Movement for locomotion only. Walk, run, sprint, crouch-walk, and strafe cycles share Movement so they can weight-blend against each other.
- Action for player-initiated moves. Light attacks, heavy attacks, blocks, reloads, and emotes go here.
- Action2 for interruptions. Flinches and parry reactions must visibly cut through an attack, so they sit one level up.
- Action3 and Action4 for control loss. Stuns, ragdoll recovery, and scripted finishers need to beat everything the player can trigger.
Once that table exists, a bug report such as "the stun doesn't show while I'm attacking" becomes a one-line check rather than an afternoon in Studio. What's more, it gives your animators a clear rule for which joints a move should key.
Keep in mind that the default Animate LocalScript plays its idle, walk, run, jump, and fall tracks at low priorities. If a custom animation authored at Core refuses to show, the default locomotion is almost always outranking it — raise the priority rather than disabling Animate.
How Do You Blend Animations With Weight?
Within a single priority, weight decides how much of each track reaches the joint. Two Movement tracks at weights 0.7 and 0.3 produce a pose that sits 70% of the way toward the first, which is exactly what you need for a walk-to-run transition driven by speed.
Play both locomotion tracks at the same priority, then call AdjustWeight on each as speed changes. Weights blend proportionally, so a walk at 0.6 and a run at 0.4 produces an intermediate gait.
Here's how that fits into a speed-driven locomotion controller:
walkTrack.Priority = Enum.AnimationPriority.Movement
runTrack.Priority = Enum.AnimationPriority.Movement
walkTrack:Play(0.2, 1, 1)
runTrack:Play(0.2, 0, 1)
humanoid.Running:Connect(function(speed)
local alpha = math.clamp((speed - 8) / 12, 0, 1)
walkTrack:AdjustWeight(1 - alpha, 0.15)
runTrack:AdjustWeight(alpha, 0.15)
end)
In this example, the gait is pure walk at 8 studs per second, pure run at 20 studs per second, and a proportional mix in between. The 0.15-second fade on each AdjustWeight call smooths out the jitter that Humanoid.Running produces on uneven terrain.
Keep Locomotion Cycles In Phase
Both tracks start together and keep advancing even while one sits at weight 0. As a result, the walk and run cycles stay phase-locked, so the left foot lands at the same moment in both and the blend never produces a foot-slide.
This only works if the cycles are authored to the same footfall timing. If your run cycle is shorter than your walk, adjust the run track's speed with AdjustSpeed so the two Length values match, or have your animator re-time the cycle so contact frames line up as a fraction of the loop.
Fade Times Are A Design Decision
Play, Stop, and AdjustWeight each accept a fadeTime argument, and Play defaults to 0.1 seconds. That number shapes how responsive your game feels more than most teams expect.
Some general starting points include:
- 0.05 to 0.1 seconds for attacks. Players read a slow fade-in as input lag, particularly in fighting or action games.
- 0.15 to 0.3 seconds for locomotion. Gait changes look natural with a longer blend because real bodies shift weight gradually.
- 0 seconds only on purpose. A hard cut reads as a snap, which can sell a heavy impact or a stun but looks like a bug almost anywhere else.
For those reasons, we suggest exposing fade times as named constants in a shared module. Tuning feel then becomes a config change, and your input action bindings can reference the same values when they decide how long to buffer a follow-up press.
How Do Animation Events Work?
Animation events let a track tell your code that it has reached a specific moment — the frame a sword connects, a foot hits the ground, or a reload clicks home. On Roblox, the modern mechanism is the keyframe marker, which an animator places on the timeline in the Animation Editor and gives a name and an optional string parameter.
Add a named marker in the Animation Editor, then connect to AnimationTrack:GetMarkerReachedSignal with that name. The signal fires when playback crosses the marker and passes the marker's string parameter.
Here's the client side of a hit marker on the cached swing track:
swingTrack:GetMarkerReachedSignal("Hit"):Connect(function(param)
hitRemote:FireServer(param)
end)
swingTrack:GetMarkerReachedSignal("Footstep"):Connect(function(surface)
playFootstep(surface)
end)
Markers are a better fit than the older KeyframeReached event, which only fires on named keyframes and couples your gameplay code to how the animator happened to key the pose. Markers sit independently on the timeline, so an animator can re-time a pose without silently moving the hit frame.
What Markers Should Drive
Markers are the right tool for presentation that needs to land on a specific frame. Some examples of good marker uses include:
- Footstep audio. A marker on each contact frame, with the surface material passed as the parameter, keeps steps in sync at any playback speed — see our Roblox sound design guide for the mixing side.
- VFX bursts. Muzzle flashes, slash trails, and dust puffs that must appear on the exact frame of the motion.
- Camera cues. Small shakes on heavy landings or impacts, coordinated with the effects covered in our guide to Roblox camera systems.
- Hit-window requests. A client-side signal that the attack has reached its active frames, sent to the server for validation.
That last item deserves its own section. After all, it is where an animation pipeline meets your anti-exploit model.
How Do Animations Replicate?
Replication is where pipelines that worked in solo Studio testing fall apart under a live server. Fortunately, the rules are short, and nearly all of them come back to the Animator.
Tracks a client plays on its own character replicate to the server and other players, but only if the server created the Animator. If a LocalScript creates the Animator, nothing it plays replicates.
Here's how the rules break down by rig type:
- Player characters. Play tracks from a LocalScript on the owning client for the lowest latency. The default character already includes a server-created Animator inside the Humanoid, so wait for it with WaitForChild rather than building one.
- NPCs. Play tracks from a server Script so every client receives the same state. For the movement side of NPC rigs, our Roblox NPC pathfinding guide covers how to drive locomotion weights from path speed.
- Custom rigs. Create the AnimationController and its Animator on the server, then play from whichever side owns the behavior.
Note that track state replicates, but your Lua connections do not. Another client that receives a replicated track sees it arrive through Animator.AnimationPlayed, and if that client wants footstep sounds on other players, it has to connect its own marker listeners to the track it receives.
Never Let A Marker Decide Damage
The client controls its own character's animation playback. That means an exploiter can play a track at ten times speed, skip straight to the Hit marker, or fire the remote without playing anything at all.
Therefore, treat the client's Hit marker as a request, not a verdict. The server should record when it accepted the attack input, compare the arrival time of the hit request against the expected window for that move, and reject anything outside it.
For instance, if a heavy attack's hit frame sits 0.42 seconds into the animation, the server might accept requests between roughly 0.3 and 0.7 seconds after the swing began. That tolerance absorbs typical ping variance while still rejecting a hit that arrives 0.05 seconds after the input, which no legitimate client can produce.
Our deep dive on Roblox replication covers the general pattern of client intent and server authority in more detail. The animation-specific point is simple: the server owns the clock, and the animation owns the look.
Common Pipeline Failures And How To Diagnose Them
When an animation misbehaves in a live server, the cause is almost always one of a short list. Here's a list of the symptoms we see most often in production rigs and where each one usually comes from:
- Plays in Studio, invisible in live. The asset is owned by a different user or group than the experience. Re-upload it under the experience owner, or grant the experience permission to use it.
- Plays for the owner, invisible to everyone else. A LocalScript created the Animator, or the track is playing on a rig the client does not own.
- Attack shows on the arms but the legs freeze. The attack keys the legs at a higher priority than locomotion. Remove the leg keys or blend them deliberately.
- Custom idle never appears. The custom track's priority is at or below the default Animate script's idle, so the default wins.
- First swing of a session has a broken hit window. The asset had not finished loading, so Length read 0 when the timer was scheduled.
- Memory climbs over a long session. LoadAnimation is being called per input or per frame rather than once per spawn.
All of these failures are cheap to catch before launch with a short checklist run in a multi-client local test server. In fact, two-player Studio tests catch nearly every replication-side item on this list in under five minutes.
Putting The Pipeline Together
A dependable character-animation pipeline on Roblox comes down to a few habits applied consistently. Load tracks through a server-created Animator once per spawn, assign priorities from an agreed table in code, blend same-priority tracks with explicit weights and fade times, and use markers for frame-accurate presentation while the server owns gameplay timing.
None of these steps is complicated on its own. That said, skipping any one of them tends to surface as a bug that only appears with real players, real ping, and real devices — the hardest place to debug it.
If you are building out the rest of your game's systems, our guides on Roblox server architecture and Roblox Studio plugins cover the structure and tooling that keep an animation pipeline maintainable as the project grows.


