[Из песочницы] Как сделать тетрис за полгода на cocos2dx
В своей статье я бы хотел поделиться технической частью игры, которую сделали два человека. Будут рассмотрены основные архитектурные шаблоны (design patterns) и приёмы, дополнительные библиотеки, особенности портирования при работе с движком cocos2dx. Исходный код здесь.
Игра называется Beaver time. За дизайн отвечала моя сестра. Разработка велась на движке cocos2dx, на языке с++.
Геймплей
За основу взята механика из игры тетрис. Добавлено уничтожение одинаковых квадратов по горизонтали и вертикали (4 в ряд), разнообразные заклинания-способности у игрока, разные условия выигрыша, разные неприятные моменты для усложнения игры и квадраты-боссы.
Архитектура
MVC
Основной шаблон, который использовался в коде игры — MVC (описание в Википедии, документация Apple). Шаблон состоит из трёх частей: модели, представления и контроллера. Контроллер является посредником между моделью и представлением. При этом каждая из его частей имеет свои обязанности. Как пример можно привести реализацию игровой доски (игрового мира):
1) Модель. Были выделены следующие сущности главной игры: квадрат, деталь из квадратов. В игре есть также игровая доска (GameBoardViewDataSource), где каждой клеточке соответствуют координаты (например (1, 1), (32, 48)). Доска это коллекция квадратов. При этом она является моделью данных. Деталь по сути это маленькая доска.
2) Контроллер. Контроллер игровой доски (GameBoardController) решает когда надо рисовать модель. Он не знает внутреннюю структуру модели (массив, связанный список). Он знает только количество клеток и как получить информацию о каждой клетке. В контексте cocos2dx это наследник класса Node.
3) Представление (Sprite). Представление рисует квадраты (код рендеринга), при этом ничего не зная об их структуре данных. В контексте cocos2dx это класс Sprite. Контроллер содержит коллекцию спрайтов, которым даёт данные для отрисовки (текстуры, позиции).
При разработке игры в основном использовалась пассивная MVC — контроллер сам берёт данные из модели, когда надо.
В игре есть главный контроллер (GameWorldController), который даёт указания контроллеру игровой доски и контроллеру анимации обновить свои состояния. Кроме этого он ещё обновляет состояния игровых систем:
1) Система выигрыша-проигрыша (WinGameSystem) — решает когда игрок выиграл, а когда проиграл.
2) Система игровых событий — решает когда нужно запускать вредоносное событие (ускорить игру, скинуть пару ненужных деталей).
3) Система игровой логики (TetrisLogicSystem) — смотрит собрался ли ряд из квадратов, 4 одинаковых квадратика по вертикали или горизонтали.
4) Система заклинаний (SpellRechargeSystem) — следит за состояниями заклинаний.
5) Система контроля текущей детали игрока (CurrentDetailController) — управление деталью, опускание её вниз на один шаг и т.д.
6) Система слежения за состояниями боссов.
Для каждого элемента геймплея создана своя система. Контроллер анимации состоит из отдельных контроллеров для каждой системы. В их основе используется класс Action и его подклассы. Обычно после анимации должно произойти какое-то событие (увеличение очков, удаление нижнего ряда и т.д), поэтому системы отправляют в контроллеры функции обратного вызова, например лямбды в с++11. Это пример использования активной MVC — модель сама обращается к контроллеру когда надо.
Плюсом такой системы является гибкость, т.к. для добавления нового элемента геймплея достаточно только добавить новую систему и возможно контроллер анимации. Минус в её сложности и в количестве кода для её реализации. Иногда возникают неявные зависимости между системами из-за их порядка обновления.
Одиночка
В разработка игры также часто использовался шаблон Одиночка (ServiceLocator). В исходном коде это не чистый Одиночка, а просто глобальная переменная, скрытая методами доступа. По сути это был контейнер для моделей. К примеру, когда игрок выбирает уровень на карте, то загрузчик (GameLogicLoader) загружает необходимые модели (данные для текущего уровня, системы) в этот контейнер. А потом контроллеры получают из него необходимые модели. Можно было бы делать модели прямо в контроллерах, но это бы вызвало дополнительные зависимости.
Фабрика
Часто использовался шаблон Фабрика (Фабричный метод) В игре есть 7 сцен (Экранов) и для каждого экрана была сделана фабрика, которая загружает необходимые модели и собирает граф сцены из контроллеров. Например класс LoadingGameSceneFactory.
Наблюдатель
Также использовался шаблон Наблюдатель. В сцене, где есть скрытые элементы, которые показываются по определённому событию (всплывающие окна), создаётся объект, к нему на определённые сообщения подписываются наблюдатели (всплывающие окна). Затем, когда происходит событие, этому объекту направляется уведомление, затем объект передаёт уведомление нужным наблюдателям и показываются всплывающие окна. Например, класс RegulateSoundPopUp при инициализации подписывается на сообщения от GamePopUpsController.
Стратегия
Использовал также шаблон Стратегия. Например, у разных боссов разное поведение: одни ничего не делают, другие убегают от опасности. Для реализации их поведения был использован набор стратегий. Создаётся коллекция стратегий и заполняется нужными. Добавляя разные стратегии, мы добавляем разное поведение боссу. Например, класс AIMovementStrategy является одной из стратегии, которая добавляет боссу возможность двигаться.
Дополнительные библиотеки.
Для сбора статистики игроков было решено использовать google analytics. Есть google analytics sdk для ios и android, но не было реализации для windows. После чтения документации sdk было решено использовать Google Analytics Measurement Protocol. В интернете был найден пример (GATrackerpp) использования такого протокола, с помощью библиотеки curl. В итоге получилась кроссплатформенная реализация отправки аналитики. Тем не менее, для отправки информации необходим уникальный идентификатор игрока по стандарту GUID. Нашёл ещё и библиотеку для кроссплатформенной генерации GUID-идентификаторов (crossguid). Правда в итоге для android было решено использовать один GUID для всех, т.к. не разобрался с подключением с++ к java.
Для парсинга xml-файлов с настройками уровней было решено использовать кроссплатформенный парсер pugiXML. Это DOM-парсер, который был выбран так как не нужно было парсить большие файлы и его посоветовали на форуме cocos2dx.
Особенности портирования.
В начале разработка велась для windows, обычное оконное приложение. Потом был сделан порт для windows store — на windowsRT. Затем была сделана версия для android. Для организации кросплатформенного проекта была выбрана система веток в git. Для каждой платформы создаётся отдельная ветка. Основная ошибка была в том что кодовая база веток немного разная, т.е. нет единого ядра, как например в репозитории cocos2dx. Уже позже я узнал что можно было бы изначально использовать препроцессор с++, при этом команды препроцессора инкапсулировать в классы-фабрики (шаблон Фабрика). Но ветки всё равно оказались полезны, т.к. в разных платформах разная графика и позиции элементов интерфейса. При этом весь код для android изначально тестировался и отлаживался под windows, а потом собирался для android.
Ещё одним важным моментом был поиск пути для сохранения файлов в файловой системе разных платформ, а также архивирование apk-файла в android.
Форматы звуков не вызвали проблем — для всех платформ был использован mp3.
Странности во время разработки:
1) В одном классе не компилировался постинкремент (i++), пришлось делать преинкремент (++i) — в версии для android.
2) Движок не поддерживал пути с кириллицей. Если имя пользователя windows написано по русски, то игра вызовет ошибку и не запуститься.
3) Один раз оказалось, что звук не работает, потому что реализации метода нет в движке. Пришлось использовать другой класс.
Итоги:
В заключении хотелось бы отметить основные шаблоны, которые использовались: MVC, Фабрика, Одиночка, Наблюдатель, Стратегия. Основными дополнительными библиотеками были curl и pugiXML. На разработку игры ушло полгода. Из них:
- 2–3 недели продумывание механики и написание технического задания,
- месяц на продумывание архитектуры и написание классов-моделей,
- месяц на встройку движка и написание классов для отрисовки и анимации,
- месяц на тестирование и настройку баланса игры,
- 2–3 недели на портирование для WindowsRT (игра в полном экране)
- 2–3 недели на портирование на android (настройка среды для сборки проекта, тестирование на симуляторе).
Исходный код всего проекта можно посмотреть здесь, проект по windows с фильтрами (каталогами для исходных файлов) здесь.
В конечном итоге я выпустил свою игру и получил много опыта и удовольствия при работе над проектом.
Всем спасибо за внимание.