Sincronización del mundo
El sistema World Sync gestiona la sincronización en tiempo real de la posición de los jugadores, el renderizado de los jugadores remotos, la coordinación del estado de los NPC y el sistema de canales/instancias. Está construido sobre la API de partidas en tiempo real de Nakama, con una partida por mapa.
Tiene dos transportes, elegidos por OnlineConfig.WorldSyncMode:
| Modo | Cómo viajan las posiciones | Quién decide quién ve a quién |
|---|---|---|
Authoritative (el predeterminado) | Cada cliente envía su posición a la partida del mundo del servidor, un plugin de Go, que envía a cada destinatario un lote por tick con solo lo que ese destinatario ve | El servidor (área de interés en la partida de Go) |
Relayed | La partida retransmite cada posición a todos los jugadores del mapa | Cada cliente (OnlineConfig.AOIRadius) |
Authoritative es el que escala a miles de jugadores; el retransmitido se degrada a partir de unos cien jugadores en una misma partida. El resto de esta página indica qué partes son específicas de cada modo.

Todo lo que describe esta página es visible en ese único fotograma: la insignia del canal y
Online — Channel 1 arriba a la izquierda, el contador de jugadores cercanos que produce el filtrado
por área de interés (Area-of-Interest) y el indicador de latencia/servidor abajo. Si el contador marca cero
cuando esperas compañía, el AOI es lo primero que hay que revisar.
Descripción general de la arquitectura
La pila de sincronización del mundo consta de estos componentes:
| Clase | Archivo | Responsabilidad |
|---|---|---|
WorldSyncService | World/WorldSyncService.cs | Unirse a la partida del mapa, difusión de posición, encaminamiento del estado de la partida, AOI del lado del cliente (modo retransmitido) |
WorldBatchDecoder | World/WorldBatchDecoder.cs | Decodifica el lote por tick del servidor (modo autoritativo) en los mismos eventos de AOI |
PlayerNetworkEntity | World/PlayerNetworkEntity.cs | Renderizado de jugadores remotos, interpolación, cartel con el nombre, sprite del seguidor |
PlayerSpawnManager | World/PlayerSpawnManager.cs | Generar/destruir los GameObjects de jugadores remotos según los eventos de AOI y dibujar sus emotes |
ChannelManager | World/WorldSyncService.cs | Cambio de canal/instancia del servidor y consultas de población |
Además, NPCSyncService (en World/PlayerNetworkEntity.cs) gestiona el bloqueo de interacción con NPC entre jugadores, y FollowerUpdateData define el protocolo de cable para sincronizar los monstruos seguidores. Ambos transportes lanzan los mismos eventos, así que el código que dibuja a los jugadores no sabe cuál está activo.
WorldSyncService
Inicialización
WorldSyncService se inicializa automáticamente durante NakamaManager.FinalizeConnection():
await WorldSync.InitializeAsync(userId);
Esto solo prepara el servicio. La propia partida se une en cuanto se conoce el mapa (la escena del jugador) y de nuevo cada vez que el jugador cambia de mapa:
- Modo autoritativo: el cliente llama a la RPC
world_joincon{ "mapId": "<map>" }. El servidor devuelve elmatchIdde la partida autoritativa de ese mapa (la crea si todavía no existe y rechaza un mapa del que no tiene censo), y el cliente se une a ella mediante el socket. Si el plugin de Go del servidor no está cargado, la unión falla con un error en la consola: no hay alternativa silenciosa al transporte retransmitido. - Modo retransmitido: el cliente crea o se une a la partida retransmitida llamada
world_<mapId>(WorldSyncService.WorldMatchNameFor).
Cuando el jugador cambia de mapa, abandona la partida anterior, olvida a sus vecinos (el gestor de generación los destruye) y anuncia su posición y su seguidor en la nueva.
Protocolo de estado de la partida
Toda la comunicación de sincronización del mundo usa mensajes de estado de partida de Nakama con los siguientes códigos de operación:
| OpCode | Constante | Dirección | Propósito |
|---|---|---|---|
1 | OP_POSITION | cliente → partida | Actualización de la posición del jugador (x, y, z, dirección, mapa, además de nombre, aspecto, nivel, gremio, indicadores de correr/bici/nadar) |
2 | OP_MAP_CHANGE | ambos | El jugador cambió a otro mapa |
3 | OP_EMOTE | ambos | Emote del jugador: ver Emotes en línea |
4 | OP_FOLLOWER_UPDATE | ambos | Cambió el monstruo seguidor del jugador |
10 | WorldBatchDecoder.OP_BATCH | partida → cliente | El lote por tick de la partida autoritativa |
El estado de partida entrante se encamina mediante HandleMatchState(). El lote (op 10) se resuelve primero, porque no tiene remitente: viene de la propia partida. Todos los demás mensajes ignoran al jugador local y se despachan según su código de operación. Las operaciones 2, 3 y 4 son eventos que el servidor reenvía tal como llegan a los demás jugadores.
El lote autoritativo
En modo autoritativo, la partida del mundo del servidor corre a 10 ticks por segundo y envía un lote op 10 por destinatario y tick, que contiene solo lo que ese destinatario puede ver:
- los amigos mutuos primero, aunque estén lejos;
- luego los jugadores más cercanos dentro del radio del área de interés (20 tiles), hasta un tope (50 por defecto,
MCE_WORLD_MAX_VISIBLEen el servidor); - con histéresis: un jugador que ya es visible sigue siéndolo hasta 4 tiles más allá del radio, para que el borde no parpadee.
El lote es compacto. Los jugadores se identifican por un slot propio de la partida y una generación que aumenta cuando se reutiliza un slot (de modo que un cliente nunca aplique a un jugador nuevo lo que sabía del anterior):
{ "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)
Los jugadores inactivos no se reenvían, un jugador que entra en el área llega completo aunque esté inactivo, y un fotograma clave cada 5 segundos reenvía las posiciones. WorldBatchDecoder convierte el lote en los mismos eventos OnPlayerEnterAOI, OnPlayerPositionUpdate y OnPlayerExitAOI que lanza el camino retransmitido.
El servidor valida cada posición que recibe dentro del bucle (ver abajo), así que un cliente no puede colocarse donde quiera.
Difusión de la posición
La posición del jugador local se envía mediante UpdateLocalPosition():
NakamaManager.Instance.WorldSync.UpdateLocalPosition(
position, mapId, direction);
Reglas de regulación:
- Límite de frecuencia: controlado por
OnlineConfig.PositionSyncRate(por defecto: 10 mensajes/segundo). - Zona muerta: se suprimen las posiciones a menos de 0,01 unidades de la última posición sincronizada.
La carga útil es un PositionData serializado en JSON con los campos x, y, z, dir (la dirección a la que mira) y map, además de lo que los receptores necesitan para dibujar al jugador: name (para el cartel con el nombre), look (el nombre del CharacterType que hay que dibujar), lvl (el nivel del monstruo más fuerte del equipo), la etiqueta del gremio, si el jugador es un game master, y los indicadores run, bike y swim.
Difusión del seguidor
Cuando cambia el monstruo seguidor del jugador local, difunde la actualización:
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 incluye: speciesId, formIndex, gender, isShiny, isEgg y nickname. Un speciesId de 0 es la señal de "borrar".
Las difusiones de seguidor se deduplican: se suprime la misma especie dentro de una ventana de enfriamiento de 5 segundos.
Filtrado por área de interés (AOI)
El filtrado por AOI evita que el cliente procese actualizaciones de jugadores demasiado lejanos.
En modo autoritativo filtra el servidor (ver el lote de arriba): el cliente solo recibe a los jugadores que debe dibujar, y OnlineConfig.AOIRadius no cambia lo que envía el servidor.
En modo retransmitido filtra el cliente. El radio se configura mediante OnlineConfig.AOIRadius (por defecto: 20 tiles). Cuando llega la actualización de posición de un jugador remoto:
- Se calcula la distancia al jugador local.
- Si está dentro del radio de AOI y aún no se le sigue, se dispara
OnPlayerEnterAOI. - Si está dentro del AOI, se dispara
OnPlayerPositionUpdatecon(userId, position, direction). - Si está fuera del AOI y se le seguía, se dispara
OnPlayerExitAOIy el jugador se quita deNearbyPlayers.
Eventos
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 */ };
El diccionario NearbyPlayers proporciona una instantánea de solo lectura:
IReadOnlyDictionary<string, OnlinePlayerInfo> nearby =
NakamaManager.Instance.WorldSync.NearbyPlayers;
PlayerNetworkEntity
Cada jugador remoto está representado por un MonoBehaviour PlayerNetworkEntity en un GameObject generado. Gestiona:
Inicialización
entity.Initialize(onlinePlayerInfo);
Establece userId, displayName, guildTag, level, el estado de batalla y el indicador de espectable a partir de OnlinePlayerInfo.
Movimiento interpolado
Cuando se llama a ReceivePositionUpdate(position, direction):
- Si la distancia supera
snapDistance(por defecto: 5 tiles), la entidad se teletransporta al instante (gestiona los cambios de mapa y los warps). - En caso contrario, la entidad interpola hacia el destino a
interpolationSpeed(por defecto: 10).
Animación del sprite
Los jugadores remotos usan un ciclo de caminar de 4 direcciones y 4 fotogramas (16 sprites en total). La animación corre a 0,2 segundos por fotograma cuando la entidad se mueve. En reposo, muestra el primer fotograma de la dirección actual. El volteo izquierda/derecha se gestiona con SpriteRenderer.flipX.
Visualización del seguidor
Los seguidores de los jugadores remotos se gestionan mediante SetFollower(FollowerUpdateData):
- Datos no nulos con
speciesId > 0muestran el hijo del sprite del seguidor. nullospeciesId == 0llama aClearFollower()y oculta el sprite.- Las propiedades
HasActiveFolloweryCurrentFollowerSpeciesIdexponen el estado actual.
Interacción
Al hacer clic en un jugador remoto se dispara OnClicked. La propiedad IsInBattle y el método SetBattleStatus() registran si el jugador está en una batalla (muestra un indicador visual en el cartel con el nombre).
PlayerSpawnManager
PlayerSpawnManager es un MonoBehaviour que escucha los eventos de AOI de WorldSyncService y gestiona automáticamente los GameObjects de los jugadores remotos.
- OnPlayerEnterAOI: instancia el
playerNetworkPrefab(o crea un GameObject vacío conSpriteRenderer+PlayerNetworkEntity), llama aInitialize()y lo registra. - OnPlayerExitAOI: destruye el GameObject correspondiente y lo quita del diccionario de seguimiento.
- OnPlayerPositionUpdate: llama a
ReceivePositionUpdate()en la entidad correspondiente.
Accede a todos los jugadores generados actualmente:
var spawned = spawnManager.GetSpawnedPlayers();
Sincronización de NPC
NPCSyncService garantiza que los NPC tengan un estado coherente entre todos los jugadores conectados.
Bloqueo de interacción
Cuando un jugador habla con un NPC en el mundo en línea, el NPC queda "bloqueado" para que otros jugadores no puedan interactuar a la vez:
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");
NPCNetworkState registra: NpcId, Position, MapId, IsEngaged, EngagedByUserId y DialogState.
OnNPCStateChanged se dispara cada vez que cambia el estado de interacción de un NPC.
Sistema de canales
ChannelManager ofrece un sistema de canales/instancias al estilo MMO, similar al de los MMO de captura de monstruos más populares.
Configuración
| Ajuste | Por defecto | Descripción |
|---|---|---|
DefaultChannel | 1 | Canal al que se une al conectar |
MaxChannels | 10 | Máximo de canales por mundo |
Cambiar de canal
bool switched = await NakamaManager.Instance.Channels.SwitchChannelAsync(3);
int current = NakamaManager.Instance.Channels.CurrentChannel;
Los valores de canal deben estar entre 1 y MaxChannels. El evento OnChannelChanged se dispara al cambiar con éxito.