Бонус: начислена 1 монета за дневную активность. Сейчас у вас 1 монета

Совместимость клиента и сервера в Unity Netcode: версии, RPC и безопасное обновление игры

Лекция



В мультиплеерной игре часто возникает ситуация: сервер уже обновили, а часть пользователей все еще играет на старом клиенте. На первый взгляд кажется, что если изменения “незначительные”, то старый клиент должен подключаться без проблем. Но в Unity Netcode это не всегда так.

Даже маленькое изменение может сломать сетевой контракт:

  • изменили сигнатуру RPC
  • удалили старый RPC
  • изменили порядок сериализации
  • поменяли тип NetworkVariable
  • изменили порядок NetworkBehaviour на prefab
  • изменили список Network Prefab

В результате клиент может получить disconnect, ошибку десериализации, рассинхрон состояния или даже WebGL/WASM-ошибку вида:

Uncaught RuntimeError: function signature mismatch

Главная идея статьи:

Старые клиенты можно поддерживать, но только если сетевой протокол заранее проектируется как версионированный и обратно совместимый.

Почему старый клиент ломается после обновления сервера

В обычном веб-приложении сервер часто может поменяться, а старый frontend еще какое-то время продолжит работать, если API совместим. В мультиплеерной игре все строже: клиент и сервер обмениваются бинарными сетевыми сообщениями, RPC-вызовами, состоянием NetworkVariable, spawn-данными и идентификаторами сетевых объектов.

Если клиент и сервер по-разному понимают один и тот же сетевой пакет, возникает проблема.

Например, раньше был RPC:

[ServerRpc]
private void AttackServerRpc(int weaponId)
{
}

Потом на сервере сделали так:

[ServerRpc]
private void AttackServerRpc(int weaponId, float power)
{
}

Для разработчика это выглядит как маленькое изменение. Но для старого клиента это уже другой сетевой контракт. Старый клиент отправляет только weaponId, а сервер может ожидать weaponId + power.

В итоге ошибка может произойти еще до входа в тело метода. Обычный try/catch внутри RPC уже не поможет.

Что такое сетевой контракт

Сетевой контракт — это все, о чем клиент и сервер должны договориться заранее(это просто формат байтов в котра передается полезная нагрузка).

В Unity Netcode к сетевому контракту относятся:

  • RPC-методы
  • параметры RPC
  • порядок и типы сериализуемых полей
  • NetworkVariable
  • NetworkObject
  • NetworkBehaviour
  • NetworkPrefab
  • spawn/despawn-логика
  • формат custom messages
  • версии игровых данных

Если этот контракт поменялся несовместимо, старый клиент не должен входить в игру. Его нужно мягко отключить с понятным сообщением:

Версия игры устарела. Обновите страницу или клиент.

Unity Netcode поддерживает Connection Approval, где сервер может проверить клиента до полноценного подключения. В ответе можно указать причину отказа через response.Reason, а клиент сможет прочитать ее через NetworkManager.DisconnectReason.

Почему нельзя просто поставить try/catch

Частая ошибка — пытаться решить это так:

try
{
    NetworkManager.Singleton.StartClient();
}
catch (Exception e)
{
    Debug.LogError(e);
}

Или так:

[ServerRpc]
private void SomeServerRpc(int value)
{
    try
    {
        // logic
    }
    catch (Exception e)
    {
        Debug.LogError(e);
    }
}

Такой try/catch полезен, если ошибка возникла внутри вашей игровой логики. Но он не спасает, если ошибка возникла:

  • при чтении сетевого пакета
  • при десериализации RPC-параметров
  • при несовпадении NetworkBehaviour
  • при несовпадении NetworkVariable
  • на уровне Unity Transport
  • на уровне WebGL/WASM runtime

То есть:

ошибка внутри RPC-метода       → try/catch может помочь
ошибка до входа в RPC-метод    → try/catch обычно не поможет
ошибка WebGL function mismatch → C# try/catch обычно не поможет

Поэтому правильная стратегия — не “глушить” такие ошибки, а не допускать несовместимый клиент до игрового состояния.

Версии: GameVersion и ProtocolVersion

Не нужно использовать только одну версию приложения. Лучше разделить две разные сущности:

GameVersion     — версия билда игры, например 1.0.6
ProtocolVersion — версия сетевого протокола, например 4

