Разработка мультиплеерных игр на Unity: от основ до продвинутых сетевых механик в Netcode for GameObjects

Комплексный курс по созданию сетевых игр с использованием современного стека NGO. Студенты пройдут путь от настройки клиент-серверной архитектуры до реализации сложных систем компенсации задержек и оптимизации трафика.

1. Основы сетевой архитектуры и введение в Netcode for GameObjects

Основы сетевой архитектуры и введение в Netcode for GameObjects

Представьте, что вы нажимаете клавишу прыжка в одиночной игре: ваш персонаж мгновенно взмывает вверх, потому что компьютер сразу же обрабатывает ввод и меняет координаты объекта. В мультиплеере всё иначе. Когда вы нажимаете «прыжок», информация об этом должна долететь до сервера, обработаться там, а затем вернуться к вам и ко всем остальным игрокам. Если расстояние между игроками — тысячи километров, данные могут путешествовать сотни миллисекунд. Без понимания того, как устроена сетевая архитектура, ваша игра превратится в хаотичное слайд-шоу, где каждый видит свою версию реальности.

Фундамент сетевого взаимодействия: Клиент-Сервер vs P2P

Прежде чем открывать Unity и устанавливать пакеты, необходимо определиться с топологией сети. В индустрии разработки игр доминируют две основные модели: Peer-to-Peer (P2P) и Клиент-Сервер.

