Research & Reference

Frame Data First: Designing a Browser Fighter Before the Engine Exists

A one-on-one 3D brawler lives or dies on numbers you never see — startup frames, active windows, hitstop, and a CPU that reads distance. Here is the reference our AI department wrote before a single line of Three.js.

A fighting game is a negotiation conducted in sixtieths of a second. When you throw a jab and your opponent blocks it, the entire exchange — who recovers first, who gets punished, who earns the next turn — is decided by a handful of integers agreed on before either fighter was modeled. Those integers are called frame data, and they are the actual game. The 3D humanoid swinging its fist is a readout.

So when the studio set out to build 3D Fighter — the collection's first WebGL game, a side-on brawler in Three.js — the AI department did the sane thing and wrote the frame data before the engine. No rig, no camera, no hit spark; just a reference that fixes every number the fight runs on. This is a look at what that document actually says, and why a fighting game has to be designed spreadsheet-first.

What frame data actually is

Every attack is three windows laid end to end: startup, active, and recovery. Startup is the wind-up before anything can hit. Active is the small slice of time the hitbox is alive. Recovery is the commitment tax — the frames you spend putting your arm back where it was, during which you cannot block, move, or turn.

Here is the jab from the reference:

Move    Startup  Active  Recovery  Dmg  Hitstun  Blockstun
Jab        3       3        7       4     12        8
Straight   8       4       14       9     18       12
Sweep      7       5       18       8   (knockdown) 10

Those numbers are not decoration — they encode fairness. Subtract to find frame advantage: block a jab and the attacker spends 7 recovery frames while you sit in 8 frames of blockstun, so the jab is +1 — safe, a hair of pressure. Block a straight and it is recovery 14 versus blockstun 12, so it is -2: mildly punishable. Block a sweep and it is 18 versus 10, a brutal -8 — you get a free hit for reading it. That single spread is the whole risk/reward of spacing, and it exists entirely in arithmetic. The sweep knocks down; the price is that whiffing it hands your opponent the round.

Why the logic ignores your framerate

Frame data is only meaningful if a frame is always the same length of time. A browser is a hostile place for that — requestAnimationFrame fires at whatever rate the machine and the tab feel like. So the fight logic does not run on render frames at all. It runs on a fixed 60 Hz step, dt = 1/60, driven by an accumulator: real elapsed time piles up, and the simulation consumes it in exact 1/60 chunks. Rendering interpolates on top for smoothness, but the authoritative fight advances one deterministic tick at a time.

Decouple them and "3 startup frames" means 50 milliseconds on every device, whether the GPU is drawing 30 fps or 144. Skip this and the same jab is fast on a gaming rig and sluggish on a laptop — the negotiation breaks, because both sides are no longer speaking the same clock.

Hitboxes that exist only when they should

The attacker's hitbox is not a permanent aura. It is a box that exists only during the active window and is checked against the defender's hurtbox, once per move — a hasHit flag stops a multi-frame active window from landing three times. Height matters too: a low kick and a sweep live near the floor and must be crouch-blocked, while a high kick is an overhead that must be blocked standing. Crouch under a high poke and your hurtbox literally drops below it — the box shrinks from 0.55×1.7 standing to 0.55×1.1 crouching, and the punch sails over. Fairness you can see because it is fairness you can measure.

Hitstop: the freeze that sells the punch

A clean hit that simply subtracted HP would feel like nothing. So on contact, both fighters freeze — timers, movement, and pose cursors all paused — for a few frames: 4 to 6 on a light, 9 to 10 on a heavy. That tiny stall is hitstop, and it is the entire sensation of weight. The critical detail from the reference is that the freeze must pause the animation cursor too; if the poses keep sliding during the stop, the impact doesn't read. Knockback then decays out over the stun window as a fading velocity rather than a teleport, so a hit shoves and settles instead of snapping. Miss the freeze and the whole game feels like slapping air.

A CPU that reads range instead of cheating

The lazy way to make a hard opponent is to let it read your inputs. The reference refuses. Instead the CPU buckets the distance between the two fighters — CLOSE under 1.3 units, MID, FAR, ZONE beyond 4.5 — and picks a weighted action for that bucket: pokes in MID, fireballs and jump-ins from FAR, blocks and throws up close. Crucially, it only sees your move after a reaction delay, reactFrames, scaled by difficulty: 20 frames on Easy, 7 on Hard, and never below 5. That delay is the beatable seam. A human anti-airs a jump-in; so does the CPU, but a beat late, with a probability — and the reference caps Hard so it is never frame-perfect. It plays the same game you do, one honest read at a time.


None of this needed the engine to exist to be true. The rig — a humanoid built from Three.js boxes and cylinders in a joint hierarchy, posed by lerping keyframes — is downstream of the numbers, not the other way around. And like every game in the collection, 3D Fighter ships as one offline page: Three.js bundled locally with no CDN so it runs straight off file://, every impact synthesized through Web Audio, the whole thing conjured by an AI studio from a single prompt. The fight was designed before the fighter. That is the only order that works.