GameVersion может изменяться часто: UI, баланс, графика, тексты, локализация.

ProtocolVersion нужно менять только тогда, когда изменился сетевой контракт.

Пример:

Клиент 1.0.5, protocol 4
Сервер 1.0.6, protocol 4

Можно подключать.

Но:

Клиент 1.0.5, protocol 3
Сервер 1.0.6, protocol 4

Можно подключать только если сервер еще поддерживает protocol 3.

Или:

Клиент 1.0.2, protocol 1
Сервер 1.0.6, minSupportedProtocol 3

Нельзя подключать.

Minimum Supported Protocol

На сервере удобно хранить:

private const int SERVER_PROTOCOL = 4;
private const int MIN_SUPPORTED_PROTOCOL = 3;

Это означает:

сервер сам работает на protocol 4
сервер еще поддерживает клиентов protocol 3
клиенты protocol 1 и 2 уже слишком старые

Схема:

clientProtocol < MIN_SUPPORTED_PROTOCOL → отказ
clientProtocol > SERVER_PROTOCOL        → клиент новее сервера, отказ
иначе                                   → подключение разрешено

Совместимость клиента и сервера в Unity Netcode: версии, RPC и безопасное обновление игры

Проверка версии через Connection Approval

На клиенте перед подключением передаем данные:

using System;
using System.Text;
using Unity.Netcode;
using UnityEngine;

public class ClientConnector : MonoBehaviour
{
    private const string GAME_VERSION = "1.0.5";
    private const int CLIENT_PROTOCOL = 3;

    public void Connect()
    {
        var payload = new ClientConnectionPayload
        {
            gameVersion = GAME_VERSION,
            protocol = CLIENT_PROTOCOL
        };

        string json = JsonUtility.ToJson(payload);

        NetworkManager.Singleton.NetworkConfig.ConnectionData =
            Encoding.UTF8.GetBytes(json);

        NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;

        NetworkManager.Singleton.StartClient();
    }

    private void OnClientDisconnected(ulong clientId)
    {
        string reason = NetworkManager.Singleton.DisconnectReason;

        if (!string.IsNullOrEmpty(reason))
        {
            Debug.LogWarning("Disconnected: " + reason);

            // Здесь можно показать UI:
            // "Версия игры устарела. Обновите игру."
        }
        else
        {
            Debug.LogWarning("Disconnected without reason.");
        }
    }

    [Serializable]
    private class ClientConnectionPayload
    {
        public string gameVersion;
        public int protocol;
    }
}

На сервере:

using System;
using System.Text;
using Unity.Netcode;
using UnityEngine;

public class ServerConnectionApproval : MonoBehaviour
{
    private const int SERVER_PROTOCOL = 4;
    private const int MIN_SUPPORTED_PROTOCOL = 3;

    private void Awake()
    {
        NetworkManager.Singleton.NetworkConfig.ConnectionApproval = true;
        NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
    }

    private void ApprovalCheck(
        NetworkManager.ConnectionApprovalRequest request,
        NetworkManager.ConnectionApprovalResponse response)
    {
        try
        {
            string json = Encoding.UTF8.GetString(request.Payload);

            ClientConnectionPayload payload =
                JsonUtility.FromJson(json);

            if (payload == null)
            {
                Reject(response, "Invalid connection data.");
                return;
            }

            if (payload.protocol < MIN_SUPPORTED_PROTOCOL)
            {
                Reject(response, "Your game version is too old. Please update.");
                return;
            }

            if (payload.protocol > SERVER_PROTOCOL)
            {
                Reject(response, "Server version is older than your client. Please try again later.");
                return;
            }

            response.Approved = true;
            response.CreatePlayerObject = true;
            response.Pending = false;

            Debug.Log(
                $"Client approved. GameVersion={payload.gameVersion}, Protocol={payload.protocol}"
            );
        }
        catch (Exception e)
        {
            Debug.LogWarning("Connection approval failed: " + e.Message);
            Reject(response, "Invalid client version data.");
        }
    }

    private void Reject(
        NetworkManager.ConnectionApprovalResponse response,
        string reason)
    {
        response.Approved = false;
        response.CreatePlayerObject = false;
        response.Reason = reason;
        response.Pending = false;
    }

    [Serializable]
    private class ClientConnectionPayload
    {
        public string gameVersion;
        public int protocol;
    }
}

