Skip to main content

World Synchronization

The World Sync system handles real-time player position synchronization, remote player rendering, NPC state coordination, and the channel/instance system. It is built on Nakama's realtime match API.

Architecture Overview

The world sync stack consists of four main components:

ClassFileResponsibility
WorldSyncServiceWorld/WorldSyncService.csPosition broadcasting, AOI filtering, match state routing
PlayerNetworkEntityWorld/PlayerNetworkEntity.csRemote player rendering, interpolation, nameplate, follower sprite
PlayerSpawnManagerWorld/PlayerNetworkEntity.csSpawning/despawning remote player GameObjects based on AOI events
ChannelManagerWorld/WorldSyncService.csServer channel/instance switching and population queries

Additionally, NPCSyncService handles NPC engagement locking across players, and FollowerUpdateData defines the wire protocol for syncing monster followers.

WorldSyncService

Initialization

WorldSyncService is initialized automatically during NakamaManager.FinalizeConnection():

await WorldSync.InitializeAsync(userId);

This creates (or joins) a Nakama realtime match named "world", stores the worldMatchId, and processes any existing presences in the match as nearby players.

Match State Protocol

All world sync communication uses Nakama match state messages with the following op codes:

OpCodeConstantPurpose
1OP_POSITIONPlayer position update (x, y, z, direction, mapId)
2OP_MAP_CHANGEPlayer changed to a different map
3OP_EMOTEPlayer emote (reserved)
4OP_FOLLOWER_UPDATEPlayer's monster follower changed

Incoming match state is routed by HandleMatchState(), which ignores messages from the local player and dispatches based on op code.

Broadcasting Position

The local player's position is sent via UpdateLocalPosition():

NakamaManager.Instance.WorldSync.UpdateLocalPosition(
position, mapId, direction);

Throttling rules:

  • Rate limit: Controlled by OnlineConfig.PositionSyncRate (default: 10 messages/second).
  • Dead zone: Positions within 0.01 units of the last synced position are suppressed.

The payload is a JSON-serialized PositionData with fields: x, y, z, dir (0=down, 1=left, 2=right, 3=up), and map.

Follower Broadcasting

When the local player's monster follower changes, broadcast the update:

var data = FollowerUpdateData.FromMonster(monster, form, gender);
NakamaManager.Instance.WorldSync.BroadcastFollowerUpdate(data);

// To clear (hide follower for remote players):
NakamaManager.Instance.WorldSync.BroadcastFollowerUpdate(
FollowerUpdateData.CreateClear());

FollowerUpdateData includes: speciesId, formIndex, gender, isShiny, isEgg, and nickname. A speciesId of 0 is the "clear" signal.

Follower broadcasts are de-duplicated: same species within a 5-second cooldown window is suppressed.

Area of Interest (AOI) Filtering

AOI filtering prevents the client from processing updates for players too far away. The radius is configured via OnlineConfig.AOIRadius (default: 20 tiles).

When a remote player's position update arrives:

  1. Distance from the local player is calculated.
  2. If within AOI radius and not already tracked, OnPlayerEnterAOI fires.
  3. If within AOI, OnPlayerPositionUpdate fires with (userId, position, direction).
  4. If outside AOI and previously tracked, OnPlayerExitAOI fires and the player is removed from NearbyPlayers.

Events

WorldSync.OnPlayerEnterAOI += (OnlinePlayerInfo info) => { /* spawn */ };
WorldSync.OnPlayerExitAOI += (string userId) => { /* despawn */ };
WorldSync.OnPlayerPositionUpdate += (string userId, Vector3 pos, int dir) => { /* move */ };
WorldSync.OnPlayerMapChanged += (string userId, string mapId) => { /* map transition */ };
WorldSync.OnFollowerUpdate += (string userId, FollowerUpdateData data) => { /* follower change */ };

The NearbyPlayers dictionary provides a read-only snapshot:

IReadOnlyDictionary<string, OnlinePlayerInfo> nearby =
NakamaManager.Instance.WorldSync.NearbyPlayers;

PlayerNetworkEntity

Each remote player is represented by a PlayerNetworkEntity MonoBehaviour on a spawned GameObject. It handles:

Initialization

entity.Initialize(onlinePlayerInfo);

Sets userId, displayName, guildTag, level, battle status, and spectatable flag from OnlinePlayerInfo.

Interpolated Movement

When ReceivePositionUpdate(position, direction) is called:

  • If the distance exceeds snapDistance (default: 5 tiles), the entity teleports instantly (handles map changes/warps).
  • Otherwise, the entity interpolates toward the target at interpolationSpeed (default: 10).

Sprite Animation

Remote players use a 4-direction, 4-frame walk cycle (16 sprites total). The animation runs at 0.2 seconds per frame when the entity is moving. When idle, it shows the first frame of the current direction. Left/right flipping is handled via SpriteRenderer.flipX.

Follower Display

Remote player followers are managed through SetFollower(FollowerUpdateData):

  • Non-null data with speciesId > 0 shows the follower sprite child.
  • null or speciesId == 0 calls ClearFollower(), hiding the sprite.
  • The HasActiveFollower and CurrentFollowerSpeciesId properties expose the current state.

Interaction

Clicking on a remote player fires OnClicked. The IsInBattle property and SetBattleStatus() method track whether the player is in a battle (shows a visual indicator on the nameplate).

PlayerSpawnManager

PlayerSpawnManager is a MonoBehaviour that listens to WorldSyncService AOI events and automatically manages remote player GameObjects.

  • OnPlayerEnterAOI: Instantiates the playerNetworkPrefab (or creates a bare GameObject with SpriteRenderer + PlayerNetworkEntity), calls Initialize(), and registers it.
  • OnPlayerExitAOI: Destroys the corresponding GameObject and removes it from the tracking dictionary.
  • OnPlayerPositionUpdate: Calls ReceivePositionUpdate() on the corresponding entity.

Access all currently spawned players:

var spawned = spawnManager.GetSpawnedPlayers();

NPC Synchronization

NPCSyncService ensures NPCs have consistent state across all connected players.

Engagement Locking

When a player talks to an NPC in the online world, the NPC is "locked" so other players cannot interact simultaneously:

npcSync.RegisterNPC("npc_nurse_joy", position, "PalletTown");

// Player starts talking
bool engaged = npcSync.TryEngageNPC("npc_nurse_joy", userId);

// Player finishes conversation
npcSync.ReleaseNPC("npc_nurse_joy");

The NPCNetworkState tracks: NpcId, Position, MapId, IsEngaged, EngagedByUserId, and DialogState.

OnNPCStateChanged fires whenever an NPC's engagement state changes.

Channel System

ChannelManager provides an MMO-style channel/instance system, similar to popular monster capture MMOs.

Configuration

SettingDefaultDescription
DefaultChannel1Channel joined on connect
MaxChannels10Maximum channels per world

Switching Channels

bool switched = await NakamaManager.Instance.Channels.SwitchChannelAsync(3);
int current = NakamaManager.Instance.Channels.CurrentChannel;

Channel values must be between 1 and MaxChannels. The OnChannelChanged event fires on successful switch.

Channel Population

Dictionary<int, int> populations =
await NakamaManager.Instance.Channels.GetChannelPopulationsAsync();

The OnChannelPopulationUpdate event provides (channel, playerCount) updates.

Bandwidth Optimization

Several mechanisms minimize network traffic:

  1. Position throttling: PositionSyncRate limits messages per second (default: 10/s).
  2. Dead zone filtering: Positions within 0.01 units of the last sync are suppressed.
  3. AOI culling: Only players within AOIRadius tiles are processed.
  4. Max visible players: MaxVisiblePlayers caps rendered entities (default: 50).
  5. Follower dedup: Same-species follower broadcasts within 5 seconds are suppressed.
  6. Main thread dispatch: All Unity API calls are marshaled to the main thread via NakamaManager.RunOnMainThread(), preventing cross-thread issues.

Graceful Shutdown

When disconnecting, WorldSyncService.StopSyncAsync() is called automatically:

await WorldSync.StopSyncAsync();

This leaves the world match, clears worldMatchId, and empties the nearbyPlayers dictionary.