Animation System
Bring your player character to life with Unity's Animator — run cycles, jumps, slides, death sequences, and blend trees that respond to gameplay speed.
Unity Animator Overview
Unity's animation system has three key pieces that work together:
- Animation Clips — Individual animations (a run cycle, a jump, a slide). Each clip stores keyframe data — positions, rotations, and scales at specific times. These can be created in Unity's Animation window or imported from external tools like Blender or Mixamo.
- Animator Controller — A visual state machine that defines which clips can play and how the character transitions between them. You create this as an asset in your Project window.
- Animator Component — A component you attach to your character GameObject. It references an Animator Controller and drives the character's mesh (or transforms) based on the current animation state.
Think of it like a music player. Animation clips are the songs. The Animator Controller is the playlist with rules for which song plays next. The Animator component is the actual speaker attached to your character.
Unity has two animation systems: the modern Mecanim system (Animator + Animator Controller) and the older Legacy system (Animation component). We use Mecanim exclusively. If you ever see tutorials referencing the "Animation" component (without the "r"), that is the legacy system — skip those.
Setting Up an Animator Controller
Let us create the Animator Controller that will govern all of our player's animation states.
- In the Project window, navigate to
Assets/Animation/. If this folder does not exist, right-click inAssets/and choose Create > Folder, name itAnimation. - Right-click inside the
Animationfolder and select Create > Animator Controller. Name itPlayerAnimatorController. - Double-click the new controller to open the Animator window. You will see a grid with an "Entry" node, an "Any State" node, and an "Exit" node. This is where we build our state machine.
- Select your Player GameObject in the Hierarchy. In the Inspector, add an Animator component (Add Component > Animator).
- Drag the
PlayerAnimatorControllerasset into the Animator component's Controller field.
If you are using a 3D humanoid model (from Mixamo, the Asset Store, etc.), make sure the model's import settings have Animation Type set to Humanoid and that an Avatar is configured. The Animator component's Avatar field should reference this avatar. For our prototype cube, you can leave Avatar empty — we will animate the transform directly.
Animation States
Our player character needs these animation states:
| State | When It Plays | Loop? |
|---|---|---|
| Idle | On the main menu, before the game starts | Yes |
| Run | During gameplay (the default playing state) | Yes |
| Jump | When the player swipes up | No |
| Slide | When the player swipes down | No |
| Stumble | When the player grazes an obstacle (optional near-miss) | No |
| Death | When the player crashes into an obstacle | No |
Creating States in the Animator Window
- Open the Animator window (double-click your
PlayerAnimatorController). - Right-click on the grid and select Create State > Empty. Name it
Idle. This is your first state. - Repeat for
Run,Jump,Slide,Stumble, andDeath. - Right-click the
Idlestate and choose Set as Layer Default State. It turns orange, meaning the Animator starts here. - For each state, select it and in the Inspector assign the corresponding Animation Clip to the Motion field. (We will create prototype clips in a moment.)
If you are using a prototype cube as your player, you can still animate it. Use Unity's Animation window to create clips that scale, rotate, or move the cube. For example, a "Jump" animation could scale the cube's Y to 1.5 and move it up, then back down. A "Slide" could flatten the cube's Y scale to 0.5. This is perfectly fine for prototyping — you can swap in a real character model later.
Creating Prototype Animations
If you do not have a 3D character model yet, here is how to create simple animations for a cube player:
- Select your Player GameObject in the Hierarchy.
- Open the Animation window (Window > Animation > Animation).
- Click Create and save as
Player_Idle.animinAssets/Animation/Clips/. -
Idle animation: A gentle bobbing motion.
- At frame 0: Position Y = 0.5
- At frame 30: Position Y = 0.6
- At frame 60: Position Y = 0.5
- Set the clip to Loop (select the .anim file, check Loop Time in the Inspector).
-
Click the clip dropdown in the Animation window and choose Create New Clip. Save as
Player_Run.anim.- At frame 0: Scale = (1, 1, 1)
- At frame 5: Scale = (1.05, 0.95, 1)
- At frame 10: Scale = (1, 1, 1)
- At frame 15: Scale = (0.95, 1.05, 1)
- At frame 20: Scale = (1, 1, 1)
- Set to Loop. This creates a subtle "squash and stretch" bounce.
-
Create
Player_Jump.anim:- At frame 0: Scale Y = 1.0
- At frame 3: Scale Y = 0.7 (anticipation squash)
- At frame 8: Scale Y = 1.3, Position Y rises
- At frame 20: Scale Y = 1.0, back to ground
- Do NOT loop.
-
Create
Player_Slide.anim:- At frame 0: Scale Y = 1.0
- At frame 5: Scale Y = 0.4 (flattened)
- At frame 25: Scale Y = 0.4 (hold)
- At frame 30: Scale Y = 1.0 (return to normal)
- Do NOT loop.
-
Create
Player_Death.anim:- At frame 0: Rotation X = 0
- At frame 15: Rotation X = 90 (topple forward)
- At frame 20: Scale = (1.2, 0.3, 1.2) (squash on impact)
- Do NOT loop.
Assign each clip to its corresponding state in the Animator Controller by selecting the state and dragging the clip into the Motion field.
Transitions with Conditions
States alone are not enough — we need transitions that define how the Animator moves from one state to another. Transitions are the arrows between states, and they are controlled by parameters.
Setting Up Parameters
- In the Animator window, click the Parameters tab (next to "Layers" at the top left).
- Click the + button and add these parameters:
- Bool:
IsRunning - Bool:
IsJumping - Bool:
IsSliding - Bool:
IsDead - Trigger:
Stumble - Float:
Speed(used for the blend tree later)
- Bool:
Creating Transitions
Right-click a state and choose Make Transition, then click the destination state. Here are the transitions we need:
| From | To | Condition |
|---|---|---|
| Idle | Run | IsRunning = true |
| Run | Idle | IsRunning = false |
| Run | Jump | IsJumping = true |
| Jump | Run | IsJumping = false |
| Run | Slide | IsSliding = true |
| Slide | Run | IsSliding = false |
| Any State | Death | IsDead = true |
| Any State | Stumble | Stumble (trigger) |
| Stumble | Run | (Has Exit Time = true, no condition) |
Transition Settings
Select each transition arrow and configure these settings in the Inspector:
- Has Exit Time: Uncheck this for most gameplay transitions. When unchecked, the transition happens immediately when the condition is met, even if the current animation is not finished. This is critical for responsive controls — when the player swipes jump, the jump animation must start NOW, not after the current run cycle completes. The exception is Stumble → Run, where we want the stumble to play fully before returning to run.
- Transition Duration: Set to 0.1 seconds for most transitions. This creates a brief blend between animations. Set to 0 for instant transitions (e.g., Any State → Death).
- Interruption Source: Set to Current State for Run → Jump and Run → Slide. This allows a new transition to interrupt the current one (e.g., the player can jump mid-slide if you allow it).
- Can Transition To Self: Uncheck this to prevent a state from re-entering itself.
Leaving Has Exit Time checked on gameplay transitions is the most common beginner mistake. It causes your character to feel "laggy" because the Animator waits for the current clip to finish before transitioning. For an infinite runner where responsiveness is everything, always uncheck Has Exit Time on transitions triggered by player input.
Blend Trees for Run Speed
As the game speeds up (from the difficulty system in Chapter 18), the player runs faster. It looks wrong if the run animation plays at the same speed regardless. A Blend Tree solves this by blending between a slow run and a fast run based on a parameter.
- In the Animator window, right-click and select Create State > From New Blend Tree. Name it
RunBlend. - Double-click
RunBlendto enter the blend tree editor. - Set the Blend Type to 1D (we are blending based on a single value: speed).
- Set the Parameter to
Speed. - Click + > Add Motion Field twice to create two slots:
- Slot 1: Your slow run animation clip. Threshold = 0.
- Slot 2: Your fast run animation clip. Threshold = 1.
- Navigate back to the base layer (click "Base Layer" in the breadcrumb at the top).
- Delete the old
Runstate. Re-create the transitions that pointed to/fromRunto now point to/fromRunBlend.
Now, when your code sets Speed to 0.0, the slow run plays. At 1.0, the fast run plays. At 0.5, Unity blends both clips 50/50. The result is a smooth acceleration in the run animation that matches the actual game speed.
If you only have one run animation, you can still use a blend tree. Add the same clip to both slots and instead adjust the Speed multiplier on each motion. Set the slow run to 0.7x speed and the fast run to 1.5x speed. The blend tree will interpolate the playback speed based on the parameter.
Animation Events
Animation Events let you call a C# method at a specific frame in an animation clip. This is perfect for timing sound effects or gameplay events to precise animation moments.
Common Use Cases
- Footstep sounds: Place events at the frames where each foot hits the ground in the run cycle.
- Dust puffs: Trigger a small particle effect at the landing frame of the jump animation.
- Slide start/end: Play a whoosh sound at the start of the slide clip.
Adding an Animation Event
- Select your player object and open the Animation window.
- Select the animation clip you want to add an event to (e.g.,
Player_Run). - Scrub the playhead to the frame where you want the event (e.g., frame 5 for a footstep).
- Click the Add Event button (the small marker icon above the timeline).
- In the Inspector, set the Function field to the name of a public method on any component attached to the same GameObject (e.g.,
PlayFootstep). - Optionally pass a parameter (float, int, string, or Object reference).
Animation Events call methods on components attached to the same GameObject that has the Animator. If your method is on a child object, it will not be found. Also, if the animation is interrupted before reaching the event frame, the event will not fire. Do not rely on Animation Events for critical gameplay logic — use them only for cosmetic effects like sounds and particles.
The PlayerAnimator Script
We need a script that acts as a bridge between the player's gameplay state (from PlayerController) and the Animator. This script translates gameplay actions into Animator parameter changes.
using UnityEngine;
using InfiniteRunner.Core;
namespace InfiniteRunner.Player
{
/// <summary>
/// Controls the player's Animator based on gameplay state.
/// Acts as a bridge between PlayerController and the Animator component.
/// Attach this to the Player GameObject (same object as the Animator).
/// </summary>
[RequireComponent(typeof(Animator))]
public class PlayerAnimator : MonoBehaviour
{
// ─── Cached References ──────────────────────────────────
private Animator _animator;
// ─── Animator Parameter Hashes ──────────────────────────
// Using hashes instead of strings is faster. Animator.StringToHash()
// converts a parameter name to an integer ID at startup, so we never
// have to do string comparisons at runtime.
private static readonly int HashIsRunning = Animator.StringToHash("IsRunning");
private static readonly int HashIsJumping = Animator.StringToHash("IsJumping");
private static readonly int HashIsSliding = Animator.StringToHash("IsSliding");
private static readonly int HashIsDead = Animator.StringToHash("IsDead");
private static readonly int HashStumble = Animator.StringToHash("Stumble");
private static readonly int HashSpeed = Animator.StringToHash("Speed");
// ─── State Tracking ─────────────────────────────────────
// We track the current state to avoid setting parameters every frame
// when nothing has changed. This is a minor optimization.
private bool _isRunning;
private bool _isJumping;
private bool _isSliding;
private bool _isDead;
// ─── Unity Lifecycle ────────────────────────────────────
private void Awake()
{
// Cache the Animator component — never call GetComponent in Update()
_animator = GetComponent<Animator>();
if (_animator == null)
{
Debug.LogError(
"[PlayerAnimator] No Animator component found! " +
"Attach an Animator to the Player GameObject.");
}
}
private void OnEnable()
{
// Subscribe to game state changes to handle menu/death/restart
GameManager.Instance.OnGameStateChanged += HandleGameStateChanged;
}
private void OnDisable()
{
if (GameManager.Instance != null)
{
GameManager.Instance.OnGameStateChanged -= HandleGameStateChanged;
}
}
// ─── Public Methods (called by PlayerController) ────────
/// <summary>
/// Transition to the running state.
/// Called when the game starts or the player lands from a jump/slide.
/// </summary>
public void SetRunning(bool running)
{
if (_isDead) return; // Do not change animation if dead
_isRunning = running;
_animator.SetBool(HashIsRunning, running);
// When starting to run, make sure jump and slide are cleared
if (running)
{
_isJumping = false;
_isSliding = false;
_animator.SetBool(HashIsJumping, false);
_animator.SetBool(HashIsSliding, false);
}
}
/// <summary>
/// Transition to the jumping state.
/// Called when the player swipes up.
/// </summary>
public void SetJumping(bool jumping)
{
if (_isDead) return;
_isJumping = jumping;
_animator.SetBool(HashIsJumping, jumping);
// Cannot be sliding and jumping at the same time
if (jumping)
{
_isSliding = false;
_animator.SetBool(HashIsSliding, false);
}
}
/// <summary>
/// Transition to the sliding state.
/// Called when the player swipes down.
/// </summary>
public void SetSliding(bool sliding)
{
if (_isDead) return;
_isSliding = sliding;
_animator.SetBool(HashIsSliding, sliding);
// Cannot be jumping and sliding at the same time
if (sliding)
{
_isJumping = false;
_animator.SetBool(HashIsJumping, false);
}
}
/// <summary>
/// Trigger the death animation. This is a one-way transition:
/// once dead, no other animation can play until Reset() is called.
/// </summary>
public void TriggerDeath()
{
_isDead = true;
_animator.SetBool(HashIsDead, true);
// Clear all other states
_isRunning = false;
_isJumping = false;
_isSliding = false;
_animator.SetBool(HashIsRunning, false);
_animator.SetBool(HashIsJumping, false);
_animator.SetBool(HashIsSliding, false);
}
/// <summary>
/// Trigger the stumble animation (plays once, then returns to run).
/// Called when the player has a near-miss with an obstacle.
/// </summary>
public void TriggerStumble()
{
if (_isDead) return;
_animator.SetTrigger(HashStumble);
}
/// <summary>
/// Updates the run speed parameter for the blend tree.
/// Call this every frame from PlayerController with the current
/// normalized speed (0 = minimum speed, 1 = maximum speed).
/// </summary>
/// <param name="normalizedSpeed">
/// Value from 0 to 1 representing current game speed.
/// </param>
public void SetSpeed(float normalizedSpeed)
{
_animator.SetFloat(HashSpeed, normalizedSpeed);
}
/// <summary>
/// Resets all animator parameters to their defaults.
/// Call this when restarting the game to prepare for a fresh run.
/// </summary>
public void ResetAnimator()
{
_isDead = false;
_isRunning = false;
_isJumping = false;
_isSliding = false;
_animator.SetBool(HashIsDead, false);
_animator.SetBool(HashIsRunning, false);
_animator.SetBool(HashIsJumping, false);
_animator.SetBool(HashIsSliding, false);
_animator.SetFloat(HashSpeed, 0f);
// Force the Animator back to the entry state
_animator.Rebind();
_animator.Update(0f);
}
// ─── Animation Event Callbacks ──────────────────────────
// These methods are called by Animation Events placed on clips.
// The method name must match exactly what is set in the Animation Event.
/// <summary>
/// Called by an Animation Event on the run cycle at footstep frames.
/// Plays a footstep sound effect.
/// </summary>
public void PlayFootstep()
{
// AudioManager.Instance.PlaySFX("Footstep");
// Uncomment above when AudioManager is set up (Chapter 20)
}
/// <summary>
/// Called by an Animation Event at the landing frame of the jump clip.
/// Plays a landing sound and optional dust particle effect.
/// </summary>
public void PlayLandingEffect()
{
// AudioManager.Instance.PlaySFX("Landing");
// ParticleManager.Instance.PlayCoinCollect(transform.position);
// Replace with a proper landing dust effect
}
// ─── Event Handlers ─────────────────────────────────────
/// <summary>
/// Handles game state changes. Resets animations on restart,
/// starts idle on menu, starts running when game begins.
/// </summary>
private void HandleGameStateChanged(GameState newState)
{
switch (newState)
{
case GameState.Menu:
ResetAnimator();
// Idle animation plays by default (it is the entry state)
break;
case GameState.Playing:
if (_isDead)
{
// Coming from GameOver — reset first
ResetAnimator();
}
SetRunning(true);
break;
case GameState.Paused:
// Optionally freeze the animator so it does not
// continue playing during pause
_animator.speed = 0f;
break;
case GameState.GameOver:
// Death animation is triggered by TriggerDeath(),
// which is called by PlayerController when collision happens.
// Nothing extra needed here.
break;
}
// Restore animator speed when unpausing
if (newState != GameState.Paused)
{
_animator.speed = 1f;
}
}
}
}
Script Breakdown
- Hashed Parameters — We convert parameter names to integer hashes using
Animator.StringToHash()at class load time (they arestatic readonly). This avoids string allocation and comparison every frame. It is a small optimization, but it is a good habit. - Guard Clauses (
if (_isDead) return) — Once the player is dead, we lock out all animation changes. This prevents the death animation from being interrupted by stale input. - Mutual Exclusivity — When jumping starts, sliding is cleared, and vice versa. The player cannot be in two action states simultaneously.
- ResetAnimator() — Clears all parameters and calls
Rebind()to force the Animator back to its entry state. This is essential when restarting the game. - Pause Handling — Setting
_animator.speed = 0ffreezes the animation in place. We restore it when unpausing. - Animation Event Callbacks —
PlayFootstep()andPlayLandingEffect()are called from Animation Events on the clips. They are placeholders until you hook up the Audio Manager from Chapter 20.
Connecting to PlayerController
The PlayerAnimator is designed to be called by your PlayerController (from Chapter 9). Here is how they work together:
using UnityEngine;
namespace InfiniteRunner.Player
{
public class PlayerController : MonoBehaviour
{
// Reference to our animation controller
[SerializeField] private PlayerAnimator playerAnimator;
// Called when the player initiates a jump
private void StartJump()
{
// ... physics/movement code ...
// Tell the animator to play the jump animation
playerAnimator.SetJumping(true);
}
// Called when the player lands
private void Land()
{
// ... physics/movement code ...
// Return to the run animation
playerAnimator.SetJumping(false);
}
// Called when the player initiates a slide
private void StartSlide()
{
// ... physics/movement code ...
playerAnimator.SetSliding(true);
}
// Called when the slide ends
private void EndSlide()
{
// ... physics/movement code ...
playerAnimator.SetSliding(false);
}
// Called when the player collides with an obstacle
private void Die()
{
// ... disable input, stop movement ...
playerAnimator.TriggerDeath();
// Notify the GameManager
// GameManager.Instance.EndGame();
}
// Called every frame during gameplay
private void Update()
{
// ... movement logic ...
// Update the animation blend tree with current speed
// normalizedSpeed ranges from 0 (start speed) to 1 (max speed)
float normalizedSpeed = CalculateNormalizedSpeed();
playerAnimator.SetSpeed(normalizedSpeed);
}
private float CalculateNormalizedSpeed()
{
// Example: map current speed to a 0-1 range
// float minSpeed = 5f;
// float maxSpeed = 20f;
// return Mathf.InverseLerp(minSpeed, maxSpeed, currentSpeed);
return 0.5f; // placeholder
}
}
}
The PlayerController handles movement and physics. The PlayerAnimator handles animations. They communicate through simple method calls. This separation means you can completely change the animation system (e.g., swap from Mecanim to a custom solution) without touching any movement code, and vice versa.
Tips for Downloaded Animation Assets
Once your prototype is working, you will probably want to use real character animations. Mixamo (mixamo.com) is a free service by Adobe that provides hundreds of humanoid animations. Here is how to use them:
- Go to mixamo.com and sign in with a free Adobe account.
- Choose a character or upload your own 3D model.
- Browse the animation library and search for "running," "jumping," "sliding," etc.
- Download each animation as FBX for Unity (.fbx). Choose "Without Skin" if you have already downloaded the character separately (this keeps file sizes small).
- Import the .fbx files into
Assets/Animation/Mixamo/. -
For each imported .fbx file:
- Select it in the Project window.
- In the Rig tab: Set Animation Type to Humanoid. Click Apply.
- In the Animation tab: Check Loop Time for run/idle clips. Uncheck it for jump/death/slide. Click Apply.
- Expand the .fbx file in the Project window to reveal the animation clip inside. Drag it into the appropriate Animator Controller state's Motion field.
Some Mixamo animations include root motion — the character physically moves forward in the clip. This can conflict with your PlayerController's movement code. To prevent this, uncheck Apply Root Motion on the Animator component. Your code should control all movement; the Animator only handles the visual pose.
What We Built
In this chapter, we set up a complete animation system for our player character:
- An Animator Controller with states for Idle, Run, Jump, Slide, Stumble, and Death.
- Transitions controlled by bool parameters, with responsive settings (no exit time, short transition durations).
- A Blend Tree that smoothly adjusts the run animation speed based on the game's difficulty.
- Animation Events for triggering footstep sounds at precise frames.
- A PlayerAnimator.cs script that bridges the gameplay layer (PlayerController) and the visual layer (Animator), using hashed parameter IDs for performance.
- Prototype animations using cube transforms, with a clear path to upgrade with Mixamo or custom 3D character models.
In the next chapter, we switch from visual polish to data persistence. We will build a Save System that stores high scores, unlocked items, and player settings across play sessions.
Animations bring the player to life. Commit.
git add .
git commit -m "Implement animation system with state machine and blend trees"
git push
That wraps up Part 5: Polish. Run git log --oneline — your commit history tells the entire story of building this game, system by system.