How this game
was made
Battle for Independence is a 3D fighting game that runs in a browser tab. It leans on one graphics library, ships no model files, and builds its fighters through an automated pipeline. This page explains how — and lets you take those fighters apart.
Drag the model to rotate it. Swap fighters, change the lighting, scrub through a pose, or pull the body apart with the Explode slider. Nothing you do there affects the game.
01 Start here
If rig, clip or GLB don't mean anything to you, read this section first. Everything after it assumes these words.
A 3D model is a hollow shell made of triangles. To make it move, you glue an invisible skeleton inside the shell and teach each bone how to bend. Every move a fighter can do — a punch, a crouch, a knockdown — is a short recording of how those bones should rotate. The game stitches the recordings together as you press buttons.
modelWhat is a 3D model?
A hollow shape built from tiny triangles — about 150,000 of them for the figure in the viewer. Each triangle carries where its corners sit (position), which way it faces (normals) and its colour.
Picture a beach ball made of coloured paper plates glued edge to edge, then shrink each plate to 3 mm. That's why models look faceted up close: there are no smooth surfaces underneath, only flat plates. More triangles means smoother curves but slower drawing.
rigWhat is rigging?
Putting a digital skeleton inside the model. The bones form a tree — hips, spine, chest, neck, head, with arms branching off the chest and legs off the hips.
Each bone is invisibly glued to a chunk of nearby triangles, so rotating the bone drags the surface with it. Every fighter here uses the same 25-bone rig.
clipWhat is an animation clip?
A short, time-stamped recording of how every bone should rotate. A jab might say: frame 0, arm at the hip; frame 4, arm fully extended; frame 12, back at the hip.
Press punch and the jab clip plays. Press block and the guard clip loops. Clips can be blended, which is how a fighter can be halfway through a punch and start a kick.
shaderWhat is a shader?
A tiny program that runs on the graphics card, once per triangle, to decide what colour to draw. The highlight on a fighter's shoulder is a shader working out where the light lands and how bright it should be.
framesWhat is frame data?
A frame is one still image. A typical screen shows 60 per second. Frame data is the list of which frame each move hits, blocks or ends on.
The jab here lasts 0.62 s — 37 frames. It hits on frame 4, recovers on 12, and is safe to interrupt by 16. Two players on different monitors see the same fight; only the smoothness differs.
GLBWhat is a .glb file?
A binary package holding everything one model needs: positions, normals, colours, the rig, every clip, and any texture images. The rigged masters here are roughly 9 MB each.
Nothing is published at master size. At build time every heavy model is compressed (meshopt geometry, WebP textures) — these two land near 2 MB — and the game itself ships an even smaller encoded form that decodes at load time. Same look, smaller download.
SIZEHow much does a match download?
About 0.6 MB to reach the menu and about 25 MB for the first fight, down from well over 100 MB. A match fetches only what is on screen: its arena, its two fighters and the weapons they can actually throw. Portraits are pre-rendered images, so the select screen no longer loads every fighter to draw a face. A build fails its size check if any file or the first fight grows past its cap.
pipelineWhat are a pipeline and a gate?
A pipeline is a fixed sequence of automated steps. This one turns reference drawings of a character into a rigged, animated, ready-to-punch fighter in eight steps. Run it again on a new character and you get the same quality without repeating anyone's trial and error.
A gate is a measured pass/fail check between steps — for example, silhouette overlap must be at least 98.5 %. Miss it and the run stops. A number with a threshold, never "looks fine".
CPUHow does the computer opponent think?
It doesn't. It's a short decision tree: how far away is the other fighter? Are they attacking? Am I low on health? It reads the exact same frame data a human reads, so it plays by the same rules.
Difficulty only changes how often it picks the best option instead of a random one. On the easiest setting it guesses about half the time.
AI artWas the art made by AI?
The raw silhouettes came from an image-to-3D service fed hand-made reference drawings. Everything else — the painted colour, the skeleton, every pose and clip, every hit effect, the sound, the music, the announcer, the code, the menus and the HUD — was authored or assembled by hand on top of those surfaces.
The game is a fictional parody and is not affiliated with any real person or organisation.
02 How the game is built
A standalone combat prototype: fictional fighters, a single stage, and a simulation locked to 60 steps per second. Every move lasts the same number of frames on a 144 Hz desktop and on a throttled phone. Drawing is the only layer that smooths anything out.
- RendererThree.js (vendored, MIT) on WebGL2. No physics engine, and no animation library beyond Three's own mixer.
- Sim loopFixed-step accumulator at 60 Hz, driven by a frame-data table and a match state machine.
- CPUDecision tree with four difficulty dials, reading the same frame data the player does.
- InputKeyboard, gamepad and touch folded into one button struct, with chord macros for two-button moves.
- Audio77 mono sound effects, three music tracks, and an announcer pool that ducks the mix. Sound starts after a user gesture.
- InterfaceHUD, menus and round reel are plain DOM and one stylesheet. No UI framework.
One library, not an engine
Three.js is a renderer, not a game engine. It turns "put this mesh here, light it that way, point a camera at it" into the thousands of low-level calls a graphics card wants. It has no opinion about what a round is, what a hitbox does, or when a match ends. Everything above the triangles is first-party.
Three.js does
- DrawsGraphics state, shader compilation, the render graph, and the per-frame callback.
- SkinsDeforms ~105k vertices against the 25-bone rig on the GPU, per fighter, per frame.
- BlendsCross-fades the 32 clips each fighter carries — the only animation code not written here.
- LightsShadow maps, filmic tone mapping, physically-based materials, and the bloom pass.
This project does
- LoopThe fixed-step 60 Hz accumulator. Three supplies a frame callback and nothing more.
- CombatFrame-data table, match state machine, hit resolution, blocking, combos and meter.
- CollisionAxis-aligned hit and hurt boxes resolved by hand. No physics library at all.
- Audio & UIRaw Web Audio buffers and gain nodes; HUD and menus in DOM and CSS.
Why that trade is worth it
- AuditableOne direct dependency, with no branches beneath it. The complete list of third-party code running in a player's browser is Three.js r170, MIT — no CDN, no analytics, no remote font.
- DurableES modules and WebGL2, nothing else. No engine version to migrate, no editor project to open, no licensed runtime. Nothing here needs a company to stay in business.
- DeterministicAn engine owns the tick and hands you a variable time step, which quietly makes your rules depend on how fast the machine ran. Because nothing outside owns the loop here, the simulation is a plain function: one call advances exactly one frame.
The combat code contains no random numbers, no clock reads and no trigonometry — nothing that can differ between two machines. A four-frame jab is four frames everywhere, and a whole round can be rebuilt from nothing but the button presses. That is very hard to retrofit and nearly impossible to guarantee inside an engine that owns your tick.
The cost is honest, and it is the rest of the project: thousands of lines doing work an engine would have handed over. "Only Three.js" is not a claim to have written a renderer — Three.js carries enormous weight. The narrower claim is that there is exactly one seam between this game and somebody else's code, and everything on this side of it can change without asking permission.
Ceremony and feedback
The round reel is fixed: title → versus shot → intros → round call → FIGHT → live input. A KO holds the impact for 32 slow-motion frames and a freeze before the result card. Flawless, Double KO, Fatality, Draw and ordinary Victory each get their own card and colour pair. Reduced-motion mode drops the scaling slams and camera kicks but keeps every callout.
03 How a fighter is made
Every fighter comes out of the same automated pipeline. Each step ends in a gate — a measured number with a threshold — so a bad result stops the run instead of slipping through. Re-rolling is cheap; a signed-off result is never touched up by hand.
Front, side and back drawings with a clean cut-out and a tight crop.
A textured mesh and a named-parts mesh of the same sculpt.
gate: bounds ≤ 1 mm/mTransfer the 8K colour atlas onto the six named body parts in 3D.
gate: median ≤ 0.5 mmCurvature- and colour-aware simplification, with a budget per part.
gate: overlap ≥ 98.5 %Joints are read from the part bounds. Only the wrist is searched, and the two arms cross-check each other.
gate: wrists agree ≤ 3 %Fit the shared 25-bone skeleton, then re-run the posing outside the browser and print the clearances.
gate: clearance in bandPack positions, normals, colours and skin weights into one stream the game imports as a module.
no model fetchKeyframe by hand on the frozen skeleton. One jab first, then idle, walk, hurt and specials.
The biggest lesson from building this was that asking a model "does this look right?" is the wrong check — that is exactly the component that waves through a grey, washed-out regression. Every gate above is a number with a threshold instead: silhouette overlap, colour delta, clearance in metres, left-right agreement. The pipeline can run unattended because the gates are measurements.
One skeleton for everyone
All fighters share the same 25-bone rig, so combat clips are authored once and inherited by the whole roster.
root > hips > spine > chest > neck > head > headEnd
> shoulderL/R > upperArmL/R > forearmL/R > handL/R > handEndL/R
> upLegL/R > legL/R > footL/R > toeL/R
No model files ship with the game
The 9 MB rigged file is the measurement source, not the deliverable. The game ships a factory that rebuilds the fighter from a packed stream imported as a JavaScript module — structurally, it is code. During play, no model, buffer or texture file is fetched at all.
04 The roster
Pick a fighter to load it into the viewer. Use the pose buttons to switch between the bound pose, idle and authored moves, and the Explode slider to separate the six named body parts.
Trump
Taller stance, narrow trunk. The first fighter through the pipeline, so every default setting was tuned against this mesh.
Carney
Slimmer build, 0.25 m deep against 0.38 m. Sculpted with closed fists so the jab reads as a punch rather than a traffic-cop gesture. The pipeline transferred unchanged apart from a small aim offset.
Officer Flock
An imported model rather than a pipeline build. The shared animation set is mapped onto its own skeleton, keeping its five textured meshes and closed hands intact.
Each fighter is a group of six named parts — head, torso, two arms and two legs. Every part already knows its own offset from the body's centre, so the Explode slider just slides them along those directions. The bones never detach; the parts only separate. Fully exploded is what you see in the versus setup before a round starts.
05 The damage system
This section is the game's dismemberment system, running live in the viewer: severed limbs, cartoon blood and organs on the floor. It only happens when you press something, and it stays collapsed until you open it. Nothing later on the page depends on it — skip to the numbers if you'd rather not.
Show the damage system
A fighting game that only drains a bar tells the same story every round. This one keeps a record. The rule everything is built around: a limb is never taken by the health bar alone. A region comes off when it is hit while the fighter is already below that region's threshold. Beat someone to the body and the torso opens; headhunt and the jaw goes. A fighter who never gets hit in the legs keeps them at any health.
Where the hit landed picks which regions it could plausibly have removed. How hurt the fighter already was decides which of those are eligible at all. Neither is enough on its own — which is what stops a body blow at 20 % health from taking a head off.
The seven regions
This is the shipped table, thresholds and all. Threshold is remaining health. The two lethal regions sit at 0 % because they can only ever be the last thing that happens; an ordinary hit is never allowed to take them, so rounds don't end on a decapitation nobody chose. Only a KO, a finisher, or the Cut button asks for those directly.
Regions are named after the bones their geometry hangs from, and matched by pattern rather than exact name — the roster doesn't agree on a skeleton, since imported models arrive on whatever rig they were built with. Pattern matching means a new model can be dismembered on arrival instead of after someone hand-writes a bone map. It also means a region can come back not rigged: neither fighter here has a jaw bone, so the jaw card marks itself and cutting it does nothing, rather than failing silently.
What happens on the frame a limb comes off
Four things, all read from the same posed skeleton, so they agree about where the cut was:
- GeometryEverything attached to the severed bone chain stops being drawn — at the limb's current position, not where the rest pose left it.
- MomentumThe limb becomes a prop carrying 55 % of the blow's force. A limb that flies off along the blow reads as caused by it; one that drops straight down reads as a bug.
- SpillThe region's organs follow, thrown a little more softly so they trail behind the limb instead of arriving in a clump.
- BleedThe stump is soaked before the limb has finished leaving. A cut that isn't bleeding at its edge reads as plastic.
Nothing is actually deleted
The obvious way to remove an arm is to rebuild the mesh without it. This doesn't. Every vertex carries how strongly it belongs to the severed chain, and the shader simply throws away anything above 0.94. The fighter keeps one draw call all round, and restoring the limb next round is a reset rather than a rebuild.
Deep in the forearm, every bit of a vertex belongs to the severed chain. Across the elbow that falls off exactly as the skinning blend does. The band left in between is the cut face, and the shader paints it raw instead of skin. Without that band a severed arm reads as a rendering glitch rather than a wound — and nobody had to model a cut cap.
The limb is the body's own vertices
At the moment of the cut, the posed positions of the removed vertices are lifted straight out of the fighter and thrown as a dense cloud of points. It is the same silhouette that was attached a frame earlier, which is the whole reason it beats throwing a generic lump. As fast-moving points they read as wet tissue in motion and cost nothing to build — the right trade for something on screen for under a second. Once it lands it fades, leaving what actually belongs there: the stain, and the solid chunk that spilled alongside it.
Organs have real sizes
Every organ is authored against one number: its longest axis in metres, on a fighter a little under two of them tall. That single source of truth is why an imported heart and a generated one come out the same size. Eyeballing it instead produced organs the size of a torso.
| Prop | Span | Mass | Notes |
|---|---|---|---|
| heart | 0.135 m | 0.5 | keeps beating for nine seconds after it lands |
| brain | 0.170 m | 0.6 | head only |
| intestine | 0.780 m | 0.9 | stretched on one axis so it reads as rope, not another lump |
| guts | 0.560 m | 1.1 | strand |
| ribcage | 0.300 m | 1.4 | heaviest piece, lands first |
| eyeball | 0.030 m | 0.2 | two of them, from the head |
| armChunk | 0.230 m | 0.9 | the piece that comes off |
The viewer generates these props procedurally from the same spans. The game loads a baked library and falls back to exactly this generated blob for anything the library doesn't carry — so the system is never blocked on art, and a test that loads no assets still exercises the whole path.
Blood goes into the mesh, not on top of it
Wounds are painted into the model's own vertex colours in rest space, so they follow every animation for free — no decals, no texture copies, no per-frame work. The wound is placed by finding the nearest vertex on the posed surface, which is what stops a crouching fighter's head wound landing on their jacket. Each splash gets a narrow downward tail so it reads as a trickle rather than a round paint blot.
Debris bleeds in flight too: a limb that only stains where it lands leaves a clean floor between body and piece, which is the tell that the two events are unrelated. Pieces get one heavily damped bounce and then stay put, because debris that skitters distracts from a round it is no longer part of.
Try it
Every control in the viewer is a real entry point in the shipped system:
- HealthDrag it and watch the cards arm themselves. This is the only thing gating which regions an ordinary hit may take.
- Land a hitRolls a height and a force, then runs the same path a real hit takes. Small hits only bleed; heavy ones take something.
- Beat downLands one every 0.42 s until there is nothing left to take. The body comes apart in the order it was beaten, and no two runs match.
- CutSevers a region directly, ignoring the gate — which is what a KO and a finisher do.
- FatalityEvery region at once, ordered from the extremities inwards so the body falls apart rather than the head simply vanishing.
- Patch upStarts a clean round: masks and blood zeroed, debris retired, floor washed.
It uses the shipped design, thresholds and numbers, but the viewer is its own small copy of the runtime rather than the game in an iframe. The difference that matters: a real hit gets its height and force from the collision solver, while Land a hit rolls them. Everything downstream of that roll is the logic described above.
One thing genuinely had to change, and it's a property of the asset rather than the system. This page loads the 9 MB measurement model, which is blended far more softly than the rig the game ships — a vertex in the middle of this head is only about half bound to the head bone, so the shipped absolute cut would leave it attached. Here the threshold is measured against the strongest hold that chain has anywhere on the fighter, then the same 0.94 and 0.02 cutoffs are applied unchanged. On a tightly rigged fighter the two are the same thing.
06 Numbers & next steps
The gate results for the first shipped fighter — the build that beat every other budget on both axes:
- Silhouette99.146 % overlap head-on and 99.318 % at three-quarters, against the 2 M-triangle source at identical framing.
- Head colourMedian delta 3.00, 95th percentile 20.27, measured inside the shared silhouette.
- Wrist position±0.4452 on both arms — a four-decimal cross-check between two independently built sides.
- Trunk width0.54 m at the belly, 0.40 m at the chest, which is what drives the elbow angle.
Where to go next
- Play itThe full game — the build everything on this page describes.
- Inspect itFighter lab puts the raw sculpt beside the textured export, with camera presets, animation scrubbing and wireframe.
This page deliberately isn't a walkthrough of the animation mixer or the inverse kinematics, a guide to the input chords, or an argument for mixing ragdoll physics with a deterministic simulation. It's the behind-the-scenes note pinned to the same build.