🌇 Мультиплеер в Цивилизации 5

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

Что за Цивилизация 5?

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

Последний официальный патч был выпущен 27 октября 2014. С тех пор Firaxis переключилась на другие части серии, но даже до сих пор достаточно много людей играют в 5 часть. Конечно, онлайн намного меньше, чем у последующих частей серии, но всё равно он достаточно большой по сегодняшним меркам, особенно для игры, которая не получает официальных патчей уже более 10 лет.

Сообщество моддеров

Одной из главных причин, почему игра до сих пор пользуется популярностью, это огромное моддерское сообщество. Сегодня более 10к+ модов доступно в Steam Workshop, а местный моддерский сабфорум насчитывает 500к+ сообщений. Моды поддерживают эту игру свежей и всё еще интересной.

На сегодняшний день практически всё в игре может быть изменено модами. Ядро игры, DLL-библиотека со всей логикой, была запаблишена для сообщества самими разработчиками. Традиционными средствами моддинга, т.е. .lua, .xml, .sql и прочими файлами можно изменить всё (почти) остальное.

Vox Populi (также известен как VP)

Как я уже сказал, на сегодняшний день доступно огромное количество модов, но “центральным” и самым популярным является мод Vox Populi (сабфорум на civfanatics, канал в Discord). Он объединяет большое количество разработчиков под “единым” курсом развития и работой над одним гига-модом. Репозиторий с кодом сегодня насчитывает более 8к коммитов и 330 звездочек, что достаточно много, как для модификации игры.

До появления VP все моды и “экосистемы” модов от отдельных моддеров были слабосовместимы между собой, поэтому тяжело было собрать стабильную сборку, в которой ничего бы не конфликтовало друг с другом. С VP такая возможность появилась, так как помимо того, что сами моддеры стали поддерживать совместимость своих модов конкретно с VP, так и сам VP часто “интегрирует” популярные решения в свой состав.

Если вдруг захотите попробовать, то есть вот инструкция “как играть в Цивилизацию 5 + VP”, а также вот еще гайд по установке модов в принципе. Еще есть отдельный тред, касающийся сетевой игры в VP. Стоит отметить, что не рекомендуется ставить моды из Steam Workshop, так как там они часто устаревшие. Если серьезно хотите заморочиться и попробовать, то для стабильности лучше ставить напрямую .zip файлы из веток форума и GitHub.

Что и как я там кодил

Я играл в Цивилизацию 5 временами с самого момента релиза, но только с друзьями по сети. Однажды мы решили попробовать мультиплеер с модами (с чистым VP), было это где-то в 2018 году, и оно достаточно сносно работало. В 2023 мы попробовали снова и мультиплеер уже был сломан и буквально неиграбелен. Примерно с тех пор и на протяжении года я контрибьютил в VP, чтобы сделать его совместимым с мультиплеером.

Совместимость VP с игрой по сети

Причина, по которой VP перестал быть совместимым с игрой по сети, была в том, что в сообществе еще не было понимания, что влияет на совместимость и как в целом работает мультиплеер. Поэтому кодили все вообще без оглядки на игру по сети. И тестировали все только в синглплеере (ну, к слову, конкретно этот пункт здесь только потому что протестировать что-то в мультиплеере - это тот еще квест). На мультиплеер официально забили, обосновав решение тем, что никто невкурсе, как его поддерживать, да и вообще якобы неткод не заопенсоршен (спойлер: незаопенсоршена только прям низкоуровневая логика пересылки байтиков, а так всё необходимое для создания мультиплеер-совместимых модов уже было) и поэтому пофиксить ничего не получится. Поэтому моды в сетевой игре работали весьма рандомно: что-то работало, что-то нет, что-то работало, но через раз. Спрос на мультиплеер был, но как его заставить снова работать - понимания не было, поэтому никто и не занимался.

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

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

Архитектура сетевой игры

