Simple Sprite Animator Documentation

Animation Controller Documentation

This guide explains how to use the Animation Controller system in code. The Animation Controller provides a lightweight, code-driven sprite animation system that works with the Animation DSL.

Overview

The Animation Controller system consists of:

  • AnimationController - Base class that manages animation state and timing
  • SpriteResolverAnimationController - Ready-to-use implementation for Unity’s SpriteResolver
  • DSLAsset - ScriptableObject that stores your animation definitions
  • Animation DSL - Text-based format for defining animation states (see DSL_DOCUMENTATION.md)

Quick Start

1. Create a DSL Asset

  1. Right-click in your Project window
  2. Select Create > PhantomCompass > DSLAsset
  3. Name it (e.g., “Main Animation Data”)
  4. Open the asset and write your animation definitions in the DSL text area:
Idle: 20, 20, 20, 20, _loop
Run: 5, 5, 5, 5, _loop
Jump: 5, 5, 5, _wait
Attack: 8, 8, 8, Idle

2. Add Animation Controller to Your GameObject

Add a SpriteResolverAnimationController component to your GameObject (or a child GameObject). This component:

  • Manages animation state
  • Updates sprite visuals automatically
  • Works with Unity’s SpriteResolver component

Required Setup:

  • The GameObject (or a child) must have a SpriteResolver component
  • Assign your DSL Asset to the _dslAsset field
  • Optionally set a Default State (e.g., “Idle”)

3. Basic Usage in Code

using PhantomCompass.Animation;

public class MyCharacter : MonoBehaviour
{
    private SpriteResolverAnimationController animController;
    
    void Start()
    {
        animController = GetComponent<SpriteResolverAnimationController>();
    }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // Change animation state
            animController.SetState("Jump");
        }
    }
}

Core Concepts

Animation States

An animation state is a named sequence of frames defined in your DSL. Each state has:

  • A name (e.g., “Idle”, “Run”, “Attack”)
  • A list of frames with durations
  • End behavior (_loop, _wait, _pingpong, or a state transition)

Frame Updates

The animation system uses a tick-based update model. You must call Tick() on the animation controller each frame (typically from a fixed-rate system).

Important: The animation controller does NOT automatically update in Update(). You must call Tick() manually, usually from:

  • Entity.Tick() (for entity-driven animations)
  • A custom fixed-rate system
  • Or enable _autoTickInUpdate for standalone animations

Sprite Updates

When a new frame is reached, the _isNewFrame flag is set. Subclasses like SpriteResolverAnimationController check this flag in LateUpdate() and update sprite visuals accordingly.

API Reference

AnimationController

Properties

CurrentState (read-only)

  • Returns the name of the currently playing animation state
  • Returns null or empty string if no state is set

DefaultState

  • The state to play when no state is set
  • Set this in the inspector or via code

DurationMultiplier

  • Multiplies all frame durations (1.0 = normal speed, 2.0 = half speed, 0.5 = double speed)
  • Minimum value: 0.001
  • Useful for slow-motion effects or speeding up animations

Methods

SetState(string stateName)

  • Changes the animation to the specified state
  • Resets to frame 0 of that state
  • State name is case-sensitive and must match a state in your DSL

Tick()

  • Advances the animation by one frame
  • Should be called every fixed update tick (typically 60 times per second)
  • Processes frame timing, events, and state transitions
  • Sets _isNewFrame flag when a new frame is reached

HasState(string stateName)

  • Checks if a state exists (currently returns false - may be implemented later)

Events

AnimationEvent (Action<string>)

  • Fired when an animation event occurs
  • Event names come from your DSL (e.g., "swingSound", "hitFrame", "_loop")
  • Subscribe to handle sound effects, hitboxes, visual effects, etc.

Protected Members (for Subclasses)

_currentFrameData (FrameData)

  • Contains data for the current frame (frameIndex, duration, events)

_currentStateData (StateData)

  • Contains data for the current state (state name, sprite category alias, frames)

_isNewFrame (bool)

  • Set to true when a new frame is reached
  • Subclasses should check this in LateUpdate() and call ClearNewFrameFlag() after updating visuals

ClearNewFrameFlag()

  • Clears the _isNewFrame flag
  • Call this after updating sprite visuals

Common Usage Patterns

Pattern 1: Entity-Driven Animation

For animations tied to game entities (players, NPCs, enemies):

public class Entity : MonoBehaviour
{
    private SpriteResolverAnimationController animController;
    
    void Start()
    {
        animController = GetComponent<SpriteResolverAnimationController>();
        
        // Subscribe to animation events
        animController.AnimationEvent += OnAnimationEvent;
    }
    
    // Called by game loop at fixed rate (60hz)
    public void Tick()
    {
        // Update animation
        animController.Tick();
        
        // ... other entity logic ...
    }
    
    public void PlayAnimation(string stateName)
    {
        animController.SetState(stateName);
    }
    
    private void OnAnimationEvent(string eventName)
    {
        // Handle animation events
        if (eventName == "swingSound")
        {
            // Play sound effect
        }
        else if (eventName == "hitFrame")
        {
            // Enable hitbox
        }
    }
    
    void OnDestroy()
    {
        // Unsubscribe from events
        if (animController != null)
        {
            animController.AnimationEvent -= OnAnimationEvent;
        }
    }
}

Pattern 2: Standalone Animation (Effects, UI)

For animations that aren’t tied to entities (visual effects, UI elements):

  1. Enable _autoTickInUpdate in the inspector (or via code)
  2. The animation will automatically tick in Update()
public class EffectAnimation : MonoBehaviour
{
    private SpriteResolverAnimationController animController;
    
