website/src/content/cookbook/multiplayer-game.mdx
Patterns for building multiplayer games with RivetKit, intended as a practical checklist you can adapt per genre.
Start with one of the working examples on GitHub and adapt it to your game. Do not start from scratch for matchmaking and lifecycle flows.
| Game Classification | Starter Code | Common Examples |
|---|---|---|
| Battle Royale | GitHub | Fortnite, Apex Legends, PUBG, Warzone |
| Arena | GitHub | Call of Duty TDM/FFA, Halo Slayer, Counter-Strike casual, VALORANT unrated, Overwatch Quick Play, Rocket League |
| IO Style | GitHub | Agar.io, Slither.io, surviv.io |
| Open World | GitHub | Minecraft survival servers, Rust-like worlds, MMO zone/chunk worlds |
| Party | GitHub | Fall Guys private lobbies, custom game rooms, social party sessions |
| Physics 2D | GitHub | Top-down physics brawlers, 2D arena games, platform fighters |
| Physics 3D | GitHub | Physics sandbox sessions, 3D arena games, movement playgrounds |
| Ranked | GitHub | Chess ladders, competitive card games, duel arena ranked queues |
| Turn-Based | GitHub | Chess correspondence, Words With Friends, async board games |
| Idle | GitHub | Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist |
| Pattern | Use When | Implementation Guidance |
|---|---|---|
| Fixed realtime loop | Battle Royale, Arena, IO Style, Open World, Ranked | Run in run with sleep(tickMs) and exit on c.aborted. |
| Action-driven updates | Party, Turn-Based | Mutate and broadcast only on actions/events rather than scheduled ticks. |
| Coarse offline progression | Any mode with idle progression | Use c.schedule.after(...) with coarse windows (for example 5 to 15 minutes) and apply catch-up from elapsed wall clock time. |
Start with custom kinematic logic for simple games. Switch to a full physics engine when you need joints, stacked bodies, high collision density, or complex shapes (rotated polygons, capsules, convex hulls, triangle meshes).
Pick one engine per simulation. Keep frontend-only libs out of backend simulation paths and treat server state as authoritative.
| Dimension | Primary Engine | Fallback Engines | Example Code |
|---|---|---|---|
| 2D | @dimforge/rapier2d | planck-js, matter-js | GitHub |
| 3D | @dimforge/rapier3d | cannon-es, ammo.js | GitHub |
For non-physics spatial queries, use a dedicated index instead of naive O(n^2) checks:
| Index Type | Recommendation |
|---|---|
| AABB index | For AOI, visibility, and non-collider entities, use rbush for dynamic sets or flatbush for static-ish sets. |
| Point index | For nearest-neighbor or within-radius queries, use d3-quadtree. |
| Model | When To Use | Implementation |
|---|---|---|
| Hybrid (client movement, server combat) | Shooters, action sports, ranked duels | Client owns movement and sends capped-rate position updates. Server validates for anti-cheat. Combat (projectiles, hits, damage) is fully server-authoritative. |
| Server-authoritative with interpolation | IO Style, persistent worlds | Client sends input commands. Server simulates on fixed ticks and publishes authoritative snapshots. Client interpolates between snapshots. |
| Server-authoritative (basic logic) | Turn-based, event-driven | Server validates and applies discrete actions (turns, phase transitions, votes). Client displays confirmed state. |
requestAnimationFrame or a Canvas/Three.js loop for simulation, not React state. Reserve UI framework state for menus, HUD, and forms.c.broadcast(...) for shared updates and conn.send(...) for private/per-player data.Shared simulation logic runs on both the client and the server. For example, an applyInput(state, input, dt) function that integrates velocity and clamps to world bounds can run on the client for prediction and on the server for validation.
src/shared/: Keep deterministic helpers in src/shared/sim/* with no side effects.Control what each client receives to reduce bandwidth and prevent information leaks.
worldId:chunkX:chunkY.rivetkit/db): Better for large or table-like state that needs queries, indexes, or long-term persistence (tiles, inventory, matchmaking pools). Serialize DB work through a queue since multiple actions can hit the same actor concurrently.Common building blocks used across the architecture patterns below.
| Primitive | Use When | Typical Ownership |
|---|---|---|
matchmaker["main"] + match[matchId] | Session-based multiplayer (battle royale, arena, ranked, party, turn-based) | Matchmaker owns discovery/assignment. Match owns lifecycle and gameplay state. |
chunk[worldId,chunkX,chunkY] | Large continuous worlds that need sharding | Each chunk owns local players, chunk state, and local simulation. |
world[playerId] | Per-player progression loops (idle/solo world state) | Per-player resources, buildings, timers, and progression. |
player[username] | Canonical profile/rating reused across matches | Durable player stats (for example rating and win/loss). |
leaderboard["main"] | Shared rankings across many matches/players | Global ordered score rows and top lists. |
Start with this baseline, then harden further for competitive or high-risk environments.
c.conn.id as the authoritative transport identity. Treat playerId/username in params as untrusted input and bind through server-issued assignment/join tickets.For any mode with client-authoritative movement (hybrid flows), clients may send position/rotation updates for smoothness, but the server must:
Each game type below starts with a quick summary table, then details actors and lifecycle.
| Topic | Summary |
|---|---|
| Matchmaking | Immediate routing to the fullest non-started lobby (oldest tie-break); players wait in lobby until capacity, then the match starts. |
| Netcode | Hybrid. Client owns movement, camera, and local prediction. Server owns zone state, projectiles, hit resolution, eliminations, loot, and final placement. |
| Tick Rate | 10 ticks/sec (100ms) with a fixed loop for zone progression and lifecycle checks. |
| Physics | Client owns movement with server anti-cheat validation; projectiles, hits, and damage are server-authoritative. Use @dimforge/rapier3d for 3D or @dimforge/rapier2d for top-down 2D. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]findMatchpendingPlayerConnectedupdateMatchcloseMatchfindMatchpendingPlayerConnectedupdateMatchcloseMatchmatchespending_playersplayer_count includes connected and pending playersmatch[matchId]connectphaseplayerszoneeliminationssnapshot dataLifecycle
sequenceDiagram
participant C as Client
participant MM as matchmaker
participant M as match
C->>MM: findMatch()
alt no open lobby
MM->>M: create(matchId)
end
MM-->>C: {matchId, playerId}
C->>M: connect(playerId)
M->>MM: pendingPlayerConnected(matchId, playerId)
MM-->>M: accepted
Note over M: lobby countdown -> live
M-->>C: snapshot + shoot events
M->>MM: closeMatch(matchId)
| Topic | Summary |
|---|---|
| Matchmaking | Mode-based fixed-capacity queues (duo, squad, ffa) that build only full matches and pre-assign teams (except FFA). |
| Netcode | Hybrid. Client owns movement plus prediction and smoothing. Server owns team or FFA assignment, projectiles, hit resolution, phase transitions, and scoring. |
| Tick Rate | 20 ticks/sec (50ms) with a tighter loop for live team and FFA snapshots. |
| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/entities. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]queueForMatchunqueueForMatchmatchCompletedqueueForMatchunqueueForMatchmatchCompletedplayer_poolmatchesassignments keyed by connection and playermatch[matchId]connectphaseplayersteam assignmentsscore and win stateLifecycle
sequenceDiagram
participant C as Client
participant MM as matchmaker
participant M as match
C->>MM: queueForMatch(mode)
Note over MM: enqueue in player_pool
Note over MM: fill when capacity reached
MM->>M: create(matchId, assignments)
Note over MM: persist assignments
MM-->>C: assignmentReady
C->>M: connect(playerId)
Note over M: waiting -> live when all players connect
M->>MM: matchCompleted(matchId)
| Topic | Summary |
|---|---|
| Matchmaking | Open-lobby routing to the fullest room below capacity; room counts are heartbeated and new lobbies are auto-created when needed. |
| Netcode | Server-authoritative with interpolation. Client sends input intents and interpolates. Server owns movement, bounds, room membership, and canonical snapshots. |
| Tick Rate | 10 ticks/sec (100ms) with lightweight periodic room snapshots. |
| Physics | Low to medium intensity; server-authoritative kinematic movement, escalating to a physics engine only when collisions get complex. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]findLobbypendingPlayerConnectedupdateMatchcloseMatchfindLobbypendingPlayerConnectedupdateMatchcloseMatchmatchespending_playersmatch[matchId]connectsetInputplayersinputsmovement statesnapshot cacheLifecycle
sequenceDiagram
participant C as Client
participant MM as matchmaker
participant M as match
C->>MM: findLobby()
alt no open lobby
MM->>M: create(matchId)
end
MM-->>C: {matchId, playerId}
C->>M: connect(playerId)
M->>MM: pendingPlayerConnected(matchId, playerId)
MM-->>M: accepted
Note over M: fixed tick simulation
M-->>C: snapshot events
M->>MM: closeMatch(matchId)
| Topic | Summary |
|---|---|
| Matchmaking | Client-driven chunk routing from world coordinates, with nearby chunk windows preloaded via adjacent chunk connections. |
| Netcode | Hybrid for sandbox (client movement with validation) or server-authoritative for MMO-like flows. Server owns chunk routing, persistence, and canonical world state. |
| Tick Rate | 10 ticks/sec per chunk actor (100ms), so load scales with active chunks. |
| Physics | Medium to high at scale; chunk-local simulation can be server-authoritative (MMO-like) or client movement with server validation (sandbox-like). |
Actors
<AccordionGroup> <Accordion title="chunk[worldId,chunkX,chunkY]">chunk[worldId,chunkX,chunkY]connectenterChunkaddPlayersetInputleaveChunkremovePlayerconnectionsplayersblocks scoped to one chunk keyLifecycle
sequenceDiagram
participant C as Client
participant CH as chunk
Note over C: resolve chunk keys from world position
loop each visible chunk
C->>CH: connect(worldId, chunkX, chunkY, playerId)
Note over CH: store connection metadata
end
C->>CH: enterChunk/addPlayer
loop movement updates
C->>CH: setInput(...)
CH-->>C: snapshot
end
C->>CH: leaveChunk/removePlayer or disconnect
Note over CH: remove membership and metadata
| Topic | Summary |
|---|---|
| Matchmaking | Host-created private party flow using party codes and explicit joins. |
| Netcode | Server-authoritative (basic logic). Server owns membership, host permissions, and phase transitions. |
| Tick Rate | No continuous tick; updates are event-driven (join, start, finish). |
| Physics | Low intensity for lobby-first flows; usually no dedicated physics or indexing unless you add realtime mini-games. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]createPartyjoinPartyverifyJoinupdatePartySizeclosePartycreatePartyjoinPartyverifyJoinupdatePartySizeclosePartypartiesjoin_tickets for party lookup and join validationmatch[matchId]connectstartGamefinishGamemembershostready statephaseparty eventsLifecycle
<Tabs> <Tab title="Host Flow">sequenceDiagram
participant H as Host Client
participant MM as matchmaker
participant M as match
H->>MM: createParty()
MM-->>H: {matchId, partyCode, playerId, joinToken}
H->>M: connect(playerId, joinToken)
M->>MM: verifyJoin(...)
MM-->>M: allowed
M->>MM: updatePartySize(playerCount)
H->>M: startGame() / finishGame()
M->>MM: closeParty(matchId)
sequenceDiagram
participant J as Joiner Client
participant MM as matchmaker
participant M as match
J->>MM: joinParty(partyCode)
MM-->>J: {matchId, playerId, joinToken}
J->>M: connect(playerId, joinToken)
M->>MM: verifyJoin(...)
MM-->>M: allowed / denied
M->>MM: updatePartySize(playerCount)
| Topic | Summary |
|---|---|
| Matchmaking | ELO-based queue pairing with a widening search window as wait time increases. |
| Netcode | Hybrid. Client owns movement with local prediction and interpolation. Server owns projectiles, hit resolution, match results, and rating updates. |
| Tick Rate | 20 ticks/sec (50ms) with fixed live ticks for deterministic pacing and broadcast cadence. |
| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/hit resolution. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]queueForMatchunqueueForMatchmatchCompletedqueueForMatchunqueueForMatchmatchCompletedplayer_poolmatchesassignments with rating window and connection scopingmatch[matchId]connectphaseplayersscorewinnercompletion payloadplayer[username]initializegetRatingapplyMatchResultratingwinslossesmatch countersleaderboard["main"]updatePlayerLifecycle
sequenceDiagram
participant C as Client
participant MM as matchmaker
participant P as player
participant M as match
participant LB as leaderboard
C->>MM: queueForMatch(username)
MM->>P: initialize/getRating
P-->>MM: rating
Note over MM: store queue row + retry pairing
MM->>M: create(matchId, assigned players)
MM-->>C: assignmentReady
C->>M: connect(username)
M->>MM: matchCompleted(...)
MM->>P: applyMatchResult(...)
MM->>LB: updatePlayer(...)
Note over MM: remove matches + assignments rows
| Topic | Summary |
|---|---|
| Matchmaking | Async private-invite and public-queue pairing in the same pattern. |
| Netcode | Server-authoritative (basic logic). Client can draft moves before submit. Server owns turn ownership, committed move log, turn order, and completion state. |
| Tick Rate | No continuous tick; move submission and turn transitions drive updates. |
| Physics | Very low intensity; no realtime physics loop, just discrete rules validation. Indexing is optional and mostly for board or query convenience at scale. |
Actors
<AccordionGroup> <Accordion title='matchmaker["main"]'>matchmaker["main"]createGamejoinByCodequeueForMatchunqueueForMatchcloseMatchcreateGamejoinByCodequeueForMatchunqueueForMatchcloseMatchmatchesplayer_poolassignments for invite and queue mappingmatch[matchId]connectmakeMoveboardturnsplayersconnection presenceresultLifecycle
<Tabs> <Tab title="Public Queue">sequenceDiagram
participant A as Client A
participant B as Client B
participant MM as matchmaker
participant M as match
A->>MM: queueForMatch()
B->>MM: queueForMatch()
Note over MM: pair first two queued players
MM->>M: create(matchId) + seed X/O players
MM-->>A: assignment/match info
MM-->>B: assignment/match info
A->>M: connect(playerId)
B->>M: connect(playerId)
A->>M: makeMove()
B->>M: makeMove()
opt all players disconnected for timeout
Note over M: destroy after idle timeout
end
M->>MM: closeMatch(matchId)
sequenceDiagram
participant A as Client A
participant B as Client B
participant MM as matchmaker
participant M as match
A->>MM: createGame()
MM-->>A: {matchId, playerId, inviteCode}
B->>MM: joinByCode(inviteCode)
MM->>M: create(matchId) + seed X/O players
MM-->>A: assignment/match info
MM-->>B: assignment/match info
A->>M: connect(playerId)
B->>M: connect(playerId)
A->>M: makeMove()
B->>M: makeMove()
M->>MM: closeMatch(matchId)
| Topic | Summary |
|---|---|
| Matchmaking | No matchmaker; each player uses a direct per-player actor and a shared leaderboard actor. |
| Netcode | Server-authoritative (basic logic). Client owns UI and build intent. Server owns resources, production rates, building validation, and leaderboard totals. |
| Tick Rate | No continuous tick; use c.schedule.after(...) for coarse intervals and compute offline catch-up from elapsed wall time. |
| Physics | None for standard idle loops; transitions are discrete (build, collect, upgrade) and do not need spatial indexing. |
Actors
<AccordionGroup> <Accordion title="world[playerId]">world[playerId]initializebuildcollectProductionresourcestimersprogression stateleaderboard["main"]updateScoreupdateScorescores table keyed by playerLifecycle
sequenceDiagram
participant C as Client
participant W as world
participant LB as leaderboard
C->>W: getOrCreate(playerId) + initialize()
Note over W: seed state + schedule collection
W-->>C: stateUpdate
loop gameplay loop
C->>W: build() / collectProduction()
W->>LB: updateScore(...)
Note over LB: upsert scores
LB-->>C: leaderboardUpdate
W-->>C: stateUpdate
end