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 timingSpriteResolverAnimationController- Ready-to-use implementation for Unity’s SpriteResolverDSLAsset- 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
- Right-click in your Project window
- Select Create > PhantomCompass > DSLAsset
- Name it (e.g., “Main Animation Data”)
- 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
SpriteResolvercomponent - Assign your DSL Asset to the
_dslAssetfield - 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
_autoTickInUpdatefor 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
nullor 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
_isNewFrameflag 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
truewhen a new frame is reached - Subclasses should check this in
LateUpdate()and callClearNewFrameFlag()after updating visuals
ClearNewFrameFlag()
- Clears the
_isNewFrameflag - 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):
- Enable
_autoTickInUpdatein the inspector (or via code) - 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:
- Check
_isNewFrame- Only update visuals when this is true - Use
_currentStateData.stateName- This handles sprite category aliases automatically - Use
_currentFrameData.frameIndex- The sprite frame index to display - Call
ClearNewFrameFlag()- After updating visuals, clear the flag - 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:
- Entity calls
Tick()- FromEntity.Tick()at fixed rate - Entity calls
SetState()- When state changes (e.g., from FSM) - Entity subscribes to
AnimationEvent- To handle animation events - EntityReference pattern - Centralizes component references
See Entity.cs and EntityReference.cs for a complete example.
Best Practices
- Always call
Tick()- Don’t forget to tick the animation controller each frame - Use fixed-rate systems - Call
Tick()from fixed-rate systems (not variableUpdate()) - Subscribe to events - Use
AnimationEventfor sound effects, hitboxes, VFX - Set default state - Always set a
DefaultStateso animations start correctly - Check state exists - Validate state names before calling
SetState()(if needed) - Unsubscribe from events - Always unsubscribe in
OnDestroy()orOnDisable() - 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
DefaultStateis set if no state is manually set
Sprites don’t update
- Verify
SpriteResolvercomponent exists - Check that
_isNewFrameis being checked inLateUpdate() - 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
DurationMultiplierproperty - 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 implementationSpriteResolverAnimationController.cs- Example implementationEntity.cs- Example integration with game entities