ConnectionData передается клиентом в процессе подключения, а сервер получает эти данные в ConnectionApprovalRequest.Payload. Это позволяет отклонить клиента до создания player object и до начала полноценной сетевой синхронизации.

Unity NetworkConfig.ProtocolVersion

В Unity Netcode есть собственная настройка NetworkConfig.ProtocolVersion. Если она отличается у клиента и сервера, Netcode может не позволить им общаться.

Это полезно для строгой несовместимости:

изменился сетевой контракт полностью
старые клиенты точно нельзя пускать

Но если тебе нужно поддерживать старые клиенты, лучше использовать осторожную схему:

Unity NetworkConfig.ProtocolVersion менять только при жесткой несовместимости.
Свою appProtocol/gameProtocol передавать через ConnectionData.

То есть у тебя может быть:

NetworkConfig.ProtocolVersion = 1
appProtocol = 3 или 4

И уже сервер сам решает, какие версии поддерживать.

Какие изменения безопасны

Обычно безопаснее:

  • добавить новый RPC, не удаляя старый
  • добавить новое необязательное поле
  • добавить новую механику, которую старый клиент не использует
  • изменить серверную логику без изменения формата сообщений
  • изменить баланс
  • изменить UI
  • изменить визуальные эффекты
  • добавить новый режим, если старый клиент туда не попадает

Пример безопасного изменения:

[ServerRpc]
private void AttackServerRpc(int weaponId)
{
    AttackInternal(weaponId, 1.0f);
}

[ServerRpc]
private void AttackV2ServerRpc(int weaponId, float power)
{
    AttackInternal(weaponId, power);
}

private void AttackInternal(int weaponId, float power)
{
    // общая логика
}

Старый клиент вызывает:

AttackServerRpc(weaponId);

Новый клиент вызывает:

AttackV2ServerRpc(weaponId, power);

Сервер поддерживает оба варианта.

Какие изменения опасны

Опасные изменения:

  • изменить сигнатуру существующего RPC
  • удалить старый RPC
  • переименовать RPC, который вызывает старый клиент
  • изменить тип параметра RPC
  • изменить порядок параметров RPC
  • изменить порядок сериализации
  • изменить тип поля в сериализуемой структуре
  • удалить поле из середины структуры
  • изменить тип NetworkVariable
  • удалить NetworkVariable, которую ожидает старый клиент
  • переставить NetworkBehaviour на prefab
  • удалить NetworkBehaviour с prefab
  • изменить порядок Network Prefab
  • изменить обязательную spawn-логику

Главное правило:

Если старый клиент может отправить или получить это сообщение — старый формат должен продолжать существовать.

Сериализация: почему нельзя менять порядок полей

Допустим, была структура:

public struct PlayerState : INetworkSerializable
{
    public int hp;
    public float speed;
    public int weaponId;

    public void NetworkSerialize(BufferSerializer serializer)
        where T : IReaderWriter
    {
        serializer.SerializeValue(ref hp);
        serializer.SerializeValue(ref speed);
        serializer.SerializeValue(ref weaponId);
    }
}

Потом ты поменял порядок:

serializer.SerializeValue(ref weaponId);
serializer.SerializeValue(ref hp);
serializer.SerializeValue(ref speed);

Для старого клиента это катастрофа. Он будет читать не те данные не в те поля.

Правильнее добавлять поля в конец и учитывать версию:

public struct PlayerState : INetworkSerializable
{
    public int dataVersion;
    public int hp;
    public float speed;
    public int weaponId;
    public float armor;

    public void NetworkSerialize(BufferSerializer serializer)
        where T : IReaderWriter
    {
        serializer.SerializeValue(ref dataVersion);
        serializer.SerializeValue(ref hp);
        serializer.SerializeValue(ref speed);
        serializer.SerializeValue(ref weaponId);

        if (dataVersion >= 2)
        {
            serializer.SerializeValue(ref armor);
        }
        else
        {
            armor = 0f;
        }
    }
}

Unity Netcode поддерживает кастомную сериализацию через INetworkSerializable, что позволяет явно контролировать, как ваши типы записываются и читаются по сети.

Не использовать один и тот же формат для разных направлений

Иногда удобнее разделять:

ClientToServerAttackRequest
ServerToClientAttackResult
PlayerStateSnapshotV1
PlayerStateSnapshotV2

Например:

public struct AttackRequestV1 : INetworkSerializable
{
    public int weaponId;