    void Start()
    {
        animController = GetComponent<SpriteResolverAnimationController>();
        animController.SetState("Explosion");
        
        // Enable auto-tick for standalone animations
        // (or set in inspector)
    }
    
    // AnimationController.Update() handles ticking automatically
    // SpriteResolverAnimationController.LateUpdate() handles sprite updates
}

Pattern 3: Responding to Animation Events

Subscribe to AnimationEvent to handle events defined in your DSL:

void Start()
{
    animController.AnimationEvent += OnAnimationEvent;
}

void OnAnimationEvent(string eventName)
{
    switch (eventName)
    {
        case "swingSound":
            audioSource.PlayOneShot(swingClip);
            break;
            
        case "hitFrame":
            EnableHitbox();
            break;
            
        case "hitOff":
            DisableHitbox();
            break;
            
        case "vfxJump":
            Instantiate(jumpVfx, transform.position, Quaternion.identity);
            break;
            
        case "_loop":
            // Animation looped (if you need to track this)
            break;
    }
}

Pattern 4: Slow Motion / Speed Control

Use DurationMultiplier to control animation speed:

// Slow motion (half speed)
animController.DurationMultiplier = 2.0f;

// Fast forward (double speed)
animController.DurationMultiplier = 0.5f;

// Normal speed
animController.DurationMultiplier = 1.0f;

Pattern 5: State-Based Animation Selection

Use your game logic to select animation states:

void UpdateAnimation()
{
    if (isGrounded)
    {
        if (isMoving)
        {
            animController.SetState("Run");
        }
        else
        {
            animController.SetState("Idle");
        }
    }
    else
    {
        animController.SetState("Jump");
    }
}

Creating Custom Animation Controllers

You can extend AnimationController to create custom visual update systems. Here’s how SpriteResolverAnimationController does it:

using UnityEngine;
using UnityEngine.U2D.Animation;
using PhantomCompass.Animation;

public class SpriteResolverAnimationController : AnimationController
{
    public SpriteResolver SpriteResolver;

    void OnValidate()
    {
        if (SpriteResolver == null)
        {
            SpriteResolver = GetComponentInChildren<SpriteResolver>();
        }
    }

    void Awake()
    {
        if (SpriteResolver == null)
        {
            SpriteResolver = GetComponentInChildren<SpriteResolver>();
        }
    }

    void LateUpdate()
    {
        // Check if we're on a new frame
        if (_isNewFrame && _currentStateData != null)
        {
            // Update sprite visuals
            // _currentStateData.stateName gives the sprite category
            // _currentFrameData.frameIndex gives the sprite frame
            SpriteResolver.SetCategoryAndLabel(
                _currentStateData.stateName, 
                _currentFrameData.frameIndex.ToString()
            );
            
            // Clear the flag after updating
            ClearNewFrameFlag();
        }
    }
}

Key points for custom controllers:

  1. Check _isNewFrame - Only update visuals when this is true
  2. Use _currentStateData.stateName - This handles sprite category aliases automatically
  3. Use _currentFrameData.frameIndex - The sprite frame index to display
  4. Call ClearNewFrameFlag() - After updating visuals, clear the flag
  5. Update in LateUpdate() - Ensures visuals update after all logic updates

Example: Custom SpriteRenderer Controller

public class SpriteRendererAnimationController : AnimationController
{
    public SpriteRenderer spriteRenderer;
    public Sprite[] sprites; // Array of sprites for each frame
    
    void LateUpdate()
    {
        if (_isNewFrame && _currentStateData != null)
        {
            int frameIndex = _currentFrameData.frameIndex;
            if (frameIndex >= 0 && frameIndex < sprites.Length)
            {
                spriteRenderer.sprite = sprites[frameIndex];
            }
            ClearNewFrameFlag();
        }
    }
}

Integration with Entity System

The animation controller is designed to work with entity-based game systems:

  1. Entity calls Tick() - From Entity.Tick() at fixed rate
  2. Entity calls SetState() - When state changes (e.g., from FSM)
  3. Entity subscribes to AnimationEvent - To handle animation events
  4. EntityReference pattern - Centralizes component references

See Entity.cs and EntityReference.cs for a complete example.

Best Practices

  1. Always call Tick() - Don’t forget to tick the animation controller each frame
  2. Use fixed-rate systems - Call Tick() from fixed-rate systems (not variable Update())
  3. Subscribe to events - Use AnimationEvent for sound effects, hitboxes, VFX
  4. Set default state - Always set a DefaultState so animations start correctly
  5. Check state exists - Validate state names before calling SetState() (if needed)
  6. Unsubscribe from events - Always unsubscribe in OnDestroy() or OnDisable()
  7. Use sprite aliases - Use StateName[Alias] syntax to reuse sprite sets across states

Troubleshooting

Animation doesn’t play

  • Check that Tick() is being called
  • Verify the state name matches your DSL (case-sensitive)
  • Ensure DefaultState is set if no state is manually set

Sprites don’t update

  • Verify SpriteResolver component exists
  • Check that _isNewFrame is being checked in LateUpdate()
  • Ensure ClearNewFrameFlag() is called after updating visuals

Events don’t fire

  • Verify you’ve subscribed to AnimationEvent
  • Check event names match your DSL exactly (case-sensitive)
  • Ensure Tick() is being called

Animation plays too fast/slow

  • Adjust DurationMultiplier property
  • Check frame durations in your DSL

State transitions don’t work

  • Verify target state exists in your DSL
  • Check state name spelling (case-sensitive)
  • Ensure state has explicit end behavior in DSL

See Also

  • AnimationController.cs - Base class implementation
  • SpriteResolverAnimationController.cs - Example implementation
  • Entity.cs - Example integration with game entities