State Synchronization

Learn how Web Engine synchronizes game state across multiple clients with delta compression, interpolation, and authority systems.

Web Engine provides automatic state synchronization with advanced optimizations including delta compression, snapshot interpolation, and client-side prediction to create smooth multiplayer experiences.

Key Concepts#

Delta Compression

Only changed properties are sent over the network, reducing bandwidth by up to 90%.

Snapshot Interpolation

Smooth entity movement by interpolating between server snapshots.

Packet Batching

Combine multiple updates into single packets to reduce overhead.

LOD System

Reduce synchronization frequency for distant entities.

Component Synchronization#

The Networked component marks an entity for network synchronization. Add it to any entity that needs to be replicated across clients:

Networked Entity
typescript
import { addComponent } from '@web-engine/core/ecs';
import { Networked, NetworkedTransform } from '@web-engine/core/network';
// Mark entity for synchronization
addComponent(world, Networked, entityId);
addComponent(world, NetworkedTransform, entityId);
// The entity's transform now syncs automatically
// Position, rotation, and optionally velocity are replicated

Automatic Sync

Once marked with Networked, the entity's transform is automatically synchronized every network tick. No manual code required!

Delta Compression#

Delta compression drastically reduces bandwidth by only sending changed values. The system uses bitmasks to indicate which fields have changed:

Delta Compression Example
typescript
import { DeltaCompressor, DeltaFlags } from '@web-engine/core/network';
const compressor = new DeltaCompressor(4096); // Pool size
// Compress entity state
const delta = compressor.compress(entityState);
if (delta) {
// Check what changed using bitmask
if (delta.changedFields & DeltaFlags.POSITION) {
// Position changed - apply update
applyPosition(delta.pos);
}
if (delta.changedFields & DeltaFlags.ROTATION) {
// Rotation changed - apply update
applyRotation(delta.rot);
}
if (delta.changedFields & DeltaFlags.VELOCITY) {
// Velocity changed - apply update
applyVelocity(delta.vel);
}
}
// Decompress delta back to full state
const fullState = compressor.decompress(delta, previousState);

Compression Thresholds#

The system uses thresholds to avoid sending insignificant changes caused by floating-point noise:

  • Position — 0.001 units (1mm) squared distance
  • Rotation — 0.0001 quaternion dot product difference
  • Velocity — 0.01 units/s (1cm/s) squared distance
Compression Metrics
typescript
// Monitor compression efficiency
const metrics = compressor.getMetrics();
console.log({
compressionRatio: metrics.compressionRatio, // 0.0 - 1.0
deltaUpdates: metrics.deltaUpdates, // Updates with deltas
fullUpdates: metrics.fullUpdates, // First-time full updates
skipped: metrics.skipped, // No changes
poolUtilization: metrics.poolUtilization, // 0.0 - 1.0
});
// Typical compression ratio: 0.7-0.9 (70-90% reduction)

Snapshot Interpolation#

Interpolation smooths entity movement by blending between server snapshots, hiding network jitter and producing fluid animation even with low network tick rates:

Snapshot Interpolation
typescript
import { SnapshotBuffer } from '@web-engine/core/network';
// Create interpolation buffer (100ms delay)
const buffer = new SnapshotBuffer<NetworkEntityState>(64, 100);
// Add snapshots from server updates
function onServerUpdate(timestamp: number, state: NetworkEntityState) {
buffer.addSnapshot(timestamp, state);
}
// Get interpolated state for rendering
function onRender(renderTime: number) {
const interpolated = buffer.getInterpolated(renderTime);
if (interpolated) {
// Render at interpolated position
entity.position.set(
interpolated.state.pos[0],
interpolated.state.pos[1],
interpolated.state.pos[2]
);
entity.quaternion.set(
interpolated.state.rot[0],
interpolated.state.rot[1],
interpolated.state.rot[2],
interpolated.state.rot[3]
);
}
}

Interpolation Delay

The interpolation delay (default 100ms) provides a buffer for out-of-order packets. Tune this based on your target latency: lower for local multiplayer, higher for global games.

Authority and Ownership#

Web Engine uses an authoritative server model where the server is the source of truth. However, clients can predict local player movement for responsiveness:

