Skip to content

Latest commit

 

History

History
561 lines (436 loc) · 24.3 KB

File metadata and controls

561 lines (436 loc) · 24.3 KB

Online Multiplayer Implementation Plan

Last researched: 2026-06-01

Summary

Build online multiplayer in stages, starting with the smallest version that can prove whether the arena brawler is fun with friends:

  • Client target: native desktop first.
  • Player target: 2 online human players first.
  • Network model: dedicated server, server-authoritative gameplay.
  • Networking stack: Lightyear 0.26.x with Bevy 0.18, using UDP/netcode for native clients.
  • Hosting target: Docker/OrbStack locally, then the old Linux desktop with UDP port forwarding.
  • Browser/Web/GitHub Pages multiplayer: later phase, after native friend testing works.

This repository already has a good ordered gameplay flow in src/app.rs:

Input -> Action -> Movement -> Combat -> Items -> Respawn -> Presentation

The main server-authority work is now to keep that ordered flow running in the shared fixed-tick headless server app, while clients send input and render predicted/interpolated state plus replicated presentation events.

Implementation Status

Status as of 2026-06-02: native online v1 has working two-client localhost and OrbStack/Docker paths for early fun testing. The server validates room slots, rejects duplicate live client IDs, accepts sanitized client StartMatch requests, starts authoritative matches from shared match config, relays player commands, runs a shared headless gameplay app at 60 Hz from the latest accepted input per slot, and broadcasts authoritative snapshots for client reconciliation/interpolation. It now owns authored movement, jumps/dashes, guard, grabs, attacks, specials, items, arena hazards, active hitboxes, health/stamina changes, knockouts, ring-outs, respawn, stocks, scores, cooldowns, relations, server-driven bot input for single-player online matches, and match results. Snapshots preserve stable action technique IDs, exact generic/Bee/Penguin special identities, active hitbox geometry, item state, hazard activity/cooldowns, and server tick metrics. Clients do first-pass local prediction replay from authoritative snapshots plus unacknowledged local commands. It is still short of presentation parity because transient feedback events, SFX/camera shake, hit sparks, and some multi-part special visuals need richer server-authored event replication.

Implemented scaffolding currently present in the worktree:

  • Cargo.toml has a default-run = "ffc-prototype" entry, an explicit ffc-prototype client binary, an explicit ffc-server server binary, and feature flags for host, online, and server.
  • src/lib.rs centralizes the crate module declarations.
  • src/app.rs contains the current client app shell startup that used to live in src/main.rs.
  • src/gameplay.rs owns the ordered gameplay sets, client gameplay system registration, the 60 Hz fixed-tick local simulation baseline, and the online local-authority gate.
  • src/main.rs now delegates to ffc_prototype::app::run_client().
  • src/bin/ffc_server.rs exists and is feature-gated on server.
  • src/bin/ffc_net_smoke.rs exists for headless two-client connection and input-relay testing.
  • src/server.rs starts the Lightyear UDP/netcode server.
  • src/net/ has protocol, room, input conversion, stable-ID, shared headless authority, reusable compact simulation tests, and Lightyear client/server integration.
  • src/app.rs accepts native online client arguments: --online, --server, --client-id, and --name.
  • Online clients map LocalSetup into a wire NetMatchConfig, request a server match start when local dev/user-mode flow starts a match, and apply the accepted MatchStart config back into local setup.
  • The live server assigns slots 0/1, rejects invalid/full-room/duplicate-client ownership, and validates PlayerCommand slot ownership before accepting commands.
  • The server queues the latest valid input per slot, generates simple bot input for configured bot slots, advances the shared headless gameplay app once per 60 Hz server tick, and broadcasts ServerSnapshot data with match state, last processed input sequences, fighter kinematics, action, technique ID, vitals, cooldowns, relations, arena items, exact specials, active hitboxes, arena hazards, stocks, scores, and server tick-time metrics.
  • Native clients reconcile the assigned local fighter to server movement snapshots, replay unacknowledged local commands with the accepted server loadout for first-pass prediction, interpolate remote fighter movement, apply authoritative action/technique/vitals/match/arena/item/special/hitbox snapshots, spawn visual proxies for authoritative special and hitbox snapshots when gameplay assets exist, and disable local mutating gameplay authority after the server claims the match.
  • ffc-net-smoke fails if welcome, slot assignment, remote command relay, snapshots, combat snapshots, core action snapshots, or hitbox snapshots are missing, can require item snapshots with --require-items, special snapshots with --require-specials, can require hazard snapshots with --require-hazards, and reports remote command, snapshot, combat snapshot, core action snapshot, item snapshot, special snapshot, hitbox snapshot, hazard snapshot, prediction replay, and local correction metrics.
  • The native HUD reuses the debug overlay during online sessions to show connection state, slot, RTT/jitter, snapshot/combat/core-action/relation/item/special/hitbox/hazard counts, inferred missed snapshots/packet loss, server tick time, remote command counts, prediction replay/pending-input counts, acknowledged input sequences, correction count, and last correction distance.
  • Dockerfile/Compose deployment files exist for ffc-server, publish UDP port 40000, and validate with docker compose config.

Validated Wave 1 gates:

  • src/net/mod.rs, src/net/room.rs, src/net/input.rs, src/net/protocol.rs, and src/net/stable_id.rs compile together.
  • Network unit tests cover protocol serialization, input conversion, room ownership validation, and stable ID mapping.
  • cargo run -- --help and cargo run --features online -- --help compile the native client entrypoint and print the expected CLI.
  • cargo test passes.
  • cargo test --features online passes: 579 tests.
  • cargo run --bin ffc-server --no-default-features --features server -- --smoke --bind 127.0.0.1:0 binds UDP and exits successfully.
  • cargo run --bin ffc-server --no-default-features --features server -- --bind 127.0.0.1:40000 accepts two localhost clients.
  • Two overlapping native ffc-net-smoke clients receive slots 0/1, both receive MatchStart, report nonzero remote_commands, snapshots, combat_snapshots, and core_action_snapshots, and expose local correction metrics.
  • docker compose build ffc-server, docker compose up -d ffc-server, and two overlapping ffc-net-smoke clients against the published 127.0.0.1:40000/udp port pass with nonzero combat snapshots; the container is then cleanly stopped with docker compose down.

Still not fully implemented:

  • Server-authored transient feedback/event replication for hit sparks, SFX/camera shake, impact cues, special despawn/impact events, and hazard impacts.
  • Full multi-part special presentation parity in the in-game client world. Exact Bee/Penguin special identities and active hitbox proxies now replicate, but some local special scenes still need richer per-entity identity than a single snapshot kind can provide.
  • Linux desktop and friend internet playtests.

Technology Decision

Use Lightyear as the primary multiplayer framework.

Reasons:

  • Lightyear 0.26 supports Bevy 0.18, which matches this repo.
  • It is designed for server-client multiplayer games.
  • It includes input buffering, replication, prediction, rollback, interpolation, lag compensation hooks, bandwidth controls, metrics, UDP, WebSocket, and WebTransport transports.
  • Alternatives such as bevy_replicon + bevy_renet are viable but would require custom prediction, reconciliation, interpolation, and input buffering.
  • Matchbox/GGRS is a better fit for deterministic peer-to-peer rollback games, not this current custom 3D arena brawler unless the whole simulation is rewritten for deterministic lockstep.

Recommended dependency direction:

  • lightyear = "0.26" with server, client, replication, prediction, interpolation, input-native, udp, netcode, metrics, and tracing-related features as needed.
  • Keep Bevy at 0.18.
  • Do not add Avian or another physics crate for v1. The current custom movement/collision should be networked first.

V1 Scope

V1 is a native two-player online duel.

In scope:

  • Dedicated headless server binary.
  • Native client can host/connect by IP or hostname.
  • Two online human slots.
  • Server owns fighter spawn, movement, combat, items, specials, ring-outs, stocks, scoring, and match phase.
  • Clients send input commands only.
  • Local fighter has client-side prediction and reconciliation.
  • Remote fighter uses snapshot interpolation.
  • Docker/Compose setup for local and Linux desktop server tests.
  • Basic network debug HUD/logging: ping, jitter, packet loss, correction distance, rollback count, and server tick time.

Out of scope for v1:

  • Browser clients.
  • Matchmaking.
  • Accounts.
  • Persistent progression.
  • Spectators.
  • More than one simultaneous room per server process.
  • Full GGPO-style deterministic rollback for the whole world.
  • Client-authoritative hit reporting.
  • Steam/EOS lobbies or relays.

Repository Changes

App Structure

Split the current app into reusable plugins:

  • ClientAppPlugin: window, render, camera, HUD, audio, user mode UI, local input capture, prediction/interpolation visuals.
  • ServerAppPlugin: headless Bevy app, fixed simulation, Lightyear server, authoritative match state.
  • SharedSimulationPlugin: systems and resources shared by local play, client prediction, and server authority.
  • NetworkProtocolPlugin: shared Lightyear protocol registration.

