Как устроена система игровых событий в проектах Playrix
В продолжение темы нашего движка VSO, которую затронули ребята в статьях про разработку нашей «маленькой Unity» и про улучшение редактора, расскажу об одной из систем, относящихся к игровой логике. Это кросс-проектная система игровых событий GameEvents.
Что такое игровые события?
В наших играх, кроме meta-геймплея (развитие города, восстановление сада/дома/зоопарка) и core-геймплея (матч-3 и т. д.), присутствует большое количество более мелких механик, которые позволяют разнообразить игру за счёт уникальных акций, возникающих в определённое время и имеющих определённую длительность. Такие механики называются игровыми событиями.
Событие может быть как простым окошком с поздравлениями и подарками, временной сменой внешнего вида какой-то части игры (например, матч-3), так и фичей, влияющей на получение очков за прохождение и даже на сам геймплей (например, соревнования по сбору яблок).
Зачем нужна кросс-проектная система событий?
Исторически так сложилось, что технически события на проектах делались как самостоятельные фичи. Иногда даже в рамках одного проекта обходились без обобщённого между событиями кода, что уж тут говорить про кросс-проектное обобщение. В лучшем случае с проекта на проект (или внутри проекта) делалась копипаста кода события, и потом всё подгонялось под проектную специфику.
Причины такого положения, в принципе, понятны и в какой-то степени естественны. Это и проектная специфика самого дизайна событий, за которой прячутся общие вещи, и проектная специфика кодовой базы, обусловленная разностью подходов, и большое количество разработчиков, которое порождает в том числе и коммуникационные проблемы, и просто недостаток времени на рефакторинг. В общем, все те проблемы, которые, так или иначе, присутствуют (хронически или периодически) в реальной разработке и поддержке больших проектов.
А тем временем росло не только количество событий, но и количество задач по переносу событий с проекта на проект. Вместе с этим становился всё более актуальным вопрос удобного шаринга кода событий. Хоть проектная специфика и не позволяет вынести весь код, но даже из части кода можно было бы сформировать некоторый обобщённый «конструктор». И на проектах стали появляться свои системы событий, которые позволяли обобщать и переиспользовать код в разных событиях. Где-то слово система можно было использовать с большой натяжкой, а где-то его употребление было вполне заслуженно.
Примерно в это же время на проектах шло активное внедрение VSO. С одной стороны, это позволило проектам часть визуальной специфики вынести из кода в VSO-редактор и отдать в руки «непрограммистов», а с другой — дало базу для организации обобщённого игрового кода вообще и построения кросс-проектной системы игровых событий в частности. Собственно, такая система (GameEvents) и зародилась на одном из проектов и впоследствии была перенесена в кросс-проектную библиотеку GameLib (входит в состав VSO).
Общая схема системы
Систему можно условно разделить на три части:
Таблица запуска
Здесь собраны условия для старта и остановки события. Например, это может быть определённая дата, уровень игрока, количество монет, пройденный квест и так далее. Условия, как правило, парные. То есть они формируются вместе для старта и остановки. Таким образом, у нас получается расписание, по которому событие стартует и останавливается.
Существует также возможность наложения одного события на другое, так как условия могут быть разными. Это значит, что одно событие может запуститься 8 марта, а второе может запуститься на 20-м уровне игрока. Таким образом, если игрок наберёт 20-й уровень 8 марта — оба события будут активными. При этом одно событие может завершиться с наступлением следующего дня, а другое, например, с получением 30-го уровня.
Таких пар условий может быть множество.
Компоненты
Набор классов для реализации логики и поведения события. Компоненты слабо связаны друг с другом, но могут общаться, например, посредством сигналов. Компоненты являются одним из методов упрощения переиспользования кода между событиями. Нет никаких ограничений по количеству компонентов на одно событие. Можно раскладывать логику на несколько несвязанных компонентов. При этом большая часть компонентов будет подключена и настроена в редакторе.
Граф состояний
Да, это всего лишь граф состояний. Машины состояний тут нет, потому что все переходы на самом деле происходят по решению компонентов. Граф состояний нужен для обозначения и визуального редактирования переходов между состояниями события. В простейшем виде событие может содержать всего лишь два состояния — активно и неактивно. В сложном — ограничений нет. Как правило, к состояниям события привязываются активация, показ окна, активная фаза, выдача награды и так далее.
Немного о внутренностях
Упрощённо, на уровне классов, система выглядит приблизительно так:
Стержнем всей системы является VSO-шный GDB-ассет, от которого унаследован класс GameEvent (нерасширяемый на стороне проектов). GameEvent агрегирует остальные части события и, благодаря своему родителю, имеет автоматическую сериализацию и возможность настройки через VSO-редактор. Этот класс закрыт для расширения на стороне проектов, т. к. всё расширение идёт через компоненты и стейты. Управление событиями осуществляется через класс GameEventManager.
Базовый класс GameEventComponent — это один из основных кирпичиков, позволяющих расширять (наследование + добавление и настройка через редактор) не только конкретное событие проектной спецификой, но и пополнять набор (проектный или же кросс-проектный) обобщённых компонентов.
Базовый класс EventState — это второй основной кирпичик, который позволяет расширять (наследование + добавление и настройка через редактор) и обобщать код состояний. Для управления состояниями предназначен класс EventStatesGraph.
Класс LaunchTable содержит множество экземпляров класса StartStopCondition, отвечающего за условия запуска/остановки событий. Сами же условия базируются на ещё одной обобщённой VSO-системе, которая используется не только в GameEvents, но и во многих других системах. Эта система условий в общих чертах является связкой дерева вычисления, контекста и инспекторов для редактора. Она не только обладает большой гибкостью и позволяет через редактор задавать условия разной сложности, но также позволяет расширять систему условий на проектах и выносить эти расширения в обобщённом виде в GameLib.
Совместно с контекстом для хранения переменных, используемых в условиях запуска/остановки событий, используется класс VariablesService.
Ну и наконец, связь всех событий с общей для них проектной спецификой осуществляется через наследников класса GameContext. Примером такой специфики является проектная сериализация прогресса игрока.
Сериализация
Как упоминалось выше, базирование на фреймворке VSO даёт нам механизм сериализации «из коробки». По умолчанию поля классов (например, компонентов или состояний), помеченные тегом @property сохраняются в ассет события. Но если сам класс пометить тегом @runtimeSave, а поля тегом условной сериализации @serializeTag с указанием целевого тега output-архива, то сериализация будет выполняться в этот архив:
/// @runtimeSave
class SomeGameEventComponent : public Game::GameEvents::GameEventComponent
{
VISUAL_CLASS()
...
/// @property
int _someOption = 0;
/// @property @dont(inspect) @serializeTag(DataStore)
int _someProgress = 0;
...
}
Архив — это ещё одна VSO-сущность, которая позволяет проектам выбирать способ сериализации либо «из коробки» (xml, json, binary), либо какой-то свой кастомный.
Что же удаётся вынести в общий код событий?
Очевидно, что далеко не весь код событий можно вынести в кросс-проектную библиотеку. Даже если событие какого-то типа на разных проектах логически идентично, то 100% оно будет отличаться в визуальной части. Да и в логику на проектах могут вноситься какие-то мелкие изменения. Поэтому в общий код уходит в основном только часть событийной логики. И этот код можно разбить на две группы:
код, который теоретически можно использовать во всех событиях, например:
базовый компонент игровой логики;
компонент описания наград;
компонент истории события;
компонент, получающий с сервера расписание запуска и генерящий условия запуска в LaunchTable;
базовый абстрактный компонент для настройки простых окон;
базовые классы часто используемых стейтов.
код, специфический для конкретного события и расширяемый на стороне проекта:
Внутри проектов тоже формируются свои пулы переиспользуемых компонентов, которые имеют проектную специфику.
Проблемы и развитие системы
Конечно, баги, архитектурные проблемы и проблемы юзабилити никто не отменял. Есть они и в нашей системе. Например, если бы LaunchTable изначально была реализована в виде компонента, то ее легко можно было бы заменить на другую реализацию (а такая необходимость есть), не боясь сломать уже существующие события на проектах. Кстати, возможность что-то у кого-то отломать изменениями в общем коде системы — это один из главных тормозов в развитии.
Другой пример больше организационного характера: не все проекты смогли легко и быстро перейти на общую систему событий. У кого-то на проекте ещё не произошёл в достаточной степени переход на VSO, у кого-то нет времени на освоение новой системы. Но процесс перехода всё-таки идет.
Даже если забыть про врождённые проблемы, то прогресс не стоит на месте. Ведь развивается VSO в целом, возникают новые идеи, возможности и потребности. И, несмотря на наличие у нас кросс-проектной команды разработчиков, основным драйвером развития системы являются сами проекты, т. к. именно они больше всего сталкиваются с конкретными живыми проблемами и лучше всего знают о своих потребностях. Очень часто проекты сами фиксят баги, разрабатывают и апробируют у себя различные доработки и потом отдают кросс-проектной команде на ревью и перенос в общую библиотеку.
Заключение
Система GameEvents для нашей команды — это, конечно, не «серебряная пуля», она не позволяет сделать идеальный общий конструктор, из которого можно собрать любое событие только с помощью редактора за 5 минут. Но тем не менее она задаёт единый подход (да ещё и в рамках VSO) в разработке игровых событий на разных проектах, что позитивно сказывается на сроках и качестве.
Ваши вопросы приветствуются, и какие-то из них могут послужить основанием для появления других статей на тему VSO.