| 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
const io = require('socket.io')(server, {
cors: { origin: process.env.CLIENT_URL },
pingTimeout: 60000,
pingInterval: 25000
});
io.on('connection', (socket) => {
socket.on('join-game', ({ gameId, playerId }) => {
socket.join(`game:${gameId}`);
socket.gameId = gameId;
socket.playerId = playerId;
});
socket.on('game-action', async (action) => {
const result = await processAction(socket.gameId, socket.playerId, action);
if (result.success) {
io.to(`game:${socket.gameId}`).emit('state-update', result.newState);
} else {
socket.emit('action-error', result.error);
}
});
socket.on('disconnect', () => {
handlePlayerDisconnect(socket.gameId, socket.playerId);
});
});
Room Management
Game Rooms Pattern
Each game instance should be a Socket.io room:
const roomName = `game:${gameId}`;
socket.join(roomName);
io.to(roomName).emit('event', data);
io.to(playerSocketId).emit('private-event', data);
socket.to(roomName).emit('event', data);
Player Presence Tracking
const gamePresence = new Map();
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);
}
io.to(`game:${gameId}`).emit('presence-update', {
playerId,
isOnline,
onlinePlayers: Array.from(players)
});
}
State Synchronization
Event Types
Define clear event categories:
const ServerEvents = {
STATE_SYNC: 'state-sync',
STATE_UPDATE: 'state-update',
ACTION_RESULT: 'action-result',
PLAYER_JOINED: 'player-joined',
PLAYER_LEFT: 'player-left',
GAME_STARTED: 'game-started',
TURN_CHANGED: 'turn-changed',
GAME_ENDED: 'game-ended'
};
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
function sendDelta(gameId, changes) {
io.to(`game:${gameId}`).emit('state-update', {
type: 'delta',
changes,
version: gameState.version
});
}
function sendFullSync(socket, gameState) {
socket.emit('state-sync', {
type: 'full',
state: gameState,
version: gameState.version
});
}
Version Vectors for Consistency
let stateVersion = 0;
function applyAction(action) {
const newState = reducer(currentState, action);
stateVersion++;
return {
state: newState,
version: stateVersion
};
}
socket.on('state-update', ({ version, changes }) => {
if (version !== localVersion + 1) {
socket.emit('request-sync');
}
});
Handling Disconnections
Reconnection Strategy
const socket = io(SERVER_URL, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
});
socket.on('connect', () => {
if (currentGameId) {
socket.emit('join-game', {
gameId: currentGameId,
playerId: myPlayerId,
lastVersion: localStateVersion
});
}
});
socket.on('disconnect', () => {
showReconnectingUI();
});
Grace Period for Disconnects
const disconnectTimers = new Map();
function handlePlayerDisconnect(gameId, playerId) {
updatePresence(gameId, playerId, false);
const timer = setTimeout(() => {
handlePlayerTimeout(gameId, playerId);
}, 60000);
disconnectTimers.set(`${gameId}:${playerId}`, timer);
}
function handlePlayerReconnect(gameId, playerId) {
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;
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
async function processAction(gameId, playerId, action) {
const game = await getGame(gameId);
if (game.currentPlayer !== playerId) {
return { success: false, error: 'Not your turn' };
}
const validationResult = validateAction(game.state, action);
if (!validationResult.valid) {
return { success: false, error: validationResult.reason };
}
const newState = applyAction(game.state, action);
await saveGame(gameId, newState);
return { success: true, newState };
}
Optimistic Updates
Client-Side Pattern
function handlePlayerAction(action) {
const optimisticState = reducer(localState, action);
renderUI(optimisticState);
socket.emit('game-action', action, (response) => {
if (response.success) {
localState = response.state;
} else {
localState = previousState;
showError(response.error);
}
renderUI(localState);
});
}
Security Considerations
Never Trust the Client
socket.on('update-state', (newState) => {
gameState = newState;
});
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
}));
Testing Multiplayer
Simulating Multiple Clients
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);
client.disconnect();
await sleep(1000);
client.connect();
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