Add a server binary:

  • src/bin/ffc_server.rs

Keep the existing src/main.rs as the native client entrypoint.

Simulation Boundary

Move authoritative gameplay from variable Update timing to a fixed tick, targeting 60 Hz.

Systems that should become authoritative fixed-tick simulation:

  • fighter::update_fighter_state
  • fighter::apply_fighter_movement
  • fighter::separate_fighters
  • combat::spawn_attack_hitboxes
  • combat::resolve_hitboxes
  • combat::update_hitboxes
  • items::handle_item_inputs
  • items::spawn_item_hitboxes
  • items::update_items
  • items::update_moving_items
  • specials::handle_special_inputs
  • specials::tick_special_cooldowns
  • specials::update_specials
  • character skill updates
  • arena hazard updates
  • fighter::ringout_and_respawn
  • match timer/phase advancement

Presentation should remain client-only:

  • HUD
  • camera
  • SFX
  • VFX spawning and fading
  • user mode menus
  • debug gizmos
  • web launch overlay
  • local setup screens

Stable IDs

Do not replicate Bevy Entity values. Add stable network IDs for gameplay objects:

  • NetEntityId
  • NetFighterId
  • NetItemId
  • NetSpecialId
  • NetHitboxId
  • NetPlayerSlot

Map network IDs to local Entity values on each peer.

Fields that currently store Entity references need either stable IDs or local-only mapping:

  • fighter inventory held item
  • grab holder/held-by pairs
  • ultimate target/owner
  • hitbox owner and already-hit list
  • item holder/owner
  • special owner
  • skill/surface relationships

Arena State

Replace process-global authoritative usage of ACTIVE_ARENA_INDEX for online matches with match-scoped state:

  • server owns selected arena ID
  • clients receive selected arena ID through replicated match setup
  • local single-player/dev flow can continue using the existing global path until it is migrated

V1 may run one room per process, so the global arena is acceptable behind a compatibility wrapper, but the network protocol should not depend on global process state.

Network Protocol

Shared protocol types should live in a new module such as src/network/protocol.rs.

Client To Server

PlayerCommand

  • client_id
  • slot
  • tick
  • sequence
  • movement_x
  • movement_z
  • buttons bitset
  • aim_direction or camera-relative direction

Buttons should cover:

  • aim/grab
  • light
  • light held
  • heavy
  • heavy held
  • heavy released
  • jump
  • dash
  • guard
  • ultimate
  • special

Clients must not send:

  • position
  • velocity
  • health
  • hit claims
  • damage claims
  • item pickup success
  • score changes

Server To Client

Replicated resources:

  • match phase
  • rule preset
  • arena ID
  • match timer
  • replay seed
  • scores/stocks
  • assigned local slot
  • last processed input sequence per client

Replicated entities:

  • fighters
  • items
  • active specials
  • skill surfaces/projectiles
  • relevant hitboxes if they need client-side visualization

Replicated fighter state:

  • stable fighter ID
  • position
  • velocity
  • facing
  • grounded state
  • action state
  • action elapsed/tick
  • health
  • stamina
  • stock count
  • invulnerability timer
  • cooldowns
  • held item ID
  • grab state by stable IDs

Use reliable ordered messages for:

  • connect/accept/reject
  • slot assignment
  • match start/reset/results
  • loadout/setup confirmation

Use unreliable sequenced channels for:

  • high-frequency player commands
  • high-frequency snapshots

Prediction And Reconciliation

Prediction starts narrow.

Predict locally:

  • movement
  • facing
  • jump
  • dash
  • action startup
  • short local cooldown feel

Keep server-confirmed:

  • hits
  • damage
  • guard result
  • item pickup success
  • grab success
  • ultimate lock success
  • ring-out
  • stocks
  • score
  • match end

Client loop:

  1. Capture local input once per render frame.
  2. Convert input edges into tick commands before fixed simulation.
  3. Send command with tick and sequence.
  4. Apply command to predicted local fighter.
  5. Store input and predicted state history.
  6. Receive authoritative snapshot.
  7. Read last acknowledged input sequence.
  8. Reset predicted state to authoritative server state.
  9. Replay unacknowledged inputs.
  10. Smooth small visual correction, snap large invalid correction.

