Skip to content

Animator Component

Namespaces: Brine2D.Animation · Brine2D.Systems.Animation

AnimatorComponent is the ECS entry point for the animation system. It owns the primary SpriteAnimator, an AnimationStateMachine, an AnimationParameters store, optional blend trees, and any additional AnimationLayers.

ECS Setup

1. Register AnimationSystem

Register the system in your scene builder once. It ticks every AnimatorComponent every frame.

builder.AddSystem<AnimationSystem>();

2. Add the Component to an Entity

AnimatorComponent is typically paired with SpriteComponent. AnimationSystem writes the current frame data to SpriteComponent each tick.

entity
    .AddComponent<SpriteComponent>(s =>
    {
        s.Texture = myTexture;
    })
    .AddComponent<AnimatorComponent>();

3. Build Clips and Play

var anim = entity.GetComponent<AnimatorComponent>()!;

var idle = new AnimationClip("idle") { PlaybackMode = PlaybackMode.Loop };
idle.AddFrame(new SpriteFrame(new Rectangle(0,  0, 32, 32)));
idle.AddFrame(new SpriteFrame(new Rectangle(32, 0, 32, 32)));

anim.Animator.AddAnimation(idle);
anim.Animator.Play("idle");

AnimationSystem calls Update and writes CurrentFrame to SpriteComponent automatically — you do not call animator.Update() yourself.


SpriteAnimator

SpriteAnimator is the engine that actually advances time and selects frames. You interact with it through AnimatorComponent.Animator.

Playing Clips

Method Behaviour
Play(name) Starts the named clip immediately; no-op if already on the same clip (use restart: true to force)
Play(name, restart: true) Always restarts from frame 0, even if already playing
PlayWithCrossFade(name, fadeDuration) Starts a cross-fade to the named clip over fadeDuration seconds
PlayQueued(name) Queues the clip to start after the current non-looping clip finishes; starts immediately if nothing is playing
PlayDirect(clip) Plays a clip instance not registered on the animator
PlayDirectQueued(clip, fadeDuration?) Queues an unregistered clip
Stop(fireCallbacks?) Stops playback and clears the current animation
Pause() Freezes playback at the current frame
Resume() Resumes a paused animation
// Hard cut
animator.Play("run");

// Cross-fade over 0.15 s
animator.PlayWithCrossFade("run", 0.15f);

// Queue "land" after "jump" finishes
animator.Play("jump");
animator.PlayQueued("land");

Queue depth

The queue is capped at MaxQueueDepth (default 32). Entries beyond the limit are dropped with a warning. Set animator.MaxQueueDepth to change the limit.

State Properties

Property Type Notes
CurrentAnimation AnimationClip? The active clip, or null
CurrentFrameIndex int Zero-based index, or -1 when nothing is playing
CurrentFrame SpriteFrame? The current frame, or null
CurrentTime float Elapsed seconds within the current clip
NormalizedTime float [0, 1] position within the current clip
TimeRemaining float Remaining seconds for non-looping clips; 0 for looping
IsPlaying bool true when playing and not paused
IsFinished bool true when a non-looping clip has completed
Speed float Playback speed multiplier (default 1.0)
Reversed bool When true, the clip plays in reverse
CrossFadeAlpha float [0, 1] cross-fade progress; 1.0 when no fade is active

Speed and Direction

animator.Speed    = 2.0f;  // 2× faster
animator.Speed    = 0.5f;  // half speed / slow-motion
animator.Reversed = true;  // play backwards

Animation Registration

animator.AddAnimation(clip);
animator.RemoveAnimation("idle");
bool exists = animator.HasAnimation("walk");

Events

Event Signature When it fires
OnAnimationStart Action<AnimationClip> A clip starts playing
OnAnimationComplete Action<AnimationClip> A non-looping clip finishes, or a RepeatCount loop limit is hit
OnLoopComplete Action<AnimationClip> Each completed pass on a looping clip
OnFrameChanged Action<SpriteFrame> The active frame index changes
OnStopped Action<AnimationClip> Playback stops entirely (after OnAnimationComplete or Stop())
animator.OnAnimationComplete += clip =>
{
    if (clip.Name == "attack")
        animator.Play("idle");
};

animator.OnFrameChanged += frame =>
{
    // react to individual frame changes (e.g. dust particles on footstep frames)
};

Cross-Fades

A cross-fade blends the outgoing clip's last frame against the incoming clip's first frames while it ramps up. AnimationSystem renders ghost frames via SpriteComponent.CrossFadeGhosts so the SpriteRenderingSystem issues one draw call per concurrent fade.

// Transition initiated manually
animator.PlayWithCrossFade("run", 0.12f);

// Initiated by the state machine
sm.AddTransition("walk", "run", () => speed > 4f, crossFadeDuration: 0.1f);

Multiple simultaneous fades (e.g. base layer + an AnimationLayer) are fully supported.


AnimatorComponent Properties

Property Type Purpose
Animator SpriteAnimator Primary animator
StateMachine AnimationStateMachine Transition evaluator for the primary animator
Parameters AnimationParameters Named parameter store for transition conditions
BlendSelector1D AnimationBlendSelector1D? Optional 1D blend tree; takes priority over BlendSelector2D
BlendSelector2D AnimationBlendSelector2D? Optional 2D blend tree
Layers IReadOnlyList<AnimationLayer> Additional animation layers (sorted by priority)
CurrentHitBox Rectangle? Primary animator's current frame HitBox, or null
Rectangle? hitbox = anim.CurrentHitBox;
Rectangle? head   = anim.GetCurrentHitBox("head");

Without ECS

SpriteAnimator can be used standalone — for example in a scene that doesn't use the full ECS, or in unit tests.

var animator = new SpriteAnimator();
animator.AddAnimation(walk);
animator.Play("walk");

// In your update loop:
animator.Update(deltaTime);

// In your render loop:
if (animator.CurrentFrame is { } frame)
    renderer.DrawTexture(texture, frame.SourceRect, destRect);

Manual update required

When using SpriteAnimator outside the ECS, you are responsible for calling animator.Update(deltaTime) every frame. Inside the ECS, AnimationSystem handles this.