jengine-fighter
A fighting game: frame data as data tables, hit and hurt boxes, an input buffer with motion inputs, and a deterministic match that runs on frames.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-fighter |
| Version | 0.1.0 |
| Entry point | jengine_fighter::FighterPackage |
| Script namespace | Fighter |
| Listed by | projects/arcade-classics |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-fighter", "source": { "type": "path", "path": "../../packages/jengine-fighter" } } ]}Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the
editor’s Rebuild viewport banner). Native and web builds link it on their own.
A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field
for a package component and keeps it as inert custom data.
A script can check that the package is linked with if Fighter == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
fighter_match | FighterMatch | package only: needs a v2 scene |
Script commands
Section titled “Script commands”Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.
| Call | Flat name |
|---|---|
Fighter.new_match(e) | fight_new_match |
Fighter.pad(e, who, dir, buttons) | fight_pad |
Fighter.bot(e, on, skill) | fight_bot |
Fighter.demo(e, on) | fight_demo |
Fighter.speed(e, x) | fight_speed |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/dojo.scene
From packages/jengine-fighter/README.md.
A fighting game: frame data, hit and hurt boxes, an input buffer, and a match that runs on frames.
Used by projects/arcade-classics (Iron Dojo).
| Module | What it is |
|---|---|
input | The buffer. A fighting game cannot read the pad; it reads the last twenty frames of it. |
moves | Frame data - startup, active, recovery - as rows in the project’s data tables. |
sim | The match, one frame at a time at 60 Hz, deterministic. |
bot | An opponent that plays by distance and emits Pad frames like anyone else. |
Frames, not seconds
Section titled “Frames, not seconds”Everything here counts frames. A fighting game is an argument about frame
counts - 4-frame jab, -8 on block, three frames of input buffer - and a
simulation running on dt would make every one of those numbers
approximate. Match::step advances exactly one frame; the caller
accumulates real time and calls it as often as it owes.
Two consequences, both load-bearing:
- The match is deterministic. The same two sequences of pad inputs give the same match, every time - which is what a replay, a training-mode playback and a rollback netcode all need, and what lets the tests assert on exact frames.
- The order inside a frame matters. Hits are resolved on the frame the
move is on, and only then does the move advance. The other order tests
the next frame’s hitbox against this frame’s world, which makes every move
land one frame early - a 4-frame jab that hits on frame 4 is a 3-frame
jab, and every number in the table is then a lie.
tests/match_rules.rscounts the frames one at a time for exactly this reason.
Both attackers are tested in the same frame, so two moves that connect together trade - deciding one first and letting it interrupt the other would make the order of the two fighters matter, and it must not.
The buffer
Section titled “The buffer”Directions are numpad notation from the fighter’s own point of view, and the
buffer mirrors as it stores, so a dragon punch is 623 in either corner:
7 8 9 up-back up up-forward4 5 6 = back none forward1 2 3 down-back down down-forwardA motion matches when its directions appear in order inside the window,
with anything in between - which is how a real quarter-circle is recognised,
because a stick passes through corners the player never meant to hit. Order
still matters: 623 (dragon punch) must not come out as 236 (fireball),
and tests/buffer.rs holds that line.
A button press is remembered for a few frames and spent when it fires, so a button pressed three frames before the recovery ends still comes out, and one press cannot fire two moves.
Frame data is data
Section titled “Frame data is data”A moves row: startup, active, recovery, damage, hitstun,
blockstun, pushback / block_pushback, guard (mid / low /
overhead / unblockable), knockdown, crouching / airborne, the
hitbox, how far the move carries the fighter (advance), what it
cancels_into, and its input (the motion and button).
Frame advantage falls out of the row rather than being authored:
// blockstun, less what the attacker has left after the first active framejab.frame_advantage_on_block() // +3: safe, and your turnheavy.frame_advantage_on_block() // -8: punishable by anything under 8 framesA fighters row carries health, walk speeds, the jump arc, the standing and
crouching hurtboxes, and how wide the body is for pushing.
Blocking
Section titled “Blocking”Blocking is holding away from the opponent at the moment the hit lands - not a state you enter - and the height has to match: a low must be blocked crouching, an overhead standing. Blocking costs ground rather than health, and in the corner the pushback moves the attacker back instead, which is what makes a corner worth having.
The component
Section titled “The component”fighter_match on one entity, in a "version": 2 scene.
"fighter_match": { "tables": { "fighters": "data/fighters", "moves": "data/moves" }, "left": "karate", "right": "karate", "half_width": 560.0, "rounds_to_win": 2, "round_seconds": 60, "bot": true, "bot_skill": 0.6}Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Fighter.new_match(e) | both fighters back to their marks, rounds at zero |
Fighter.pad(e, who, dir, buttons) | one frame of input: numpad direction, button mask |
Fighter.bot(e, on, skill) | the second fighter is driven by the bot |
Fighter.demo(e, on) | both are, for an attract mode |
Fighter.speed(e, x) | run at x times speed |
Buttons are a mask: 1 light punch, 2 heavy punch, 4 light kick, 8 heavy kick.
What scripts read
Section titled “What scripts read”FightStateevery frame:phase,round,clock,gap, andp1/p2- position, facing, health, state, the move being performed and which frame of it, whether its hitbox is out, combo, rounds won.FightBoxes: the hit and hurt boxes in world space, for a training mode that draws them.FightMoves: the move list with its frame data, for a pause screen.FightEvents.events:round_start,fight,move,hit,blocked,round_over,match_over.