I have been building a roguelite deckbuilder in the workspace for a while now — apps/card-battler, a Slay-the-Spire-shaped thing that exists mostly so I have somewhere to be wrong about game design in public. It reached the point where the placeholder duelist standing in the middle of every fight needed to actually move.

The obvious route, in 2026, is to ask an image model for the frames. I did that. I did it many times, with increasingly elaborate prompts, and I got quite good at it. The sheets looked great as sheets. Then you play them back and the character seethes — a buckle appears and vanishes, the cape gains a fold and loses it, the whole figure breathes half a centimetre taller between frame three and frame four.

That flicker is not a prompting problem. It is structural, and the fix turned out to be a different shape of work entirely: draw the character once, cut it into nine pieces, and hang the pieces on a skeleton. Everything below is playable. Press things.

01

What kept breaking, and why prompting could not fix it

I got far enough with generated frames to write down the failure modes properly, because each one cost a day. They are worth listing, because every one of them is the same bug wearing a different hat.

img2img does not preserve a character. Feeding an existing sprite back with a strength of 0.3–0.7 to get a new pose reliably returns a different person in similar clothes. In one test a hooded male duelist came back as a long-haired woman in a coat. Reference images hold identity where img2img does not, which is why every pose has to be generated in one image rather than one at a time — six separate runs, however well prompted, produce six cousins.

The second row of a grid is treated as a fresh start. On a 3×2 or 4×2 layout the model resets to a neutral standing pose at the first cell of row two. A death sequence popped back upright at frame 4 in three consecutive attempts. You can beat it — state that row two is a continuation, then hand over a numeric progression ("height of the head as a fraction of standing height: 1 = 100%, 2 = 90%, 3 = 75%…") — but note what that means: I was hand-authoring the motion curve in prose and hoping it got interpolated.

The sword changes hands. This is the defect that hides best. An attack sheet held the blade in the right hand for five frames and the left in the sixth. Nobody spots it in a static grid. It screams the moment the frames move.

Each frame was drawn independently, so nothing connected one cell to the next. A buckle could disappear because, as far as the model was concerned, it was a different buckle.

Mark Wilkins

There is a version of this article where I keep going: better prompts, a video model interpolating between hand-checked keyframes, a hundred re-rolls. I tried that too — wan-2-2-i2v-a14b genuinely preserves pixel art without smearing, which surprised me — but given a first and last frame of a whole action it has to invent the middle, and it invents badly. Asked to get from sword-in-right-hand to sword-in-left-hand, it politely animated the character swapping grip mid-swing.

The common thread: consistency was being asked for, not enforced. Nothing in the pipeline made frame 4 contain the same pixels as frame 3. It was a hope, restated every generation.

02

One drawing, nine parts, hung on a skeleton

So the duelist got drawn once, and then cut up: head, torso, cape, two arms, a forearm, sword, and two legs. Nine parts on a bone hierarchy. Every frame composites the same pixels at different angles and offsets.

This changes the category of the problem. A buckle cannot disappear between frames because it is the same buckle — not a similar one, the same one, at a different rotation. Handedness cannot flip because the sword is parented to a grip bone. Proportions cannot drift because there is exactly one torso bitmap, and it is the same bitmap in every frame of every clip.

Here are four of them running. The left viewer is a 168-pixel inspection view; the framed one on the right is the sprite at its real in-game size, which is the only size that matters for judging whether it reads.

Body
Rate
Body variant Intact, 60 frames per second, playing.

Drag any scrubber to step through frames one at a time — the rest pause while you do. The controls above apply to every clip in this panel.

Idle

204 frames · 3.40 s · loops
168 px
In the real frame

Standing and breathing over 3.4 s. Deliberately no lateral weight shift and no leg motion — either one makes it read as a very slow walk.

Walk cycle

54 frames · 0.90 s · loops
168 px
In the real frame

Two-step cycle, knees flexing on the trailing leg, sword carried forward at the ready rather than hanging.

Chop

53 frames · 0.88 s · one-shot
168 px
In the real frame

Overhead chop — the same arm motion as the stab with the blade mirrored about the forearm, so it points UP out of the fist. Wrist held at 239–299°.

Guard

31 frames · 0.52 s · one-shot
168 px
In the real frame

Hips drop, stance widens, both arms bring the blade level across the chest.

The idle is the one I would point at. It is 204 frames — 3.4 seconds — of standing still and breathing, with deliberately no lateral weight shift and no leg motion, because adding either makes it read as a very slow walk rather than a person standing. That is an animation decision I got to make by moving a curve, not by re-rolling a generation and hoping.

03

Sixty frames a second stops being a budget