На всякий случай отмечу, что я не C++ разработчик и никак не связан с геймдэвом. Все описанное - просто наблюдения фаната серии, который по счастливой случайности разбирается в компьютерах.

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

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

  • Хост и “владелец” состояния текущей сессии, с которым синкают свое состояние все остальные игроки. Либо выделенный сервер вместо хоста.
  • Участники сессии, которые пуллят состояние с хоста и пушат свои действия туда же, чтобы это состояние обновить.

Полагаю, это “традиционная” модель, которая работает в большинстве игр, но в Цивилизации 5 модель совершенно другая.

В Цивилизации 5 как такового “эталонного” состояния у игроков в сессии формально нет. Нет ни “владельца” состояния текущей сессии, нет и выделенного сервера, на котором ранится вся логика и с которого пуллили бы состояние все участники сессии. Тут вообще никто ничего ниоткуда не пуллит, есть только пуш.

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

Если в сессии есть боты, то их действия тоже никуда не бродкастятся (потому что бродкастятся только действия реальных игроков). Их логика работы также вычисляется каждым участником сессии самостоятельно: например, бот решает “строить в городе казармы” или “строить в городе библиотеку” на основе какого-то алгоритма, который работает одинаково у каждого участника сессии, и по итогу везде бот выполняет одинаковое действие.

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

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

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

Например, если представить, что боты выбирали бы “строить казармы” или “строить библиотеку” на основе рандома, а не на основе четкого алгоритма, то у игрока А бот строил бы казармы, а у игрока Б он строил бы библиотеку. Действия ботов не бродкастятся между участниками сессии и состояние игры никогда никем ниоткуда не пуллится, “эталонного” состояния нет и вовсе, а потому наличие рандома в такой логике привело бы к тому, что игрок А и игрок Б играли бы как бы разную игру.

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

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

Но вернемся к идеальному миру, где рандома в логике игры нет, состояние сессии вычисляется всегда одинаково у каждого участника, и по итогу все прекрасно работает. Это и есть “главное правило” стабильной сетевой игры в Цивилизации 5. И еще одно правило, что каждое действие человека нужно бродкастить на всех остальных, но это, в принципе, достаточно очевидное правило.

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

Примеры кода, который делал сетевую игру невозможной

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

Но представим, что для действий пользователя реализовали корректный бродкаст и его обработку, и что никто не использовал в коде условный math.random() (потому что всё-таки это “плохой тон” и весь рандом там обычно условный, основанный на сиде сессии), и вроде как от всего видимого рандома мы избавились, но сетевая игра продолжает постоянно вылетать. В чем же быть проблема?

Как выяснилось, проблема все в том же - один и тот же код на разных устройствах возвращает разные результаты. Примеры фиксов:

  • #9768: использование sort, который в C++ не гарантирует воспроизводимый порядок для одинаковых элементов.
  • #10112: использование set с произвольной сортировкой.
  • #9867: использование указателей в качестве ключей в map и произвольный порядок как следствие.
  • #10250: использование одного и того же кэша для UI и для логики ядра. Кэш считается один раз в ход и если он уже посчитан на этом ходу, то он не пересчитывается снова. Поэтому если кэш инициализировался по клику пользователем на кнопку, то значение кэша в момент работы ядра будет устаревшим и обновлено не будет. Поэтому рассинхронится ли состояние у двух игроков, зависело от того, нажмет ли кто-то из них на какую-то плохо-реализованную кнопку.
  • #9767, #9970: использование в процессе вычисления логики переменных, которые принимают разное значение в зависимости от игрока, который производит эти вычисления.

Больше примеров таких багов можно найти здесь. Иногда попадаются очень экзотические кейсы, которые прям очень тяжело отследить. Обычная дэбаг-сессия работы мода в мультиплеере - это дэбаг логами и сравнение этих логов с двух одновременно-запущенных инстансов игры, и она занимает прямо много времени. Пока всё соберешь, пока везде подложишь, пока запустишь, пока создашь сессию, пока воспроизведешь, пока сравнишь логи - дэбаг даже простого рассинхрона занимает часы, если повезет, не говоря уже про какие-то более душные сценарии. Суть проблемы всегда одна - каким-то образом логика отработала на разных “концах” по разному.