    public void NetworkSerialize(BufferSerializer serializer)
        where T : IReaderWriter
    {
        serializer.SerializeValue(ref weaponId);
    }
}

public struct AttackRequestV2 : INetworkSerializable
{
    public int weaponId;
    public float power;

    public void NetworkSerialize(BufferSerializer serializer)
        where T : IReaderWriter
    {
        serializer.SerializeValue(ref weaponId);
        serializer.SerializeValue(ref power);
    }
}

Такой код длиннее, но безопаснее и понятнее.

NetworkVariable: почему это особенно чувствительно

NetworkVariable — это не одноразовое сообщение, а синхронизируемое состояние. Когда клиент подключается, он получает актуальное значение NetworkVariable, а дальше может получать обновления. Unity указывает, что при первом подключении клиент синхронизируется с текущим значением NetworkVariable; обычно обработчики OnValueChanged регистрируют в OnNetworkSpawn.

Опасно менять:

public NetworkVariable int Health = new();

на:

public NetworkVariable float Health = new();

Для старого клиента это уже другой формат.

Лучше:

public NetworkVariable int HealthLegacy = new();
public NetworkVariable float HealthV2 = new();

И дальше:

private void SetHealth(float health)
{
    HealthV2.Value = health;
    HealthLegacy.Value = Mathf.RoundToInt(health);
}

Старые клиенты читают HealthLegacy, новые — HealthV2.

NetworkObject и NetworkBehaviour: почему нельзя менять порядок

Это одна из самых неприятных тонкостей.

На prefab может быть несколько NetworkBehaviour:

  • NetworkObject
  • PlayerMovement
  • PlayerCombat
  • PlayerInventory

Если ты переставишь их так:

  • NetworkObject
  • PlayerCombat
  • PlayerMovement
  • PlayerInventory

то старый клиент и новый сервер могут по-разному сопоставлять сетевые сообщения с компонентами.

Даже если классы остались теми же, порядок может иметь значение для сетевого поведения.

Безопаснее:

  • NetworkObject
  • PlayerMovement
  • PlayerCombat
  • PlayerInventory
  • PlayerCosmeticsV2

То есть:

  • не переставлять
  • не удалять из середины
  • новые NetworkBehaviour добавлять в конец
  • legacy-компоненты оставлять до прекращения поддержки старых клиентов

RPC: как делать новые версии

Плохо:

// Было:
[ServerRpc]
private void BuyItemServerRpc(int itemId)
{
}

// Стало:
[ServerRpc]
private void BuyItemServerRpc(int itemId, int amount)
{
}

Схема жизненного цикла сетевого изменения

Пример хорошего изменнения, Допустим, нужно добавить силу атаки power.

Версия 1

Версия 2

Версия 3

Версия 4

[ServerRpc]
private void AttackServerRpc(int weaponId)
{
    AttackInternal(weaponId, 1f);
}

Сервер поддерживает старый и новый RPC:

[ServerRpc]
private void AttackServerRpc(int weaponId)
{
    AttackInternal(weaponId, 1f);
}

[ServerRpc]
private void AttackV2ServerRpc(int weaponId, float power)
{
    AttackInternal(weaponId, power);
}

Новый клиент использует:

AttackV2ServerRpc(weaponId, power);

Старый клиент продолжает использовать:

AttackServerRpc(weaponId);

Когда старые клиенты больше не поддерживаются:

private
  const int MIN_SUPPORTED_PROTOCOL = 4;

Старый RPC можно удалить.

остается только

[ServerRpc]
private void AttackV2ServerRpc(int weaponId, float power)
{
    AttackInternal(weaponId, power);
}
Существующий проект Проект поддерживаемый старых и новых клиентов ограничение с проверкой по версии Проект поддерживаемый только новых клиентов

Network Prefabs и Force Same Prefabs

Если у клиента и сервера отличаются сетевые prefab-ы, это тоже может привести к ошибкам подключения или spawn-логики. В NetworkManager есть настройки, связанные с проверкой сетевой конфигурации, включая Force Same Prefabs. Документация Unity описывает настройки NetworkManager, включая проверки конфигурации и параметры протокола.

Практическое правило:

Если старые клиенты должны подключаться, не ломай список старых network prefab.

Можно добавлять новые prefab, но нужно следить, чтобы старый клиент не получал spawn объекта, которого он не знает.

