Repository navigation
Multiplayer and Netcode
Two browsers connect directly, run the same simulation and exchange only button presses. When a press arrives late, the game quietly rewinds a few frames, replays them with the right input and catches up. That is why online matches feel close to local ones.
两个浏览器直接相连,各自算同一局,只互相发按键;某一帧的按键来晚了,就悄悄退回几帧用正确的按键重算再追上。所以联机手感接近本机。
flowchart LR
A["Host browser"] <-->|"WebRTC DataChannel (inputs, checksums)"| B["Guest browser"]
A -.->|"room code handshake"| P["PeerJS broker"]
B -.-> P
A -.->|"/api/ice: short-lived TURN credentials"| W["Cloudflare Worker"]
B -.-> W
- The host creates a room. The 4-character room code is registered with the PeerJS broker, which only introduces the two browsers.
- Both sides gather network candidates with public STUN servers and open a WebRTC DataChannel directly.
- On networks that block direct links (school, office, symmetric NAT) the page asks the Worker for short-lived TURN credentials and relays through TURN instead. Without the Worker (offline edition, server down) only STUN is used.
- Both sides must run the same game version (
VERSIONinsrc/game/01-core.js); a mismatch is reported before the match starts.
Code: src/net-p2p.js (connection), src/game/06-input-net.js (inputs), src/game/20-tutorial-rollback-touch.js (rollback and resync).
- The game logic runs at a fixed 120 steps per second (
STEPinsrc/game/01-core.js), independent of the screen refresh rate. - Everything that affects the match uses a seeded random generator (
grnd()), neverMath.random. Visual-only effects may differ between the two sides; the fight itself may not. - Each side keeps recent snapshots of the match state in a ring buffer. When a remote input arrives for a past frame, the game restores that snapshot, re-simulates up to the present with the correct input (
RESIM), and draws the result. - Platforms, wind, lightning, clones, puppet balls and every other piece of match state live in the snapshot, so maps and summons rewind correctly too.
- The two sides compare a state checksum periodically. On a mismatch the host sends its full state and the guest adopts it, so the match carries on in sync.
所有影响对局的随机数都用同一个种子;最近的每一帧都存着快照,远端按键晚到就恢复快照重算;双方定期比对校验和,对不上时房主把整份状态发过去,对方直接采用。
Room rules are listed in ROOM_MODES in src/data/10-modes.js:
| Room | Players | Notes |
|---|---|---|
normal |
1 vs 1 | 3v3 relay with snake draft |
chaos, mayhem, maps
|
1 vs 1 | Special rules; in maps both sides vote for the map |
pp_cc |
2 humans vs 2 AI | 2v2 co-op |
pp_pp |
2 vs 2 humans | Four browsers |
raid |
Host + friend + AI ally vs boss | Co-op raid |
Matchmaking and ranked queues run on the Worker; once two players are matched, the match itself is still peer-to-peer.
The open world (江湖共斗) is different: it runs on the server.
- Each world line is one Cloudflare Durable Object holding up to 40 players. When a line is full, new players go to the next line, so there is no total limit. You can type a line number to join a friend.
- Monsters and the world boss run on the server. Each browser simulates only its own hero, so controls feel like single player.
- Each player only receives players and monsters within 2,000 px. The server caps damage per hit and per second.
- The server-wide story (江湖长卷) is one shared row in D1. Lines report contributions every 20 seconds; a report sent twice counts once.
| Command | Checks |
|---|---|
node tools/rollback-test.mjs --rollback |
Snapshot, restore and re-simulation give identical results |
node tools/netplay-test.mjs |
Two game copies over a laggy, jittery link end every match identically |
node tools/world-test.mjs |
Open world line: player cap, view range, damage caps, world boss, restarts |
刃影 RenYing 1.0.1 · Play / 游玩 · Repository / 仓库 · Report a bug / 报告问题 · MIT License