Подготовка игры к детерминистическому сетевому коду

Что такое неткод?
Если это выглядит слегка знакомо, это потому, что так и есть

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

Тут речь идет о детерминистическом неткоде, так как о нем спрашивают чаще всего, и на вопросы о нем можно дать более конкретный совет, чем "зависит от ситуации".

Что такое детерминистический неткод?

Детерминистический неткод (анго. deterministic netcode) основывается на том, что каждый игровой клиент приходит в идентичное состояние, учитывая одно и то же исходное состояние и входные данные (input'ы) на кадр.

Распространенные виды детерминистического неткода включают lockstep и rollback.

Lockstep

Lockstep - это (относительно) простая реализация детерминистического протокола, при которой следующий игровой кадр обрабатывается только после того, как известны input'ы каждого игрока (отсюда и название).

Lockstep подразумевает добавление задержки на вводе input'ов в половину медианного времени тура (пинга), чтобы позволить удаленным input'ам прибыть вовремя, и требует остановки игры до тех пор, пока не будут известны все удаленные input'ы для кадра, что может сделать ее неоптимальным выбором для игр, в которые можно играть на устройствах с нестабильным подключением (таких как мобильные устройства или Nintendo Switch).

Rollback

Rollback стоит на уровень выше Lockstep'a, позволяя игрокам угадывать действия других игроков, когда они не поступают вовремя, а затем перематывать игру и переигрывать игровые кадры с исправленными input'ами как только input'ы становятся известны.

Это означает, что если, скажем, происходит прерывание соединения длительностью 100 мс, мы можем исходить из предположения, что удаленный игрок продолжал удерживать input'ы, которые он уже удерживал в течении этих ~6 кадров, и подправить, когда придут настоящие input'ы, визуально лишь сдвинув персонажа на новую точку.

Конечно, чем больше кадров предсказано, тем больше вероятность того, что что-то пойдет совершенно по-другому после исправления состояния - откат на несколько кадров обычно не вызывает заметных проблем, но предсказание целой секунды input'ов, скорее всего, приведет к тому, что удаленный персонаж переместится в совершенно другое место, поэтому соревновательные игры, как правило, ограничивают максимальное количество предсказываемых кадров, и останавливают игру (как с lockstep) при их превышении.

Плюсы

  • Низкое использование сети
    (в основном должны передаваться только input'ы/действия.)
  • Относительная справедливость
    (хост не имеет преимущества перед другими игроками, как и игроки, которые находятся ближе к хосту.)
  • Игровой код остается отделенным от сетевого кода
    То есть, если вы, например, добавляете новую атаку в вашу игру-файтинг, вам обычно не нужно прикасаться к какому-либо неткоду, что может уменьшить количество телодвижений, необходимого в командах, где разные люди делают код геймплея и неткод.

Минусы

  • Задержка ввода
    Хотя rollback может помочь в этом, настройка задержки ниже половины медианного времени полного сетевого пути (пинг) приведет к тому, что удаленные игроки будут постоянно сбиваться с пути из-за регулярно-неправильного прогноза.
    Несмотря на это, многие люди предпочитают rollback задержке ввода.
  • Масштабирование
    Каждому игроку необходимо отправить свои входные данные каждому другому игроку, то есть требуется сумма(1...кол-во игроков-1) соединений между игроками - 1 для 2 игроков, 3 для 3 игроков, 6 для 4 игроков, 10 для 5 игроков, 28 для 8 игроков, 66 для 12 игроков... нет необходимости говорить, что чем больше у вас соединений, тем выше шансы, что у какой-то пары игроков будет плохая связь друг с другом и возникнут проблемы для всех остальных.

    Обычно детерминированный неткод не используется в сочетании с сетчатой топологией в играх с >4 игроками - для игр со скромными требованиями к задержке входа (например, RTS игры), вместо него может использоваться звездная топология (все соединяются с хостом).

  • Читы
    Поскольку у каждого игрока всегда полное игровое состояние, люди могут придумать, как найти то, чего они не должны видеть (см.: взлом-карты/взлом-мира).

    В динамичных играх это не так уж и важно - например, в файтинге редко бывает разница между тем, что видишь ты и твой противник, и единственная дополнительная информация, которую может дать преимущество, - это хитбоксы/перезарядки, которые многие игроки уже знают наизусть.

  • Рассинхронизация
    Ваш злейшний враг!

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

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

    Отладка причин рассинхронизаций является одной из более сложных частей работы с детерминистическим неткодом и включает в себя сравнение дампов/логов состояния игры.

Предостережения

  • Важность качества соединения
    Так как запаздывание пакетов приводит либо к паузам (lockstep), либо к визуальным сбоям (rollback), становится крайне важно поддерживать как можно более хорошую связь между игроками.

    Методологии для подавления потери пакетов становятся обязательными, а недавно вышедшие игры все чаще склоняются к маршрутизации трафика через частные сети (что могут предложить лучшие задержки/стабильность, чем прямые подключения).

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

    Например, GameMaker находится в относительно хорошем положении в этом отношении, так как имеет встроенный эпсилон для сравнения (достаточно близкие числа считаются равными) и встроенные функции проверки столкновений исторически округляют координаты (что означает, что небольшие ошибки с плавающей точкой могут остаться совершенно незамеченными)

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

  • Переменная кадровая частота кадров
    Заставить вашу игру работать с различной частотой обновления экрана может быть сложнее с детерминистическим неткодом, так как логика игры должна шагать с одинаковой частотой у всех игроков, в то время как видимые объекты должны быть интерполированы/экстраполированы.

Каким играм нужен детерминистический неткод?

Традиционно, детерминистический неткод используется для

  • Быстрые соревновательные игры
    Например, почти любой файтинг или платформер-файтер, который вы можете найти, имеет rollback (предпочтительно) или lockstep неткод.

    Меж-жанровые быстрые игры (например, Lethal League) также склоняются к rollback неткоду.

  • Быстрые коорперативные игры (иногда)
    Зачастую, если ваша игра является строго кооперативной, вы можете использовать классическую клиент-серверную модель и отдавать предпочтение игроку там, где это возможно, но в играх с кооперативным и соревновательными режимами и/или высокими требованиями к точности может быть использован rollback неткод.

    Возможно, самым известным недавним примером этого является Spelunky 2, но rollback неткод также может быть найден в крупнобюджетных beat-em-up играх.

  • Стратегии в реальном времени (и другие игры со слишком большим количеством сущностей)
    Если ваша игра имеет сотни движущихся юнитов, эффективно синхронизировать информацию о них может быть непросто, по причине чего RTS игры исторически склонялись к lockstep.

    В 2021 году, медианные скорости интернета, как правило, достаточны для того, чтобы многие игры в РТС использовали клиент-серверную модель, что также избавляет их от некоторых проблем с читерством.

  • Эмуляторы
    Модификация ROM'а каждой игры для включения сетевой логики обычно нерентабельна, и эмуляторы по своей сути являются детерминированными, что делает их отлично подходящими для детерминированного сетевого кода.

Подготовка

Это можно удобно разделить на уровни того насколько вы хотите побеспокоиться об этом:

Уровень 0: Общее

Это хорошо делать, даже если вы не уверены, будете ли именно вы делать неткод:

  • Я об этом регулярно говорю, но, если вы собираетесь когда-то делать сетевой мультиплеер в вашей игре, у вас должен быть рабочий локальный мультиплеер какого-то рода — даже, если это сплит-скрин и в него тяжело играть без большого/широкого

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

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

Уровень 1: Lockstep на ПК

Для этого нужно:

  • (шаги из прошлого уровня)
  • Абстрагируйте свой опрос устройств ввода в функции рода button_check(player_index, button_index), или просто имейте кусок кода, где состояние всех нужных input'ов записывается в переменные.

Для практического примера, попробуйте реализовать систему реплеев (как в super meat boy!) в своей игре.

Реплей - это файл, содержащий нужное начальное состояние (например, настройки, связанные с геймплеем, или разблокированные игровые элементы) и содержащий input'ы игрока за каждый кадр с момента начала матча/сессии.

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

Если вы можете заставить реплеи работать без рассинхронизации, то все хорошо!

Уровень 2: Lockstep в веб/смартфонах/консолях

Во-первых, объясняю различие от прошлого уровня:

На настольных платформах сетевые API, как правило, имеют синхронные версии функций, что означает, что если вам нужно остановить игру на некоторое время, вы можете в цикле опрашивать сокет/API и ждать долю секунды до тех пор, пока данные не станут доступны или не наступит timeout.

На других платформах, с другой стороны, синхронный опрос может быть непредпочитаемым или вовсе не поддерживаться (см. HTML5).

Итого, для этого вам нужно

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

    Обычно это достигается путем перемещения кода логики игры в другое место, что облегчает вызов по требованию - например, перемещение кода события Step на User Event в GameMaker, или перемещение Update/FixedUpdate на вашу собственную функцию в Unity.

    Обратите внимание, что вы также должны позаботиться о любой логике, которая обрабатывается выбранным вами движком автоматически! (например, анимация/схожие состояния)

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

Уровень 3: Rollback

К этому трудно полностью подготовиться/тестировать, но:

  • (шаги из прошлого уровня)
  • Реализация сохранения/загрузки состояния игры по требованию.

    Она должна сериализовать/десериализовать всё игровое состояние (всё, что влияет на игровой процесс) в какой-нибудь формат, который можно прочитать позже - традиционно, бинарная сериализация, но технически вы можете делать все, что захотите, лишь бы она была достаточно быстрая (срабатывает за <10% времени игрового кадра).

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

Для практического примера, реализуйте возможность сохранения/загрузки позиции в ранее сделанной системе реплеев - сохранение будет означать сохранение состояния игры и текущей прочитанной позиции файла, в то время как загрузка будет означать загрузку состояния игры и сброс прочитанной позиции файла в ранее сохраненную, таким образом, эффективно перематывая реплей.

Если вы можете заставить это работать, не вызывая рассинхронизации, ваши дела хороши! (но, конечно, может понадобиться дополнительная оптимизация).

Когда начать писать неткод?

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

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

Модернизация существующей игры под мультиплеер может быть сложнее - особенно для rollback.

В частности, для игр GameMaker, lockstep достижим для большинства игр, так как в них меньше вещей, которые могут пойти не так (как было упомянуто выше).

Тестирование до неткода

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

К счастью, в наши дни нет нехватки решений для потоковой передачи игр, будь то с компьютера одного из игроков (NVIDIA GameStream, Moonlight, Parsec, Steam Remote Play и т.д.) или с сервера (Parsec, GeForce Now, а может и больше - сложно проверять).

А если ваша игра создается в GameMaker, вы можете использовать сделанный мной инструмент, чтобы тестировать игру онлайн без нужды реализовывать что-либо на стороне игры - благодаря различным ухищрениям инструмент динамически встраивает lockstep неткод в игры, обеспечивая лучшую пропускную способность и задержку по сравнению с потоковыми подходами.

Для дальнейшего чтения

(все ресурсы в этой секции на английском, так как довольно тяжело найти сопоставимые материалы на русском)

  • Концепции Lockstep & Rollback от Meseta
    (автор также написал несколько статей об общих сетевых концепциях)
  • "Deterministic lockstep" от "Gaffer On Games"
    (также включает в себя объяснение подхода для подавления утери пакетов)
  • Rollback Networking in INVERSUS
    Поясняет rollback на примере отдельно взятой игры, включая архитектурные решения и подвохи.
  • Overwatch Gameplay Architecture and Netcode
    Много технических деталей о том, как работает Overwatch и неткод Overwatch'а, от архитектуры до предугадывания до изощренного rollback'а. Лучше смотреть после того, как вы уже прочли несколько других материалов о rollback.
  • Этот список различных ресурсов

Удачи!


Помощь с переводом от Terisback.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.