Pull to refresh

Синхронные движки RTS и история рассинхронизаций

Reading time 7 min
Views 8.9K
Original author: Forrest Smith
Случалось ли Вам играть в игру вроде StarCraft или Supreme Commander и получать сообщение об ошибке вроде “Обнаружена рассинхронизация”, за которым следует закрытие игры? Хотите узнать отчего так происходит? Это наследие архитектуры игрового движка, часто используемой стратегиями в реальном времени.1

Мой опыт в этой области происходит из работы с движком Supreme Commander в студии Gas Powered Games. В период бета тестирования в Starcraft и Warcraft 3 тоже были проблемы с синхронизацией, так что можно сказать что в целом они работают так же. Для простоты я буду говорить именно о движке Supreme Commander. Нахождение сходства с другими играми оставлю как упражнение для читателя :)

Требования



Во-первых, каковы требования к нашей игре? Чтобы дать Вам это понять, вот ролик для первой части Supreme Commander (2006 год).



Игра должна поддерживать 8 игроков в сетевой игре через Интернет, с сотнями юнитов в каждой армии. Это несколько тысяч юнитов в одной игре. Ёшкин кот. Типичный клиент-серверный подход для шутеров здесь явно не годится. С таким числом юнитов он потребует гораздо большей пропускной способности, чем доступно большинству игроков.

Так как же можно подступиться к задаче?..

Синхронная архитектура движка



… С полностью синхронной архитектурой с одновременным шагом! В синхронном движке
каждый клиент выполняет один и тот же код с одной и той же скоростью. Обдумайте это немного. В игре Supreme Commander на 8 игроков каждый игрок хранит одинаковое состояние игры и выполняет одинаковый код. Вместо передачи информации о состоянии юнитов (позиция, здоровье и т.п.) по сети можно передавать только команды, вводимые игроками2. Если у всех игроков одинаковое состояние игры и обрабатывается одинаковый ввод, полученное состояние тоже должно быть одинаковым.

По тому же принципу действуют запускаемые повторы игр, в том числе и в шутерах. Вам не случалось удивляться, почему размер файлов реплеев такой маленький? Это потому что такой файл должен хранить только пользовательский ввод. Потом мы просто запускаем игру, скармливаем ей ввод из файла повтора и получаем тот же самый результат что и при оригинальной игре. Именно поэтому файлы повторов часто перестают работать3 при обновлении игры и именно поэтому их часто нельзя перематывать назад4. Кстати, по той же причине многие стратегии не поддерживают подключение новых игроков по ходу игры — для подключения нового игрока ему должно быть передано полное состояние игры. Для игры с тремя тысячами юнитов это займет слишком много времени.

Уровни



Посмотрите видео в начале, если ещё не успели. Как вы думаете, с каким количеством кадров в секунду идёт игра? Правильный ответ — 10 fps. «Постойте, что? Она выглядит гораздо более плавно!», скажете Вы! Да — и одновременно нет. Вообще говоря, игра использует одновременно две частоты.

Движок SupCom использует два уровня — симуляции и пользовательский. Уровень симуляции работает на фиксированной частоте 10 кадров в секунду. Именно его можно считать “настоящей игрой”. Все юниты, все AI и вся физика обновляются в функции SimTick, которая запускается 10 раз в секунду. Каждый SimTick должен отработать менее чем за 100 мс, иначе игра будет идти в замедленном темпе. В сетевой игре, если какой то игрок не успевает полностью обработать SimTick за 100 мс, все остальные игроки вынуждены остановиться и ждать отстающего.

Пользовательский уровень работает на полной частоте кадров. Этот уровень можно считать исключительно графическим. Интерфейс пользователя, рендеринг, анимация и даже позиция юнитов могут рассчитываться с частотой 60 fps. Каждый UserTick обновляется во время delta, которое используется для интерполяции состояния игры до промежуточного значения (к примеру, промежуточные позиции юнитов). Именно поэтому игра может выглядеть и играться плавно, хотя основное ядро движка работает на довольно низкой частоте.

Детерминизм



“Постойте ка минутку!”, воскликнет умный читатель. “Если каждый игрок независимо обновляет состояние игры, это должно означать что симуляция игры полностью детерминирована?” Так и есть. Сложно ли этого достичь? Да. Особенно в современном многопоточном мире.

Много неприятностей разработчикам движка доставляют числа с плавающей запятой. Вместо того чтобы описывать эту тему подробно, я дам ссылку на фантастический пост Гленна Филдера — Floating Point Determinism.

В комментариях к нему Elijah обсуждает именно Supreme Commander. Если заставить процессор чётко следовать стандарту IEEE754, это решает большинство проблем. Но такое решение означает снижение производительности и игра не может выполнять вычислений с неопределйнным результатом (впрочем этого и так не следует делать).

Внутренние задержки



У синхронной многопользовательской игры есть определённые минусы. Кроме сложности создания огромного полностью детерминированного симулятора существует задержка обработки ввода. Я уже писал как каждый пользователь в многопользовательской игре обновляет одинаковое состояние игры с использованием одинакового ввода. Это означает что любой новый ввод будет обработан только когда все клиенты согласятся в какой шаг симуляции его обрабатывать!

