From game-dev
Builds server-authoritative multiplayer game backends using Elysia and Bun: WebSocket connections, room management, fixed-timestep tick loops, REST APIs, and Redis pub/sub scaling.
npx claudepluginhub fcsouza/agent-skills --plugin standalone-skillsThis skill uses the workspace's default tool permissions.
WebSocket + REST patterns, room/session management, game tick loops, genre-agnostic server design using Elysia + Bun. Covers server-authoritative architecture, fixed-timestep simulation, room lifecycle, typed message protocols, and horizontal scaling via Redis pub/sub.
Provides expertise in real-time multiplayer game networking: lag compensation, state synchronization, authoritative servers, rollback netcode, matchmaking, and anti-cheat.
Game server architecture, scalability, matchmaking, and backend systems for online games. Build robust, scalable multiplayer infrastructure.
Adds real-time or turn-based multiplayer to existing Phaser 3 or Three.js browser games using PartyKit on Cloudflare Durable Objects. Scaffolds room-based server, NetworkManager client, EventBus events, GameState fields, and extends render_game_to_text().
Share bugs, ideas, or general feedback.
WebSocket + REST patterns, room/session management, game tick loops, genre-agnostic server design using Elysia + Bun. Covers server-authoritative architecture, fixed-timestep simulation, room lifecycle, typed message protocols, and horizontal scaling via Redis pub/sub.
Trigger: game server, backend architecture, WebSocket, room management, game loop, tick system, server-authoritative, real-time, Elysia game server, multiplayer backend, session management, game tick, fixed timestep server
postgres-game-schema — database layer for persistent game stateredis-game-patterns — caching, pub/sub, presence, and ephemeral state"I'm not interested in creating a game that does things for the player. I want to create a simulation that responds to the player." — Will Wright
"A game is a series of interesting decisions." — Sid Meier
The server is a simulation system that responds to player intentions and produces meaningful state changes. Every design decision should preserve determinism, enforce authority, and keep the door open for any genre.
Start with templates/message-types.ts. Define discriminated unions for every client-to-server and server-to-client message. This is the contract between frontend and backend — get it right first.
import type { ClientMessage, ServerMessage } from './templates/message-types';
Use boilerplate/server.ts as the entry point. It wires together:
bun run boilerplate/server.ts
Use boilerplate/room-manager.ts. The room manager handles:
Each room owns its game state and tick loop instance.
Use boilerplate/game-loop.ts. The GameLoop class provides:
Attach one GameLoop per room. The tick callback receives dt in seconds and updates room state.
After each tick, diff the room state and broadcast deltas to all room members:
room.gameLoop.onTick = (dt) => {
room.state = updateGameState(room.state, dt);
const delta = computeDelta(room.previousState, room.state);
if (delta) {
room.broadcast({ type: 'state_update', roomId: room.id, state: delta, tick: room.tick });
}
room.previousState = structuredClone(room.state);
room.tick++;
};
When running multiple Elysia instances behind a load balancer:
import { Redis } from 'ioredis';
const pub = new Redis(process.env.REDIS_URL!);
const sub = new Redis(process.env.REDIS_URL!);
sub.subscribe('game:broadcast', 'game:direct');
sub.on('message', (channel, message) => {
const parsed = JSON.parse(message);
if (channel === 'game:broadcast') {
server.publish(parsed.channel, parsed.data);
}
if (channel === 'game:direct') {
const ws = connections.get(parsed.userId);
if (ws) ws.send(parsed.data);
}
});
ping every 30 secondspong + server timestampconst TICK_RATE = 20;
const TICK_DURATION_MS = 1000 / TICK_RATE;
const loop = new GameLoop(TICK_RATE, (dt) => {
for (const room of roomManager.getActiveRooms()) {
room.update(dt);
room.broadcastState();
}
});
loop.start();
const processAction = (
room: Room,
playerId: string,
action: { type: string; payload: Record<string, unknown> },
): ActionResult => {
// 1. Validate: is this action legal in current state?
const validation = validateAction(room.state, playerId, action);
if (!validation.valid) {
return { success: false, error: validation.reason };
}
// 2. Execute: apply to authoritative state
const events = applyAction(room.state, playerId, action);
// 3. Broadcast: notify all room members of resulting events
for (const event of events) {
room.broadcast({
type: 'game_event',
event: event.type,
data: event.data,
tick: room.tick,
});
}
return { success: true, events };
};
const updateRoomState = (room: Room, dt: number) => {
// Run all registered systems in order
for (const system of room.systems) {
system.update(room.state, dt);
}
// Process queued player actions
while (room.actionQueue.length > 0) {
const { playerId, action } = room.actionQueue.shift()!;
processAction(room, playerId, action);
}
room.tick++;
};
const handleMessage = (ws: GameWebSocket, data: ClientMessage) => {
const { playerId } = ws.data;
switch (data.type) {
case 'join_room':
roomManager.joinRoom(data.roomId, playerId, ws);
break;
case 'leave_room':
roomManager.leaveRoom(data.roomId, playerId);
break;
case 'action':
roomManager.queueAction(data.roomId, playerId, data.action);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', serverTime: Date.now() }));
break;
}
};
const computeDelta = (
previous: Record<string, unknown>,
current: Record<string, unknown>,
): Record<string, unknown> | null => {
const delta: Record<string, unknown> = {};
let hasChanges = false;
for (const key of Object.keys(current)) {
if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
delta[key] = current[key];
hasChanges = true;
}
}
return hasChanges ? delta : null;
};
redis-game-patterns — presence storage, pub/sub for cross-server communication, room state caching, rate limitingbullmq-game-queues — offload heavy computations (matchmaking, leaderboard recalc, scheduled events) to background workersgame-state-sync — client-side state reconciliation, interpolation, optimistic updates, rollbackpostgres-game-schema — persistent storage for player profiles, inventory, match history, room configurationsWrong: client sends "my position is (100, 200)" and server trusts it. Right: client sends "move north" intention, server validates movement speed, collision, and updates position.
Wrong: using wall-clock delta time directly in game formulas.
Right: fixed-timestep accumulator — game logic always sees the same dt, regardless of how fast the server runs.
Wrong: single shared game state object for all players. Right: each room owns its state, tick loop, and player set. Rooms are independent simulation contexts.
Wrong: await db.update(players).set(...) inside the tick callback.
Right: batch state changes and persist asynchronously outside the tick loop (e.g., every N ticks or on room close).
Wrong: sending the entire game state to every player on every tick. Right: compute deltas and only send what changed. Use interest management to filter updates per player.
Wrong: accumulator grows unbounded when server lags, causing hundreds of catch-up ticks. Right: cap accumulated time (e.g., 1 second max) to prevent spiral of death.
Wrong: game systems import WebSocket types and send messages directly. Right: game systems produce events, a separate broadcast layer translates events to WebSocket messages.
Wrong: rooms disappear instantly when the last player leaves, losing unsaved state. Right: transition through closing state — persist state, notify players, clean up resources, then destroy.
Will Wright's Simulation Thinking: the server is not a script executor — it is a simulation. Players interact with systems that have emergent behavior. The server defines rules and constraints; the interesting outcomes arise from player interactions within those rules. Design your tick loop as a simulation step, not a sequence of hardcoded events.
Sid Meier's Interesting Decisions: every message from the client represents a decision. The server's job is to make that decision meaningful by enforcing constraints (can you afford this action?), producing consequences (what happens as a result?), and communicating outcomes (what changed?). If an action has no validation and no consequence, it is not an interesting decision — remove it or make it matter.
Practical Implications: