Agent skill
Real-time Multiplayer
Enabling simultaneous gameplay across networks using WebSocket or WebRTC, including state synchronization, lag compensation, client-side prediction, and server authority for fair gameplay.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/real-time-multiplayer
SKILL.md
Real-time Multiplayer
Current Level: Advanced
Domain: Gaming / Networking
Overview
Real-time multiplayer enables simultaneous gameplay across networks. This guide covers networking, state synchronization, and lag compensation for building responsive multiplayer games that handle network latency and maintain game state consistency.
Real-time Networking
WebSocket vs WebRTC
| Feature | WebSocket | WebRTC |
|---|---|---|
| Latency | 50-100ms | 10-30ms |
| Topology | Client-Server | Peer-to-Peer |
| Reliability | TCP (reliable) | UDP (unreliable) |
| Use Case | Turn-based, MOBA | FPS, Racing |
Client-side Prediction
// Client-side prediction for smooth gameplay
export class ClientPrediction {
private pendingInputs: Input[] = [];
private lastProcessedInput = 0;
processInput(input: Input): void {
// Apply input immediately (prediction)
this.applyInput(input);
// Store for server reconciliation
this.pendingInputs.push(input);
// Send to server
this.sendToServer(input);
}
private applyInput(input: Input): void {
// Update local state based on input
switch (input.type) {
case 'move':
this.player.position.x += input.dx;
this.player.position.y += input.dy;
break;
case 'jump':
this.player.velocity.y = input.force;
break;
}
}
private sendToServer(input: Input): void {
socket.emit('player-input', {
inputId: input.id,
type: input.type,
data: input.data,
timestamp: Date.now()
});
}
}
interface Input {
id: number;
type: string;
data: any;
dx?: number;
dy?: number;
force?: number;
}
Server Reconciliation
// Server reconciliation to correct client predictions
export class ServerReconciliation {
reconcile(serverState: GameState): void {
// Find last processed input
const lastProcessedInput = serverState.lastProcessedInput;
// Remove processed inputs
this.pendingInputs = this.pendingInputs.filter(
input => input.id > lastProcessedInput
);
// Set state to server's authoritative state
this.player.position = serverState.position;
this.player.velocity = serverState.velocity;
// Re-apply pending inputs
for (const input of this.pendingInputs) {
this.applyInput(input);
}
}
}
interface GameState {
lastProcessedInput: number;
position: { x: number; y: number };
velocity: { x: number; y: number };
}
Lag Compensation
// Lag compensation for hit detection
export class LagCompensation {
private stateHistory: Map<number, GameState> = new Map();
private maxHistorySize = 60; // 1 second at 60 FPS
saveState(timestamp: number, state: GameState): void {
this.stateHistory.set(timestamp, state);
// Limit history size
if (this.stateHistory.size > this.maxHistorySize) {
const oldest = Math.min(...this.stateHistory.keys());
this.stateHistory.delete(oldest);
}
}
rewindToTimestamp(timestamp: number): GameState | null {
// Find closest state
let closestTime = 0;
let minDiff = Infinity;
for (const time of this.stateHistory.keys()) {
const diff = Math.abs(time - timestamp);
if (diff < minDiff) {
minDiff = diff;
closestTime = time;
}
}
return this.stateHistory.get(closestTime) || null;
}
checkHit(
shooterTimestamp: number,
targetPosition: { x: number; y: number },
shotPosition: { x: number; y: number }
): boolean {
// Rewind to shooter's timestamp
const historicalState = this.rewindToTimestamp(shooterTimestamp);
if (!historicalState) return false;
// Check if shot hit target at that time
const distance = Math.sqrt(
Math.pow(historicalState.position.x - shotPosition.x, 2) +
Math.pow(historicalState.position.y - shotPosition.y, 2)
);
return distance < 10; // Hit radius
}
}
State Synchronization
// State synchronization strategies
export class StateSynchronization {
// Full state sync (simple but bandwidth-heavy)
syncFullState(state: GameState): void {
socket.emit('state-update', state);
}
// Delta compression (only send changes)
syncDeltaState(previousState: GameState, currentState: GameState): void {
const delta = this.calculateDelta(previousState, currentState);
if (Object.keys(delta).length > 0) {
socket.emit('state-delta', delta);
}
}
private calculateDelta(prev: GameState, current: GameState): Partial<GameState> {
const delta: Partial<GameState> = {};
if (prev.position.x !== current.position.x || prev.position.y !== current.position.y) {
delta.position = current.position;
}
if (prev.velocity.x !== current.velocity.x || prev.velocity.y !== current.velocity.y) {
delta.velocity = current.velocity;
}
return delta;
}
// Interpolation for smooth movement
interpolate(from: GameState, to: GameState, alpha: number): GameState {
return {
position: {
x: from.position.x + (to.position.x - from.position.x) * alpha,
y: from.position.y + (to.position.y - from.position.y) * alpha
},
velocity: {
x: from.velocity.x + (to.velocity.x - from.velocity.x) * alpha,
y: from.velocity.y + (to.velocity.y - from.velocity.y) * alpha
},
lastProcessedInput: to.lastProcessedInput
};
}
}
Input Handling
// Input buffering and processing
export class InputHandler {
private inputBuffer: Input[] = [];
private inputSequence = 0;
captureInput(type: string, data: any): void {
const input: Input = {
id: ++this.inputSequence,
type,
data,
timestamp: Date.now()
};
this.inputBuffer.push(input);
}
processInputs(deltaTime: number): void {
while (this.inputBuffer.length > 0) {
const input = this.inputBuffer.shift()!;
this.processInput(input, deltaTime);
}
}
private processInput(input: Input, deltaTime: number): void {
switch (input.type) {
case 'move':
const speed = 5;
this.player.position.x += input.data.direction.x * speed * deltaTime;
this.player.position.y += input.data.direction.y * speed * deltaTime;
break;
case 'shoot':
this.createProjectile(input.data.angle);
break;
}
}
private createProjectile(angle: number): void {
// Create projectile
}
}
Physics Synchronization
// Deterministic physics for multiplayer
export class PhysicsSync {
private fixedTimeStep = 1 / 60; // 60 FPS
private accumulator = 0;
update(deltaTime: number): void {
this.accumulator += deltaTime;
// Fixed timestep updates
while (this.accumulator >= this.fixedTimeStep) {
this.fixedUpdate(this.fixedTimeStep);
this.accumulator -= this.fixedTimeStep;
}
}
private fixedUpdate(dt: number): void {
// Update physics deterministically
for (const entity of this.entities) {
// Apply velocity
entity.position.x += entity.velocity.x * dt;
entity.position.y += entity.velocity.y * dt;
// Apply gravity
entity.velocity.y += 9.8 * dt;
// Collision detection
this.checkCollisions(entity);
}
}
private checkCollisions(entity: Entity): void {
// Collision detection logic
}
}
interface Entity {
position: { x: number; y: number };
velocity: { x: number; y: number };
}
Cheating Prevention
// Server-side validation
export class AntiCheatService {
validatePlayerAction(
playerId: string,
action: PlayerAction,
gameState: GameState
): boolean {
switch (action.type) {
case 'move':
return this.validateMovement(playerId, action, gameState);
case 'shoot':
return this.validateShot(playerId, action, gameState);
default:
return false;
}
}
private validateMovement(
playerId: string,
action: PlayerAction,
gameState: GameState
): boolean {
const player = gameState.players.get(playerId);
if (!player) return false;
// Check if movement is within max speed
const maxSpeed = 10;
const distance = Math.sqrt(
Math.pow(action.data.x - player.position.x, 2) +
Math.pow(action.data.y - player.position.y, 2)
);
const timeDelta = (Date.now() - player.lastUpdate) / 1000;
const speed = distance / timeDelta;
if (speed > maxSpeed) {
this.flagCheater(playerId, 'speed_hack');
return false;
}
return true;
}
private validateShot(
playerId: string,
action: PlayerAction,
gameState: GameState
): boolean {
const player = gameState.players.get(playerId);
if (!player) return false;
// Check fire rate
const minFireInterval = 100; // ms
const timeSinceLastShot = Date.now() - player.lastShotTime;
if (timeSinceLastShot < minFireInterval) {
this.flagCheater(playerId, 'rapid_fire');
return false;
}
return true;
}
private flagCheater(playerId: string, reason: string): void {
console.log(`Cheating detected: ${playerId} - ${reason}`);
// Log to database, potentially ban player
}
}
interface PlayerAction {
type: string;
data: any;
}
Room/Lobby System
// Room management with Colyseus
import { Room, Client } from 'colyseus';
export class GameRoom extends Room {
maxClients = 4;
onCreate(options: any): void {
console.log('Room created:', this.roomId);
this.setState({
players: new Map(),
gameStarted: false
});
this.setSimulationInterval((deltaTime) => this.update(deltaTime));
}
onJoin(client: Client, options: any): void {
console.log('Player joined:', client.sessionId);
this.state.players.set(client.sessionId, {
id: client.sessionId,
position: { x: 0, y: 0 },
velocity: { x: 0, y: 0 }
});
// Start game when room is full
if (this.clients.length === this.maxClients) {
this.startGame();
}
}
onLeave(client: Client): void {
console.log('Player left:', client.sessionId);
this.state.players.delete(client.sessionId);
}
onMessage(client: Client, message: any): void {
if (message.type === 'input') {
this.handleInput(client.sessionId, message.data);
}
}
private startGame(): void {
this.state.gameStarted = true;
this.broadcast('game-started');
}
private update(deltaTime: number): void {
// Update game state
for (const [id, player] of this.state.players) {
player.position.x += player.velocity.x * deltaTime;
player.position.y += player.velocity.y * deltaTime;
}
}
private handleInput(playerId: string, input: any): void {
const player = this.state.players.get(playerId);
if (player) {
player.velocity = input.velocity;
}
}
}
Quick Start
Client-Side Prediction
// Client predicts movement
function predictMovement(playerId: string, input: Input) {
const player = getPlayer(playerId)
player.position.x += input.deltaX
player.position.y += input.deltaY
// Send to server
socket.emit('player-move', { playerId, input })
}
// Server corrects if needed
socket.on('server-update', (update) => {
const player = getPlayer(update.playerId)
// Reconcile if different
if (player.position.x !== update.position.x) {
player.position = update.position // Server is authoritative
}
})
Lag Compensation
// Server rewinds time for hit detection
function checkHit(shooterId: string, targetId: string, shotTime: number) {
const lag = getPlayerLatency(shooterId)
const rewindTime = shotTime - lag
// Check hit at rewind time
const targetPos = getPlayerPositionAtTime(targetId, rewindTime)
return isHit(shotPosition, targetPos)
}
Production Checklist
- Networking: Choose WebSocket or WebRTC
- Client Prediction: Implement client-side prediction
- Server Authority: Server is authoritative
- Reconciliation: Correct client predictions
- Lag Compensation: Compensate for network latency
- State Synchronization: Efficient state sync
- Anti-cheat: Validate all actions server-side
- Bandwidth: Optimize network usage
- Testing: Test with various latencies
- Documentation: Document networking architecture
- Monitoring: Monitor network performance
- Error Handling: Handle network errors
Anti-patterns
❌ Don't: Trust Client
// ❌ Bad - Trust client position
function movePlayer(playerId: string, newPosition: Position) {
players[playerId].position = newPosition // Client can cheat!
}
// ✅ Good - Server calculates
function movePlayer(playerId: string, input: Input) {
const player = players[playerId]
// Server calculates movement
player.position.x += input.deltaX * player.speed
player.position.y += input.deltaY * player.speed
}
❌ Don't: No Lag Compensation
// ❌ Bad - No lag compensation
function checkHit(shooterPos: Position, targetPos: Position) {
return distance(shooterPos, targetPos) < HIT_RADIUS
// Unfair for high latency players!
}
// ✅ Good - Lag compensation
function checkHit(shooterId: string, targetId: string, shotTime: number) {
const lag = getPlayerLatency(shooterId)
const rewindTime = shotTime - lag
const targetPos = getPlayerPositionAtTime(targetId, rewindTime)
return distance(shotPosition, targetPos) < HIT_RADIUS
}
Integration Points
- Matchmaking (
38-gaming-features/matchmaking/) - Player matching - WebSocket Patterns (
34-real-time-features/websocket-patterns/) - WebSocket implementation - Game Analytics (
38-gaming-features/game-analytics/) - Game metrics
Further Reading
Best Practices
- Client Prediction - Predict locally for responsiveness
- Server Authority - Server is authoritative
- Reconciliation - Correct client predictions
- Lag Compensation - Rewind for hit detection
- Fixed Timestep - Use fixed timestep for physics
- Delta Compression - Send only changes
- Interpolation - Smooth remote player movement
- Anti-cheat - Validate all actions server-side
- Bandwidth - Optimize network usage
- Testing - Test with various latencies
Resources
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?