Например, три игрока — A, B и C — запускают SimTick[1]. В это время игрок A даёт юниту команду атаковать. UI сразу показывает отклик, т.к. UserTick отрабатывает 60 раз в секунду. В однопользовательской игре эта команда будет обработана в SimTick[2] (задержка 0-100 мс). Однако, все три игрока должны обработать команду в одном и том же запуске SimTick чтобы получить одинаковый результат. Вместо того чтобы пытаться обработать команду в SimTick[2], игрок A отправляет сетевые пакеты игрокам B и C с данными для выполнения в SimTick[4] (задержка 200-300 мс). Это даёт всем игрокам время получить команду. Игра может сорваться если информация о вводе не получена или не подтверждена вовремя. Я не знаю какой именно механизм использовался для этого в SupCom, но я обновлю этот пост если выясню. Конкретное число выполнений SimTick, через которое надо обработать ввод, определяется динамически с использованием топологии p2p5.

Задержка от клика пользователя до реакции юнита всегда будет составлять как минимум 0-100 мс (следующий SimTick). Это может быть замаскировано несколькими способами. Интерфейс обычно отзывается сразу — что нибудь мигает, раздаётся соответствующий звук: “Жизнь за Аюра!” или “Зуг Зуг”.

В однопользовательской игре это нормально, но в многопользовательском сражении задержка начинает становиться заметной, достигая нескольких сотен миллисекунд. Я всегда хотел поэкспериментировать с мгновенным ответом анимацией в UserTick. Например, если вы даёте команду двигаться, пользовательский уровень начинает медленно двигать юнит и “подмешивает” движение в направлении точки, указанной симуляцией, когда команда будет на самом деле выполнена. Это может оказаться очень полезным в более “дерганных” играх вроде DOTA или Demigod. Есть правда определённые крайние случаи, которые придётся обрабатывать особо, поэтому я так толком и не взялся за реализацию. Если кто то такое делал, отпишитесь в комментариях. :)

Рассинхронизации — Баги из Ада



Одни из самых сложных багов во Вселенной — баги рассинхронизации. Это довольно злобные сволочи. Основное предположение движка — что все игроки полностью синхронизированы. Что если это не так? Что случается если симуляции у разных игроков расходятся? Хаос. Гнев. Страдание6.

В SupCom всё состояние игры хешируется раз в секунду. Если у каких то клиентов хеши не совпадут — концерт окончен. Game over. Конец. Выскакивает окошко с сообщением “Ошибка синхронизации” и приходится выходить из игры. Что то в результатах SimTick не совпало и теперь состояния игры различаются. Пути симуляций разошлись и дальше будет только хуже. Восстановительного механизма тут не предусмотрено.

Рассинхронизации это обычно результат ошибки программиста. Рассинхронизация может воспроизводиться в 5% игр, продолжающихся более 60 минут. Нахождение и исправление такой ошибки обычно включает в себя двоичный поиск в хешах состояния памяти, распечатываемых в консоль при игре. При рассинхронизациях с низким шансом воспроизведения это приводит к огромному количеству сообщений в консоли пока полдюжины машин просчитывают симуляцию так быстро, как только возможно, ожидая пока она сломается. Если этого Вам недостаточно, добавлю что одна из самых частых причин рассинхронизации — необъявленная переменная.

История Полубога



Большинство моей работы над движком SupCom было сделано при работе над Demigod, в котором использовалась модифицированная версия движка.



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

Я точно помню что не был уверен, смогу ли пофиксить этот баг, и наш ведущий программист сказал “Я знаю, ты сможешь это починить. Я в тебя верю.” Никакого прессинга, правда? Каждое утро у нас было десятиминутное совещание и каждый день мой отчёт был короток и прост: “охота на рассинхронизацию”. После почти недельного спуска в пучины безумия я нашёл причину ошибки. Если Вы смотрели трейлер, Вы видели у некоторых героев способность подбрасывать противников в воздух. Когда огромный шагающий замок опускает свой молот, юниты вокруг разлетаются. Баг был в одном из указателей в системе поиска пути, который указывал в никуда, из за чего после приземления юнит просто исчезал.

Но для воспроизведения рассинхронизации этого было недостаточно. Для начала, юнит должен был быть убит одним из нескольких специальных умений. Это удаляло его из системы поиска пути и оставляло висящий указатель. Диспетчер памяти перемещал память удалённого компонента в связанный список свободных областей, не изменяя её содержимого. Потом, до того как юнит приземлялся, должно было произойти новое выделение памяти. Это выделение должно было затронуть тот блок, который был недавно удалён. Только после этого происходила рассинхронизация. Установка указателя в NULL решала проблему.

Финальные мысли



Это был очень короткий обзор синхронного движка, используемого в Supreme Commander. Я знаю что многие старые игры были устроены примерно так же. Последнее поколение наверняка применяет какие то новые трюки, в особенности для борьбы с задержкой ввода. Я знаю что рассинхронизации есть и в StarCraft 2, так что он скорее всего устроен похоже. Другие игры, на которые можно посмотреть это Heroes of Newarth или League of Legends. Они не так сложны как SupCom, и играются довольно плавно, но я не разбирал их и не знаю какие методы они для этого используют.

  1. Halo использует синхронную модель с одновременным шагом в режимах Campaign Co-op и Firefight
  2. В SupCom ввод обрабатывается как команды группам юнитов. Команды движения, атаки, защиты, использования способностей и т.п.
  3. Старые файлы повторов могут поддерживаться если есть возможность запустить старый код со старыми данными
  4. Перемотка в Halo была сделана с помощью “точек сохранения”, хранящих состояние игры на определённый момент. Плавно перематывать назад было нельзя, но можно было перейти на предыдущую точку сохранения и двигаться оттуда вперед. ЕМНИП.
  5. SupCom полностью использует сетевую архитектуру peer-to-peer
  6. Молния Силы, к сожалению, не прилагается
Tags:
Hubs:
+108
Comments 56
Comments Comments 56

Articles