Animation Systems in Unity
Introduction to Unity Animations
Animation brings your game objects to life, allowing characters to move realistically, environments to be dynamic, and interfaces to be engaging. Unity provides a powerful and flexible animation system that can handle everything from simple property changes to complex character animations with blending and state management.
In this guide, you'll learn:
- Unity's animation tools and components
- Creating and working with animation clips
- Using Animation Controllers and State Machines
- Controlling animations through scripting
- Animation blending and transitions
- Animation events and callbacks
- Performance optimization for animations
Unity Animation Components
Unity's animation system consists of several key components that work together:
Core Animation Components:
- Animation Clip: A recording of animated properties over time
- Animator Component: The main component that plays animations on a GameObject
- Animator Controller: State machine asset that manages animation clips and transitions
- Animation State: A specific animation clip within the Animator Controller
- Blend Tree: Allows for blending between multiple animations based on parameters
- Animation Layer: Allows different parts of the character to be animated independently
- Avatar: Maps animations to a specific character's skeleton (for humanoid animations)
Animation workflow:
- Create Animation Clips (in Unity or import from 3D software)
- Set up an Animator Controller with States and Transitions
- Attach the Animator component to your GameObject
- Assign the Controller to the Animator component
- Control animations via Parameters from scripts
Creating Animation Clips
There are several ways to create animation clips in Unity:
Methods for Creating Animations:
- Import from 3D software: Blender, Maya, 3ds Max, etc.
- Unity's Animation Window: Record property changes over time
- Animation Rigging: Procedural and constraint-based animation
- Scripting: Create animations at runtime
Creating Animation Clips in Unity:
- Select the GameObject you want to animate
- Open the Animation window (Window > Animation > Animation)
- Click "Create" to create a new animation clip
- Enter a name and save location for the clip
- Use the Record button (red circle) to start recording property changes
- Set keyframes by changing properties in the Inspector
- Use the dopesheet and curves view to fine-tune timing and values
Common properties to animate:
- Transform: Position, rotation, scale
- Visibility: Enabling/disabling GameObjects
- Material properties: Color, texture offsets
- Light properties: Intensity, color, range
- Sprite changes: For 2D animations
- Custom properties: Any serialized property in your components
Animator Controllers and State Machines
The Animator Controller is a powerful state machine that manages transitions between animations.
Setting Up an Animator Controller:
- Create an Animator Controller asset (Right-click in Project window > Create > Animator Controller)
- Double-click to open in the Animator window
- Add animation clips by dragging them into the Animator window
- Create transitions between states by right-clicking a state and selecting "Make Transition"
- Configure transition conditions using Parameters
- Set the default state (orange color)
Animator Parameters:
- Float: Decimal values (e.g., speed, blend amount)
- Int: Whole numbers (e.g., animation ID, weapon type)
- Bool: True/false values (e.g., isJumping, isGrounded)
- Trigger: Special boolean that automatically resets after use (e.g., jump, attack)
State machine features:
- States: Each represents an animation clip or blend tree
- Transitions: Connections between states with conditions
- Sub-State Machines: Nested state machines for organization
- Any State: Special node for transitions that can occur from any state
- Entry/Exit: Define start and end points of state machines
Transition Settings
Important settings when configuring transitions:
- Has Exit Time: If enabled, transition waits until a certain point in the current animation
- Exit Time: When to trigger the transition (0-1, percentage of animation)
- Transition Duration: How long the blend between animations takes
- Transition Offset: Where to start the next animation
- Interruption Source: How other transitions can interrupt this one
- Conditions: Parameter-based rules that determine when to transition
Controlling Animations with C# Scripts
Scripts allow you to control animations at runtime by manipulating Animator parameters.
Basic Animation Control:
using UnityEngine;
public class CharacterAnimationController : MonoBehaviour
{
// Reference to the Animator component
private Animator animator;
// Parameter IDs for faster access
private int speedId;
private int groundedId;
private int jumpId;
void Start()
{
// Get the Animator component
animator = GetComponent();
// Cache parameter IDs (more efficient than using strings repeatedly)
speedId = Animator.StringToHash("Speed");
groundedId = Animator.StringToHash("Grounded");
jumpId = Animator.StringToHash("Jump");
// Using hash IDs is more efficient than using string names directly
}
void Update()
{
// Get input for movement
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
// Calculate movement speed for animation
Vector2 input = new Vector2(horizontalInput, verticalInput);
float speed = input.magnitude;
// Set the Speed parameter (controls blend between idle and walk/run)
animator.SetFloat(speedId, speed);
// This updates the Speed parameter in the Animator Controller
// Handle jump input
if (Input.GetButtonDown("Jump") && IsGrounded())
{
// Trigger the jump animation
animator.SetTrigger(jumpId);
// Trigger parameters automatically reset after they're consumed
}
// Update grounded state
animator.SetBool(groundedId, IsGrounded());
// This updates the Grounded boolean parameter
}
// Check if character is on the ground
bool IsGrounded()
{
// Simple ground check using raycasting
return Physics.Raycast(transform.position, Vector3.down, 0.2f);
}
// Change animation state directly
public void PlayAttackAnimation()
{
// Trigger an attack animation
animator.SetTrigger("Attack");
// This sets a trigger parameter named "Attack"
}
// Get information about current animation state
public bool IsPlayingAttackAnimation()
{
// Check if we're in a specific state
return animator.GetCurrentAnimatorStateInfo(0).IsName("Attack");
// This checks if the current state in layer 0 is named "Attack"
}
// Adjust animation speed
public void SetAnimationSpeed(float speedMultiplier)
{
// Change the playback speed of all animations
animator.speed = speedMultiplier;
// This affects the entire Animator - all layers and states
}
}
Key Animator methods:
- SetFloat/SetInt/SetBool/SetTrigger: Set parameter values
- GetFloat/GetInt/GetBool: Get parameter values
- Play: Force a specific animation to play
- GetCurrentAnimatorStateInfo: Get information about the current state
- CrossFade: Smoothly transition to a specific state
Advanced Animation Control:
using UnityEngine;
public class AdvancedAnimationController : MonoBehaviour
{
private Animator animator;
private int layerIndex = 0; // Base layer
void Start()
{
animator = GetComponent();
}
// Play an animation with CrossFade for smoother transitions
public void CrossFadeAnimation(string stateName, float transitionDuration)
{
// Smoothly transition to the specified state
animator.CrossFade(stateName, transitionDuration);
// This creates a smooth blend to the target animation
}
// Work with animation layers
public void SetLayerWeight(int layerIndex, float weight)
{
// Adjust the influence of a specific animation layer
animator.SetLayerWeight(layerIndex, weight);
// This controls how much a layer affects the final pose (0-1)
}
// Access animation state information
public void CheckAnimationProgress()
{
// Get info about the current state in the specified layer
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(layerIndex);
// Get the normalized time (0-1 represents progress through the clip)
float normalizedTime = stateInfo.normalizedTime;
Debug.Log($"Animation progress: {normalizedTime * 100}%");
// Check if we're in a specific state
bool isRunning = stateInfo.IsName("Run");
// Get the current animation length
float length = stateInfo.length;
Debug.Log($"Animation length: {length} seconds");
// Get the current animation speed
float speed = stateInfo.speed;
Debug.Log($"Animation speed: {speed}x");
}
// Play a specific state immediately
public void PlayState(string stateName)
{
// Force immediate play of a state
animator.Play(stateName);
// This immediately switches to the specified state
}
// Work with animator transitions
public void CheckTransitionInfo()
{
// Get information about any ongoing transition
AnimatorTransitionInfo transitionInfo = animator.GetAnimatorTransitionInfo(layerIndex);
// Check if a transition is in progress
bool isTransitioning = animator.IsInTransition(layerIndex);
if (isTransitioning)
{
// Get the normalized time of the transition (0-1)
float transitionTime = transitionInfo.normalizedTime;
Debug.Log($"Transition progress: {transitionTime * 100}%");
}
}
// Match target allows precise positioning during animations
public void MatchTargetExample()
{
// Make the character's hand reach for a door handle at a specific position
// during frames 10-20 of the "OpenDoor" animation
Vector3 doorHandlePosition = new Vector3(1, 1, 0);
animator.MatchTarget(
doorHandlePosition, // Target position
Quaternion.identity, // Target rotation (not matching rotation in this case)
AvatarTarget.RightHand, // Which body part to match
new MatchTargetWeightMask(Vector3.one, 0), // Match position only, not rotation
0.1f, // Start time (normalized)
0.2f // End time (normalized)
);
// This makes the animation adjust to match environmental objects
}
// Apply root motion manually
public void HandleRootMotion()
{
// Get the root motion delta position and rotation
Vector3 deltaPosition = animator.deltaPosition;
Quaternion deltaRotation = animator.deltaRotation;
// Apply to transform (if you've disabled Apply Root Motion on the Animator)
transform.position += deltaPosition;
transform.rotation *= deltaRotation;
}
}
Advanced animation features:
- Animation Layers: Control different body parts independently
- Cross Fading: Create smooth transitions between animations
- Animation Events: Trigger functions at specific points in animations
- Root Motion: Use animation to drive character movement
- Match Target: Adjust animations to match environmental interactions
Animation Blending and Blend Trees
Blend Trees allow you to create smooth transitions between multiple animations based on one or more parameters.
Setting Up Blend Trees:
- In the Animator window, right-click and select Create State > From New Blend Tree
- Double-click the Blend Tree to open it
- Set the Blend Type (1D, 2D Simple Directional, 2D Freeform Directional, or 2D Freeform Cartesian)
- Set the Parameter that will control the blending
- Add motion fields by clicking the + button
- Assign animation clips to each motion
- Set threshold values that determine when each animation plays
Common Blend Tree uses:
- 1D Blend Tree: For blending based on one parameter (e.g., speed: idle -> walk -> run)
- 2D Blend Tree: For directional movement (e.g., walk forward, backward, left, right)
- Nested Blend Trees: Blend trees within blend trees for complex animations
Controlling Blend Trees from Scripts:
using UnityEngine;
public class BlendTreeController : MonoBehaviour
{
private Animator animator;
private int speedId;
private int horizontalId;
private int verticalId;
void Start()
{
animator = GetComponent();
// Cache parameter hashes
speedId = Animator.StringToHash("Speed");
horizontalId = Animator.StringToHash("Horizontal");
verticalId = Animator.StringToHash("Vertical");
}
void Update()
{
// 1D BLEND TREE EXAMPLE (e.g., Idle-Walk-Run)
// Get player input
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
// Calculate movement speed (0-1 range)
Vector2 input = new Vector2(horizontalInput, verticalInput);
float speed = Mathf.Clamp01(input.magnitude);
// Apply to animator - this controls a 1D blend tree
animator.SetFloat(speedId, speed);
// 2D BLEND TREE EXAMPLE (e.g., Directional movement)
// The raw input values control the 2D blend
animator.SetFloat(horizontalId, horizontalInput);
animator.SetFloat(verticalId, verticalInput);
// For smoother animation transitions, you can use Mathf.SmoothDamp
float currentHorizontal = animator.GetFloat(horizontalId);
float currentVertical = animator.GetFloat(verticalId);
float smoothTime = 0.1f;
float velocityH = 0f, velocityV = 0f;
float smoothHorizontal = Mathf.SmoothDamp(currentHorizontal, horizontalInput, ref velocityH, smoothTime);
float smoothVertical = Mathf.SmoothDamp(currentVertical, verticalInput, ref velocityV, smoothTime);
animator.SetFloat(horizontalId, smoothHorizontal);
animator.SetFloat(verticalId, smoothVertical);
}
}
Animation Events
Animation Events allow you to trigger functions at specific points in an animation clip.
Creating and Handling Animation Events:
using UnityEngine;
public class AnimationEventsHandler : MonoBehaviour
{
// References for effects and sounds
public ParticleSystem footstepDustEffect;
public AudioClip footstepSound;
public AudioClip swordSwingSound;
private AudioSource audioSource;
void Start()
{
audioSource = GetComponent();
if (audioSource == null)
{
// Add an AudioSource component if one doesn't exist
audioSource = gameObject.AddComponent();
}
}
// Called by animation event - no parameters
public void FootstepEvent()
{
// Play footstep sound
if (audioSource != null && footstepSound != null)
{
audioSource.PlayOneShot(footstepSound);
}
// Play dust particle effect
if (footstepDustEffect != null)
{
footstepDustEffect.Play();
}
Debug.Log("Footstep event triggered at: " + Time.time);
}
// Animation event with integer parameter
public void AttackEvent(int attackType)
{
switch (attackType)
{
case 0: // Light attack
Debug.Log("Light attack executed");
break;
case 1: // Heavy attack
Debug.Log("Heavy attack executed");
break;
case 2: // Special attack
Debug.Log("Special attack executed");
break;
}
// Play sword swing sound
if (audioSource != null && swordSwingSound != null)
{
audioSource.PlayOneShot(swordSwingSound);
}
}
// Animation event with string parameter
public void PlaySound(string soundName)
{
Debug.Log("Playing sound: " + soundName);
// You could have a dictionary of sounds and play the appropriate one
// Or use Resources.Load to load audio clips dynamically
}
// Animation event with float parameter
public void ApplyDamage(float damageAmount)
{
Debug.Log("Applying damage: " + damageAmount);
// Find nearby enemies and apply damage
Collider[] hitColliders = Physics.OverlapSphere(transform.position, 2f);
foreach (Collider hitCollider in hitColliders)
{
// Check if it's an enemy
if (hitCollider.CompareTag("Enemy"))
{
// Try to get the enemy's health component
EnemyHealth enemyHealth = hitCollider.GetComponent();
if (enemyHealth != null)
{
// Apply damage
enemyHealth.TakeDamage(damageAmount);
}
}
}
}
// Animation event with object parameter
public void SpawnObject(Object prefab)
{
if (prefab is GameObject)
{
GameObject go = prefab as GameObject;
Instantiate(go, transform.position, transform.rotation);
Debug.Log("Spawned object: " + go.name);
}
}
}
// Example of an enemy health component
public class EnemyHealth : MonoBehaviour
{
public float health = 100f;
public void TakeDamage(float amount)
{
health -= amount;
Debug.Log(gameObject.name + " took " + amount + " damage. Health: " + health);
if (health <= 0)
{
Die();
}
}
void Die()
{
Debug.Log(gameObject.name + " died!");
Destroy(gameObject);
}
}
Creating Animation Events:
- Select the Animation Clip in the Project window
- Open the Animation window
- Add an event marker by right-clicking the timeline and selecting "Add Event"
- In the Inspector, specify the function name and any parameters
Animation Event parameters:
- No parameter: Simple function call
- Int: Pass an integer value
- Float: Pass a float value
- String: Pass a string
- Object: Pass a reference to an Object (e.g., GameObject, AudioClip)
Animation Performance Optimization
Animations can be performance-intensive. Here are some tips to optimize them:
Performance Optimization Techniques:
- Culling Mode: Configure the Animator's Culling Mode to stop animating objects that aren't visible
- Keyframe Reduction: Remove unnecessary keyframes from animation clips
- Animation Compression: Use appropriate compression settings in the Import Settings
- LOD System: Use simpler animations for distant characters
- Animation Pooling: Reuse animations across similar characters
- Avatar Masks: Animate only necessary body parts
- Update Mode: Consider using "Normal" or "AnimatePhysics" instead of "UnscaledTime" when possible
Setting Up Animator Culling:
using UnityEngine;
public class AnimatorOptimizer : MonoBehaviour
{
private Animator animator;
void Start()
{
animator = GetComponent();
// Set culling mode
animator.cullingMode = AnimatorCullingMode.CullCompletely;
// Options:
// - AlwaysAnimate: Always animate, even when off-screen (most expensive)
// - CullUpdateTransforms: Skip animation that update transforms when off-screen
// - CullCompletely: Don't animate at all when off-screen (most efficient)
}
// Toggle animation based on distance from player
void Update()
{
// Get distance to player
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// Disable animator if too far away
if (distanceToPlayer > 50f)
{
animator.enabled = false;
}
else
{
animator.enabled = true;
// Additionally, you can adjust animation quality based on distance
if (distanceToPlayer > 30f)
{
// Use lower quality for distant characters
animator.updateMode = AnimatorUpdateMode.Normal;
animator.speed = 0.5f; // Lower animation frame rate
}
else
{
// Use full quality for nearby characters
animator.updateMode = AnimatorUpdateMode.Normal;
animator.speed = 1f;
}
}
}
}
Best Practices for Unity Animation
- Organize your Animator Controller with sub-state machines for clarity
- Use parameters efficiently - minimize the number of parameters and use appropriate types
- Cache parameter hashes with StringToHash for better performance
- Create smooth transitions between animations for natural movement
- Use Animation Layers to animate different body parts independently
- Incorporate Animation Events for synchronized effects and gameplay logic
- Adjust animation weights for partial blending between animations
- Consider performance by using culling and appropriate update modes
- Test animations on target platforms to ensure smooth performance
- Standardize animation naming for easier management