name: multiplayer-and-network-sync description: "Multiplayer networking -- LiveKit rooms, movement encoding, interpolation, profile sync, entity-participant mapping. Use when working with RoomHub, movement systems, EntityParticipantTable, or remote player sync." user-invocable: false
Multiplayer & Network Sync
Sources
docs/livekit-networking.md-- Entity sync via LiveKit rooms, host vs remote entity semantics
Dual-Room Architecture
RoomHub orchestrates four LiveKit rooms: Island, Scene (GateKeeper), Chat, and VoiceChat. Island + Scene are "local rooms" for entity synchronization; Chat + VoiceChat are independent.
RoomHub.StartAsync() starts Island + Scene + Chat in parallel; VoiceChat starts on demand. AllLocalRoomsRemoteParticipantIdentities() merges participants from Island + Scene (cached per frame).
IConnectiveRoom Lifecycle
ConnectiveRoom runs an infinite reconnection loop: PrewarmAsync -> repeated CycleStepAsync with 1s heartbeat. State machine: Stopped -> Starting -> Running -> Stopping -> Stopped. Uses Atomic<State> for thread-safe state transitions (LiveKit callbacks arrive off main thread). Recovery delay on failure is 5 seconds.
Why two rooms for entities: The Scene room connects only to the scene the host is standing in, so only co-located players sync scene-specific CRDT state. The Island room provides global visibility -- remote avatars appear even when the host is in a different scene, but Island data is NOT used for scene CRDT synchronization (bandwidth concern).
Host Entity Semantics (CRITICAL GOTCHA)
See
docs/livekit-networking.md
Host components (PBIdentityData, PBAvatarEquippedData, PBAvatarBase) are ALWAYS present on the scene from startup through the entire lifecycle -- regardless of whether the host is inside or outside the scene.
Remote entities behave completely differently:
- If the scene is NOT the current scene, remote entities cannot be present in CRDT state (even if visually nearby via Island room)
- When the host enters a scene, all remote participants within bounds are propagated as if they just entered
- When the host is within the scene and others enter/leave, it works as expected
- Remote entities themselves are never explicitly deleted; their CRDT entity range is reserved
Implication for JS scenes: Host identity components are always queryable. Do not assume their presence means the host is physically inside the scene. Remote player presence accurately reflects co-location only when the host is current.
Movement Encoding & Transmission
NetworkMovementMessage Fields
timestamp, position, velocity, velocitySqrMagnitude, rotationY, headIKYawEnabled, headIKPitchEnabled, headYawAndPitch, movementKind, animState, velocityTier, isSliding, isStunned, isInstant, isEmoting.
Encoding Pipeline
FloatQuantizer-- Fixed-size quantization via scaled integers.Compress(value, min, max, bits)/Decompress.ParcelEncoder-- Flattens 2D parcel coordinates(x, y)into a 1D index relative to the Genesis City grid.TimestampEncoder-- Circular buffer encoding with wraparound detection on decompression.
Send Throttling
PlayerMovementNetSendSystem caps at 10 messages/sec. Adaptive send rate: drops to MoveSendRate on movement, doubles (up to StandSendRate) when idle. Immediate sends on grounded/jump state changes. Messages are sent to both Island and Scene pipes via MultiplayerMovementMessageBus.Send(), supporting both compressed and uncompressed schemas.
Movement Interpolation
InterpolationSpline Types
| Type | Description |
|---|---|
Linear |
Vector3.Lerp wrapper |
Hermite |
Cubic Hermite spline matching start/end positions and velocities |
MonotoneYHermite |
Hermite with Y-axis monotonicity (prevents vertical overshoot) |
FullMonotonicHermite |
Monotonic on all axes |
Bezier |
Cubic Bezier with velocity-derived control points |
VelocityBlending |
Projective velocity blending (Murphy & Lengyel 2011) |
PositionBlending |
Projective position blending (Murphy & Lengyel 2011) |
RemotePlayersMovementSystem
Runs in PresentationSystemGroup. Processes a priority queue of NetworkMovementMessage per remote player:
- First message -- teleport to position
- Cooldown -- wait 2x
MoveSendRatefor stability - Interpolation -- starts between past and new message using selected spline; falls back to
Linearwhen velocity is near-zero or grounded state changes - Extrapolation -- when no messages arrive and speed > threshold,
ExtrapolationComponentcontinues movement along last velocity for up toTotalMoveDuration - Blend -- after extrapolation stops, blends back with speed-limited interpolation
Catch-Up: When inbox has more messages than CatchUpMessagesMin, interpolation duration is shortened proportionally to smooth DeltaTime.
Profile & Entity Sync
EntityParticipantTable
Bidirectional mapping between wallet IDs and ECS entities, with room source tracking (ISLAND, GATEKEEPER, or both). NOT thread-safe -- only accessed from main thread.
Key API: Register(walletId, entity, fromRoom), Release(walletId, fromRoom) (returns true only when ConnectedTo == RoomSource.NONE), AddRoomSource(walletId, fromRoom).
Release only removes the entity when disconnected from all rooms. This allows a player visible via Island to remain alive when they leave the Scene room.
ThreadSafe Disconnect Buffering
LiveKit participant callbacks fire off the main thread. LiveKitRemoveIntentions buffers disconnect events using MutexSync. Main thread consumes via OwnedBunch<RemoveIntention> which acquires the lock on construction, exposes the collection, and clears + releases on Dispose().
ProfileBroadcast
Sends AnnounceProfileVersion to both Island and Scene pipes. Async: fetches self-profile version before sending via SendAndDisposeAsync with KindReliable.
SDK Propagation Systems
PlayerTransformPropagationSystem(Global -> Scene) -- CopiesCharacterTransforminto the scene world'sSDKTransformwhen transform has changed and player is assigned to scene.WritePlayerTransformSystem(Scene -> CRDT) -- ConvertsSDKTransformto scene-relative coordinates and writes viaIECSToCRDTWriter.WritePlayerIdentityDataSystem(Scene -> CRDT) -- WritesPBPlayerIdentityData(address + isGuest) on dirty, with force-write onInitialize(). Uses static lambda to avoid closures.PlayerProfileDataPropagationSystem(Global -> Scene) -- CopiesProfiledata to scene entities viaCharacterDataPropagationUtilitywhen profile or CRDT entity assignment is dirty.
Detailed Reference
For detailed code examples, see reference.md.
Cross-References
- ecs-system-and-component-design -- System patterns,
ref varmutation rules, cleanup lifecycle - async-programming -- LiveKit async flows,
CancellationTokenSourcemanagement,UniTaskVoidexception handling - scene-runtime-and-crdt -- CRDT bridge (
IECSToCRDTWriter), scene state machine,MultiThreadSync - cross-world-ecs-access -- Global/scene world access,
PlayerCRDTEntity/PlayerSceneCRDTEntitybridge,ISceneIsCurrentListener