Animation System (frames & timing)

Advancing a sprite through an ordered list of timed frames, accumulating elapsed milliseconds so playback stays correct regardless of framerate.

Why it matters

Claw has dozens of states — idle, run, jump, swipe, climb, hurt — and each is an animation whose frame timing is baked into the .ANI data, not the source code. Tie advancement to frames-per-update instead of elapsed time and the game animates twice as fast on a 120 Hz monitor. The accumulator pattern decouples animation speed from the frame rate and lets one slow frame “catch up” without skipping art.

How it works

OpenClaw models an animation as vector<AnimationFrame>, each frame a small struct that names a sprite and how long to hold it:

FieldMeaning
imageIdwhich sprite to show
durationhow long to hold it (ms)
eventNameoptional sound/event tag
hasEventwhether to fire it

Animation::Update(msDiff) runs each tick and drives a millisecond accumulator.

The core of Update is a carry accumulator (paraphrased from the repo):

_currentTime += msDiff;
if (_currentTime >= frame.duration) {
    _currentTime -= frame.duration;   // carry remainder, don't zero it
    OnAnimationFrameFinished(frame);  // notify FSM / fire events
    SetNextFrame();
}
  • Carry, don’t reset. Subtracting duration (rather than setting _currentTime = 0) preserves the leftover, so timing never drifts even with jittery msDiff.
  • Frame events. When hasEvent is set, entering frame 0 fires eventName — this is how a footstep or sword shing lands on the exact frame; routed to the mixer.
  • End-of-loop hook. IsAtLastAnimFrame() lets the state machine decide: loop a run cycle, or transition out of a one-shot like “hurt”.
  • Pause & delay. A _delay countdown holds the first frame (spawn stagger); _paused freezes the whole clip.

Example

A 3-frame swipe [40 ms, 40 ms, 80 ms] updated at a steady 16 ms/tick:

tick+ms_currentTimeshown frame
116160
31648 → 81
51640 → 02
81680 → 0back to 0

If one tick is a laggy 50 ms, frame 0’s 40 ms is consumed and the extra 10 ms carries into frame 1 — no visual stall.

Pitfalls

  • Counting frames, not time. frame++ every update binds speed to FPS; always accumulate msDiff.
  • Zeroing the accumulator. Resetting to 0 on advance discards remainder and slowly desyncs long animations.
  • Firing events every frame the clip sits on frame 0. Guard with a “just entered” check or a swipe replays its sound each loop.
  • Updating animation in the render pass. Advance in the fixed-timestep update, not in draw, or variable render rate corrupts timing.

See also