Networking
The networking stack is split across three packages that work together to provide a complete multiplayer solution:
| Package | Purpose |
|---|---|
@web-engine-dev/netcode | Core networking types, configuration, and validation |
@web-engine-dev/netcode-ecs | ECS integration -- shared simulation, replication, prediction systems |
@web-engine-dev/netcode-server | Server framework -- rooms, state sync, transport abstraction |
Architecture Overview
Client Server
+---------------------------+ +---------------------------+
| Client World | | Server World |
| - Rendering | Input | - Authority |
| - Prediction |--------->| - Physics |
| - Interpolation | | - Game Logic |
| - Input Collection | State | - State Replication |
| |<---------| |
+---------------------------+ +---------------------------+
| |
| WebSocket / WebRTC / WebTransport |
+----------------------------------------+The engine supports two networking models:
Client-Server (Authoritative):
- Server is the source of truth for game state
- Clients send inputs, receive authoritative state updates
- Best for: shooters, MMOs, most multiplayer games
Rollback (Peer-to-Peer):
- Deterministic simulation on all peers
- Peers exchange inputs, not state
- Best for: fighting games, sports games, real-time competitive games
Core Types (netcode)
The @web-engine-dev/netcode package provides branded types, configuration schemas, and validation for all networking concepts.
Branded Types
All identifiers use branded types to prevent accidental misuse:
import {
createNetworkId,
createClientId,
createSessionId,
createTickNumber,
createServerTime,
} from '@web-engine-dev/netcode';
const entityNetId = createNetworkId('entity-123');
const playerId = createClientId('player-1');
const session = createSessionId('sess-abc');
const tick = createTickNumber(1000);
const time = createServerTime(Date.now());State Synchronization Modes
| Mode | Description | Use Case |
|---|---|---|
full | Send complete entity state | Initial spawn, recovery |
delta | Send only changed properties | Normal updates (bandwidth-efficient) |
interest | Send based on spatial relevance | Large worlds with many entities |
Quality of Service (QoS)
| Level | Guarantee | Use Case |
|---|---|---|
unreliable | None (fire-and-forget) | Position updates |
unreliable-sequenced | Drop outdated packets | Frequent state sync |
reliable | Guaranteed delivery | Events, spawns, deaths |
reliable-ordered | Delivery + ordering | Chat, turn-based commands |
Interpolation Modes
Remote entity positions are interpolated between snapshots for smooth rendering:
| Mode | Description | Use Case |
|---|---|---|
none | Snap to latest position | Teleports |
linear | Linear interpolation | Most movement |
cubic | Cubic spline | Smooth curves |
hermite | Velocity-aware interpolation | Vehicles |
slerp | Spherical interpolation | Rotations |
Client-Side Prediction
Client-side prediction lets the local player feel responsive despite network latency. The client applies inputs immediately and reconciles with the server's authoritative state when it arrives.
1. Client presses "move right"
2. Client sends input to server AND applies it locally (prediction)
3. Client stores predicted state with input sequence number
4. Server processes input, sends authoritative state back
5. Client compares prediction with authoritative state
6. If mismatch: correct state, replay unacknowledged inputsConfiguration
import type { PredictionConfig } from '@web-engine-dev/netcode';
const predictionConfig: PredictionConfig = {
enabled: true,
maxBufferSize: 128, // Input buffer size
correctionThreshold: 0.01, // Position delta threshold for correction
smoothCorrection: true, // Blend corrections instead of snapping
correctionBlendTime: 100, // ms to blend correction
};All configuration objects have factory functions (e.g., createDefaultPredictionConfig()) and validation functions (e.g., validatePredictionConfig()) that return arrays of structured ConfigValidationError objects.
Lag Compensation
The server can rewind time to validate hits at the client's perceived perspective, compensating for network latency:
1. Client sees enemy at position X at their local time T
2. Client fires, sends: { targetId, attackTime: T, origin, direction }
3. Server receives request, rewinds world to T - RTT/2
4. Server validates hit against historical (rewound) positions
5. Server applies or rejects the hitConfiguration
import type { LagCompensationConfig } from '@web-engine-dev/netcode';
const lagCompConfig: LagCompensationConfig = {
enabled: true,
maxRewindTime: 200, // Max ms to rewind
historyBufferSize: 64, // Ticks of history to store
interpolationPeriod: 100, // Render interpolation delay (ms)
serverHitValidation: true, // Server validates all hits
};Hit Validation
The server validates hits and returns structured results:
import type { HitValidationResult, HitRejectionReason } from '@web-engine-dev/netcode';
// Result: { valid: true, ... } or { valid: false, reason: HitRejectionReason }
// Rejection reasons: 'missed' | 'out-of-range' | 'invalid-target' | etc.Rollback Netcode
For games requiring frame-perfect synchronization (fighting games, sports):
Each frame:
1. Receive remote inputs (may arrive late)
2. If late input for a past frame --> rollback to that frame
3. Re-simulate from rollback frame to current frame
4. Continue with current frameConfiguration
import type { RollbackConfig } from '@web-engine-dev/netcode';
const rollbackConfig: RollbackConfig = {
enabled: true,
maxRollbackFrames: 7, // Max frames to rollback
inputDelayFrames: 2, // Local input delay (smooths rollback frequency)
frameAdvantageLimit: 8, // Max frames ahead before waiting for peers
syncTestMode: false, // Debug: verify determinism via state hash comparison
};Rollback Events
| Event | Description |
|---|---|
late-input | Remote input arrived late, triggering rollback |
input-correction | Input prediction was wrong |
desync-detected | State hash mismatch (non-deterministic bug) |
prediction-error | Predicted input differed from actual |
Interest Management (AOI)
For large worlds, the Area of Interest system limits bandwidth by sending only relevant entity updates to each client:
import type { AOIConfig } from '@web-engine-dev/netcode';
const aoiConfig: AOIConfig = {
enabled: true,
updateRadius: 100, // Full update radius (units)
awarenessRadius: 150, // Reduced update radius
maxEntitiesPerClient: 100, // Hard cap per client
cellSize: 25, // Spatial hash grid cell size
priorityWeight: 100, // Weight for entity priority in scoring
distanceWeight: 50, // Weight for distance in scoring
updateFrequency: 5, // Ticks between AOI recalculation
};Entities within updateRadius receive full updates. Between updateRadius and awarenessRadius, updates are reduced. Beyond awarenessRadius, entities are culled.
Connection Lifecycle
disconnected --> connecting --> handshaking --> connected
| |
(timeout) reconnecting
| |
disconnected <--------------- (failure)Configuration
import { DEFAULT_CONNECTION_CONFIG } from '@web-engine-dev/netcode';
// Default connection settings:
// serverUrl, timeout: 10000ms, maxReconnectAttempts: 5,
// reconnectDelay: 1000ms, heartbeatInterval: 5000msNetwork Simulation (Testing)
Simulate adverse network conditions during development:
import type { NetworkSimulation } from '@web-engine-dev/netcode';
const simulation: NetworkSimulation = {
latency: 100, // Base latency (ms)
latencyVariance: 20, // Jitter (ms)
packetLoss: 5, // 5% packet loss
packetDuplication: 1, // 1% duplicates
packetReordering: 2, // 2% out-of-order
bandwidthLimit: 50000, // 50 KB/s limit
};ECS Integration (netcode-ecs)
The @web-engine-dev/netcode-ecs package bridges the ECS and netcode packages, enabling a shared simulation architecture where game logic runs on both server and client.
Key Concepts
| Concept | Description |
|---|---|
| Authority | Who controls an entity: server, owner, or shared |
| RunsOn | Where a system executes: server, client, authority, or both |
| NetworkIdentity | Component marking an entity as networked with a stable network ID |
| Replication | Server serializes networked entities; client deserializes and applies |
Defining Networked Components
import { defineNetworkedComponent } from '@web-engine-dev/netcode-ecs';
const Position = defineNetworkedComponent('Position', {
x: 'f32',
y: 'f32',
}, {
interpolation: 'linear',
});Defining Shared Systems
Systems can be annotated with where they should run:
import { defineSharedSystem } from '@web-engine-dev/netcode-ecs';
const MovementSystem = defineSharedSystem({
name: 'Movement',
runsOn: 'server', // Only runs on server
run: (world, dt) => {
// Move entities based on input...
},
});
const RenderSystem = defineSharedSystem({
name: 'Render',
runsOn: 'client', // Only runs on client
run: (world, dt) => {
// Render entities...
},
});Run Conditions
The package exports run condition functions for conditional system execution:
import { isServer, isClient, isOffline, isAuthority } from '@web-engine-dev/netcode-ecs';
// Use as system run conditions
// isServer() -- true when running as server
// isClient() -- true when running as client
// isOffline() -- true in offline/local mode
// isAuthority() -- true when the local world has authority over the entityNetwork Topologies
| Topology | Server | Client | Transport |
|---|---|---|---|
offline | Embedded (same World) | Same process | LoopbackTransport |
listen-server | Host process | Host + remote | Loopback + WebSocket |
dedicated-client | Remote | Local | WebSocket |
dedicated-server | Local (headless) | Remote | WebSocket |
Setting Up Networking
import { configureNetwork } from '@web-engine-dev/netcode-ecs';
import { createWorld } from '@web-engine-dev/ecs';
const world = createWorld();
const result = configureNetwork(world, {
type: 'offline', // or 'listen-server', 'dedicated-client', 'dedicated-server'
}, [MovementSystem, RenderSystem]);Built-in Systems
| System | Purpose |
|---|---|
ServerReplicationSystem | Serializes networked entity state on the server |
ClientReplicationSystem | Deserializes and applies replicated state on the client |
InterpolationSystem | Smoothly interpolates remote entity positions |
PredictionSystem | Manages client-side prediction buffer and reconciliation |
Entity Mapping
The EntityMapper maps between local ECS entity IDs and stable network IDs:
import { EntityMapper, NetworkEntityMapResource } from '@web-engine-dev/netcode-ecs';
// Network IDs are stable across server and client
// Local entity IDs may differServer Framework (netcode-server)
The @web-engine-dev/netcode-server package provides a runtime-agnostic game server framework inspired by Colyseus. It handles transport, room management, state synchronization, and client sessions.
Runtime-Agnostic Core
The main entry point has zero platform-specific imports. Platform adapters are separate:
| Entry Point | Platform | Transport |
|---|---|---|
@web-engine-dev/netcode-server | Any | Custom ServerTransport |
@web-engine-dev/netcode-server/node | Node.js | ws + node:http |
@web-engine-dev/netcode-server/deno | Deno | Deno.serve() |
Defining a Game Room
Games extend the GameRoom base class with custom logic:
import { GameRoom } from '@web-engine-dev/netcode-server';
class ArenaRoom extends GameRoom {
// Room state
players = new Map();
onCreate(options: any) {
// Initialize room state
this.setTickRate(20); // 20 ticks/second
}
onJoin(client: ClientSession, options: any) {
this.players.set(client.id, { x: 0, y: 0, score: 0 });
}
onLeave(client: ClientSession) {
this.players.delete(client.id);
}
onMessage(client: ClientSession, channel: string, data: any) {
// Handle game messages
}
onTick(deltaTime: number) {
// Server tick -- update game state
}
onDispose() {
// Cleanup
}
}Starting a Server (Node.js)
import { createNodeGameServer } from '@web-engine-dev/netcode-server/node';
const { server, listen, close } = createNodeGameServer({
port: 3000,
compression: { enabled: true, minSize: 1024 },
});
server.define('arena', ArenaRoom, { maxClients: 8 });
await listen();Starting a Server (Deno)
import { createDenoGameServer } from '@web-engine-dev/netcode-server/deno';
const { server, listen, close } = createDenoGameServer({
port: 3000,
compression: { enabled: true, minSize: 1024 },
});
server.define('arena', ArenaRoom, { maxClients: 8 });
await listen();Custom Transport
For other runtimes, implement the ServerTransport interface and pass it directly:
import { GameServer } from '@web-engine-dev/netcode-server';
const server = new GameServer({ transport: myCustomTransport });
server.define('arena', ArenaRoom, { maxClients: 4 });
server.start();Room Management
The RoomManager handles room lifecycle, discovery, and matchmaking:
// Define room types
server.define('arena', ArenaRoom, { maxClients: 8 });
server.define('lobby', LobbyRoom, { maxClients: 50 });
// Clients join rooms via the protocol:
// - JoinRoom, LeaveRoom, ListRooms, ReconnectState Synchronization
The StateSynchronizer automatically diffs room state and sends patches to clients:
import { StateSynchronizer, JSONSerializer } from '@web-engine-dev/netcode-server';
// Supports multiple serializers:
// JSONSerializer -- human-readable, easy to debug
// BinarySerializer -- compact binary format with schema
// CustomSerializer -- bring your own serializationThe PatchGenerator computes diffs between state snapshots for delta compression.
Wire Protocol
Messages use a binary frame format:
[channelLen: 1 byte][channel: N bytes UTF-8][payload: remaining bytes]With optional compression envelope:
[flag: 1 byte][inner frame]
flag = 0x00 (uncompressed) | 0x01 (compressed) | 0x02 (batch)System messages use the __sys channel with defined opcodes for room join/leave, ping/pong, state sync, and room discovery.
Server Clock
The ServerClock provides a high-resolution tick timer with drift compensation for consistent simulation:
import { ServerClock } from '@web-engine-dev/netcode-server';Bandwidth Management
Configure bandwidth allocation per client:
import type { BandwidthConfig } from '@web-engine-dev/netcode';
const bandwidthConfig: BandwidthConfig = {
maxBytesPerSecond: 50000,
maxBytesPerTick: 2500,
priorityBased: true,
};Configuration Validation
All configuration objects can be validated before use:
import {
validateConnectionConfig,
validatePredictionConfig,
validateLagCompensationConfig,
validateRollbackConfig,
validateAOIConfig,
validateBandwidthConfig,
validateNetworkConfig,
createValidatedNetworkConfig,
} from '@web-engine-dev/netcode';
const errors = validatePredictionConfig(config);
if (errors.length > 0) {
// Each error: { code, message, field, value }
console.error('Invalid config:', errors);
}
// Or create a validated config in one step
const result = createValidatedNetworkConfig(rawConfig);
if (result.errors.length > 0) { /* handle errors */ }
const validConfig = result.config;Related Packages
@web-engine-dev/ecs-- Entity Component System used for shared simulation@web-engine-dev/serialization-- Binary serialization used for state snapshots@web-engine-dev/input-- Input system for collecting player commands