Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/next-js/next.config.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rivetkit-typescript/packages/rivetkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
"tar": "^7.5.0",
"uuid": "^12.0.0",
"vbare": "^0.0.4",
"wa-sqlite": "^1.0.0",
"@rivetkit/sqlite": "^0.1.0",
"zod": "^4.1.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion rivetkit-typescript/packages/sqlite-vfs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"dependencies": {
"@rivetkit/bare-ts": "^0.6.2",
"wa-sqlite": "^1.0.0",
"@rivetkit/sqlite": "^0.1.0",
"vbare": "^0.0.4"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

// Note: wa-sqlite VFS.Base type definitions have incorrect types for xRead/xWrite
// The actual runtime uses Uint8Array, not the {size, value} object shown in types
import * as VFS from "wa-sqlite/src/VFS.js";
import * as VFS from "@rivetkit/sqlite/src/VFS.js";

import SQLiteESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs";
import { Factory } from "wa-sqlite";
import SQLiteESMFactory from "@rivetkit/sqlite/dist/wa-sqlite-async.mjs";
import { Factory } from "@rivetkit/sqlite";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { CHUNK_SIZE, getMetaKey, getChunkKey } from "./kv";
Expand Down Expand Up @@ -235,7 +235,7 @@ export class SqliteVfs {
this.#initPromise = (async () => {
// Load WASM binary (Node.js environment)
const require = createRequire(import.meta.url);
const wasmPath = require.resolve("wa-sqlite/dist/wa-sqlite-async.wasm");
const wasmPath = require.resolve("@rivetkit/sqlite/dist/wa-sqlite-async.wasm");
const wasmBinary = readFileSync(wasmPath);

// Initialize wa-sqlite module - each instance gets its own module
Expand Down
6 changes: 3 additions & 3 deletions rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
declare module "wa-sqlite" {
declare module "@rivetkit/sqlite" {
export function Factory(module: any): any;
}

declare module "wa-sqlite/src/VFS.js" {
declare module "@rivetkit/sqlite/src/VFS.js" {
export class Base {
handleAsync(fn: () => Promise<number>): number;
}
Expand All @@ -20,7 +20,7 @@ declare module "wa-sqlite/src/VFS.js" {
export const SQLITE_OPEN_READWRITE: number;
}

declare module "wa-sqlite/dist/wa-sqlite-async.mjs" {
declare module "@rivetkit/sqlite/dist/wa-sqlite-async.mjs" {
const factory: (config?: { wasmBinary?: ArrayBuffer }) => Promise<any>;
export default factory;
}
153 changes: 79 additions & 74 deletions website/src/content/cookbook/multiplayer-game.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,21 @@ Start with one of the working examples on [GitHub](https://github.com/rivet-dev/
| Turn-Based | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/turn-based/) | Chess correspondence, Words With Friends, async board games |
| Idle | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/idle/) | Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist |

## Server Simulation

## Game Loop And Tick Rates
### Game Loop And Tick Rates

Per-pattern tick rate recommendations are listed in the summary bullets at the start of each architecture pattern.

### Implementing Tick Loops
#### Implementing Tick Loops

| 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. |

## Realtime Data Model

- Publish game state from the server as events, using snapshots and/or diffs. In hybrid modes, this covers combat, entities, and other players. In server-authoritative modes, this covers everything including movement.
- Example pattern: send a full snapshot on join/resync, then send per-tick diffs for regular updates.
- Keep events small and typed. For high-frequency updates, batch per tick.
- Avoid using UI-framework state/hooks for high-frequency game updates. Keep simulation and render loops in engine code, and use framework state for basic UI only (menus, HUD, forms), especially with Canvas or Three.js renderers.
- When all players should receive the same update, use `c.broadcast(...)`. When players should receive different/private data, send per-connection payloads with `conn.send(...)`.
- Frontend bundling: place shared types, constants, and pure helpers in `src/shared/` (for example `src/shared/sim/*`). Avoid importing backend actor modules into browser bundles, since backend modules often include Node-only dependencies (`fs`, SQLite bindings, etc).

## Interest Management

### Per-Player Replication Filters

- Send each client only the state relevant to that player (for example by proximity, line-of-sight, team, or game phase).
- For shooters and action games, limit replication by proximity and optional field-of-view checks.
- Keep filtering server-side so clients never receive data they should not see.

### Sharded Worlds

- Partition very large worlds across data actors (for example chunk actors keyed by `worldId:chunkX:chunkY`).
- Clients subscribe only to nearby partitions (for example a 3x3 chunk window around the player).
- Use this when you are confident the world is large and state-heavy (for example sandbox builders or MMOs), not as a default for small matches.

## Netcode

Per-pattern netcode recommendations live under each architecture pattern in `#### Netcode`.

| Model | When To Use | How To Implement (Basic) |
| --- | --- | --- |
| Hybrid (client movement, server combat) | Latency-sensitive competitive games where movement responsiveness is critical but combat integrity matters (shooters, action sports, ranked duels). | Client owns movement locally and sends position updates at a capped rate. Server validates speed, bounds, and collision for anti-cheat. Projectiles, hits, damage, and other game entities are fully server-authoritative. Server simulates combat from its own state and broadcasts results. |
| Server-authoritative with interpolation | Realtime games where fairness and simplicity outweigh instant local feedback (IO Style, persistent worlds). | Client sends input commands. Server simulates on fixed ticks, resolves movement and collision, and publishes authoritative snapshots. Client interpolates between snapshots and extrapolates briefly on missed updates. |
| Server-authoritative (basic logic) | Event-driven or turn-based games with no realtime simulation. | Server validates and applies discrete actions (turns, phase transitions, votes). No continuous tick loop needed. Client displays confirmed server state. |

## Shared Simulation Logic

Use this for logic shared between client and server.

- Keep shared logic basic and pure (movement integration, input transforms, constants).
- Put shared code in `src/shared/`, and keep deterministic simulation helpers in `src/shared/sim/*` with no side effects.
- Do not share actor runtime code (DB access, network calls, `c.*` context, timers).
- In hybrid modes, the client runs shared movement logic as the primary authority and the server runs it for anti-cheat validation. In server-authoritative modes, the client uses shared logic for interpolation and prediction only.

## Physics And Spatial Indexing
### Physics And Spatial Indexing

Per-pattern physics recommendations and optional spatial indexing notes are listed in the summary tables at the start of each architecture pattern.

Expand All @@ -105,40 +64,53 @@ Per-pattern physics recommendations and optional spatial indexing notes are list
| Backend/runtime boundary | Keep frontend-only libs out of backend simulation paths; treat server state as authoritative. |
| Client mesh raycasts | `three-mesh-bvh` is optional and mostly for client-side fast raycasts on dense static meshes. |

## Security And Anti-Cheat
## Networking & State Sync

Start with this baseline, then harden further for competitive or high-risk environments.
### Netcode

### Baseline Checklist
Per-pattern netcode recommendations live under each architecture pattern in `#### Netcode`.

Apply this checklist to all implementations.
| Model | When To Use | How To Implement (Basic) |
| --- | --- | --- |
| Hybrid (client movement, server combat) | Latency-sensitive competitive games where movement responsiveness is critical but combat integrity matters (shooters, action sports, ranked duels). | Client owns movement locally and sends position updates at a capped rate. Server validates speed, bounds, and collision for anti-cheat. Projectiles, hits, damage, and other game entities are fully server-authoritative. Server simulates combat from its own state and broadcasts results. |
| Server-authoritative with interpolation | Realtime games where fairness and simplicity outweigh instant local feedback (IO Style, persistent worlds). | Client sends input commands. Server simulates on fixed ticks, resolves movement and collision, and publishes authoritative snapshots. Client interpolates between snapshots and extrapolates briefly on missed updates. |
| Server-authoritative (basic logic) | Event-driven or turn-based games with no realtime simulation. | Server validates and applies discrete actions (turns, phase transitions, votes). No continuous tick loop needed. Client displays confirmed server state. |

- Identity:
- Use `c.conn.id` as the authoritative transport identity of the caller.
- If you accept `playerId`/`username` in params, treat it as untrusted input and bind it through server-issued assignment/join tickets before attaching player state.
- Authorization:
- Validate that the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions).
- Input validation:
- Clamp sizes/lengths and validate enums.
- 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).
- Avoid client-authoritative changes to inventory/currency/leaderboard totals.
### Realtime Data Model

### Movement Validation
- Publish game state from the server as events, using snapshots and/or diffs. In hybrid modes, this covers combat, entities, and other players. In server-authoritative modes, this covers everything including movement.
- Example pattern: send a full snapshot on join/resync, then send per-tick diffs for regular updates.
- Keep events small and typed. For high-frequency updates, batch per tick.
- Avoid using UI-framework state/hooks for high-frequency game updates. Keep simulation and render loops in engine code, and use framework state for basic UI only (menus, HUD, forms), especially with Canvas or Three.js renderers.
- When all players should receive the same update, use `c.broadcast(...)`. When players should receive different/private data, send per-connection payloads with `conn.send(...)`.
- Frontend bundling: place shared types, constants, and pure helpers in `src/shared/` (for example `src/shared/sim/*`). Avoid importing backend actor modules into browser bundles, since backend modules often include Node-only dependencies (`fs`, SQLite bindings, etc).

Use this for any mode with client-authoritative movement (hybrid and client-authoritative flows).
### Shared Simulation Logic

Clients may send position/rotation updates for smoothness, but the server must:
Use this for logic shared between client and server.

- 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).
- Keep shared logic basic and pure (movement integration, input transforms, constants).
- Put shared code in `src/shared/`, and keep deterministic simulation helpers in `src/shared/sim/*` with no side effects.
- Do not share actor runtime code (DB access, network calls, `c.*` context, timers).
- In hybrid modes, the client runs shared movement logic as the primary authority and the server runs it for anti-cheat validation. In server-authoritative modes, the client uses shared logic for interpolation and prediction only.

## Persistence
### Interest Management

#### Per-Player Replication Filters

- Send each client only the state relevant to that player (for example by proximity, line-of-sight, team, or game phase).
- For shooters and action games, limit replication by proximity and optional field-of-view checks.
- Keep filtering server-side so clients never receive data they should not see.

#### Sharded Worlds

- Partition very large worlds across data actors (for example chunk actors keyed by `worldId:chunkX:chunkY`).
- Clients subscribe only to nearby partitions (for example a 3x3 chunk window around the player).
- Use this when you are confident the world is large and state-heavy (for example sandbox builders or MMOs), not as a default for small matches.

## Backend Infrastructure

### Persistence

Use this section to decide when SQLite is a better fit than in-memory actor state.

Expand All @@ -150,11 +122,11 @@ Use this section to decide when SQLite is a better fit than in-memory actor stat
- Matchmakers/coordinators may also use SQLite when their indexing state can grow large (room registries, matchmaking pools, large queues).
- When using `rivetkit/db`, assume multiple actions can hit the same actor DB concurrently. Serialize DB work through a queue.

## Matchmaking Orchestration Primitives
### Matchmaking Orchestration Primitives

Use these cross-cutting primitives as a baseline before picking a specific architecture pattern.

### Actor Topology Primitives
#### Actor Topology Primitives

| Primitive | Use When | Typical Ownership |
| --- | --- | --- |
Expand All @@ -164,7 +136,7 @@ Use these cross-cutting primitives as a baseline before picking a specific archi
| `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. |

### Matchmaking Building Blocks
#### Matchmaking Building Blocks

- **Queue row**: A pending intent to match (mode/rating/capacity metadata).
- **Assignment row**: Binds a connection/player to a specific `matchId`.
Expand All @@ -173,13 +145,46 @@ Use these cross-cutting primitives as a baseline before picking a specific archi
- **Occupancy heartbeat**: Match reports connected and pending counts back to matchmaker.
- **Close/cleanup flow**: Remove queue, assignment, and reservation rows when a match ends.

### Queueing Strategy
#### Queueing Strategy

- Prefer actor queues to serialize DB mutations on shared matchmaking tables.
- Queue matchmaker actions that can race: find/create, queue/unqueue, verify/pending-connected, update/close.
- Keep queueing on the actor that owns the shared index tables (usually the matchmaker).
- Let match-local actions run without queueing unless they mutate shared matchmaking indexes.

## Security And Anti-Cheat

Start with this baseline, then harden further for competitive or high-risk environments.

### Baseline Checklist

Apply this checklist to all implementations.

- Identity:
- Use `c.conn.id` as the authoritative transport identity of the caller.
- If you accept `playerId`/`username` in params, treat it as untrusted input and bind it through server-issued assignment/join tickets before attaching player state.
- Authorization:
- Validate that the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions).
- Input validation:
- Clamp sizes/lengths and validate enums.
- 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).
- Avoid client-authoritative changes to inventory/currency/leaderboard totals.

### Movement Validation

Use this for any mode with client-authoritative movement (hybrid and client-authoritative 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.
Expand Down
Loading