Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bag Fighters

A small, extensible 1v1 netplay fighting game in Rust.

  • Networking: web-transport (QUIC / WebTransport) — web-transport-quinn on native, web-transport-wasm in the browser.
  • Rendering: wgpu (instanced 2D sprites).
  • GUI: egui via eframe (native window + web canvas).
  • Server: authoritative, embedded into the native client (host a match from the game itself) and also available as a standalone binary.

The same client binary runs natively and on the web. The server is native-only (browsers can't host QUIC), but a browser can join a native host.

Workspace layout

crates/core    bf-core    shared simulation, wire protocol, asset registry (no platform deps)
crates/server  bf-server  authoritative WebTransport server (native)
crates/client  bf-client  eframe + wgpu + egui client (native + wasm); embeds the server
assets/        characters/, stages/, registry.ron   ← all content, embedded at build time
web/           index.html + Trunk.toml for the browser build
tools/         gen_art.py   regenerates the placeholder sprites

Running natively

cargo run -p bf-client --bin bagfighters
  1. Pick a name and character, click Host a match. The game starts the embedded server and connects to it locally. The connecting screen shows a certificate hash — the other player needs it.
  2. Player 2 runs the same binary (or the web build), enters https://<your-ip>:4433, pastes the certificate hash, and clicks Join.

Two native clients on one machine: run it twice; host in one, join https://127.0.0.1:4433 (with the shown hash) in the other.

Couch co-op (local two-player)

Set Local players to 2 in the menu, give each player a character and a control scheme (a connected gamepad is picked for player 2 automatically), then Host a match. Both fighters are driven from this one machine over the embedded server — no second client, no network needed. One connection simply drives two fighter slots.

Standalone dedicated server

cargo run -p bf-server            # binds [::]:4433, prints the cert hash
cargo run -p bf-server 0.0.0.0:5000

Running on the web

Requires a browser with WebGPU and WebTransport (recent Chrome/Edge).

cargo install trunk
cd web
trunk serve            # http://127.0.0.1:8080

Only the client compiles to wasm — the server is native-only (it pulls in quinn/rustls, which don't build for wasm32). Trunk builds just bf-client via the href in web/index.html, so don't run a workspace-wide cargo build --target wasm32-unknown-unknown; scope it to the client: cargo build -p bf-client --lib --target wasm32-unknown-unknown.

The page opens to the same menu. There's no Host button on web (browsers can't host); enter a native host's URL + cert hash and Join.

WebTransport requires the server's certificate to be pinned by SHA-256 and to be valid for under 14 days — the server mints exactly such a self-signed cert at startup and shows its hash. Native clients pin the same way, so there is one code path for both platforms.

Controls

Each local player picks one scheme in the menu, so two people can share a keyboard or grab gamepads for couch co-op.

Action Keyboard (WASD) Keyboard (Arrows) Gamepad
Move A / D / left stick / d-pad
Jump W / Space A (south) / up
Attack J . X / Y
Block K / B / shoulders / triggers

Gamepads are read via gilrs on native; the web build supports the keyboard schemes only.

Adding content

Content is data-driven and embedded at build time (so it works identically on web). After editing assets/, rebuild.

A new character — drop a folder of PNGs in assets/characters/<id>/ and add a CharacterDef to assets/registry.ron:

CharacterDef(
    id: "ninja",
    name: "Ninja",
    dir: "characters/ninja",
    sprite_size: (96.0, 160.0),
    animations: {
        "idle":   AnimClip(frames: ["idle_0.png", "idle_1.png"], fps: 3.0, looping: true),
        "walk":   AnimClip(frames: ["walk_0.png", "walk_1.png"], fps: 10.0, looping: true),
        "attack": AnimClip(frames: ["attack_0.png"], fps: 18.0, looping: false),
        // jump / hurt fall back to idle if omitted
    },
    stats: CombatStats(max_hp: 90.0, walk_speed: 320.0, attack_damage: 7.0),
)

Recognized clip names: idle, walk, jump, attack, hurt (missing ones fall back to idle). Every CombatStats field has a default, so specify only what you want to change.

A new stage — drop one background PNG in assets/stages/<id>/ and add a StageDef (id, name, sprite, width, height, floor_y).

The placeholder art is generated by python3 tools/gen_art.py (needs Pillow).

How the netcode works

The server steps one authoritative World at a fixed 60 Hz and broadcasts a Snapshot datagram each tick. Clients send their buttons as datagrams and render the latest snapshot directly (server-authoritative, no prediction — simple and robust on a LAN). The handshake (Hello/Welcome) uses a reliable bidirectional stream; the realtime loop uses unreliable datagrams. The whole simulation lives in bf-core and is pure, leaving room for client-side prediction or rollback later.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages