~3 мин чтения

Мультиплеер — Netcode for GameObjects

NetworkObject, NetworkVariable, ServerRpc/ClientRpc — основы сетевой игры.

Мультиплеер — отдельная вселенная сложности, но Unity Netcode for GameObjects (NGO) сильно упрощает старт. Это официальный пакет Unity для синхронизации GameObject между сервером и клиентами в архитектуре client-server.

Веб

WebSocket-сервер с собственным протоколом + ваш state-менеджмент. Каждое поле, которое должно быть видно у всех, вы синхронизируете руками: emit → on → update Redux/Zustand.

Unity

NetworkObject автоматически синхронизирует Transform и NetworkVariable между сервером и клиентами. RPC — это вызовы методов через сеть, как удалённый emit. Транспорт абстрагирован (Unity Transport, Relay, Steam, и т.д.).

Архитектура NGO

NGO работает в режиме authoritative server (сервер — источник правды). Варианты роли:

  • Server — выделенный сервер, не имеет локального игрока. Для соревновательных игр и dedicated servers.
  • Host — клиент, который одновременно сервер. Удобно для P2P и кооператива.
  • Client — подключённый игрок.

NetworkManager — главный синглтон, который запускает соединение: StartHost(), StartServer(), StartClient(). На сцене один.

NetworkObject — главный компонент

Чтобы GameObject жил в сети, ему нужен NetworkObject. Это даёт:

  • Уникальный сетевой ID, одинаковый у сервера и клиентов.
  • Привязку к owner (обычно — клиенту, который этот объект “контролирует”).
  • Spawning — NetworkObject.Spawn() на сервере создаёт объект и на всех клиентах.
using UnityEngine;
using Unity.Netcode;

public class EnemySpawner : NetworkBehaviour
{
    [SerializeField] private GameObject enemyPrefab;

    public override void OnNetworkSpawn() {
        if (!IsServer) return;

        var enemy = Instantiate(enemyPrefab, transform.position, Quaternion.identity);
        enemy.GetComponent<NetworkObject>().Spawn();
    }
}

Prefab, который вы спавните по сети, должен быть зарегистрирован в NetworkPrefabsList на NetworkManager. Иначе клиенты не знают, как его собрать на своей стороне.

NetworkBehaviour — сетевой MonoBehaviour

Вместо MonoBehaviour для сетевых компонентов наследуйтесь от NetworkBehaviour. Это даёт:

  • IsServer, IsClient, IsOwner, IsHost — где сейчас выполняется код.
  • OnNetworkSpawn() / OnNetworkDespawn() — аналоги Start/OnDestroy в сетевом контексте.
  • Доступ к NetworkManager.Singleton.

NetworkVariable — синхронизированное поле

Хотите, чтобы здоровье врага было одинаковым у всех клиентов? Объявите его как NetworkVariable<T>:

public class NetEnemy : NetworkBehaviour
{
    public NetworkVariable<int> Health = new NetworkVariable<int>(
        100,
        readPerm: NetworkVariableReadPermission.Everyone,
        writePerm: NetworkVariableWritePermission.Server
    );

    public override void OnNetworkSpawn() {
        Health.OnValueChanged += (oldVal, newVal) => {
            Debug.Log($"Health: {oldVal}{newVal}");
            if (newVal <= 0) Die();
        };
    }

    public void TakeDamage(int amount) {
        if (!IsServer) return; // только сервер может писать
        Health.Value = Mathf.Max(0, Health.Value - amount);
    }
}

Параметры:

  • readPerm — кто может читать (Everyone, Owner).
  • writePerm — кто может писать (Server или Owner).

NetworkVariable передаётся каждый “tick” (по умолчанию ~30 раз/с), если значение поменялось.

RPC — удалённые вызовы

NGO 2.x (поставляется с Unity 6) поддерживает унифицированный атрибут [Rpc] с параметром SendTo. Это рекомендованный синтаксис для новых проектов:

using Unity.Netcode;
using UnityEngine;

public class Shooter : NetworkBehaviour
{
    [SerializeField] private GameObject bulletPrefab;
    [SerializeField] private AudioClip shotClip;

    // Клиент → сервер. Сервер создаёт пулю и рассылает звук.
    [Rpc(SendTo.Server)]
    public void FireRpc(Vector3 origin, Vector3 direction) {
        var bullet = Instantiate(bulletPrefab, origin, Quaternion.LookRotation(direction));
        bullet.GetComponent<NetworkObject>().Spawn();

        PlayShotSoundRpc(origin);
    }

    // Сервер → все клиенты (включая host'а). Презентационный звук.
    [Rpc(SendTo.ClientsAndHost)]
    private void PlayShotSoundRpc(Vector3 position) {
        AudioSource.PlayClipAtPoint(shotClip, position);
    }
}

