Один из наших новых проектов, которые мы сейчас разрабатываем — игра в жанре Match-3. В этой статье расскажем о некоторых интересных технологических решениях, которые мы для нее используем. Речь пойдет о разрабатываемом фреймворке для Match-3 игр (M3Engine) и прилагающемся к нему инструментарии.
Идея создания подобного фреймворка возникла не вчера и уходит глубоко в историю проекта. Казалось бы, что может быть проще создания Match-3 игры?! Было предпринято несколько подходов к реализации, каждый из них был уникален в своем ключе, но обладал теми или иными недостатками. В конечном итоге мы пришли к решению, которым здесь и поделимся.
Основными целями, которые мы преследовали при создании этого фреймворка, были:
Чтобы достичь поставленных целей, грамотно спроектировать и выбрать наиболее удачные решения, мы провели подробный анализ существующих игр в данном жанре и используемых в них механик. Буквально несколько недель мы не вылезали из переговорок, выпили триллионы литров кофе и замучили вопросами гейм-дизайнеров. В результате проведенных исследований нам удалось выделить основные сущности игры и определить взаимосвязи между ними.
Рис 1. Общая идея Match-3 фреймворка
Философия M3Engine держится на следующих постулатах:
Благодаря тому, что фреймворк максимально абстрагируется от какого-либо графического движка, нам не составило большого труда перейти с in-house движка на Unreal Engine 4. Всего-то потребовалось написать плагин-обертку.
Основу M3Engine составляют два больших модуля (есть и другие, но их оставим за пределами этой статьи): Model и Logic. Модель (Model) описывает игровые сущности, их свойства и структуру уровня. Логика (Logic), или поведение, представляет собой набор интерфейсов (и реализацию для основных механик) для создания геймплея. Но обо всем по порядку, и начнем с самого начала, а именно – модели.
Самая простая сущность в модели — это уровень (Level). Уровень содержит информацию о геометрии поля/доски (Board) и его содержимом, условиях выигрыша (Targets) и поражения (Restrictions), настройках рандомизации, а также данные о балансе.
Рис 2. Базовые сущности модели: уровень и его компоненты.
В свою очередь доска (Board) представляет собой набор тайлов (Tile) на бесконечной координатной сетке, элементы (Element), располагающиеся на этой доске, генераторы элементов, телепорты и многие другие сущности, которые, как правило, располагаются на самой доске во время игры. Для отображения видимой части доски используется механизм вьюпорта (Viewport). Вьюпорт прицепляется к определенным точкам на поле, которые называются вьюпоинтами (Viewpoint). Во время игры вьюпорт может перемещаться от одного вьюпоинта к другому, что позволяет создавать уровни на несколько экранов.
Рис 3. Игровая сетка и тайл.
Тайлом называется клетка игровой доски (рис. 3). Они могут быть игровыми и неигровыми. На игровой тайл можно разместить бóльшую часть элементов, с которыми игрок будет иметь возможность взаимодействовать. Неигровые тайлы используются для размещения элементов геймплея, с которыми пользователь напрямую взаимодействовать не может. Тайлы размещаются в ячейках сетки доски. В M3Engine предусмотрен механизмы открепления (detach) тайлов от ячейки и прикрепления к ячейкам доски (attach). Таким образом имеется возможность реализации механик смены тайлов (со всем содержимым) местами. Элементы в свою очередь так же не имеют жесткой привязки к тайлам, так как могут между ними свободно перемещаться.
Элементы, цели и ограничения могут быть разных видов и обладать различными свойствами, которые влияют на их поведение. Для этого мы ввели понятие игровой сущности (Entity) и ее типа (EntityType). Типы сущностей и их свойства описываются в конфигурационном файле, который называется модель данных. Благодаря этому вводить новые элементы и цели уровней (один из самых часто используемых способов внедрения механики) стало очень просто и быстро.
Рис 4. Сущности и их типы.
Сам M3Engine предоставляет набор встроенных свойств для определения базовых возможностей элементов. Например, способность элемента к движению по полю, восприимчивости к урону, способность свапаться в соседнюю клетку и др. При этом, имеется возможность добавлять новые свойства.
Поскольку в одном тайле может находиться множество элементов, то для определения результативного значения свойства необходимо использовать суперпозицию (Superimposition, мат). В общем случае это выглядит так:
За упорядочивание элементов в тайле отвечает специальное свойство — диапазон занимаемых слоев (LayerRange). Благодаря этому происходит регулирование сочетания элементов в одном тайле.
Таким образом мы имеем довольно гибкую систему сущностей, в которую можно легко добавлять новые элементы на поле, цели уровня и добавлять свойства уже существующим элементам.
Поведенческая модель игры представляет собой конечный автомат, который определяет игровой процесс. На схеме ниже представлены состояния и переходы между ними (некоторые состояния упущены для простоты восприятия).
Рис 5. Конечный автомат состояния игры.
Конечный автомат инкапсулируется в сущности, которую мы называем Игровой Логикой (Logic). Игровая логика также обладает контекстом (LogicContext), который содержит модель игры, данные уровня, очередь исполняемых сценариев и шину событий для сообщения между компонентами.
Игровые состояния (LogicState) можно добавлять, убирать и настраивать переходы между ними. У каждого состояния есть три основных метода:
Логика каждого состояния определяется набором систем (System). Каждая система отвечает за какую-либо часть игры (как правило, определенную механику). Например:
При входе в состояние у систем вызывает метод start, а при выходе — stop. Также на каждом тике состояние вызывает у систем метод tick. Любая система может:
Под сценарием (Scenario) мы понимаем некоторую последовательность атомарных действий (как правило конечную), которые исполняются с течением времени. Сценарий может быть простым, например MoveScenario, который передвигает фишку из одного тайла в другой, так и сложным, состоящим из нескольких сценариев исполняемых последовательно или параллельно. Таким образом мы переиспользуем простые сценарии для создания более сложных.
Атомарные действия, посредством которых сценарий изменяет и манипулирует данными, называются Actions. Примерами экшенов могут служить: MoveElementAction, DamageElementAction, SwapAction, и т. д. Изначально мы полагали, что действий будет очень много, но, как показала практика, новые действия добавляются крайне редко. Чаще всего приходится реализовывать новые системы и сценарии.
Реализация новых механик таким образом сводится к следующему порядку действий:
Одной из целей было создание удобного инструментария для работы дизайнеров уровней. Благодаря тому, что сам Match-3 движок построен независимо от каких-либо библиотек и фреймворков для рендеринга, мы были довольно свободны в выборе UI фреймворка для редактора и его общей архитектуры. Как следствие – редактор является кроссплатформенным и поддерживает macOS, Windows и Linux.
Рис 6. Взаимодействие редактора уровней, игры и M3Engine.
Редактор уровней для работы с уровнями использует проекты, которые синхронизируются между членами команды посредством git. Это позволяет использовать один билд редактора для разных игр. Структура типичного проекта выглядит следующим образом:
Рис 7. Пример структуры проекта для редактора уровней.
Данный лэйаут проекта является необязательным и настраивается в файле проекта.
Для переключения между различными проектами редактор имеет окно выбора проекта (рис. 8). Здесь можно выбрать проект, который открывался до этого, либо новый с файловой системы. По умолчанию редактор откроет последний используемые проект.
Рис. 8. Окно выбора проекта редактора уровней.
После выбора проекта запустится основное окно редактора (представлено на рис. 9).
Рис. 9. Основное окно редактора уровней.
Интерфейс главного окна состоит из следующих компонентов:
Лэйаут основного окна можно гибко настроить и поделиться настройкой с коллегами.
Редактор также позволяет редактировать модель данных игры. Он дает возможностьсоздавать новые и редактировать уже имеющиеся элементы: редактировать свойства элемента, его состояния (грейды), форму, занимаемые слои, и задавать текстуру для редактора.
Рис. 10. Окно редактирования элементов.
Для проверки играбельности уровня редактор предоставляет запуск уровня в двух режимах:
Редактор постоянно дорабатывается по обратной связи от дизайнеров уровней: появляются новые фичи и исправляются баги.
Разрабатываемый нами фреймворк для игр жанра Match-3 имеет большой потенциал и уже сейчас полностью интегрирован в одну из будущих игр компании на Unreal Engine 4. В M3Engine уже заложена поддержка как основных механик жанра, так и более интересных. Благодаря выстроенной архитектуре, разработка одной механики в ядре не занимает много времени. Само ядро легко встроить в любой, совместимый с C++ игровой движок, а редактор не имеет никакой привязки к какому-либо движку. Редактор уровней позволяет создавать дизайнерам новые и интересные уровни и даже в какой-то степени менять свойства элементов.
Несмотря на большой прогресс и огромную проделанную нами работу, M3Engine есть куда расти. Вот всего лишь несколько направлений развития (не исключают друг друга):
Идея создания подобного фреймворка возникла не вчера и уходит глубоко в историю проекта. Казалось бы, что может быть проще создания Match-3 игры?! Было предпринято несколько подходов к реализации, каждый из них был уникален в своем ключе, но обладал теми или иными недостатками. В конечном итоге мы пришли к решению, которым здесь и поделимся.
Основными целями, которые мы преследовали при создании этого фреймворка, были:
- максимально абстрагироваться от графических движков и сосредоточиться на геймплее;
- реализовать инструментарий для дизайнеров уровней;
- обеспечить возможность прототипирования и быстрого внедрения новых механик (возможно руками гейм-дизайнеров).
Чтобы достичь поставленных целей, грамотно спроектировать и выбрать наиболее удачные решения, мы провели подробный анализ существующих игр в данном жанре и используемых в них механик. Буквально несколько недель мы не вылезали из переговорок, выпили триллионы литров кофе и замучили вопросами гейм-дизайнеров. В результате проведенных исследований нам удалось выделить основные сущности игры и определить взаимосвязи между ними.
Рис 1. Общая идея Match-3 фреймворка
Философия M3Engine держится на следующих постулатах:
- M3Engine предоставляет механизмы для введения новых и изменения свойств существующих элементов игры Match-3;
- для реализации механик и поведения элементов имеются интерфейсы состояний, сценариев и систем, позволяющих управлять и изменять модель в процессе игры;
- движок не декларирует способы визуализации игры для пользователя: эффекты, отображение поля, элементов и других сущностей остается за пользователем движка;
- уведомления об изменениях состояния происходит посредством системы событий.
Благодаря тому, что фреймворк максимально абстрагируется от какого-либо графического движка, нам не составило большого труда перейти с in-house движка на Unreal Engine 4. Всего-то потребовалось написать плагин-обертку.
Основу M3Engine составляют два больших модуля (есть и другие, но их оставим за пределами этой статьи): Model и Logic. Модель (Model) описывает игровые сущности, их свойства и структуру уровня. Логика (Logic), или поведение, представляет собой набор интерфейсов (и реализацию для основных механик) для создания геймплея. Но обо всем по порядку, и начнем с самого начала, а именно – модели.
Модель и данные
Disclaimer: для простоты восприятия часть сущностей в этой и последующих частях будет упущены или упомянуты вскользь без объяснений.
Самая простая сущность в модели — это уровень (Level). Уровень содержит информацию о геометрии поля/доски (Board) и его содержимом, условиях выигрыша (Targets) и поражения (Restrictions), настройках рандомизации, а также данные о балансе.
Рис 2. Базовые сущности модели: уровень и его компоненты.
В свою очередь доска (Board) представляет собой набор тайлов (Tile) на бесконечной координатной сетке, элементы (Element), располагающиеся на этой доске, генераторы элементов, телепорты и многие другие сущности, которые, как правило, располагаются на самой доске во время игры. Для отображения видимой части доски используется механизм вьюпорта (Viewport). Вьюпорт прицепляется к определенным точкам на поле, которые называются вьюпоинтами (Viewpoint). Во время игры вьюпорт может перемещаться от одного вьюпоинта к другому, что позволяет создавать уровни на несколько экранов.
Рис 3. Игровая сетка и тайл.
Тайлом называется клетка игровой доски (рис. 3). Они могут быть игровыми и неигровыми. На игровой тайл можно разместить бóльшую часть элементов, с которыми игрок будет иметь возможность взаимодействовать. Неигровые тайлы используются для размещения элементов геймплея, с которыми пользователь напрямую взаимодействовать не может. Тайлы размещаются в ячейках сетки доски. В M3Engine предусмотрен механизмы открепления (detach) тайлов от ячейки и прикрепления к ячейкам доски (attach). Таким образом имеется возможность реализации механик смены тайлов (со всем содержимым) местами. Элементы в свою очередь так же не имеют жесткой привязки к тайлам, так как могут между ними свободно перемещаться.
Элементы, цели и ограничения могут быть разных видов и обладать различными свойствами, которые влияют на их поведение. Для этого мы ввели понятие игровой сущности (Entity) и ее типа (EntityType). Типы сущностей и их свойства описываются в конфигурационном файле, который называется модель данных. Благодаря этому вводить новые элементы и цели уровней (один из самых часто используемых способов внедрения механики) стало очень просто и быстро.
Рис 4. Сущности и их типы.
Сам M3Engine предоставляет набор встроенных свойств для определения базовых возможностей элементов. Например, способность элемента к движению по полю, восприимчивости к урону, способность свапаться в соседнюю клетку и др. При этом, имеется возможность добавлять новые свойства.
Поскольку в одном тайле может находиться множество элементов, то для определения результативного значения свойства необходимо использовать суперпозицию (Superimposition, мат). В общем случае это выглядит так:
За упорядочивание элементов в тайле отвечает специальное свойство — диапазон занимаемых слоев (LayerRange). Благодаря этому происходит регулирование сочетания элементов в одном тайле.
Таким образом мы имеем довольно гибкую систему сущностей, в которую можно легко добавлять новые элементы на поле, цели уровня и добавлять свойства уже существующим элементам.
Логика и поведенческая модель
Поведенческая модель игры представляет собой конечный автомат, который определяет игровой процесс. На схеме ниже представлены состояния и переходы между ними (некоторые состояния упущены для простоты восприятия).
Рис 5. Конечный автомат состояния игры.
Конечный автомат инкапсулируется в сущности, которую мы называем Игровой Логикой (Logic). Игровая логика также обладает контекстом (LogicContext), который содержит модель игры, данные уровня, очередь исполняемых сценариев и шину событий для сообщения между компонентами.
Игровые состояния (LogicState) можно добавлять, убирать и настраивать переходы между ними. У каждого состояния есть три основных метода:
- onTick – вызывается каждый тик, если состояние активно;
- onEnter – вызывается при входе в состояние;
- onLeave – вызывается при выходе из состояния.
Логика каждого состояния определяется набором систем (System). Каждая система отвечает за какую-либо часть игры (как правило, определенную механику). Например:
- MatchSystem – отвечает за определение матчей на игровом поле;
- SheddingSystem – отвечает за движение и осыпание всех элементов;
- GenerationSystem – отвечает за спаун новых элементов из генераторов;
- … .
При входе в состояние у систем вызывает метод start, а при выходе — stop. Также на каждом тике состояние вызывает у систем метод tick. Любая система может:
- подписаться на события игровой логики, используя EventDispatcher;
- добавить сценарий (Scenario) в очередь исполнения (ScenarioQueue) в процессе обработки какого-либо события или в onTick;
- сгенерировать и отправить событие через EventDispatcher.
Под сценарием (Scenario) мы понимаем некоторую последовательность атомарных действий (как правило конечную), которые исполняются с течением времени. Сценарий может быть простым, например MoveScenario, который передвигает фишку из одного тайла в другой, так и сложным, состоящим из нескольких сценариев исполняемых последовательно или параллельно. Таким образом мы переиспользуем простые сценарии для создания более сложных.
Атомарные действия, посредством которых сценарий изменяет и манипулирует данными, называются Actions. Примерами экшенов могут служить: MoveElementAction, DamageElementAction, SwapAction, и т. д. Изначально мы полагали, что действий будет очень много, но, как показала практика, новые действия добавляются крайне редко. Чаще всего приходится реализовывать новые системы и сценарии.
Реализация новых механик таким образом сводится к следующему порядку действий:
- добавить новые элементы и цели (при необходимости) в модель данных;
- реализовать необходимые системы и сценарии для поведения элементов;
- добавить новые состояния в процесс игровой логики (используется крайне редко).
Редактор
Disclaimer: Скриншоты интерфейса редактора показаны на тестовом проекте с использованием стокового арта.
Одной из целей было создание удобного инструментария для работы дизайнеров уровней. Благодаря тому, что сам Match-3 движок построен независимо от каких-либо библиотек и фреймворков для рендеринга, мы были довольно свободны в выборе UI фреймворка для редактора и его общей архитектуры. Как следствие – редактор является кроссплатформенным и поддерживает macOS, Windows и Linux.
Рис 6. Взаимодействие редактора уровней, игры и M3Engine.
Редактор уровней для работы с уровнями использует проекты, которые синхронизируются между членами команды посредством git. Это позволяет использовать один билд редактора для разных игр. Структура типичного проекта выглядит следующим образом:
Рис 7. Пример структуры проекта для редактора уровней.
- В папке bin находятся билды игры для разных платформ, чтобы у дизайнеров уровней была возможность запуска разрабатываемых уровней в игре (об этом механизме запуска уровней из редактора речь пойдет дальше).
- Папка configs содержит файлы модели данных.
- В папке editor лежат файлы конфигурации редактора, которые необходимо синхронизировать между level-дизайнерами.
- Все уровни сохраняются в папку levels и ее саб-директории.
- Ассеты и настройки для арта элементов находятся в папке views.
Данный лэйаут проекта является необязательным и настраивается в файле проекта.
Пример файла конфигурации проекта
{
"project": {
"name": "LevelDesignTest",
"m3engineVersion": {
"minimum": [0,3],
"target" : [0,4]
},
"settings": {
"ProjectViewsPath": {
"innerType": "std::string",
"value": "${ProjectBasePath}/views"
}
},
"levels": [
"${ProjectBasePath}/levels"
],
"views": [
"${ProjectBasePath}/views/LevelDesignTest.m3vtbl"
],
"configs": "${ProjectBasePath}/configs",
"dataModels": [
"${ProjectBasePath}/datamodels/LevelDesignTest.dm"
],
"editor": {
"defaultLevel": "${ProjectBasePath}/editor/DefaultLevel.lvl"
},
"game": {
"LevelDesignTest (Win64)": {
"name": "LevelDesignTest (Win64)",
"workingDirectory": "${ProjectBasePath}/bin/Win64",
"executableName": "Game.exe",
"commandLineArguments": ["-Windowed"],
"platform": "Windows"
},
"LevelDesignTest (MacOS)": {
"name": "LevelDesignTest (MacOS)",
"workingDirectory": "${ProjectBasePath}/bin/MacOS",
"executableName": "Game",
"commandLineArguments": ["-Windowed"],
"platform": "MacOS"
}
},
"revivalParams": [
{ "moves": 3},
{ "moves": 5, "boosters": { "LinearHorzBomb": 3 } },
{ "moves": 10, "boosters": { "HomingBomb": 3 } }
],
"layerAliases": [
{ "index": -1, "name": "Carpet" },
{ "index": 0, "name": "Item" },
{ "index": 1, "name": "Jellyfish" },
{ "index": 2, "name": "Chain" },
{ "index": 3, "name": "Box" },
{ "index": 4, "name": "Frog" }
],
"levelValidatorPath": "${ProjectBasePath}/editor/LevelValidator.vldr"
},
"metainfo": {
"formatVersion": [0,1]
}
}
Для переключения между различными проектами редактор имеет окно выбора проекта (рис. 8). Здесь можно выбрать проект, который открывался до этого, либо новый с файловой системы. По умолчанию редактор откроет последний используемые проект.
Рис. 8. Окно выбора проекта редактора уровней.
После выбора проекта запустится основное окно редактора (представлено на рис. 9).
Рис. 9. Основное окно редактора уровней.
Интерфейс главного окна состоит из следующих компонентов:
- тулбар с основными действиями (открыть, сохранить, запустить уровень и т.д.);
- окно управления уровнями проекта;
- панель с инструментами редактирования. Здесь отображаются все зарегистрированные типы элементов и дополнительные инструменты (умный ластик, управление вьюпортами, генераторами, разные типы клеток поля, и т.д.);
- окно текущего инструмента и окно опций текущего инструмента;
- окно цветового баланса;
- окна настройки целей и ограничений уровня;
- окно управления отображением слоев;
- и самое основное — область редактирования уровня. Эта область представляет собой бесконечную координатную сетку, на которую можно размещать сущности уровня. При создании нового уровня, по умолчанию берутся конфигурации из файла DefaultLevel.lvl, указанного в конфигурации проекта.
Лэйаут основного окна можно гибко настроить и поделиться настройкой с коллегами.
Редактор также позволяет редактировать модель данных игры. Он дает возможностьсоздавать новые и редактировать уже имеющиеся элементы: редактировать свойства элемента, его состояния (грейды), форму, занимаемые слои, и задавать текстуру для редактора.
Рис. 10. Окно редактирования элементов.
Для проверки играбельности уровня редактор предоставляет запуск уровня в двух режимах:
- Внутренний проигрыватель, встроенный в редактор. Данный способ позволяет совершать отладку для разработчиков и увидеть работу новых механик для level-дизайнеров. Несмотря на то, что этот подход довольно прост, он обладает рядом недостатков. Самым большим недостатком является отсутствие эффектов и продвинутых анимаций.
- Запуск уровня непосредственно в билде игры. Это возможно посредством Networking модуля M3Engine. Данный модуль предоставляет интерфейсы редактору и билду для осуществления коммуникации. Общение между редактором и игрой осуществляется посредством сетевых команд.
- Для запуска уровня необходимо запустить билд из редактора. При запуске установится TCP-соединение. После запуска билда можно нажать «Run» с любым активным уровнем в редакторе, редактор отправит команду на запуск уровня и он запустится в билде. Данный способ немного сложнее в реализации, но позволяет увидеть созданный уровень, как он будет представлен в самой игре.
Редактор постоянно дорабатывается по обратной связи от дизайнеров уровней: появляются новые фичи и исправляются баги.
Заключение и дальнейшее развитие
Разрабатываемый нами фреймворк для игр жанра Match-3 имеет большой потенциал и уже сейчас полностью интегрирован в одну из будущих игр компании на Unreal Engine 4. В M3Engine уже заложена поддержка как основных механик жанра, так и более интересных. Благодаря выстроенной архитектуре, разработка одной механики в ядре не занимает много времени. Само ядро легко встроить в любой, совместимый с C++ игровой движок, а редактор не имеет никакой привязки к какому-либо движку. Редактор уровней позволяет создавать дизайнерам новые и интересные уровни и даже в какой-то степени менять свойства элементов.
Несмотря на большой прогресс и огромную проделанную нами работу, M3Engine есть куда расти. Вот всего лишь несколько направлений развития (не исключают друг друга):
- Model Editor. Сейчас уже можно менять и добавлять новые типы элементов в игру через интерфейс редактора. Имеет смысл расширить эту функциональность до всех сущностей игры.
- Scripting API. В ядро уже встроена система рефлексии типов, что позволяет в будущем добавить возможность скриптования механик посредствам, например Lua. Это позволит создавать и встраивать новые механики без модификации сборки ядра;
- Bot API. Создать интерфейс для отыгрывания уровней машиной, что позволит, используя разные эвристики и стратегии, оценивать сложность уровней, их играбельность и игровые механики, где пользователь играет против бота. И все это можно запустить на server-side;
- Server-side API. Запуск ядра на стороне сервера может позволить мультиплеер или, например, валидацию игры пользователя. Sky is the limit! :)