Where Should an Animation Update Itself?
Category: SDL Adventure Game
Animations in VaniaVolpe are humble: a sprite sheet plus a little .anim file
listing each frame’s rectangle, played at 12 FPS. For a long time the code that
advanced them lived in the wrong place — inside the render function — and pulling
it out turned into a small lesson about where responsibility belongs. I got it
wrong once before I got it right, and there was a third design I deliberately
walked past. This is that story.
Timing in the wrong place
render_animation() did two jobs. It drew the current frame, and — from the time
elapsed since the animation started — it also computed which frame that was and,
for a one-shot animation, noticed when the animation had finished and fired its
end callback.
Both of those are the wrong job for a draw call. Because the frame was derived from “now” each time you rendered, playback speed was quietly tied to how often you drew: render twice in a frame and the fox talks twice as fast. Worse, the end callback fired from inside rendering — and in this engine an end callback can do something as drastic as switch to another scene. Advancing the gate animation to completion, mid-draw, would tear down the scene you were in the middle of painting. It worked, but only because nothing had leaned on the seam yet.
First take: split update from render
The obvious fix is to separate the two responsibilities. I added
animation_update(anim, now) — it advances the current frame and, for a one-shot,
stops the animation and fires the callback. render_animation() became a pure blit:
draw whatever current_frame says, nothing more. Timing now happens in the update
phase of the loop, so speed no longer depends on the renderer and callbacks fire at
a sane moment.
Clean — except it left a smell. Something now had to call animation_update every
frame, and that something turned out to be each scene. The playground-entrance scene
suddenly had this at the top of its update():
1
2
3
animation_update(excavator, now);
animation_update(gate, now);
animation_update(shovel, now);
That bothered me. A scene that merely draws an animation shouldn’t have to remember to drive it every frame. Forget one line and the animation silently freezes — and its end callback never fires, so the gate never opens and the game soft-locks, with nothing to point at. The abstraction was leaking its bookkeeping onto every caller.
The shortcut I didn’t take
The tempting fix is to make animations update themselves globally. Have
make_animation_data() quietly register every animation it creates in an
engine-owned list, and tick that whole list once per frame. Scenes would declare
nothing and call nothing; create an animation and it just animates.
I didn’t do this, for two reasons. The small one is that it’s global mutable state, which I try to avoid. The bigger one is subtler: every scene in an adventure is loaded up front, so every animation exists at all times, including ones belonging to scenes that aren’t on screen. A global tick would drive all of them — and a one-shot in an off-screen scene could reach its end and fire its callback while a completely different scene is showing. That’s a real cross-scene footgun hiding behind a convenient API. Convenient and wrong.
Second take: let the framework own it
The right answer was already sitting in the codebase; I’d just failed to notice the
pattern I’d been following everywhere else. A scene doesn’t manage its own
hotspots, images, and sound effects
imperatively — it declares them as data on the Scene struct, and the framework
loads and frees them for it. Animations were the one resource that had wandered off
and done everything by hand.
So I made animations a declared scene resource like the rest. The Scene struct
grew an animations array; the framework ticks the active scene’s animations each
frame (game_update calls update_scene_animations before handing control to the
scene) and frees them on teardown (free_scene_animations, right next to the
existing free_scene_images). The scenes went back to declaring a list and nothing
more — the three animation_update lines vanished, and so did the matching
free_animation calls in their teardown.
Actors didn’t need any of this. An actor is already a self-contained object that
ticks its own animations inside actor_update, and a scene drives it with a single
actor_update call, not one call per animation. It was only the loose scene props —
buttons, the gate, the shovel — that had been leaking, and now they don’t.
The lesson
The fix wasn’t clever; it was consistent. The first version solved the real bug and then invented a new chore for every caller. The global registry would have hidden the chore but smuggled in a worse bug. The version that shipped just made animations behave like every other thing a scene owns: declare it, and let the engine take care of the rest — scoped to the scene that’s actually on screen, with nothing to remember. When a change feels like it needs a new rule for everyone to follow, that’s usually a sign the abstraction is pointed the wrong way.
You can play the current build here: potomak.github.io/VaniaVolpe.