Authority Checks
typescript
import { NetworkManager } from '@web-engine/core/network';
const network = NetworkManager.getInstance();
// Check if we're the server (authority)
if (network.isServer) {
// Server logic - validate and process
processPlayerInput(input);
updateGameState();
}
// Check if this is our local player
if (isLocalPlayer(entityId)) {
// Predict movement immediately for responsiveness
predictMovement(input);
// Send input to server for validation
network.sendInput(input);
}
// For remote entities, just render server state
if (isRemotePlayer(entityId)) {
renderInterpolatedState(entityId);
}

Entity Ownership#

Each networked entity has an owner (the client/player who controls it):

Ownership Example
typescript
// Spawn entity with ownership
const spawnPacket: SpawnPacket = {
type: PacketType.SPAWN,
senderId: network.getClientId(),
timestamp: network.getServerTime(),
entity: {
netId: generateNetId(),
ownerId: network.getClientId(), // This client owns it
pos: [0, 0, 0],
rot: [0, 0, 0, 1],
}
};
network.sendPacket(spawnPacket);
// Check ownership
function isOwnedByLocalPlayer(entity: Entity): boolean {
const netComponent = getComponent(world, Networked, entity);
return netComponent?.ownerId === network.getClientId();
}

Network Level of Detail (LOD)#

Reduce bandwidth for distant entities by lowering their synchronization frequency and precision:

Network LOD System
typescript
import {
NetworkLODSystem,
LOD_PRESETS,
quantizePosition,
quantizeRotation
} from '@web-engine/core/network';
// Initialize LOD system
const lodSystem = new NetworkLODSystem({
levels: [
{ maxDistance: 10, updateRate: 60, precision: 16 }, // High detail
{ maxDistance: 50, updateRate: 30, precision: 12 }, // Medium detail
{ maxDistance: 200, updateRate: 10, precision: 8 }, // Low detail
{ maxDistance: Infinity, updateRate: 2, precision: 4 }, // Very low detail
]
});
// Or use presets
const lodSystem = new NetworkLODSystem(LOD_PRESETS.BALANCED);
// Process entities with LOD
const updates = lodSystem.processEntities(
entities,
observerPosition,
deltaTime
);
// Only entities that need updates this frame are returned
for (const update of updates) {
const quantizedPos = quantizePosition(
entity.pos,
update.lodLevel.precision
);
sendUpdate(entity.netId, quantizedPos);
}

Batch Serialization#

For games with thousands of entities, use the high-performance batch serialization system optimized for 100k+ entities:

Batch Serialization (Advanced)
typescript
import {
BatchSerializer,
BatchDeltaCompressor,
CompressionStrategy
} from '@web-engine/core/network';
// Create batch serializer with SoA (Structure of Arrays) layout
const serializer = new BatchSerializer({
maxEntities: 100000,
enableCompression: true,
compressionStrategy: CompressionStrategy.DELTA_RLE,
});
// Serialize all entities in one batch
const batch = serializer.serializeEntities(entities);
// Batch delta compression for even more savings
const compressor = new BatchDeltaCompressor();
const compressed = compressor.compress(batch);
// Send single packet with all entity updates
network.sendBinary(compressed);
// Receive and decompress
const decompressed = compressor.decompress(receivedData);
const entities = serializer.deserializeEntities(decompressed);

When to Use Batch Serialization

Use batch serialization for MMO-scale games with 10,000+ networked entities. For smaller games (< 1000 entities), the standard delta compression is sufficient.

Synchronization Best Practices#

  • Minimize state — Only synchronize essential data. Derive what you can locally (e.g., animations, visual effects).
  • Use delta compression — It's enabled by default and provides massive bandwidth savings.
  • Configure LOD — Tune LOD distances based on your game's scale and player density.
  • Batch updates — Send state updates at a fixed rate (e.g., 20Hz) rather than every frame.
  • Monitor metrics — Track compression ratios, packet sizes, and pool utilization to detect issues.
  • Test on realistic networks — Simulate latency and packet loss during development.

Performance Characteristics#

System Performance
typescript
// DeltaCompressor Performance (per frame)
// - compress(): O(1), zero allocations, ~0.5μs per entity
// - Pool capacity: 2048 deltas (configurable)
// - Typical compression: 70-90% bandwidth reduction
// SnapshotInterpolation Performance
// - Binary search for bracketing: O(log n)
// - Interpolation: O(1)
// - Zero allocations in hot path
// NetworkLODSystem Performance
// - Spatial partitioning: O(log n) for distance queries
// - Update culling: O(n) but only processes visible entities
// - Can handle 100k+ entities at 60fps
Multiplayer | Web Engine Docs | Web Engine Docs