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, with one match per map.
It has two transports, chosen by OnlineConfig.WorldSyncMode:
| Mode | How positions travel | Who decides who sees whom |
|---|---|---|
Authoritative (the default) | Each client sends its position to the server's overworld match, a Go plugin, which sends every recipient one batch per tick with only what that recipient sees | The server (area-of-interest in the Go match) |
Relayed | The match relays every position to every player of the map | Each client (OnlineConfig.AOIRadius) |
Authoritative is the one that scales to thousands of players; the relay degrades from about a hundred players in one match. The rest of this page says which parts are specific to one mode.

Everything this page describes is visible in that one frame: the channel badge and
Online — Channel 1 at the top left, the nearby-player count that Area-of-Interest
filtering produces, and the latency/server indicator at the bottom. If the count reads zero
when you expect company, AOI is the first thing to check.
Architecture Overview
The world sync stack consists of these components:
| Class | File | Responsibility |
|---|---|---|
WorldSyncService | World/WorldSyncService.cs | Joining the map's match, position broadcasting, match state routing, client-side AOI (relayed mode) |
WorldBatchDecoder | World/WorldBatchDecoder.cs | Decodes the server's per-tick batch (authoritative mode) into the same AOI events |
PlayerNetworkEntity | World/PlayerNetworkEntity.cs | Remote player rendering, interpolation, nameplate, follower sprite |
PlayerSpawnManager | World/PlayerSpawnManager.cs | Spawning/despawning remote player GameObjects based on AOI events, and drawing their emotes |
ChannelManager | World/WorldSyncService.cs | Server channel/instance switching and population queries |
Additionally, NPCSyncService (in World/PlayerNetworkEntity.cs) handles NPC engagement locking across players, and FollowerUpdateData defines the wire protocol for syncing monster followers. Both transports raise the same events, so the code that draws players does not know which one is active.
WorldSyncService
Initialization
WorldSyncService is initialized automatically during NakamaManager.FinalizeConnection():
await WorldSync.InitializeAsync(userId);
This only prepares the service. The match itself is joined as soon as the map is known (the player's scene), and again every time the player changes map:
- Authoritative mode: the client calls the
world_joinRPC with{ "mapId": "<map>" }. The server returns thematchIdof the authoritative match of that map (it creates it if it does not exist yet and refuses a map it has no census for), and the client joins it through the socket. If the server's Go plugin is not loaded the join fails with an error in the console: there is no silent fallback to the relayed transport. - Relayed mode: the client creates or joins the relayed match named
world_<mapId>(WorldSyncService.WorldMatchNameFor).
When the player changes map it leaves the old match, forgets its neighbours (the spawn manager despawns them) and announces its position and follower in the new one.
Match State Protocol
All world sync communication uses Nakama match state messages with the following op codes:
| OpCode | Constant | Direction | Purpose |
|---|---|---|---|
1 | OP_POSITION | client → match | Player position update (x, y, z, direction, map, plus name, look, level, guild, run/bike/swim flags) |
2 | OP_MAP_CHANGE | both | Player changed to a different map |
3 | OP_EMOTE | both | Player emote: see Online Emotes |
4 | OP_FOLLOWER_UPDATE | both | Player's monster follower changed |
10 | WorldBatchDecoder.OP_BATCH | match → client | The per-tick batch of the authoritative match |
Incoming match state is routed by HandleMatchState(). The batch (op 10) is resolved first, because it has no sender: it comes from the match itself. Every other message ignores the local player and is dispatched on its op code. Ops 2, 3 and 4 are events that the server forwards as they come to the other players.
The Authoritative Batch
In authoritative mode the server's overworld match runs at 10 ticks per second and sends one op 10 batch per recipient per tick, containing only what that recipient can see:
- mutual friends first, even if far away;
- then the nearest players within the area-of-interest radius (20 tiles), up to a cap (50 by default,
MCE_WORLD_MAX_VISIBLEon the server); - with hysteresis: a player already visible stays visible until 4 tiles beyond the radius, so the edge does not flicker.
The batch is compact. Players are identified by a per-match slot and a generation that goes up when a slot is reused (so a client never applies to a new player what it knew about the previous one):
{ "t": tick, "k": 1 on a keyframe,
"p": [[slot, gen, x, y, dir, flags(, z)]], positions; flags: run=1 bike=2 swim=4
"s": [[slot, gen, userId, name, look, lvl, guild, gm]], identity, only when the recipient does not know it yet
"f": [[slot, gen]] } left the recipient's area (or the match)
Idle players are not resent, a player entering the area arrives complete even if idle, and a keyframe every 5 seconds resends positions. WorldBatchDecoder turns the batch into the same OnPlayerEnterAOI, OnPlayerPositionUpdate and OnPlayerExitAOI events the relayed path raises.
The server validates every position it receives inside the loop (see below), so a client cannot place itself anywhere.
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 the fields x, y, z, dir (the facing direction) and map, plus what the receivers need to draw the player: name (for the nameplate), look (the name of the CharacterType to draw), lvl (the level of the strongest monster of the team), the guild tag, whether the player is a game master, and the run, bike and swim flags.
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.