Например:

if (clientProtocol >= 4)
{
    SpawnNewCosmeticObjectForClient(clientId);
}
else
{
    // старому клиенту не спавним новый объект
}
Совместимость клиента и сервера в Unity Netcode: версии, RPC и безопасное обновление игры

Как хранить версию каждого клиента на сервере

После approval полезно сохранить версию клиента:

private readonly Dictionary _clientProtocols = new();

private void ApprovalCheck(
    NetworkManager.ConnectionApprovalRequest request,
    NetworkManager.ConnectionApprovalResponse response)
{
    var payload = DecodePayload(request.Payload);

    if (payload.protocol < MIN_SUPPORTED_PROTOCOL)
    {
        Reject(response, "Your game version is too old. Please update.");
        return;
    }

    _clientProtocols[request.ClientNetworkId] = payload.protocol;

    response.Approved = true;
    response.CreatePlayerObject = true;
    response.Pending = false;
}

private bool IsLegacyClient(ulong clientId)
{
    return _clientProtocols.TryGetValue(clientId, out int protocol)
           && protocol < 4;
}

Но нужно не забыть удалять запись при disconnect:

private void OnEnable()
{
    NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
}

private void OnDisable()
{
    if (NetworkManager.Singleton != null)
    {
        NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnected;
    }
}

private void OnClientDisconnected(ulong clientId)
{
    _clientProtocols.Remove(clientId);
}

Как отправлять разные данные старым и новым клиентам

Пример:

private void SendMatchState(ulong clientId)
{
    if (IsLegacyClient(clientId))
    {
        SendMatchStateLegacyClientRpc(
            CreateLegacyState(),
            new ClientRpcParams
            {
                Send = new ClientRpcSendParams
                {
                    TargetClientIds = new[] { clientId }
                }
            });
    }
    else
    {
        SendMatchStateV2ClientRpc(
            CreateV2State(),
            new ClientRpcParams
            {
                Send = new ClientRpcSendParams
                {
                    TargetClientIds = new[] { clientId }
                }
            });
    }
}

[ClientRpc]
private void SendMatchStateLegacyClientRpc(
    MatchStateLegacy state,
    ClientRpcParams clientRpcParams = default)
{
}

[ClientRpc]
private void SendMatchStateV2ClientRpc(
    MatchStateV2 state,
    ClientRpcParams clientRpcParams = default)
{
}

Идея простая:

  • старому клиенту — старые данные
  • новому клиенту — новые данные

Что делать с WebGL и function signature mismatch

Ошибка:

Uncaught RuntimeError: function signature mismatch

в WebGL может быть связана не только с Netcode. Часто это проблема несовместимости загруженных файлов, кэша или WASM/JS-связки.

Например браузер мог загрузить:

  • новый loader.js
  • старый game.wasm
  • старый game.data

Или наоборот.

Для WebGL обязательно нужно:

  • версионировать папку билда
  • не кэшировать index.html агрессивно
  • обновлять все build-файлы атомарно
  • не смешивать старые и новые файлы Unity WebGL build

Хорошая схема:

  • /game/1.0.5/Build/...
  • /game/1.0.6/Build/...

И HTML должен ссылаться на конкретную версию.

Плохая схема:

/game/Build/game.wasm
/game/Build/game.data
/game/Build/game.framework.js

если файлы перезаписываются поверх старых, а браузер/прокси/CDN может отдать смешанные версии.

JS-перехватчик для WebGL

Можно добавить глобальный перехватчик в index.html:

window.addEventListener("error", function (event) {
    console.error("Global JS error:", event.message);

    if (
        event.message &&
        event.message.includes("function signature mismatch")
    ) {
        alert("Ошибка версии игры. Обновите страницу.");
    }
});

window.addEventListener("unhandledrejection", function (event) {
    console.error("Unhandled promise rejection:", event.reason);
});

Но это только аварийная диагностика.

Важно:

  • После function signature mismatch нельзя считать игру стабильной.
  • Лучше показать сообщение и предложить обновить страницу.

Логирование ошибок в Unity

Можно собирать критические ошибки:

using UnityEngine;

public class ClientErrorLogger : MonoBehaviour
{
    private void OnEnable()
    {
        Application.logMessageReceived += OnLogMessage;
    }

    private void OnDisable()
    {
        Application.logMessageReceived -= OnLogMessage;
    }

