SKILL.md
Multiplayer Game
**IMPORTANT: Before doing anything, you MUST read BASE_SKILL.md in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.**
Patterns for building multiplayer games with RivetKit, intended as a practical checklist you can adapt per genre.
Starter Code
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
Fortnite, Apex Legends, PUBG, Warzone
Arena
Call of Duty TDM/FFA, Halo Slayer, Counter-Strike casual, VALORANT unrated, Overwatch Quick Play, Rocket League
IO Style
Agar.io, Slither.io, surviv.io
Open World
Minecraft survival servers, Rust-like worlds, MMO zone/chunk worlds
Party
Fall Guys private lobbies, custom game rooms, social party sessions
Physics 2D
Top-down physics brawlers, 2D arena games, platform fighters
Physics 3D
Physics sandbox sessions, 3D arena games, movement playgrounds
Ranked
Chess ladders, competitive card games, duel arena ranked queues
Turn-Based
Chess correspondence, Words With Friends, async board games
Idle
Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist
Server Simulation
Game Loop And Tick Rates
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.
Physics
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
3D
@dimforge/rapier3d
cannon-es, ammo.js
Spatial Indexing
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.
Networking & State Sync
Netcode
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.
Realtime Data Model
- Snapshots and diffs: Publish state as events. Send a full snapshot on join/resync, then per-tick diffs for regular updates.
- Batch per tick: Keep events small and typed. Batch high-frequency updates per tick.
- Avoid UI framework state for game updates: Use
requestAnimationFrameor a Canvas/Three.js loop for simulation, not React state. Reserve UI framework state for menus, HUD, and forms.
- Broadcast vs per-connection: Use
c.broadcast(...)for shared updates andconn.send(...)for private/per-player data.
Shared Simulation Logic
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.
- Hybrid modes: Client runs shared movement as primary authority, server runs it for anti-cheat validation.
- Server-authoritative modes: Client uses shared logic for interpolation and prediction only.
- Keep it pure: Movement integration, input transforms, collision helpers, and constants only.
- **Put shared code in
src/shared/**: Keep deterministic helpers insrc/shared/sim/*with no side effects.
Interest Management
Control what each client receives to reduce bandwidth and prevent information leaks.
#### Per-Player Replication Filters
- Filter by relevance: Send each client only state relevant to that player (proximity, line-of-sight, team, or game phase).
- Shooters and action games: Limit replication by proximity and optional field-of-view checks.
- Server-side only: Clients should never receive data they should not see.
#### Sharded Worlds
- Partition large worlds: Use chunk actors keyed by
worldId:chunkX:chunkY.
- Subscribe to nearby chunks: Clients connect only to nearby partitions (for example a 3x3 chunk window).
- Use sparingly: Only when the world is large and state-heavy (sandbox builders, MMOs), not as a default for small matches.
Backend Infrastructure
Persistence
- In-memory state: Best for realtime game state that changes every tick (player positions, inputs, match phase, scores).
- **SQLite (
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.
Matchmaking Patterns
Common building blocks used across the architecture patterns below.
#### Actor Topology
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.
#### Queueing Strategy
- Multiple players can hit the matchmaker at the same time, so actions like find/create, queue/unqueue, and close need to be serialized through actor queues to avoid races.
- Match-local actions (gameplay, scoring) do not need queueing unless they write back to the matchmaker.
Security And Anti-Cheat
Start with this baseline, then harden further for competitive or high-risk environments.
Baseline Checklist
- Identity: Use
c.conn.idas the authoritative transport identity. TreatplayerId/usernamein params as untrusted input and bind through server-issued assignment/join tickets.
- Authorization: Validate the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions).
- Input validation: Clamp sizes/lengths, validate enums, and validate usernames (length, allowed chars, avoid unbounded Unicode).
- Rate limiting: Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates).
- State integrity: Server recomputes derived state (scores, win conditions, placements). Never allow client-authoritative changes to inventory/currency/leaderboard totals.
Movement Validation
For any mode with client-authoritative movement (hybrid flows), clients may send position/rotation updates for smoothness, but the server must:
- Enforce max delta per update (speed cap) based on elapsed time.
- Reject or clamp teleports.
- Enforce world bounds (and basic collision if applicable).
- Rate limit update frequency (for example 20Hz max).
Architecture Patterns
Each game type below starts with a quick summary table, then details actors and lifecycle.
Battle Royale
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
-
Key: matchmaker["main"]
-
Responsibility: Finds or creates lobbies, tracks pending reservations, and maintains occupancy.
-
Actions
findMatch
pendingPlayerConnected
updateMatch
closeMatch
-
Queues
findMatch
pendingPlayerConnected
updateMatch
closeMatch
-
State
- SQLite
matches
pending_players
player_countincludes connected and pending players
-
Key: match[matchId]
-
Responsibility: Runs lobby/live/finished phases, owns player state, zone progression, and eliminations.
-
Actions
connect
- Movement and combat actions
-
Queues
- None
-
State
- JSON
phase
players
zone
eliminations
snapshot data
Lifecycle
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)
Arena
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
-
Key: matchmaker["main"]
-
Responsibility: Runs mode queues, builds full matches, assigns teams, and publishes assignments.
-
Actions
queueForMatch
unqueueForMatch
matchCompleted
-
Queues
queueForMatch
unqueueForMatch
matchCompleted
-
State
- SQLite
player_pool
matches
assignmentskeyed by connection and player
-
Key: match[matchId]
-
Responsibility: Runs match phases and in-match player/team state for score and win conditions.
-
Actions
connect
- Gameplay actions
-
Queues
- None
-
State
- JSON
phase
players
team assignments
score and win state
Lifecycle
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)
IO Style
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
-
Key: matchmaker["main"]
-
Responsibility: Routes players into the fullest open lobby and tracks reservations and occupancy.
-
Actions
findLobby
pendingPlayerConnected
updateMatch
closeMatch
-
Queues
findLobby
pendingPlayerConnected
updateMatch
closeMatch
-
State
- SQLite
matches
pending_players
- Occupancy includes pending reservations
-
Key: match[matchId]
-
Responsibility: Runs per-match movement simulation and broadcasts snapshots.
-
Actions
connect
setInput
-
Queues
- None
-
State
- JSON
players
inputs
movement state
snapshot cache
Lifecycle
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)
Open World
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
- Key:
chunk[worldId,chunkX,chunkY]
- Responsibility: Owns chunk-local players, blocks, movement tick, and chunk membership.
- Actions
connect
enterChunk
addPlayer
setInput
leaveChunk
removePlayer
- Queues
- None
- State
- JSON
connections
players
blocksscoped to one chunk key
Lifecycle
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
Party
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
-
Key: matchmaker["main"]
-
Responsibility: Handles party create/join flow, validates join tickets, and tracks party size.
-
Actions
createParty
joinParty
verifyJoin
updatePartySize
closeParty
-
Queues
createParty
joinParty
verifyJoin
updatePartySize
closeParty
-
State
- SQLite
parties
join_ticketsfor party lookup and join validation
-
Key: match[matchId]
-
Responsibility: Owns party members, host role, ready flags, and phase transitions.
-
Actions
connect
startGame
finishGame
-
Queues
- None
-
State
- JSON
members
host
ready state
phase
party events
Lifecycle
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)
Joiner Flow
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)
Ranked
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
-
Key: matchmaker["main"]
-
Responsibility: Runs rating-based queueing, pairing, assignment persistence, and completion fanout.
-
Actions
queueForMatch
unqueueForMatch
matchCompleted
-
Queues
queueForMatch
unqueueForMatch
matchCompleted
-
State
- SQLite
player_pool
matches
assignmentswith rating window and connection scoping
-
Key: match[matchId]
-
Responsibility: Runs ranked match phase, score, and winner reporting.
-
Actions
connect
- Gameplay actions
-
Queues
- None
-
State
- JSON
phase
players
score
winner
completion payload
-
Key: player[username]
-
Responsibility: Stores canonical player MMR and win/loss profile.
-
Actions
initialize
getRating
applyMatchResult
-
Queues
- None
-
State
- JSON
rating
wins
losses
match counters
-
Key: leaderboard["main"]
-
Responsibility: Stores and serves top-ranked players.
-
Actions
updatePlayer
-
Queues
- None
-
State
- SQLite
- Leaderboard score rows
- Top-list ordering
Lifecycle
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
Turn-Based
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
-
Key: matchmaker["main"]
-
Responsibility: Handles private invite and public queue pairing for async matches.
-
Actions
createGame
joinByCode
queueForMatch
unqueueForMatch
closeMatch
-
Queues
createGame
joinByCode
queueForMatch
unqueueForMatch
closeMatch
-
State
- SQLite
matches
player_pool
assignmentsfor invite and queue mapping
-
Key: match[matchId]
-
Responsibility: Owns board state, turn order, move validation, and final result.
-
Actions
connect
makeMove
-
Queues
- None
-
State
- JSON
board
turns
players
connection presence
result
Lifecycle
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)
Private Invite
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)
Idle
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
-
Key: world[playerId]
-
Responsibility: Owns one player's progression, buildings, production scheduling, and state updates.
-
Actions
initialize
build
collectProduction
-
Queues
- None
-
State
- JSON
- Per-player buildings
resources
timers
progression state
-
Key: leaderboard["main"]
-
Responsibility: Stores global scores and serves leaderboard updates.
-
Actions
updateScore
-
Queues
updateScore
-
State
- SQLite
scorestable keyed by player
- Current leaderboard totals
Lifecycle
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