Remote fighters:

  • Do not predict remote human fighters for v1.
  • Buffer confirmed snapshots.
  • Render remote fighters a few ticks behind the server.
  • Interpolate position/facing/action visuals between snapshots.
  • Extrapolate only briefly during packet loss, then hold or blend to confirmed state.

Server Authority

The server is the only peer allowed to decide:

  • movement truth
  • combat hit detection
  • damage
  • stamina spending/restoration
  • item pickup/throw/drop
  • special spawn/despawn/impact
  • arena hazard impact
  • ring-out and knockout resolution
  • stock loss
  • score award
  • match phase transitions

Client visuals may preview likely outcomes, but must reconcile to server state.

For melee fairness, do not allow clients to report successful hits. If v1 feels unfair under latency, add limited server-side lag compensation later:

  • keep recent hurtbox history by server tick
  • rewind defender hurtboxes by capped command age
  • validate attack volume on server
  • cap rewind to a small window to avoid obviously unfair late hits

Fixed Tick And Determinism

The project does not need perfect deterministic lockstep for v1, because the server is authoritative. It does need stable enough simulation for prediction to be useful.

Rules:

  • Run authoritative simulation at fixed 60 Hz.
  • Avoid Time::elapsed_secs() for gameplay decisions in networked simulation.
  • Use tick counters or fixed tick delta for action windows and timers.
  • Keep ordering stable for fighter collision, hit resolution, item updates, and special updates.
  • Use server-owned RNG for gameplay randomness.
  • Do not let HashMap iteration order affect gameplay outcomes.
  • Keep Bevy Transform as render state where possible; use explicit simulation position/facing data for network truth.

Deployment Plan

Present deployment files:

  • Dockerfile
  • compose.yml
  • optional .dockerignore

Recommended server environment:

  • FFC_BIND_ADDR=0.0.0.0:40000
  • FFC_PUBLIC_ADDR=127.0.0.1:40000
  • FFC_TRANSPORT=udp
  • FFC_MAX_PLAYERS=2
  • RUST_LOG=info,lightyear=info,ffc_prototype=info

Recommended Compose shape:

services:
  ffc-server:
    build: .
    command: ["/usr/local/bin/ffc-server"]
    environment:
      FFC_BIND_ADDR: "0.0.0.0:40000"
      FFC_PUBLIC_ADDR: "${FFC_PUBLIC_ADDR:-127.0.0.1:40000}"
      FFC_TRANSPORT: "udp"
      FFC_MAX_PLAYERS: "2"
      RUST_LOG: "info,lightyear=info,ffc_prototype=info"
    ports:
      - "40000:40000/udp"
    restart: unless-stopped
    logging:
      driver: local

Local Docker/OrbStack test:

docker compose up -d --build
docker compose logs -f ffc-server
cargo run --features online -- --online --server 127.0.0.1:40000 --client-id 1 --name p1

Linux desktop friend test:

docker compose up -d --build
docker compose logs -f ffc-server
sudo ufw allow 40000/udp

Router setup:

  • assign the Linux desktop a static LAN IP or DHCP reservation
  • forward UDP 40000 to that LAN IP
  • friends connect to public-ip-or-ddns-name:40000
  • if port forwarding fails, check for CGNAT or double NAT
  • fallback options: Tailscale, public IPv4 from ISP, or IPv6 if available

Disable the public router forward after playtests unless the server is intentionally staying public.

Browser Multiplayer Later

Keep browser multiplayer out of v1.

When adding it later:

  • GitHub Pages client build must still output to root web_dist/.
  • Browser clients cannot use raw UDP.
  • Viable transports are WebSocket/WSS, WebTransport, or WebRTC DataChannel.
  • WebTransport is the better long-term fast-game browser transport because it supports QUIC datagrams, but it requires HTTPS/TLS and UDP-capable hosting.
  • WebSocket is simpler but TCP head-of-line blocking can hurt action feel.
  • A GitHub Pages HTTPS client should connect to wss://... or HTTPS WebTransport endpoints, not plain ws:///http://.

Testing And Validation

Per AGENTS.md, every code change must be validated with:

cargo run
cargo test

Additional test coverage to add during implementation:

  • protocol serialization round trips
  • stable network ID mapping
  • connect/slot assignment
  • duplicated and late input command handling
  • fixed-tick command replay
  • local prediction correction threshold
  • remote interpolation buffer behavior
  • server-only damage authority
  • server-only item pickup authority
  • server-only ring-out/score authority
  • reconnect/disconnect cleanup
  • Docker server startup smoke test