    private void OnLogMessage(string condition, string stackTrace, LogType type)
    {
        if (type != LogType.Exception && type != LogType.Error)
        {
            return;
        }

        bool looksLikeNetworkCompatibilityError =
            condition.Contains("RPC") ||
            condition.Contains("NetworkVariable") ||
            condition.Contains("NetworkBehaviour") ||
            condition.Contains("NetworkObject") ||
            condition.Contains("serialization") ||
            condition.Contains("deserialization") ||
            condition.Contains("DisconnectReason");

        if (looksLikeNetworkCompatibilityError)
        {
            Debug.LogWarning("Possible network compatibility error: " + condition);

            // Здесь можно отправить лог в аналитику:
            // gameVersion, protocolVersion, serverVersion, userId, platform
        }
    }
}

Что стоит логировать:

  • client game version
  • client protocol version
  • server protocol version
  • platform
  • Unity version
  • Netcode package version
  • scene name
  • connection state
  • disconnect reason
  • stack trace

Как правильно обновлять сервер

Хорошая стратегия обновления:

  • Шаг 1. Старый сервер работает с client protocol 3.
  • Шаг 2. Новый сервер поддерживает protocol 3 и protocol 4.
  • Шаг 3. Новый клиент начинает использовать protocol 4.
  • Шаг 4. Старые клиенты protocol 3 еще могут играть.
  • Шаг 5. Через время сервер поднимает minSupportedProtocol до 4.
  • Шаг 6. Legacy RPC и legacy поля удаляются.

То есть сервер сначала становится совместимым с двумя версиями, а уже потом старый протокол отключается.

Плохая стратегия:

  • обновили сервер
  • удалили старые RPC
  • изменили NetworkVariable
  • старые клиенты все еще пытаются подключиться
  • получили function signature mismatch / disconnect / deserialization error

Чеклист перед релизом сервера

Перед обновлением сервера проверь:

  • Менялись ли сигнатуры RPC?
  • Удалялись ли старые RPC?
  • Менялся ли порядок параметров RPC?
  • Менялись ли типы NetworkVariable?
  • Удалялись ли NetworkVariable?
  • Менялся ли порядок NetworkBehaviour на prefab?
  • Менялся ли список Network Prefab?
  • Менялась ли custom serialization?
  • Менялся ли порядок SerializeValue?
  • Появились ли новые обязательные поля?
  • Может ли старый клиент получить новый unknown prefab?
  • Может ли старый клиент попасть в новый режим игры?
  • Есть ли minSupportedProtocol?
  • Есть ли понятное сообщение при отказе?
  • Есть ли логирование версии клиента?

Если ответ “да” на опасные пункты, нужно либо:

оставить legacy-совместимость

либо:

поднять минимальную версию и мягко отключить старых клиентов

Чеклист безопасного изменения RPC

  • Не менять существующую сигнатуру.
  • Не удалять старый RPC сразу.
  • Новый RPC называть с V2/V3 или по смыслу.
  • Старый RPC должен вызывать общую server-side логику.
  • Новый RPC должен вызывать ту же общую server-side логику.
  • Для каждого клиента знать protocol version.
  • Удалять старый RPC только после поднятия minSupportedProtocol.

Пример:

[ServerRpc]
private void UseSkillServerRpc(int skillId)
{
    UseSkillInternal(skillId, Vector3.zero, false);
}

[ServerRpc]
private void UseSkillV2ServerRpc(int skillId, Vector3 targetPosition, bool charged)
{
    UseSkillInternal(skillId, targetPosition, charged);
}

private void UseSkillInternal(int skillId, Vector3 targetPosition, bool charged)
{
    // вся серверная логика здесь
}

Чеклист безопасной сериализации

  • Не менять порядок старых полей.
  • Не менять тип старых полей.
  • Не удалять поля из середины.
  • Новые поля добавлять в конец.
  • Добавлять dataVersion.
  • Для старой версии задавать default-значения.
  • Для сложных изменений создавать V2-структуру.

Пример:

public struct InventoryItemV2 : INetworkSerializable
{
    public int dataVersion;
    public int itemId;
    public int amount;
    public int rarity;

