Сетевая синхронизация
Сетевая синхронизация
Сетевая синхронизация необходима для согласованного и честного игрового процесса для всех игроков. Вы уже синхронизировали перемещение и анимацию NetworkObject игрока с помощью модели под управлением клиента. Но игровой процесс редко ограничивается персонажем: игрок может стрелять, открывать двери и взаимодействовать с объектами сцены. Такие взаимодействия тоже должны работать по сети, а их состояния синхронизироваться между клиентами и сервером.
В этом разделе мы настроим клиент-серверный обмен для взаимодействия игрока с окружением. Для этого реализуем сетевые состояния игры и отправку удалённых вызовов процедур (RPC) на сервер и обратно.
Игровая механика Рассмотрим клиент-серверный обмен на примере простой игровой механики.
Добавим на уровень триггерные коллайдеры, которые меняют цвет при контакте с игроком: при касании одного игрока объект принимает один цвет, а при касании другого - другой. Создайте в сцене новый GameObject, например куб. Добавьте BoxCollider и включите параметр IsTrigger. Создайте прозрачный материал, назначьте его компоненту MeshRenderer - в примере используется шейдер URP/Lit - и задайте нейтральный исходный цвет, например белый.
ColorTrigger меняет цвет при срабатывании триггера
Триггер будет получать сетевое значение цвета.
Теперь добавим сетевую синхронизацию. Для передачи изменения цвета по сети используем NetworkVariable и RPC.
Объявление NetworkVariable NetworkVariable - специализированная переменная для синхронного управления состоянием по сети. Изменения на сервере передаются всем клиентам. Такой механизм подходит для непрерывно синхронизируемых данных: позиции, здоровья или, как в этом примере, цвета триггера.
Создадим NetworkBehaviour с именем ColorTrigger и добавим его к объекту триггера. Скрипт будет содержать NetworkVariable m_NetworkColor со значением типа Color.
using UnityEngine;
using Unity.Netcode;
public class ColorTrigger : NetworkBehaviour {
public NetworkVariable<Color> m_NetworkColor = new NetworkVariable<Color>(Color. white);
private Material m_InstanceMaterial;
public override void OnNetworkSpawn() {
m_NetworkColor.OnValueChanged += OnColorChanged;
MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer != null) {
m_InstanceMaterial = new Material(meshRenderer.material);
meshRenderer.material = m_InstanceMaterial;
UpdateMaterialColor(m_NetworkColor.Value);
}
}public override void OnNetworkDespawn() {
m_NetworkColor.OnValueChanged -= OnColorChanged;
}
private void OnColorChanged(Color oldColor, Color newColor) { UpdateMaterialColor(newColor); }
private void UpdateMaterialColor(Color newColor) {
if (m_InstanceMaterial != null) {
m_InstanceMaterial.SetColor(“_BaseColor”, newColor);
}
}
}Когда игрок входит в триггер, скрипт переключает свойство базового цвета экземпляра материала. Для этого используется NetworkVariable m_NetworkColor. Разберём принцип работы: - NetworkVariable хранит фактическое значение цвета и синхронизирует его между всеми клиентами. Скрипт выполняется и на сервере, и на клиентах, но по умолчанию записывать NetworkVariable может только сервер; клиенты могут лишь читать значение Color. - Событие OnValueChanged обновляет базовый цвет материала триггера при каждом изменении NetworkVariable. Скрипт подписывается на событие в OnNetworkSpawn и отписывается в OnNetworkDespawn. NetworkTransform предназначен именно для данных Transform - позиции, поворота и масштаба, - тогда как NetworkVariable может синхронизировать более общие данные: примитивные типы, пользовательские структуры и другие значения игрового состояния. На клиенте нельзя напрямую изменить NetworkVariable, поскольку право записи принадлежит серверу. Клиент должен запросить изменение у сервера; сервер обновляет состояние и передаёт его обратно, после чего клиент видит результат. Для такого обмена используется RPC. Удалённый вызов процедур позволяет одному устройству потребовать от другого выполнить действие или обновление. RPC можно отправлять с клиента на сервер и в обратном направлении.
Рассмотрим, как реализовать RPC для этой задачи.
Серверные RPC Клиент КЛИЕНТ
Только место вызова; на клиенте не выполняется Задержка между клиентом и сервером
MyServerRPC() выполняется в серверной версии obj
Сервер СЕРВЕР
obj существует и на клиенте, и на сервере
Серверный RPC удалённо вызывается клиентом и выполняется на сервере.
Добавление RPC RPC позволяют удалённо вызывать функции на сервере или других клиентах. Метод с атрибутом [Rpc(SendTo.Server)] клиент вызывает на сервере, а метод с [Rpc(SendTo.Client)] сервер вызывает на клиентах. Также поддерживается устаревший синтаксис [ServerRpc] и [ClientRpc] для серверных и клиентских RPC.
RPC похожи на обычные методы, но должны соответствовать нескольким соглашениям:
- Атрибут Rpc: пометьте метод атрибутом [Rpc] и укажите получателя. Например, [Rpc(SendTo.Server)] вызывает метод только на сервере. - Именование: имя метода должно оканчиваться суффиксом Rpc, например DoSomethingRpc.
RPC лучше подходят для отдельных событий - действий игрока или изменений игрового состояния, которые не требуют непрерывной синхронизации. Добавьте следующие методы в скрипт TriggerColor:
private void OnTriggerEnter(Collider other) {
NetworkObject networkObject = other.GetComponent<NetworkObject>();
if (IsClient && networkObject != null && networkObject.IsOwner) {
ChangeColorServerRpc(networkObject.OwnerClientId);
}
}private void ChangeColorServerRpc(ulong playerId) {
∕∕ система команд: для чётных, для нечётных // Простая Simple team system: blueсиний for even, red forкрасный odd Color newColor = (playerId % 2 == 0) ? new Color(0, 0, 1, 0.5f) : new Color(1, 0, 0, 0.5f);
m_NetworkColor.Value = newColor;
}В одиночной игре нужную логику можно добавить в OnTriggerEnter. В многопользовательской игре взаимодействия обычно обрабатываются через клиент-серверный обмен, чтобы состояния всех клиентов оставались согласованными. Вместо прямого изменения m_NetworkColor метод OnTriggerEnter проверяет, является ли текущий экземпляр игры клиентом. Если да, он вызывает ChangeColorServerRpc и передаёт OwnerClientId. Метод выбирает новый цвет по идентификатору игрока - в этом примере чётным номерам соответствует синий, нечётным красный - и обновляет m_NetworkColor. Изменение передаётся всем подключённым клиентам, поэтому каждый экземпляр игры видит одинаковое состояние. Когда m_NetworkColor меняется, на всех клиентах вызывается OnColorChanged и обновляет цвет материала триггера.
NetworkVariable меняет цвет на сервере и синхронизирует его у всех клиентов
Триггер вызывает RPC
Эта простая механика демонстрирует клиент-серверное взаимодействие.
Механика триггера Пример прост, но похожая механика встречается во многих многопользовательских играх, где действия игрока вызывают визуальные изменения окружения. Например:
- В кооперативной головоломке игроки могут воздействовать на окружение кнопками и триггерами. Визуальная реакция игрового мира служит общим сигналом и помогает согласовать действия. - В соревновательном шутере игроки захватывают контрольные точки, которые часто окрашиваются в цвет владеющей ими команды. Этот визуальный сигнал помогает игрокам корректировать стратегию. - В сетевых RPG отдельные области уровня могут накладывать усиления, ослабления и другие эффекты.
RPC и NetworkVariable Выбор между NetworkVariable и RPC при синхронизации данных зависит от задачи:
- NetworkVariable и NetworkTransform подходят для непрерывно синхронизируемых данных: позиции, здоровья или, как здесь, цвета триггера. В этом примере NetworkVariable хранит активный цвет триггера.
- Примеры: здоровье, позиционные данные, игровой счёт.
- Удалённые вызовы процедур (RPC) лучше подходят для отдельных событий, например действий игрока или изменений состояния, не требующих непрерывной синхронизации. В этом примере RPC сообщает серверу, что игрок вошёл в зону триггера, после чего сервер меняет цвет с учётом идентификатора игрока.
- Примеры: действия игрока - выстрел или применение способности; игровые события - создание объекта, начало игры или победа. Оба механизма необходимы для синхронизации сетевой игры: NetworkVariable управляет длительными состояниями, а RPC - отдельными событиями и действиями.
Следующие примеры помогут определить, когда применять NetworkVariable, а когда RPC.
Задача / система
RPC
NetworkVariables
Управление инвентарём
Уведомляют клиентов о подборе или использовании предмета, его добавлении и удалении.
Хранят инвентарь каждого игрока; NetworkList синхронизирует список предметов.
Боевая система
Выполняют атаки и особые приёмы; RPC может наносить урон и эффекты.
Синхронизируют здоровье, состояние игрока и активные эффекты.
Взаимодействие с окружением
Запускают действия: открытие двери или включение механизма.
Хранят состояние интерактивных объектов, например открыта ли дверь.
Цели
Сообщают о выполнении цели.
Отслеживают прогресс, число собранных предметов и выполненных задач.
Подробнее см. на странице документации RPC vs NetworkVariable.
Проектирование многопользовательской игры Освоив основы Netcode, при разработке важно мыслить категориями сетевой многопользовательской игры. Поведение, простое в одиночной игре, часто усложняется, когда его необходимо воспроизвести на нескольких устройствах с помощью NetworkVariable или RPC.
Заранее спланируйте синхронизацию состояний между клиентами и сервером. Для постоянно обновляемых данных используйте NetworkVariable, для отдельных событий RPC. Передавайте только необходимые данные, чтобы сократить сетевой трафик. Одиночную игру можно преобразовать в многопользовательскую, но обычно эффективнее учитывать сетевой режим с самого начала. Заранее решите, какие объекты и действия контролирует сервер, а какие - клиенты. Серверные полномочия дают максимальную безопасность и согласованность, но для снижения задержки некоторых действий их следует разумно сочетать с полномочиями клиента.
Задержка часто становится серьёзной проблемой при создании сетевой игры. Далее рассмотрим её влияние на многопользовательский процесс. Для вдохновения посмотрите доклад Unite 2024 о наиболее распространённых и серьёзных ошибках при создании многопользовательских игр и способах их избежать.
Network synchronization
Network synchronization is essential for maintaining a consistent – and fair – gaming experience for all players. You’ve already seen how to synchronize player movements and animations with the Player NetworkObject using a client-driven model. However, gameplay often involves more than just the player character. Depending on your game design, your player may need to shoot projectiles, open doors, or interact with other scene objects. These interactions will need to be networkable, with their own game states that need to be synced between the clients and the server. In this section, we will set up client-server communication for gameplay actions, where the player may interact with part of the game environment. This involves implementing networked game states and sending remote procedure calls (RPCs) to and from the server.
Gameplay mechanic To illustrate server-client communication, let’s recreate a simple game mechanic. Let’s start by adding Trigger Colliders to the game level that change color when making contact with a player. They can change one color when one player touches it and another color when a different player touches it. In the scene, create a new GameObject (e.g., a cube). Add a BoxCollider component and enable the IsTrigger option. Create and assign a transparent material to the MeshRenderer (this example uses the URP/Lit shader). Set its initial color to something neutral like white.
The trigger will receive a networked color value.
Next, we need to add network synchronization. Let’s use NetworkVariables and RPCs to synchronize the color change across the network.
Define a NetworkVariable A NetworkVariable is a specialized variable designed for synchronized state management across the network. Changes to a NetworkVariable on the server propagate to all clients. This is ideal for continuously synchronized data, such as positions, health points, or in this case, the color state of the trigger. We’ll create a NetworkBehaviour called ColorTrigger and attach it to the trigger object. This script will contain a NetworkVariable called m_NetworkColor that contains a Color value.
using UnityEngine;
using Unity.Netcode;
public class ColorTrigger : NetworkBehaviour {
public NetworkVariable<Color> m_NetworkColor = new NetworkVariable<Color>(Color. white);
private Material m_InstanceMaterial;
public override void OnNetworkSpawn() {
m_NetworkColor.OnValueChanged += OnColorChanged;
MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer != null) {
m_InstanceMaterial = new Material(meshRenderer.material);
meshRenderer.material = m_InstanceMaterial;
UpdateMaterialColor(m_NetworkColor.Value);
}
}public override void OnNetworkDespawn() {
m_NetworkColor.OnValueChanged -= OnColorChanged;
}
private void OnColorChanged(Color oldColor, Color newColor) { UpdateMaterialColor(newColor); }
private void UpdateMaterialColor(Color newColor) {
if (m_InstanceMaterial != null) {
m_InstanceMaterial.SetColor(“_BaseColor”, newColor);
}
}
}When a player enters the trigger, this script will toggle the base color property of its material instance. The script uses a NetworkVariable called m_NetworkColor. Here’s a breakdown of how this works: —
This NetworkVariable keeps track of the actual color value and then syncs across all clients. Though the script runs on both the server and the clients, by default, only the server has write permissions to the NetworkVariable. Clients can only read its Color value.
The OnValueChanged event updates the trigger’s material base color whenever the NetworkVariable changes. The script subscribes to the event in OnNetworkSpawn and unsubscribes in OnNetworkDespawn.
While a NetworkTransform is specific to syncing transform data (position, rotation, scale), a NetworkVariable can sync more general data types, including primitive types, custom structs, and other data necessary for game state management. When working on a client, you can’t change the NetworkVariable directly because it is serverauthoritative. Instead, the client must notify the server to make any changes. The server updates the state and propagates it back, and only then does the client see the change take effect. To handle communication for changes like this, we use an RPC. RPCs can allow one device to require another device to perform specific actions or updates. RPCs can be called from the client to the server, or vice versa.
Let’s look at how to implement an RPC for this purpose.
A server RPC runs remotely from the client to the server.
Adding an RPC RPCs allow you to invoke functions on the server or other clients remotely. Methods marked with [Rpc(SendTo.Server)] are called on the server from a client, and those marked with [Rpc(SendTo.Client)] are called on clients from the server. You can also use the legacy syntax [ServerRpc] or [ClientRpc] to indicate a server RPC or client RPC here. RPCs are much like other methods, except they must follow a few conventions: —
Rpc attribute: Annotate your method with the [Rpc] attribute and specify a possible target, e.g., [Rpc(SendTo.Server)] will call the method only on the server.
Naming convention: End the method name with the suffix “Rpc” e.g., DoSomethingRpc.
RPCs are better suited for discrete events, such as player actions or specific game state changes that do not need continuous synchronization. Append these methods to the TriggerColor script:
private void OnTriggerEnter(Collider other) {
NetworkObject networkObject = other.GetComponent<NetworkObject>();
if (IsClient && networkObject != null && networkObject.IsOwner) {
ChangeColorServerRpc(networkObject.OwnerClientId);
}
}private void ChangeColorServerRpc(ulong playerId) {
// Simple team system: blue for even, red for odd Color newColor = (playerId % 2 == 0) ? new Color(0, 0, 1, 0.5f) : new Color(1, 0, 0, 0.5f);
m_NetworkColor.Value = newColor;
}
In a single-player game, you could add the appropriate logic to OnTriggerEnter. However, in a multiplayer game, you typically handle interactions
using client-server communication to ensure that all clients remain in sync. Instead of setting the m_NetworkColor value directly, OnTriggerEnter checks if the current game instance is a client. If it is, it calls ChangeColorServerRpc and passes in the OwnerClientId. This method determines the new color based on the player’s ID (in this example, even ID numbers become blue, while odd IDs become red) and updates the m_NetworkColor value. This change is propagated to all connected clients, ensuring every game instance has a consistent view of the game state. When the m_NetworkColor changes, the OnColorChanged method is triggered on all clients, updating the trigger’s material color.This simple game mechanic shows client-server interactions.
Trigger mechanic Even though this is a simple example, similar game mechanics can be found in many multiplayer games where the player’s actions can trigger visual changes to the environment. For example: —
In a cooperative puzzle game, players may need to use buttons or triggers to interact with the environment. The visual cues that happen in the game environment serves as a signal for everyone to coordinate their actions.
In a competitive shooter, players can take control points on the level. These often change color to the team that controls them. Again, this doubles as a visual cue to players to adjust their strategies accordingly.
In online RPGs, specific areas of the level can trigger buffs, debuffs, or other effects.
RPCs versus NetworkVariables When synchronizing data, choosing between NetworkVariables and RPCs depends on the use case: —
NetworkVariables and NetworkTransforms are ideal for continuously synchronized data, such as positions, health points, or in this case, the color state of the trigger. Here, the color trigger uses a NetworkVariable to store its active color. —
Use cases: Health points, positional data, game scores
Remote procedure calls (RPCs) are better suited for discrete events, such as player actions or specific game state changes that do not need continuous synchronization. In this example, the RPC notifies all clients when a player enters the trigger zone, prompting the server to change the color based on the player’s ID. —
Use cases: Player actions (e.g., shooting, using an ability), game events (e.g., spawning, game start, game won)
Both mechanisms are essential for data synchronization in networked games, where NetworkVariables manage ongoing states and RPCs handle specific events and actions.
These gameplay examples can help you determine when to use NetworkVariables or RPCs.
Task/system
RPCs
NetworkVariables
Inventory management
Notify clients when an item is picked up or used, to be added or removed from the inventory.
Maintain the current inventory for each player. The inventory can be a NetworkList that syncs the list of items.
Combat systems
Execute combat actions like attacks or special moves. An RPC can apply damage and effects.
Track the health and status of each player. Health points and active status effects can be synced using NetworkVariables.
Environmental interactions
Trigger specific actions like opening a door or activating a mechanism.
Maintain the state of interactive objects, such as whether a door is open or closed.
Objectives
Signal the completion of an objective.
Track ongoing progress toward an objective. Store the number of items collected or completed tasks in a NetworkVariable.
For a more detailed discussion, see the RPC vs NetworkVariable documentation page.
Designing for multiplayer Now that you’re familiar with the basics of netcode, it’s essential to adopt a “networked multiplayer” mindset when creating your games. What might be simple in a single-player game often becomes more complex when we need to incorporate NetworkVariables or RPCs to create the same behavior over multiple devices. Plan how you’ll synchronize the states between clients and the server. Use NetworkVariables for continuously synced data and RPCs for discrete events. Then, make sure to minimize network traffic by syncing only what’s necessary. While it’s possible to convert a single-player game into a multiplayer game, it’s usually more efficient to design with multiplayer in mind from the start. Decide early which objects and actions will be owned by the server and which can be managed by the clients. While server authority offers the most security and consistency, balance that with client authority to reduce latency for certain actions. This latency can be a sore sticking point in building your networked game, so let’s next examine how it impacts the multiplayer experience. For some inspiration, see this Unite 2024 session that explores the most common and significant mistakes in making a multiplayer game, and how to avoid them to improve your chances of success.