| name | realtime-multiplayer |
| description | Real-time multiplayer game networking with Socket.io. Use when implementing WebSocket connections, game state synchronization, room management, reconnection handling, or optimistic updates. Covers latency compensation and conflict resolution. |
Real-Time Multiplayer Skill
Overview
This skill provides expertise for building real-time multiplayer games using WebSockets and Socket.io. It covers connection management, state synchronization, latency handling, and the specific challenges of turn-based games with real-time updates.
Core Architecture
Client-Server Model for Games
┌─────────────┐ WebSocket ┌─────────────┐
│ Client │◄──────────────────►│ Server │
│ (Browser) │ │ (Node.js) │
└─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Local UI │ │ Game State │
│ State │ │ (Source │
│ (Optimistic) │ of Truth) │
└─────────────┘ └─────────────┘
Key Principle: The server is the authoritative source of truth. Clients can have optimistic local state for responsiveness, but server state always wins on conflict.
Socket.io Setup Pattern
// Server setup
const io = require('socket.io')(server, {
cors: { origin: process.env.CLIENT_URL },
pingTimeout: 60000,
pingInterval: 25000
});
io.on('connection', (socket) => {
// Join game room
socket.on('join-game', ({ gameId, playerId }) => {
socket.join(`game:${gameId}`);
socket.gameId = gameId;
socket.playerId = playerId;
});
// Handle game actions
socket.on('game-action', async (action) => {
const result = await processAction(socket.gameId, socket.playerId, action);
if (result.success) {
// Broadcast to all players in game
io.to(`game:${socket.gameId}`).emit('state-update', result.newState);
} else {
// Send error only to acting player
socket.emit('action-error', result.error);
}
});
// Handle disconnection
socket.on('disconnect', () => {
handlePlayerDisconnect(socket.gameId, socket.playerId);
});
});
Room Management
Game Rooms Pattern
Each game instance should be a Socket.io room:
// Room naming convention
const roomName = `game:${gameId}`;
// Player joins game
socket.join(roomName);
// Broadcast to all players in game
io.to(roomName).emit('event', data);
// Send to specific player
io.to(playerSocketId).emit('private-event', data);
// Send to all except sender
socket.to(roomName).emit('event', data);
Player Presence Tracking
const gamePresence = new Map(); // gameId -> Set of playerIds
function trackPresence(gameId, playerId, isOnline) {
if (!gamePresence.has(gameId)) {
gamePresence.set(gameId, new Set());
}
const players = gamePresence.get(gameId);
if (isOnline) {
players.add(playerId);
} else {
players.delete(playerId);
}
// Notify other players
io.to(`game:${gameId}`).emit('presence-update', {
playerId,
isOnline,
onlinePlayers: Array.from(players)
});
}
State Synchronization
Event Types
Define clear event categories:
// Server -> Client events
const ServerEvents = {
STATE_SYNC: 'state-sync', // Full state (on join/reconnect)
STATE_UPDATE: 'state-update', // Partial state change
ACTION_RESULT: 'action-result', // Response to player action
PLAYER_JOINED: 'player-joined',
PLAYER_LEFT: 'player-left',
GAME_STARTED: 'game-started',
TURN_CHANGED: 'turn-changed',
GAME_ENDED: 'game-ended'
};
// Client -> Server events
const ClientEvents = {
JOIN_GAME: 'join-game',
LEAVE_GAME: 'leave-game',
GAME_ACTION: 'game-action',
REQUEST_SYNC: 'request-sync',
PING: 'ping'
};
Delta Updates vs Full Sync
// Send delta updates for efficiency
function sendDelta(gameId, changes) {
io.to(`game:${gameId}`).emit('state-update', {
type: 'delta',
changes,
version: gameState.version
});
}
// Send full state on reconnect or desync
function sendFullSync(socket, gameState) {
socket.emit('state-sync', {
type: 'full',
state: gameState,
version: gameState.version
});
}
Version Vectors for Consistency
// Track state version to detect desync
let stateVersion = 0;
function applyAction(action) {
// Validate and apply
const newState = reducer(currentState, action);
stateVersion++;
return {
state: newState,
version: stateVersion
};
}
// Client requests sync if versions mismatch
socket.on('state-update', ({ version, changes }) => {
if (version !== localVersion + 1) {
socket.emit('request-sync'); // Ask for full state
}
});
Handling Disconnections
Reconnection Strategy
// Client-side reconnection
const socket = io(SERVER_URL, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
});
socket.on('connect', () => {
if (currentGameId) {
// Rejoin game room after reconnect
socket.emit('join-game', {
gameId: currentGameId,
playerId: myPlayerId,
lastVersion: localStateVersion // For delta sync
});
}
});
socket.on('disconnect', () => {
showReconnectingUI();
});
Grace Period for Disconnects
// Server-side: Don't immediately remove disconnected players
const disconnectTimers = new Map();
function handlePlayerDisconnect(gameId, playerId) {
// Mark as disconnected but give grace period
updatePresence(gameId, playerId, false);
const timer = setTimeout(() => {
// After grace period, handle as true disconnect
handlePlayerTimeout(gameId, playerId);
}, 60000); // 60 second grace period
disconnectTimers.set(`${gameId}:${playerId}`, timer);
}
function handlePlayerReconnect(gameId, playerId) {
// Cancel timeout if player reconnects
const key = `${gameId}:${playerId}`;
if (disconnectTimers.has(key)) {
clearTimeout(disconnectTimers.get(key));
disconnectTimers.delete(key);
}
updatePresence(gameId, playerId, true);
}
Turn-Based Game Patterns
Turn Timer Implementation
class TurnTimer {
constructor(gameId, onTimeout) {
this.gameId = gameId;
this.onTimeout = onTimeout;
this.timer = null;
}
start(playerId, durationMs) {
this.clear();
const endTime = Date.now() + durationMs;
// Broadcast timer start to all clients
io.to(`game:${this.gameId}`).emit('turn-timer', {
playerId,
endTime,
durationMs
});
this.timer = setTimeout(() => {
this.onTimeout(playerId);
}, durationMs);
}
clear() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
Action Validation
// Always validate on server
async function processAction(gameId, playerId, action) {
const game = await getGame(gameId);
// Validate it's player's turn
if (game.currentPlayer !== playerId) {
return { success: false, error: 'Not your turn' };
}
// Validate action is legal
const validationResult = validateAction(game.state, action);
if (!validationResult.valid) {
return { success: false, error: validationResult.reason };
}
// Apply action
const newState = applyAction(game.state, action);
await saveGame(gameId, newState);
return { success: true, newState };
}
Optimistic Updates
Client-Side Pattern
// For responsive UI, apply optimistically then reconcile
function handlePlayerAction(action) {
// 1. Optimistically apply locally
const optimisticState = reducer(localState, action);
renderUI(optimisticState);
// 2. Send to server
socket.emit('game-action', action, (response) => {
if (response.success) {
// 3a. Server confirmed - update to authoritative state
localState = response.state;
} else {
// 3b. Server rejected - rollback
localState = previousState;
showError(response.error);
}
renderUI(localState);
});
}
Security Considerations
Never Trust the Client
// BAD: Client sends new state
socket.on('update-state', (newState) => {
gameState = newState; // Never do this!
});
// GOOD: Client sends action, server validates and applies
socket.on('game-action', (action) => {
if (isValidAction(gameState, action, socket.playerId)) {
gameState = applyAction(gameState, action);
broadcast(gameState);
}
});
Rate Limiting
const rateLimit = require('socket.io-rate-limit');
io.use(rateLimit({
windowMs: 1000,
max: 10 // Max 10 messages per second per client
}));
Testing Multiplayer
Simulating Multiple Clients
// Test helper for multiple socket connections
async function createTestClients(count, gameId) {
const clients = [];
for (let i = 0; i < count; i++) {
const socket = io(SERVER_URL);
await new Promise(resolve => socket.on('connect', resolve));
socket.emit('join-game', { gameId, playerId: `player-${i}` });
clients.push(socket);
}
return clients;
}
Testing Reconnection
it('should handle reconnection gracefully', async () => {
const client = await createTestClient(gameId);
// Force disconnect
client.disconnect();
// Wait and reconnect
await sleep(1000);
client.connect();
// Should receive full state sync
const state = await waitForEvent(client, 'state-sync');
expect(state).toBeDefined();
});
When This Skill Activates
Use this skill when:
- Setting up WebSocket/Socket.io connections
- Implementing game room management
- Building state synchronization
- Handling player disconnection/reconnection
- Implementing turn timers
- Adding optimistic updates
- Securing multiplayer communications