Once motion lives in keyframe curves, frames are samples. You ask the rig to resolve the skeleton at time t and it does; asking it 60 times a second costs no more authoring than asking it 12 times. 120 would be the same work.

That is why the rate control above does something slightly unusual. It does not slow the clip down — it holds the real duration and shows fewer distinct frames inside it. Flip the chop between 24 and 60 and you are seeing an honest comparison of frame rates on identical motion, which is a thing you simply cannot do with a hand-drawn sheet of twelve cells.

The catch arrives at ship time, and I want to be straight about it: 60 fps sheets are impractical as shipped assets. The idle alone is 204 frames; the thirteen clips on this page total 993 frames per body variant, and this article is carrying three variants, which is 39 sheet images and about 6.6 MB of WebP. That is fine for a demo that lazy-loads as you scroll. It is not how the game should ship. The rig wants to travel as data and be posed at runtime — which is a much bigger change to the game's BattleSprite component than swapping one img tag, and is exactly why none of this is wired into the game yet.

04

Damage is a bone you switch off

This is the part that justifies the whole detour.

The card battler tracks body state — limbs get disabled, capabilities get lost, and the fighter is supposed to look like that happened. Under the generated-frames approach, "the same walk cycle but with no right hand" is a full re-authoring job: new prompts, new re-rolls, new hand-checking of every frame, and a fresh set of drift bugs.

With a rig it is a bone you disable. Turn off elbowNear and everything parented below it — forearm, grip, sword — stops resolving. The keyframes are untouched. Nothing is regenerated. The clip just runs on a body that has less of it.

Body
Rate
Body variant No right hand, 60 frames per second, playing.

Same keyframes, different body. Switching the body variant does not reset the frame index, so you can pause mid-swing and watch only the body change underneath the pose.

Chop

53 frames · 0.88 s · one-shot
168 px
In the real frame

Overhead chop — the same arm motion as the stab with the blade mirrored about the forearm, so it points UP out of the fist. Wrist held at 239–299°.

Defeat

97 frames · 1.62 s · one-shot
168 px
In the real frame

Sinks under his own weight. The hip drop is bounded by what the folding legs give up, so the feet stay on the floor.

Pause one of those mid-swing and click between the three bodies. The pose is identical; the arm is simply not there. Producing that took no generation, no prompting and no artist — and, more to the point, it is automatically as correct as the intact version, because it is the same motion data.

05

Two verification passes, and one bug in the checker

I did not trust any of this on sight, because "looks smooth to me at 2am" is how the flicker survived three rounds of generated sheets. So every frame gets checked, and so does every relationship between neighbouring frames. Two independent passes, one on pixels and one on the skeleton.

The pixel pass, per frame: not empty, nothing clipped at the canvas edge, and opaque coverage within 14% of the clip's median. That last one is the limb detector — a piece quietly failing to composite shows up as a coverage cliff long before you notice it by eye. Between frames it checks for bounded change (no pop), non-zero change (no stall), and for looping clips it checks the wrap-around pair exactly like an interior pair, so a seam cannot hide at the join.

The joint pass resolves the skeleton at every frame and takes second differences of each bone's world rotation and position. This catches what pixels cannot. The pixel centroid conflates "the body lurched" with "the sword swept fast" — a big prop moving quickly drags the centroid even when the motion is perfectly smooth. The joint pass also flags outliers rather than magnitude, because a fast swing legitimately produces large second differences; a pop is a frame whose value dwarfs its own clip's median.

ClipFramesMean ΔMax ΔLoop seamPixel passJoint pass
Idle2041.06%2.21%0.66%passpass
Walk cycle543.39%4.34%3.40%2 flaggedpass
Chop535.18%7.83%passpass
Stab535.44%8.18%passpass
Guard313.04%4.14%passpass
Draw482.89%4.27%2 flaggedpass
Sheathe552.53%3.66%5 flaggedpass
Idle — sheathed2040.82%1.93%0.36%passpass
Walk — sheathed543.27%4.23%3.38%2 flaggedpass
Hurt — light324.84%7.08%2 flaggedpass
Hurt — heavy445.24%8.47%1 flagged7 flagged
Hurt — critical644.72%8.99%2 flagged8 flagged
Defeat973.04%3.86%passpass

Sixteen pixel flags and fifteen joint flags across 993 frames, and I have left them in the table rather than tuning the thresholds until they went away. They cluster where you would expect: the two heavy hit reactions, which are supposed to be violent, are the clips whose joint pass complains, and that is a threshold conversation rather than a defect. The five flags on the sheathe are not — that clip has a real problem where the blade meets the scabbard, and it is on the list.

Both passes earned their keep during the build:

Clipped windup

The overhead chop's windup ran off the top of the canvas. Fixed, then it ran off the right instead. The edge check caught both; neither was visible at in-game size.

Acceleration break

The windup snapped into the strike — smooth positions, discontinuous acceleration. Invisible to the pixel pass by construction, obvious to second differences.

A bug in the checker

On one-shot clips the checker wrapped frame −1 round to the last frame and invented a discontinuity at frame 0. It was reporting a seam on clips that do not loop.

That third one is my favourite, and the reason I am wary of verification code generally: a checker that reports a defect which does not exist will happily send you to fix animation that was already correct. It went undetected because a red result feels like the tool working.

06

The rest of the set

Thirteen clips, 993 frames, about 16.6 seconds of motion per body. Four of them loop; the rest are one-shots and restart with a visible cut, which is honest — a one-shot's wrap is not a seam it is meant to survive.

Body
Rate
Body variant Intact, 60 frames per second, playing.

Every clip, lazy-loaded as it scrolls into view. The draw and sheathe pair is the one to look at: it is a single clip in two states, swapping the full scabbard for the empty one at the instant the blade clears it.

Idle

204 frames · 3.40 s · loops
168 px
In the real frame

Standing and breathing over 3.4 s. Deliberately no lateral weight shift and no leg motion — either one makes it read as a very slow walk.

Walk cycle

54 frames · 0.90 s · loops
168 px
In the real frame

Two-step cycle, knees flexing on the trailing leg, sword carried forward at the ready rather than hanging.

Chop

53 frames · 0.88 s · one-shot
168 px
In the real frame

Overhead chop — the same arm motion as the stab with the blade mirrored about the forearm, so it points UP out of the fist. Wrist held at 239–299°.

Stab

53 frames · 0.88 s · one-shot
168 px
In the real frame

Downward stab — the arm lifts and drives back down with the blade held pointing DOWN relative to the forearm. Wrist held at 61–121°.

Guard

31 frames · 0.52 s · one-shot
168 px
In the real frame

Hips drop, stance widens, both arms bring the blade level across the chest.

Draw

48 frames · 0.80 s · one-shot
168 px
In the real frame

Cross-body draw from the far hip. One clip, two states — the sheathed scabbard swaps for the empty one at the instant the blade clears it.

Sheathe

55 frames · 0.92 s · one-shot
168 px
In the real frame

The reverse: blade brought across, fed into the scabbard, hand falls away.

Idle — sheathed

204 frames · 3.40 s · loops
168 px
In the real frame

The same breathing idle with the blade stowed and the hand empty.

Walk — sheathed

54 frames · 0.90 s · loops
168 px
In the real frame

The same walk cycle with the blade stowed — arms swing freely.

Hurt — light

32 frames · 0.53 s · one-shot
168 px
In the real frame

A light hit: rocked back onto the rear foot and straight back to guard.

Hurt — heavy

44 frames · 0.73 s · one-shot
168 px
In the real frame

A heavy hit: driven back a full step, knees buckle, and the recovery takes noticeably longer.

Hurt — critical

64 frames · 1.07 s · one-shot
168 px
In the real frame

A crushing hit — the only reaction that puts him on the ground. He drops to a knee and has to push back up.

Defeat

97 frames · 1.62 s · one-shot
168 px
In the real frame

Sinks under his own weight. The hip drop is bounded by what the folding legs give up, so the feet stay on the floor.

What is left is not small. The rest pose needs another tuning pass — the sword-hand forearm reads faintly against the tabard at real size. Nothing is wired into the game. And the runtime-posing change described above is the actual project, of which all of this is the feasibility study.

But the thing I set out to answer is answered. The flicker was never going to be prompted away, because the pipeline that produced it had no mechanism for holding a character still.

Key Takeaways

  • Generated animation frames drift because each frame is an independent drawing. Consistency has to be structural — one drawing, cut into parts, composited — not requested in a prompt.

  • Rigging converts the expensive operations. Frame rate becomes a sampling choice, and a missing limb becomes a disabled bone rather than a full re-authoring job.

  • Verify pixels and the skeleton. The pixel centroid cannot tell a lurching body from a fast-moving prop, and a smooth position curve can still have discontinuous acceleration.

  • Check the checker. Mine invented a loop seam on non-looping clips, and a false positive is more expensive than no test, because it sends you to repair working code.

  • Judge sprites at their real in-game size, on the real background. Fine, low-contrast art that reads beautifully at 1K turns to mud at 112 pixels.

If you want the neighbouring story about building things you can actually verify rather than things that merely look finished, Making Deploys Invisible is the same instinct pointed at infrastructure.