Что можно было бы сделать изначально иначе?

У такой архитектуры сетевой игры есть свои плюсы. Но перевешивают ли эти плюсы сложность в поддержке такого решения? Я бы сказал, что, конечно, нет. Прям очень тяжело вести такую разработку и держать постоянно в уме все эти нюансы. Неудивительно, что, как я уже говорил, даже в ванильной Цивилизации 5 без всяких модов в сетевой игре были постоянные рассинхроны.

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

Дела еще изначально были бы получше, если бы Firaxis вместе с DLL-ем прикрепили бы и тесты (если они вообще существовали, хотя в такой кодовой базе они точно должны были существовать). Сегодня кодовая база VP (т.е. изначально вся логика Цивилизации 5) на сотни тысяч строк не покрыта ни единым тестом, поэтому сломать там что-то намного проще, чем починить. Спасибо, что правки вручную тестируют хотя бы в одиночной игре. А тестировать правки вручную в мультиплеере - это уже извращение, на которое далеко не каждый готов идти, потому это уже не просто “скомпилировать и запустить”.

Реверс-инжениринг закрытого кода

Как я уже говорил, Firaxis выложила в открытый доступ Windows DLL с логикой всей игры, с логикой ботов, со всеми алгоритмами принятия решений и т.д. Изменяя этот DLL можно изменить практически все. Закрытой частью, которая реализована вне DLL, остается рендеринг, UI, взаимодействие с камерой, пересылка байтиков по сети, взаимодействие со Steam, с локальной базой и еще всякие мелочи, которые обычно и не нужно править. Но в этой закрытой части есть и баги, которые, получается, пофиксить никак нельзя. Конечно, грех жаловаться - хотя бы что-то отдали сообществу, уже хорошо.

То, что моддеры могут модифицировать только DLL, а не весь код полностью, накладывает определенные ограничения:

  • Разработка залочена на Visual Studio 2008 и поэтому нет возможности использовать какие-то более современные фичи C++.
  • Все скомпилированные библиотеки и исполняемые файлы 32-битные, что ограничивает доступное количество памяти, которую игра может использовать. С большим количеством игроков и со всей обновленной логикой из VP, этой памяти иногда перестает хватать.
  • Баги и утечки памяти в закрытом коде невозможно пофиксить.
  • Невозможно моддить рендеринг и всё ранее-перечисленное.

Более того, исполняемые файлы защищены CEG (DRM-защита от Steam, которая, хоть и устаревшая, но всё равно рабочая), поэтому пропатчить закрытый код и распространять его вместе с модом невозможно.

Но есть и хорошие новости:

  • Хоть и исполняемые файлы защищены CEG, их всё равно можно пропатчить в рантайме. Это достаточно легко реализовать, учитывая, что мы уже можем править код в подгружаемой DLL.
  • Исполняемые файлы не обфусцированы какой-то супер-модной технологией, как это делают сейчас.
  • Ну и самое приятное: непонятно, было ли это сделано намеренно или нет, но в Linux-версии исполняемый файл поставлялся со всеми сохраненными нэймингами и виртуальными таблицами. Учитывая, что он практически идентичен Windows-версии, можно их сопоставить и найти необходимую функцию по нэймингу из Linux-версии в Windows-версии.

Как-то так выглядит исполняемый файл под Linux

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

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

Эта правка служит примером, что ничего невозможного в моддинге Цивилизации 5 нет. При желании достучаться можно и до закрытого кода.

Заключение

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

Такая вот история. Если вдруг захотите попробовать вспомнить 2010 и поиграть в Цивилизацию 5 с модами, уверяю, это будет того стоить. Ну, синглплеер уж точно. Long live VP!

И подписывайтесь на мой 🛫 Telegram-канал, кстати. Там я размещаю всякие мысли про повседневную ра��работку, которые не подходят под формат блога.

Спасибо за прочтение этой небольшой статьи. Надеюсь, она была полезна.

Посмотрите также статьи на похожие темы: