跪拜 Guibai
← Back to the summary

A Fighting Game Prototype Got Its Hitboxes Right, Then Stick Figures Killed It

I Spent a Day Making a Fighting Game Prototype, All Hit Detection Was Correct, Then the "Art" Talked Me Out of It

Source code repository (v1.0.0)


A 1,321-line single-file HTML, zero dependencies, no build, double-click to play. It has Guan Yu and Zhao Yun, can walk, attack, and unleash special moves, with hitbox visualization, frame data panel, and frame-by-frame stepping.

Then a user opened it and said: "Stick figures are really ugly, I can't keep playing."

The project ended after that sentence.

This post doesn't talk about "how hard I tried"; it covers three things: where fighting game code gets hard, where the cost really goes, and when you should stop on the third day.

1. Conclusion first

The cost structure of fighting games is "code is reusable, art is strictly linear".

The code half is one-time: hit detection system, state machine, frame data table, hit resolution. After it's done, adding a character just means filling in data on the table. The art half is strictly linear: a character's frame-by-frame animation needs 300–600 images; adding a character adds three hundred images, with zero leverage.

And there's an even harder constraint in this project: AI cannot produce frame-consistent frame-by-frame animation. Generative models have no cross-frame memory: frame 1 has three folds on the cuff, frame 8 has five; video models can produce smooth motion, but the output is compressed pixel video without an alpha channel, so it can't be imported into a game.

Thus the route of "using AI to save money on fighting game art" is blocked under current technology.

2. Hit detection has nothing to do with art

This was the first thing I wanted to verify before starting. Conclusion: whether the sprite on top is Guan Yu's portrait or a red rectangle, the hit detection code doesn't change a single line.

Fighting game hit detection uses a "three-box system":

Box Purpose Who is detected
hitbox Where this frame can hit Only exists on active frames of an attack
hurtbox Where this frame can be hit from Always exists, changes with pose
pushbox Body occupancy, prevents overlap Always exists, usually narrower than hurtbox

Each logic frame runs through a fixed sequence: read input → advance frame → get the frame's box lists → rectangle intersection on each pair → hit resolution.

Why rectangles instead of pixels: for predictability and fairness. Pixel-level detection would make "one pixel difference" a common cause of missed hits, and players would see it as a bug. Fighting games have used rectangles for decades; this is a deliberate choice.

3. How actions work: frame data table

This is the core structure of the whole project. It's not "cutting images on a timer" but writing image indices into a table, read by the state machine per frame.

Guan Yu's light attack looks like this in code:

light: {
  frames: [
    { dur: 5,  pose: 'lWind' },
    { dur: 3,  pose: 'lHit',  fx: 'slash',
      hit: { ox: 30, oy: -152, w: 78, h: 62,
             dmg: 8, hitstun: 15, kb: 3.0, stop: 6 } },
    { dur: 10, pose: 'lRec' }
  ]
}

Three fields are all it takes:

The special move works the same way, just with more extreme numbers:

special: {
  label: 'Qinglong Yanyue Slash',
  frames: [
    { dur: 14, pose: 'sWind'  },
    { dur: 4,  pose: 'sWind2', fx: 'charge' },
    { dur: 5,  pose: 'sHit',   fx: 'slashBig',
      hit: { ox: 24, oy: -172, w: 132, h: 124,
             dmg: 24, hitstun: 34, kb: 9.5, stop: 14, heavy: true } },
    { dur: 22, pose: 'sRec' }
  ]
}

Startup is 18 frames (14+4), active is 5 frames, recovery is 22 frames. The hitbox is 132×124, more than double the light attack's 78×62. stop: 14 is a 14-frame hitstop—that is the first source of impact feel, more important than the visuals.

4. Five structural decisions that decide success or failure

1. A single logic clock. Fixed 60 logic frames per second, accumulated via an accumulator, rendering can interpolate. Hit detection results are identical on 60Hz and 144Hz monitors. Without this, online play never syncs, and different machines feel different.

const STEP = 1 / 60;
acc += dt;
while (acc >= STEP) { step(); acc -= STEP; }

2. Frame data table driven; logic and presentation separated. Adding a move adds only data, not code. This is why hit detection doesn't need to change when swapping art.

3. Pose library with per-character overrides. Common poses plus a per-character override set; adding a wAng field directly controls weapon orientation:

// Guan Yu: blade swings in an arc, from behind the head to the front
lWind: { lean:-12, hipY: 94, armF: {x: -12, y: -78}, wAng: -128 },
lHit:  { lean: 24, hipY: 88, armF: {x:  50, y:  22}, wAng:   46 },

// Zhao Yun: spear thrusts straight, first pulled to the side then leveled

The same renderer draws two different weapon personalities.

4. Hit dedup by frame number, not boolean. I wrote this wrong in the first version:

// ✗ Wrong: one move can only hit once, multi-hit moves get collapsed into one hit
if (att.hasHit) return;

// ✓ Correct: dedup by frame number; different frames can each hit once
if (att.hitDone[att.frameIdx]) return;

The three-hit thrust sequence (6+6+11 damage) relies on this. After changing to frame-index dedup, a real 9-second battle's damage breakdown is 23 + 23 + 5×5—two full triple thrusts plus five light pokes, each segment applying separately.

5. Effects layer doesn't participate in hit detection and isn't affected by hitstop. Otherwise, during the freeze frames of a landed hit, the slash effect and sparks freeze too, which weakens visual feedback.

5. Three real pitfalls

Pit 1: Active windows cross sprite frame boundaries

The frame data table indexes by "image"; one image usually lasts 4 logic frames (67ms). But a fighting game light attack's active phase is only 2–4 logic frames.

If an artist evenly splits a 12-frame jab into three images [4,4,4], the hit frame covers logic frames 5–8, while the real active is frames 4–5, and the frame data table can't express that. The player feels "sometimes it hits, sometimes not, and the visible frames don't match the feel."

Fix: the duration of the image that contains the hit frame must equal that move's activeFrames. A jab should be [3,2,7] instead of [4,4,4]. Only loop frames get long durations.

This costs zero on the art side, but if it's not written into the contract, the day you change artists will blow up collectively.

Pit 2: Background tabs throttle requestAnimationFrame

While tuning hit detection, I saw through screenshots that the AI seemed frozen, only moving 20 frames in 12 seconds. My first thought was "the AI logic is broken."

Actually, the browser throttles rAF in background tabs to 1 frame per second, and key edge events get swallowed too. The game logic itself was fine.

The correct approach isn't to increase sleep time; it's to bring the tab to the foreground, then use "pause + frame step" for deterministic verification. Relying on screenshot bursts to capture active frames is unreliable.

Pit 3: The stick figure's weapon stuck into the ground while idle

If weapon orientation is purely derived from the arm vector, and the idle hand is low, the Qinglong crescent blade stabs into the ground. Weapon angle needs its own field (wAng), not derived from the arm.

This kind of "looks fake at a glance" problem will never be discovered by reading code; you have to actually render it and look.

6. The scope account

After the prototype was done, I had three people (designer, engineer, artist) estimate separately. The numbers didn't match, off by four to five times. After aligning assumptions and re-estimating:

Tier Characters Moves/character Code person-days Art person-days Total
Vertical slice (graybox) 1 1 18 0 18
Minimum playable (pure graybox) 2 5 93 0 93
Minimum playable + full art 2 5 93 ≈60 ≈153
Standard 4 9 183 120 ≈300
Complete 8 10 280 376 ≈656

Converting to calendar time makes it meaningful:

Cumulative person-days Hobby 1 day/week Part-time 3 days/week Full-time 5 days/week
18 18 weeks 6 weeks 4 weeks
93 93 weeks 31 weeks 19 weeks
153 153 weeks (3 years) 51 weeks 31 weeks
300 300 weeks (6 years) 100 weeks (2 years) 60 weeks

Key fact: at 4 characters, art is 1.9 times code; at 8 characters, it's 3.1 times. Code grows sub-linearly and AI can speed it up; art is strictly linear and AI speeds up nothing. The decision to "add one more character" lands almost entirely on art assets.

7. How big was KOF'97's team?

About 50–70 people, 10–12 months, which works out to over 13,000 person-days. That's 85 times the 2-character prototype above.

But team size and timeline are only the surface. The real gap lies in three places where we're not on equal footing:

  1. SNK didn't redraw all 28 characters every year. Most sprites in KOF'97 were ported from '96, '95, even Fatal Fury; some didn't even have colors changed. They started accumulating fighting game sprites in 1991; by 1997 they were building incrementally on six years of inventory. You're building the first floor on empty ground.
  2. They built their own hardware. Neo Geo is SNK's self-developed platform; toolchains, cartridge writers, debuggers were all internal. Artists drew to hardware spec from day one, so there was no "drew it all and then found it doesn't fit."
  3. Those 50 people were professional pixel animators. One person drawing 300–600 frames of coherent animation is a trade, thousands of hours of training. They even used the crude method of filming real people as reference and tracing per frame.

8. How modern small teams survive

No successful small team survives by "drawing 300 frames per character." They all survive by changing art style.

Game Team Art strategy Result
Skullgirls ~a dozen High-res hand-drawn per frame, over a thousand frames per character Crowdfunded $830k to make it; per-character budget later about $250k
Divekick Two people Minimal geometric shapes + a line Sold hundreds of thousands of copies
Nidhogg Two people Pure color silhouettes Critical and commercial success
Single-character fighting games (Footsies, etc.) One person Blocks, color patches, very few animations Used by the fighting game community as training tools

This is the wall people with art constraints run into: the art cost of a fighting game is almost unaffected by "how simple you draw." Because it doesn't require "pretty"; it requires that every frame of every action can be independently referenced and aligns with the hitbox. This requirement closes half of the road to "reducing drawing volume."

9. When to stop

Looking back, the most expensive lesson of this project isn't technical; it's ordering.

Stick figures as "placeholders to verify feel" are entirely reasonable from an engineering standpoint, but they are placeholders for the developer, not a product for the player. I conflated these two things.

The correct approach: before starting, spend half a day producing one complete character with the finished look—even by using free assets, or a style like ink silhouette that can be drawn in code—and let the stakeholder decide on "ugly or not" first, then discuss architecture.

Because feature validation passing ≠ project viability.

As for those 1,321 lines of code, they're not wasted. Hit detection system, state machine, frame data table, multi-hit detection, hitstop, debug layer—this skeleton only requires swapping the renderer to change art. It's something that can be reused indefinitely, just not right now.


If this post saves you a month of trial and error, it was worth it.

Thinking clearly about whether to do it is more important than grinding for three months.