Manual acceptance criteria for v1:

  • one server and two native clients can connect on one machine
  • one server and two native clients can connect across LAN
  • local player remains responsive under simulated 80-120 ms RTT
  • remote player movement remains readable
  • both clients show the same health, stocks, score, and match result
  • server logs show connect, disconnect, tick rate, ping, packet loss, corrections, and match result

Useful network impairment tests:

  • local loopback with no latency
  • LAN with two clients
  • simulated 80 ms RTT
  • simulated 150 ms RTT
  • 2-5 percent packet loss
  • jitter spikes
  • client disconnect mid-match

Implementation Order

  1. Add the root roadmap document. Done.
  2. Start Wave 1 app scaffolding: Cargo feature/bin shape, library entrypoint, client app extraction, server smoke placeholder, protocol/room/input/stable-ID placeholders, and deployment file placeholders. Done.
  3. Fix current Wave 1 compile blockers so default and server builds compile. Done.
  4. Add Lightyear dependency and a minimal shared network module. Done.
  5. Add dedicated server binary behavior that starts and listens. Done.
  6. Register the shared DTOs with Lightyear and configure UDP/netcode server/client startup. Done.
  7. Wire the unit-tested room slot assignment into a live server connection flow. Done.
  8. Send client input commands to server with slot validation. Done.
  9. Add server-authored 60 Hz movement snapshots. Done for fighter kinematics.
  10. Add client-side movement reconciliation/interpolation metrics. Done in ffc-net-smoke.
  11. Extract the shared simulation seam from src/app.rs and move local mutating gameplay systems to a 60 Hz fixed tick. Done as src/gameplay.rs.
  12. Add basic server-authoritative combat/ring-out/vitals/stock/score snapshots. Done as a combat-lite online slice, then replaced by shared headless gameplay authority.
  13. Extract compact server simulation into a reusable module. Done as src/net/simulation.rs.
  14. Add first-pass local prediction replay for unacknowledged local commands. Done for the compact fighter simulation.
  15. Add server-authoritative core jump/dash/guard-step/grab/special-cast state, cooldown, relation snapshots, and smoke/HUD counters. Done.
  16. Add compact server-authoritative arena hazard effects, match-scoped --arena selection, replicated hazard snapshots, and smoke/HUD counters. Done.
  17. Add compact server-authoritative arena item pickup/use/throw/bomb state, replicated item snapshots, and smoke/HUD counters. Done.
  18. Add server-authored compact special lifecycle snapshots, native visual proxies, and special smoke/HUD counters. Done.
  19. Add stable compact action technique IDs and restore them into client authored visual state. Done.
  20. Add client-requested authoritative match start config and single-player server-bot online matches. Done.
  21. Apply accepted character/style/equipment loadout tuning to compact server authority and client prediction replay. Done.
  22. Move full local combat/full-specials server-authoritative via shared headless gameplay authority. Done.
  23. Replicate exact generic/Bee/Penguin special identities plus active authored hitbox snapshots and online proxies. Done.
  24. Add first-pass in-game network debug HUD counters and RTT/jitter. Done.
  25. Add packet loss, hitbox counters, and server tick-time HUD/logging. Done.
  26. Upgrade exact special proxies toward authored Bee/Penguin scene assets. Done for first-pass one-entity proxy coverage.
  27. Add server-authored transient feedback/event stream for hit sparks, SFX/camera shake, impact cues, special despawn/impact events, and hazard impacts.
  28. Finish multi-part special presentation parity where one snapshot kind is not enough.
  29. Run OrbStack local two-client test. Done.
  30. Run Linux desktop LAN test.
  31. Run friend internet playtest.

Main Risks

  • Current simulation and presentation are interleaved.
  • Current gameplay uses variable delta_secs.
  • Gameplay state contains Bevy Entity references.
  • Local setup and user mode mutate authoritative resources directly.
  • ACTIVE_ARENA_INDEX is process-global.
  • Replay seed is metadata today, not full deterministic RNG.
  • Bot behavior and some item variation use time/math patterns that are not ideal for deterministic replay.
  • Lightyear has useful features but meaningful API learning curve and some docs churn.

Mitigations:

  • Keep v1 small: 2 native players, one room, one dedicated server.
  • Server authority first, prediction second.
  • Do not build browser transport until native is fun.
  • Do not add matchmaking until direct connect works.
  • Do not add full deterministic rollback unless the game design demands it later.

Sources