Значения SendTo:

  • SendTo.Server — на сервер (любой клиент может вызвать).
  • SendTo.ClientsAndHost — на всех клиентов + host’а.
  • SendTo.NotServer — на всех, кроме сервера.
  • SendTo.Owner / SendTo.NotOwner — владельцу / всем кроме владельца.
  • SendTo.Me, SendTo.Everyone, SendTo.SpecifiedInParams — гибкие варианты.
Legacy [ServerRpc] / [ClientRpc] всё ещё работают

Старые атрибуты [ServerRpc] / [ClientRpc] (с обязательными суффиксами ServerRpc / ClientRpc в имени метода) поддерживаются для совместимости с NGO 1.x, но в новом коде используйте [Rpc] — он гибче и без жёсткого требования к имени метода. Также с NGO 2.9+ [ServerRpc(RequireOwnership=...)] deprecated в пользу параметра InvokePermission со значениями RpcInvokePermission.Everyone / Authority.

// Старый стиль (всё ещё валиден):
[ServerRpc]
public void FireServerRpc(Vector3 origin) { /* ... */ }

[ClientRpc]
private void PlayShotSoundClientRpc(Vector3 position) { /* ... */ }
Не пишите состояние из ClientRpc

ClientRpc — для презентационных вещей (звуки, эффекты, всплывающие надписи). Игровое состояние всегда на сервере, синхронизируется через NetworkVariable. Иначе клиенты разойдутся.

NetworkTransform — синхронизация Transform

Простой случай — синхронизировать позицию и поворот объекта. Поставьте на GameObject компонент NetworkTransform. Опции:

  • Sync Position X/Y/Z — какие оси синхронизировать.
  • Sync Rotation X/Y/Z — то же для поворота.
  • Interpolation — сглаживать ли движение на клиенте.
  • Threshold — насколько изменилась позиция, чтобы отправить (минимизирует трафик).

Для контролируемого игроком объекта в NGO 2.x используйте встроенное поле NetworkTransform.AuthorityMode = Owner — это main path. По умолчанию AuthorityMode = Server (только сервер пишет). ClientNetworkTransform из Multiplayer Samples — legacy/sample из 1.x эпохи, в NGO 2.x избегайте.

Distributed Authority — альтернатива чистому client-server

NGO 2.x также поддерживает модель Distributed Authority (через Unity Cloud Multiplayer Services / Multiplayer Services Package). Идея: владение узлами распределено между клиентами, а не сосредоточено на сервере. Один клиент владеет своим игроком, другой — общим инвентарём, третий — боссом.

Это даёт:

  • Меньшую задержку для действий локального игрока (не нужно ждать сервер).
  • Дешевле в инфраструктуре (relay-сервер, не authoritative host).
  • Сложнее в plot-проверке (anti-cheat) — нет одного источника правды.

Подходит для кооперативных и party-игр, не подходит для соревновательных competitive shooter’ов.

Активируется через NetworkConfig.NetworkTopology = NetworkTopologyTypes.DistributedAuthority на NetworkManager + подключение к Unity Cloud Multiplayer Services (Multiplayer Services Package). RPC-синтаксис [Rpc] с SendTo остаётся тот же, но семантика “у кого authority” меняется.

Транспорт и соединение

UnityTransport — стандартный транспортный слой. Для прямого подключения по IP:

public class ConnectMenu : MonoBehaviour
{
    public void HostGame() {
        NetworkManager.Singleton.StartHost();
    }

    public void JoinGame(string ip) {
        var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
        transport.SetConnectionData(ip, 7777);
        NetworkManager.Singleton.StartClient();
    }
}

Для реальной игры IP-адреса неудобны (NAT, фаерволы). Решение — Unity Relay + Lobby: Unity Services дают relay-сервер и матчмейкинг, клиенты соединяются через “join code” вроде “ABCD12”.

Что обычно идёт не так

  1. Логика в Update без проверки IsServer/IsOwner. Спавн врагов, расчёт урона, движение AI — только на сервере. Иначе у каждого клиента свой “враг” и они не совпадают.
  2. Передача больших объектов через RPC. Параметры RPC должны быть простыми (примитивы, простые struct’ы с INetworkSerializable). Большие данные шлются плохо.
  3. Слишком частые NetworkVariable updates. Каждое поле, меняющееся 60 раз в секунду — это нагрузка. Группируйте, throttling, либо отправляйте через RPC по событию.
  4. Игнорирование лагов и предсказания. На 200 мс пинге без предсказания всё ощущается резиновым. Изучите Client-Side Prediction и Server Reconciliation (это уже advanced topic).

В следующей главе — Addressables: современная система загрузки ассетов.