В модели Peer-to-Peer (равный с равным) нет выделенного главного узла. Каждый компьютер в сети является одновременно и клиентом, и сервером. Все участники обмениваются данными напрямую друг с другом.

  • Плюсы: отсутствие затрат на содержание серверов, простота реализации для небольших сессий.
  • Минусы: катастрофическая уязвимость к читерству (каждый клиент «хозяин» своих данных) и проблема «бутылочного горлышка» (если у одного игрока плохой интернет, страдают все).
  • Современный стандарт для соревновательных и масштабных игр — архитектура Клиент-Сервер. Здесь существует выделенный узел (Сервер), который обладает «абсолютной истиной». Клиенты лишь отправляют запросы на действия (например, «я хочу сдвинуться вправо») и получают от сервера подтвержденное состояние мира.

    > Авторитарный сервер — это концепция, при которой сервер не доверяет данным от клиента. Если клиент сообщает: «Я нахожусь в точке Б», сервер проверяет, мог ли игрок физически переместиться из точки А в точку Б за прошедшее время. Если нет — сервер принудительно возвращает игрока назад.

    Netcode for GameObjects (NGO) ориентирован именно на архитектуру с сервером, хотя и поддерживает режим Host (Хост). В режиме хоста один из игроков совмещает функции клиента и сервера. Это удобно для кооперативных игр на 2–4 человека, так как избавляет разработчика от необходимости арендовать облачные мощности, но сохраняет логическую структуру «главного узла».

    Что такое Netcode for GameObjects и почему он заменил UNet

    Долгое время в Unity существовала система UNet, которая была признана устаревшей (deprecated) из-за плохой масштабируемости и сложности в поддержке. На смену ей пришел Netcode for GameObjects — высокоуровневый фреймворк, построенный поверх транспортного уровня Unity Transport Package (UTP).

    NGO берет на себя самые рутинные и сложные задачи:

  • Синхронизация объектов: автоматическое создание (спавн) и удаление объектов на всех клиентах.
  • Управление состоянием: передача значений переменных (здоровье, патроны, счет) по сети.
  • Удаленные вызовы (RPC): возможность вызвать функцию на сервере из кода клиента и наоборот.
  • Сериализация данных: упаковка сложных структур C# в компактные пакеты байтов для передачи по кабелю.
  • Важно понимать разницу между высокоуровневым неткодом (NGO) и транспортным уровнем. Если транспорт — это «грузовик», который перевозит байты из точки А в точку Б, то NGO — это «логистическая компания», которая знает, какой товар (данные объекта) в какой грузовик положить и кому именно его доставить.

    Ключевые сущности NGO: NetworkManager и NetworkObject

    Работа с любым сетевым проектом в Unity начинается с создания объекта-диспетчера.

    NetworkManager

    Это «сердце» вашей сетевой игры. Без этого компонента, расположенного на сцене, ни один сетевой скрипт не будет работать. NetworkManager отвечает за:
  • Выбор транспортного протокола (обычно Unity Transport).
  • Список префабов, которые могут быть созданы по сети (Network Prefabs).
  • Управление подключением и отключением игроков.
  • Определение роли приложения (запуск как Сервер, как Клиент или как Хост).
  • NetworkObject

    Чтобы обычный игровой объект (GameObject) стал «видимым» для сети, на него должен быть повешен компонент NetworkObject. Каждому такому объекту при создании присваивается уникальный NetworkObjectId. Именно по этому идентификатору сервер понимает, что «Меч игрока 1» на клиенте А — это тот же самый объект, что и «Меч игрока 1» на клиенте Б.

    Если вы попытаетесь вызвать сетевую функцию у объекта без NetworkObject, Unity выдаст ошибку, так как у системы не будет контекста: кому принадлежит этот объект и кто имеет право им управлять.

    Поток данных и концепция Tick Rate

    В сетевом программировании время не течет непрерывно, как в Update(). Оно дискретно. Сервер обрабатывает логику с определенной частотой, называемой Tick Rate (частота тиков).

    Представим, что ваш сервер работает на Гц. Это значит, что каждые мс сервер делает «снимок» состояния мира и отправляет его клиентам.

    Где — длительность одного сетевого кадра, а — частота обновления сети.

    Если ваш игровой цикл (FPS) работает на Гц, а сеть на Гц, возникает рассинхронизация. Игрок видит плавную картинку, но сетевые данные приходят реже. Именно здесь вступают в игру методы интерполяции, которые мы будем изучать позже, чтобы «дорисовать» промежуточные положения объектов между сетевыми пакетами.

    Жизненный цикл сетевого подключения

    Понимание того, что происходит в момент нажатия кнопки «Connect», критически важно для архитектуры. В NGO этот процесс выглядит так:

  • Запрос на одобрение (Connection Approval): Клиент отправляет запрос на сервер. На этом этапе сервер может проверить версию игры игрока, наличие модов или заполненность лобби. Если проверка не пройдена, сервер обрывает соединение еще до загрузки сцены.
  • Синхронизация сцены (Scene Management): После одобрения сервер сообщает клиенту, какая сцена сейчас активна. NGO автоматически загружает нужную сцену на клиенте.
  • Спавн игрока: Обычно после загрузки сцены сервер создает объект игрока. В NetworkManager есть поле Player Prefab — это шаблон, который будет автоматически создан для каждого подключившегося.
  • Передача владения (Ownership): Сервер назначает конкретного клиента «владельцем» (Owner) созданного объекта игрока. Это дает клиенту право (в определенных рамках) управлять этим объектом.
  • Проблема задержки (Latency) и RTT

    Любое действие в сети ограничено скоростью света и задержками в оборудовании провайдеров. Основной показатель здесь — RTT (Round Trip Time).

    > RTT — это время, за которое пакет проходит путь от клиента до сервера и обратно. Часто это значение называют «пингом», хотя технически пинг — это время в одну сторону.

    Если ваш RTT составляет мс, то любое ваше действие (выстрел) будет осознано сервером только через мс, а результат вы увидите еще через мс. В соревновательных шутерах это целая вечность.

    Для борьбы с этим в NGO применяются техники, которые мы разберем в следующих главах:

  • Client-side Prediction (Предсказание): клиент мгновенно перемещает персонажа, не дожидаясь ответа сервера.
  • Server Reconciliation (Примирение): если сервер не согласен с позицией клиента, он «поправляет» его.
  • Сериализация: как данные превращаются в байты

    Компьютеры не умеют передавать «объекты C#». Они передают потоки байтов. Процесс превращения структуры данных в байты называется сериализацией.

    В NGO используется эффективная бинарная сериализация. Когда вы используете NetworkVariable<int>, число int (4 байта) упаковывается в пакет. Если вы передаете строку, она занимает гораздо больше места. Золотое правило сетевого разработчика: передавайте как можно меньше данных и как можно реже.

    Вместо того чтобы передавать каждый кадр позицию игрока как Vector3 (12 байт), иногда выгоднее передать только факт нажатия клавиши (1 байт), если сервер может сам рассчитать траекторию. Однако это порождает риск рассинхронизации из-за плавающей запятой (floating point errors), о чем мы также поговорим в разделах про детерминизм.

    Режимы работы: Server, Host, Client

    При запуске проекта через API NetworkManager.Singleton у вас есть три пути:

  • StartServer(): Приложение становится «чистым» сервером (Dedicated Server). У него нет графического интерфейса пользователя-игрока, оно только считает логику. Это самый безопасный вариант для коммерческих проектов.
  • StartHost(): Приложение является и сервером, и клиентом. Игрок видит мир, управляет персонажем, но его машина также обрабатывает запросы других игроков. Идеально для инди-проектов и кооператива.
  • StartClient(): Приложение только подключается к существующему серверу/хосту. Оно не имеет права принимать решения о состоянии мира.
  • Рассмотрим пример. Если два игрока одновременно подбегают к аптечке, в режиме Host компьютер первого игрока (хоста) мгновенно решит: «Я поднял аптечку». Второй игрок (клиент) отправит запрос, сервер его обработает и ответит: «Извини, аптечки уже нет». В этой схеме у хоста всегда есть преимущество в пинге ( мс), что является главным недостатком модели Host-Client в соревновательных играх.

    Безопасность и авторитарность

    Главная ошибка новичков — позволять клиенту самому решать, сколько у него здоровья или золота. В Netcode for GameObjects по умолчанию принята модель Server Authoritative (Авторитарный сервер).

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

  • Клиент отправляет намерение (Input).
  • Сервер выполняет действие (Logic).
  • Сервер рассылает состояние (State).
  • Если вы пишете код для сетевого шутера, логика выстрела должна выглядеть так:

  • Клиент нажимает ЛКМ.
  • Клиент отправляет RPC-запрос на сервер: «Я хочу выстрелить из оружия X в направлении Y».
  • Сервер проверяет: а есть ли у игрока это оружие? А не находится ли оно на перезарядке? А не целится ли он сквозь стену?
  • Если всё в порядке, сервер вычитает патрон и сообщает всем клиентам: «Игрок А выстрелил, создайте эффект вспышки и звука».
  • Такой подход делает создание читов (например, на бесконечные патроны) практически невозможным, так как клиентские данные о патронах — это просто визуальное отображение того, что хранится в памяти сервера.

    Подготовка окружения

    Для работы с NGO в Unity (версии 2021.3 LTS и выше) необходимо установить пакет через Package Manager: com.unity.netcode.gameobjects. Вместе с ним автоматически подтянется Unity Transport.

    После установки важно правильно настроить NetworkManager. Одной из критических настроек является Tick Rate. Для динамичных экшенов рекомендуется значение или даже . Для пошаговых стратегий достаточно –. Чем выше Tick Rate, тем больше нагрузка на процессор сервера и на канал связи игрока.

    Также стоит обратить внимание на параметр Network Transport. По умолчанию это UnityTransport. В его настройках вы указываете IP-адрес (для клиента) и порт. По умолчанию используется порт . Если вы тестируете игру на одном компьютере, используйте адрес 127.0.0.1 (localhost).

    Первые шаги в коде: NetworkBehaviour

    Все ваши скрипты, которые должны взаимодействовать с сетью, должны наследоваться не от MonoBehaviour, а от NetworkBehaviour. Это дает вам доступ к специфическим свойствам:

  • IsServer: истина, если код выполняется на стороне сервера.
  • IsClient: истина, если код выполняется на стороне клиента.
  • IsOwner: истина, если данный экземпляр объекта принадлежит текущему игроку.
  • OnNetworkSpawn(): метод, который вызывается, когда объект успешно инициализирован в сети (аналог Start(), но для сетевой логики).
  • Именно через эти проверки строится ветвление логики. Например:

    Без проверки IsOwner вы бы управляли всеми персонажами на сцене одновременно, так как скрипт висит на каждом из них.

    Сложности, с которыми вы столкнетесь

    Сетевое программирование — это всегда борьба с неопределенностью. Пакеты могут теряться (Packet Loss), приходить в неправильном порядке или задерживаться.

  • Потеря пакетов: Если вы используете протокол UDP (на котором базируется Unity Transport), данные не гарантируют доставку. Это хорошо для позиции игрока (следующий пакет всё равно обновит её), но плохо для события «Игрок купил предмет». В NGO есть механизмы надежной доставки (Reliable), которые мы разберем далее.
  • Джиттер (Jitter): Это колебания пинга. Если один пакет пришел через мс, а второй через мс, движение объекта будет дерганым.
  • Дублирование: Иногда сеть доставляет один и тот же пакет дважды. NGO умеет фильтровать такие пакеты на основе порядковых номеров.
  • Понимание этих основ — это фундамент, на котором строится всё остальное. В следующей главе мы перейдем к практике и разберем, как синхронизировать данные между игроками без постоянной пересылки тяжелых сообщений, используя мощный инструмент NetworkVariable.

    2. Синхронизация состояния игрового мира через NetworkVariable

    Синхронизация состояния игрового мира через NetworkVariable

    Представьте, что вы создаете шутер, где у каждого игрока есть показатель здоровья. Вы меняете значение переменной health на сервере, когда пуля попадает в цель, но на экранах других игроков полоска здоровья жертвы остается неподвижной. В классическом однопользовательском режиме изменение переменной мгновенно отражается на логике, но в сетевой среде данные «заперты» внутри памяти одного процесса. Чтобы переменная «ожила» на всех клиентах, нам нужен механизм, который не просто хранит значение, но и автоматически упаковывает его в пакеты, отправляет по сети и распаковывает на принимающей стороне. В Netcode for GameObjects (NGO) основным инструментом для решения этой задачи является NetworkVariable.

    Природа синхронизации состояний

    В сетевом программировании существует два фундаментальных способа передачи информации: события (Events) и состояния (States). События говорят нам: «Что-то произошло прямо сейчас» (например, взрыв гранаты). Состояния отвечают на вопрос: «Каков мир в данный момент?» (например, сколько очков у команды или открыта ли дверь).

    NetworkVariable — это контейнер для синхронизации состояний. В отличие от RPC (Remote Procedure Calls), которые мы разберем в следующей главе, NetworkVariable гарантирует, что любой клиент, подключившийся к игре даже спустя десять минут после начала матча, автоматически получит актуальное значение переменной. Это свойство называется «Late Joiner Support», и оно критически важно для создания стабильного игрового мира.

    Когда вы используете NetworkVariable<int>, вы не просто объявляете число. Вы создаете обертку, которая интегрирована в жизненный цикл NetworkBehaviour. Как только значение на сервере меняется, NGO помечает эту переменную как «грязную» (dirty) и при следующем сетевом тике отправляет обновленные данные всем подписанным клиентам.

    Анатомия и типы NetworkVariable

    NetworkVariable<T> является обобщенным типом, но он накладывает жесткие ограничения на T. Поскольку данные должны передаваться по сети, тип T должен быть сериализуемым. NGO из коробки поддерживает все примитивные типы C# (int, float, bool и т.д.), а также типы Unity, такие как Vector3, Quaternion, Color и FixedString.

    Если вам нужно синхронизировать сложную структуру данных, она должна реализовывать интерфейс INetworkSerializable. Однако стоит помнить, что NetworkVariable лучше всего подходит для данных, которые меняются относительно редко или требуют гарантированной актуальности. Для потоковой передачи координат игрока 60 раз в секунду часто используются специализированные компоненты вроде NetworkTransform, хотя внутри они могут опираться на схожие механизмы.

    Объявление и инициализация

    Типичное объявление переменной в классе, наследуемом от NetworkBehaviour, выглядит так:

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

  • Default Value: Начальное значение (100).
  • Read Permission: Кто может видеть это значение. По умолчанию — все (Everyone).
  • Write Permission: Кто имеет право изменять значение. По умолчанию — только сервер (Server).
  • Авторитарность и права доступа

    Выбор NetworkVariableWritePermission.Server — это стандарт де-факто для архитектуры с авторитарным сервером. Это означает, что если клиент попытается написать Health.Value = 50;, NGO выбросит исключение или просто проигнорирует это действие. Только серверная логика имеет право менять состояние мира.

    Однако существуют сценарии, где допустим NetworkVariableWritePermission.Owner. Это полезно для данных, которые не критичны для безопасности игры, например, цвет кастомизации персонажа, который игрок выбирает в лобби. Но даже в этом случае стоит быть осторожным: если владелец объекта (клиент) решит «начитерить» и выставить себе бесконечные патроны через переменную с правами доступа Owner, сервер послушно разошлет это значение всем остальным.

    Ограничение видимости (Read Permission)

    Иногда данные не должны быть публичными. В карточных играх сервер знает карты всех игроков, но клиент А не должен знать карты клиента Б. Установка NetworkVariableReadPermission.Owner позволяет скрыть данные от всех, кроме сервера и владельца объекта. Это не только вопрос безопасности, но и оптимизации трафика: зачем забивать канал соседа информацией, которую он не должен видеть?

    Жизненный цикл и событие OnValueChanged

    Одной из самых частых ошибок новичков является попытка обновить UI или запустить визуальные эффекты в методе Update, проверяя значение NetworkVariable. Это крайне неэффективно. Правильный подход — использование делегата OnValueChanged.

    Это событие срабатывает на клиенте только тогда, когда пришел сетевой пакет с новым значением. Оно предоставляет как старое, так и новое значение, что идеально подходит для интерполяции или сравнения.

    Важный нюанс: OnValueChanged не вызывается автоматически при спавне объекта (OnNetworkSpawn), если значение переменной совпадает с дефолтным. Если вам нужно инициализировать UI текущим состоянием при подключении, вы должны вручную вызвать метод обновления, обратившись к Health.Value сразу после спавна.

    Глубокое погружение в сериализацию структур

    Когда стандартных типов недостаточно, мы создаем свои структуры. Рассмотрим пример синхронизации состояния двери, которая может быть заперта, открыта или сломана.

    Почему мы реализуем IEquatable? NGO использует сравнение значений, чтобы определить, стала ли переменная «грязной». Если вы используете структуру без переопределения Equals, C# будет использовать рефлексию для побайтового сравнения, что медленно, либо (в некоторых версиях) всегда считать объект изменившимся, что приведет к избыточному сетевому трафику.

    В методе NetworkSerialize используется универсальный сериализатор. Он работает в обе стороны: и на чтение, и на запись. Это изящное решение NGO, позволяющее описать схему данных один раз.

    Оптимизация трафика и частота обновлений

    Каждая NetworkVariable добавляет накладные расходы. Когда значение меняется, сервер формирует сообщение, содержащее:

  • NetworkObjectId (к какому объекту относится переменная).
  • Индекс переменной внутри этого объекта.
  • Само значение.
  • Если у вас 100 врагов и у каждого по 5 синхронизируемых переменных, которые меняются каждый кадр, пропускная способность сети быстро исчерпается.

    Стратегии оптимизации:

  • Тик-рейт (Tick Rate): По умолчанию NGO работает на частоте 30 или 60 тиков в секунду. Вы можете снизить частоту сетевых обновлений в настройках NetworkManager, если ваша игра не требует мгновенной реакции (например, пошаговая стратегия).
  • Группировка данных: Вместо трех NetworkVariable<float> для координат X, Y, Z, используйте одну NetworkVariable<Vector3>. Это уменьшит количество заголовков сообщений.
  • Мертвая зона (Dead-zone): Не обновляйте переменную, если изменение незначительно. Например, если здоровье персонажа — это float, и оно восстанавливается на 0.001 в кадр, нет смысла синхронизировать это каждую секунду. Синхронизируйте только при достижении целых чисел или значимых порогов.
  • Разница между NetworkVariable и RPC

    Часто возникает вопрос: «Когда использовать переменную, а когда — удаленный вызов процедуры (RPC)?».

    Представьте механизм рычага, открывающего ворота.

  • Если вы используете RPC OpenGateClientRpc(), то все игроки, которые были в сети в момент нажатия, увидят анимацию открытия. Но если игрок подключится через секунду после завершения анимации, для него ворота будут закрыты, так как он «пропустил» событие.
  • Если вы используете NetworkVariable isGateOpen, то новый игрок при подключении получит значение true и его локальный скрипт поймет, что ворота должны быть открыты.
  • Правило большого пальца: Используйте NetworkVariable для всего, что определяет текущее состояние мира. Используйте RPC для кратковременных визуальных или звуковых эффектов (взрывы, щелчки, сообщения в чате).

    Работа с коллекциями: NetworkList

    Иногда нам нужно синхронизировать не одно значение, а список — например, инвентарь игрока или список захваченных точек на карте. Обычный NetworkVariable<List<T>> не сработает, так как List — это ссылочный тип, и NGO не сможет эффективно отследить изменение одного элемента внутри списка.

    Для этих целей существует NetworkList<T>. Он работает аналогично NetworkVariable, но предоставляет методы Add(), Remove(), Insert() и специфическое событие OnListChanged.

    Важно: NetworkList требует инициализации в Awake, а не в OnNetworkSpawn, так как его внутренняя структура должна быть готова к приему данных еще до того, как объект официально появится в сетевом пространстве.

    Нюансы владения (Ownership) и предсказания

    Хотя NetworkVariable идеально подходит для авторитарного сервера, она вносит задержку. Когда игрок нажимает кнопку «Включить фонарик», запрос идет на сервер, сервер меняет NetworkVariable<bool> IsFlashlightOn, и только через время, равное пингу (RTT), клиент получает подтверждение и включает свет.

    Эта задержка может делать игру «кисельной». Для решения этой проблемы в продвинутых проектах используют Client-side Prediction (предсказание на стороне клиента). Суть в том, что клиент мгновенно меняет локальное состояние (включает фонарик визуально), а когда приходит подтверждение от сервера, он либо продолжает работу, либо (в случае расхождения) корректирует состояние.

    Однако NetworkVariable в базовом виде не поддерживает автоматический откат (rollback) состояний. Если вы планируете сложную систему предсказания перемещений, вам придется либо писать свою обертку над NetworkVariable, либо использовать более низкоуровневые инструменты NGO, которые позволяют работать с буфером состояний. Мы подробно разберем это в главе 7.

    Практический пример: Синхронизация захвата точки

    Рассмотрим задачу: есть зона, в которой игроки накапливают прогресс захвата.

  • Серверная логика: Сервер считает количество игроков в триггере. Если игроков команды А больше, прогресс растет.
  • Синхронизация: Прогресс хранится в NetworkVariable<float> CaptureProgress.
  • Визуализация: Все клиенты подписаны на OnValueChanged. Когда значение меняется, они обновляют ползунок над точкой.
  • В этом примере четко разделены обязанности. Сервер управляет физикой и логикой, а клиенты лишь отображают результат. Даже если у игрока случится кратковременный разрыв соединения, при восстановлении он получит точное значение CaptureProgress.Value и ползунок мгновенно прыгнет в нужное положение.

    Безопасность и проверка данных

    Поскольку NetworkVariable с правами записи Server защищена от изменений клиентом, она является фундаментом безопасности. Однако помните, что данные, которые вы отправляете с сервера на клиент, могут быть прочитаны кем угодно. Никогда не помещайте в NetworkVariable секретную информацию (например, пароли или скрытые координаты других игроков в режиме «туман войны»), если у переменной стоит ReadPermission.Everyone.

    Для критически важных систем, таких как расчет урона, всегда храните базовые показатели (базовая атака, модификаторы) в NetworkVariable на сервере. Когда клиент запрашивает действие, сервер берет эти значения из своей памяти, а не доверяет тем цифрам, которые прислал клиент.

    Распространенные ошибки и способы их решения

  • Изменение Value на клиенте: Если вы случайно попытаетесь изменить NetworkVariable.Value на клиенте при серверных правах записи, NGO не синхронизирует это изменение, и у вас возникнет рассинхрон (Desync). Локально значение может измениться, но сервер об этом не узнает и при следующем обновлении перезапишет клиентское значение своим. Всегда проверяйте if (IsServer) перед записью.
  • Забытая инициализация: Ссылочные типы внутри структур INetworkSerializable должны быть инициализированы. Если ваша структура содержит FixedString32Bytes, убедитесь, что она имеет значение по умолчанию.
  • Переподписка на события: Если вы подписываетесь на OnValueChanged в OnNetworkSpawn, не забывайте отписываться в OnNetworkDespawn. Хотя NGO старается чистить связи, явная отписка — признак хорошего тона и защита от утечек памяти.
  • Итог: когда выбирать NetworkVariable?

    NetworkVariable — это ваш основной инструмент для построения «источника истины» в игре. Она идеально подходит для:

  • Здоровья, маны, опыта и уровней.
  • Состояний интерактивных объектов (двери, рычаги, лампочки).
  • Глобальных параметров матча (время до конца раунда, счет команд).
  • Настроек персонажа, видимых другим игрокам.
  • Используя этот механизм, вы создаете надежную архитектуру, устойчивую к задержкам и поддерживающую динамическое подключение игроков. В следующей главе мы перейдем к RPC, которые дополнят вашу систему синхронизации, позволяя передавать не только «что есть в мире», но и «что в нем произошло».