    public void NetworkSerialize(BufferSerializer serializer)
        where T : IReaderWriter
    {
        serializer.SerializeValue(ref dataVersion);
        serializer.SerializeValue(ref itemId);
        serializer.SerializeValue(ref amount);

        if (dataVersion >= 2)
        {
            serializer.SerializeValue(ref rarity);
        }
        else
        {
            rarity = 0;
        }
    }
}

. Чеклист для NetworkBehaviour и prefab

  • Не переставлять NetworkBehaviour.
  • Не удалять legacy NetworkBehaviour из середины.
  • Новые NetworkBehaviour добавлять в конец.
  • Не менять NetworkObject structure без повышения protocol.
  • Не спавнить старому клиенту prefab, которого у него нет.
  • Не отправлять старому клиенту ClientRpc на компонент, которого у него нет.

Что показывать игроку для того чтобы он обновил игру

Плохое сообщение:

Connection failed.

Лучше:

Версия игры устарела. Обновите страницу или перезапустите игру.

Для WebGL:

Версия игры обновилась. Перезагрузите страницу, чтобы загрузить новую версию.

Для мобильной игры:

Доступно обновление. Установите новую версию из магазина.

Для Steam/desktop:

Версия клиента устарела. Перезапустите игру, чтобы получить обновление.
Для десктоп версий можно укзать что именно обновилось, добавилось
Совместимость клиента и сервера в Unity Netcode: версии, RPC и безопасное обновление игры

Минимальная архитектура для проекта

Делательно иметь такие классы:

NetworkVersionInfo
ClientConnectionPayload
ServerConnectionApproval
ClientDisconnectHandler
NetworkCompatibilityService
LegacyRpcSupport
ClientErrorLogger

Пример NetworkVersionInfo:

public static class NetworkVersionInfo
{
    public const string GameVersion = "1.0.6";

    public const int CurrentProtocol = 4;

    public const int MinSupportedProtocol = 3;
}

Payload:

[Serializable]
public class ClientConnectionPayload
{
    public string gameVersion;
    public int protocol;
    public string platform;
}

Когда нужно повышать ProtocolVersion

Повышай protocol, если изменилось:

RPC API
формат RPC-параметров
custom serialization
NetworkVariable type
NetworkBehaviour порядок
NetworkObject структура
NetworkPrefab список
обязательная scene/spawn логика
формат custom messages

Не обязательно повышать protocol, если изменилось:

UI
тексты
анимации
иконки
баланс на сервере
визуальные эффекты
локализация
не сетевые MonoBehaviour
внутренняя server-side логика без изменения формата сообщений

Выводы

Старые клиенты можно поддерживать после обновления сервера, но нельзя полагаться на случайную совместимость.

Главные правила:

  • 1. Разделяй GameVersion и ProtocolVersion.
  • 2. Проверяй клиента через Connection Approval.
  • 3. Храни minSupportedProtocol на сервере.
  • 4. Не меняй старые RPC — добавляй новые.
  • 5. Не меняй порядок сериализации.
  • 6. Не меняй типы NetworkVariable без legacy-слоя.
  • 7. Не переставляй NetworkBehaviour на prefab.
  • 8. Не спавни старому клиенту неизвестные prefab.
  • 9. Логируй версию клиента и причину disconnect.
  • 10. WebGL-билды версионируй, чтобы не смешивались старые и новые файлы.

Самое важное:

GameVersion может отличаться.
ProtocolVersion должен быть совместим.

А ошибки вроде:

function signature mismatch

лучше не пытаться “глушить”. Это уже признак того, что клиент и сервер или WebGL-файлы оказались несовместимы. Правильное решение — обнаружить несовместимость заранее, мягко отключить клиента и показать понятное сообщение об обновлении.

создано: 2026-05-19
обновлено: 2026-05-20
1



Помог ли вам этот ответ?
Нажмите оценку и напишите коротко почему. Так мы сможем сделать следующие ответы точнее и полезнее.
Насколько вы довольны ответом?
Ваш отзыв напрямую влияет на качество следующих подсказок и ответов.


Поделиться:
Пожаловаться

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выполнения задания
  • Возможно применение функции гаранта на сделку
  • Приоритетная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можете продать (как исполнитель) или купить (как заказчик) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно применение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии

Оставить комментарий

Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Лекции и учебник по "Разработка компьютерных игр на движке Unity (перенести статьи в Разработка компьютерных игр)"

Термины: Разработка компьютерных игр на движке Unity (перенести статьи в Разработка компьютерных игр)