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:
import { addComponent } from '@web-engine/core/ecs';import { Networked, NetworkedTransform } from '@web-engine/core/network'; // Mark entity for synchronizationaddComponent(world, Networked, entityId);addComponent(world, NetworkedTransform, entityId); // The entity's transform now syncs automatically// Position, rotation, and optionally velocity are replicatedAutomatic 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:
import { DeltaCompressor, DeltaFlags } from '@web-engine/core/network'; const compressor = new DeltaCompressor(4096); // Pool size // Compress entity stateconst 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 stateconst 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
// Monitor compression efficiencyconst 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:
import { SnapshotBuffer } from '@web-engine/core/network'; // Create interpolation buffer (100ms delay)const buffer = new SnapshotBuffer<NetworkEntityState>(64, 100); // Add snapshots from server updatesfunction onServerUpdate(timestamp: number, state: NetworkEntityState) { buffer.addSnapshot(timestamp, state);} // Get interpolated state for renderingfunction 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:
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 playerif (isLocalPlayer(entityId)) { // Predict movement immediately for responsiveness predictMovement(input); // Send input to server for validation network.sendInput(input);} // For remote entities, just render server stateif (isRemotePlayer(entityId)) { renderInterpolatedState(entityId);}Entity Ownership#
Each networked entity has an owner (the client/player who controls it):
// Spawn entity with ownershipconst 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 ownershipfunction 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:
import { NetworkLODSystem, LOD_PRESETS, quantizePosition, quantizeRotation} from '@web-engine/core/network'; // Initialize LOD systemconst 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 presetsconst lodSystem = new NetworkLODSystem(LOD_PRESETS.BALANCED); // Process entities with LODconst updates = lodSystem.processEntities( entities, observerPosition, deltaTime); // Only entities that need updates this frame are returnedfor (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:
import { BatchSerializer, BatchDeltaCompressor, CompressionStrategy} from '@web-engine/core/network'; // Create batch serializer with SoA (Structure of Arrays) layoutconst serializer = new BatchSerializer({ maxEntities: 100000, enableCompression: true, compressionStrategy: CompressionStrategy.DELTA_RLE,}); // Serialize all entities in one batchconst batch = serializer.serializeEntities(entities); // Batch delta compression for even more savingsconst compressor = new BatchDeltaCompressor();const compressed = compressor.compress(batch); // Send single packet with all entity updatesnetwork.sendBinary(compressed); // Receive and decompressconst 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#
// 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