Skip to content

Networking

The networking stack is split across three packages that work together to provide a complete multiplayer solution:

PackagePurpose
@web-engine-dev/netcodeCore networking types, configuration, and validation
@web-engine-dev/netcode-ecsECS integration -- shared simulation, replication, prediction systems
@web-engine-dev/netcode-serverServer 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:

typescript
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

ModeDescriptionUse Case
fullSend complete entity stateInitial spawn, recovery
deltaSend only changed propertiesNormal updates (bandwidth-efficient)
interestSend based on spatial relevanceLarge worlds with many entities

Quality of Service (QoS)

LevelGuaranteeUse Case
unreliableNone (fire-and-forget)Position updates
unreliable-sequencedDrop outdated packetsFrequent state sync
reliableGuaranteed deliveryEvents, spawns, deaths
reliable-orderedDelivery + orderingChat, turn-based commands

Interpolation Modes

Remote entity positions are interpolated between snapshots for smooth rendering:

ModeDescriptionUse Case
noneSnap to latest positionTeleports
linearLinear interpolationMost movement
cubicCubic splineSmooth curves
hermiteVelocity-aware interpolationVehicles
slerpSpherical interpolationRotations

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 inputs

Configuration

typescript
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 hit

Configuration

typescript
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:

typescript
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 frame

Configuration

typescript
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

EventDescription
late-inputRemote input arrived late, triggering rollback
input-correctionInput prediction was wrong
desync-detectedState hash mismatch (non-deterministic bug)
prediction-errorPredicted 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:

typescript
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

typescript
import { DEFAULT_CONNECTION_CONFIG } from '@web-engine-dev/netcode';

// Default connection settings:
// serverUrl, timeout: 10000ms, maxReconnectAttempts: 5,
// reconnectDelay: 1000ms, heartbeatInterval: 5000ms

Network Simulation (Testing)

Simulate adverse network conditions during development:

typescript
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

ConceptDescription
AuthorityWho controls an entity: server, owner, or shared
RunsOnWhere a system executes: server, client, authority, or both
NetworkIdentityComponent marking an entity as networked with a stable network ID
ReplicationServer serializes networked entities; client deserializes and applies

Defining Networked Components

typescript
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:

typescript
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:

typescript
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 entity

Network Topologies

TopologyServerClientTransport
offlineEmbedded (same World)Same processLoopbackTransport
listen-serverHost processHost + remoteLoopback + WebSocket
dedicated-clientRemoteLocalWebSocket
dedicated-serverLocal (headless)RemoteWebSocket

Setting Up Networking

typescript
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

SystemPurpose
ServerReplicationSystemSerializes networked entity state on the server
ClientReplicationSystemDeserializes and applies replicated state on the client
InterpolationSystemSmoothly interpolates remote entity positions
PredictionSystemManages client-side prediction buffer and reconciliation

Entity Mapping

The EntityMapper maps between local ECS entity IDs and stable network IDs:

typescript
import { EntityMapper, NetworkEntityMapResource } from '@web-engine-dev/netcode-ecs';

// Network IDs are stable across server and client
// Local entity IDs may differ

Server 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 PointPlatformTransport
@web-engine-dev/netcode-serverAnyCustom ServerTransport
@web-engine-dev/netcode-server/nodeNode.jsws + node:http
@web-engine-dev/netcode-server/denoDenoDeno.serve()

Defining a Game Room

Games extend the GameRoom base class with custom logic:

typescript
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)

typescript
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)

typescript
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:

typescript
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:

typescript
// Define room types
server.define('arena', ArenaRoom, { maxClients: 8 });
server.define('lobby', LobbyRoom, { maxClients: 50 });

// Clients join rooms via the protocol:
// - JoinRoom, LeaveRoom, ListRooms, Reconnect

State Synchronization

The StateSynchronizer automatically diffs room state and sends patches to clients:

typescript
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 serialization

The 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:

typescript
import { ServerClock } from '@web-engine-dev/netcode-server';

Bandwidth Management

Configure bandwidth allocation per client:

typescript
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:

typescript
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;

Proprietary software. All rights reserved.