Игра называется 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 с фильтрами (каталогами для исходных файлов) здесь.
В конечном итоге я выпустил свою игру и получил много опыта и удовольствия при работе над проектом.
Всем спасибо за внимание.
Комментарии (37)
Voley
01.09.2015 08:48-2>месяц на продумывание архитектуры и написание классов-моделей,
wat? Это можно ну за неделю сделать с головойdarkwinddev
01.09.2015 09:35-1За неделю я только смог придумать основные классы моделей(интерфейсы классов), а уже потом писал их реализации. В ходе разработки придерживался следующей схемы: сделал класс модели, затем класс контроллера и потом, если необходимо, класс представления. Месяц был потрачен на написание всех реализаций классов моделей и их рефакторинг при необходимости.
Voley
01.09.2015 09:46-3Значит вы медленно работаете. Я за 2 месяца сделал игру с нехилой бизнес логикой, разными механиками, под 3 платформы, работая по 2-3 часа в день. Включая рисование всего.
bay73
01.09.2015 12:00+1Полгода на тетрис — реально многовато. Помнится был тут цикл статей «по игре в неделю» — это была нормальная производительность. Но если считать, что полгода потрачено на изучение языка и шаблонов проектирования, то нормально.
skor
01.09.2015 20:00Почти образцовый, очень аккуратный проект.
Удивляюсь, конечно, как он не падает при таком довольно старомодном подходе к программированию на C++.darkwinddev
01.09.2015 20:52Спасибо. А что значит старомодный подход к программированию на C++.? В моём коде есть несколько проблем:
1) Не использовал умные указатели, т.к. поздно про них прочитал(когда уже половина кода была написана). Я попытался их внедрить но почему-то получал утечки памяти, возможно я плохо следил за слабыми ссылками.
2) Почти не ставил идентификатор const перед переменными.
3) Иногда передавал по значению, где можно передать по ссылке или указателю.
При этом я активно использовал лямбда-выражения, и один раз потоки из с++11.skor
01.09.2015 21:23Проблем много, лень всё описывать тут, но главное, что они все решаемы. Перебор контейнеров, например.
darkwinddev
01.09.2015 21:48вы правы). Было бы неплохо если вы предложите ресурсы(книги, ссылки) по их улучшению.
skor
02.09.2015 14:53+1По смарт-поинтерам основы: www.boost.org/doc/libs/1_58_0/libs/smart_ptr/sp_techniques.html
по шаблонам основы: Александреску «Современное проектирование на с++»
а дальше по мере нужды: из списка: www.boost.org/doc/libs/1_59_0
sova
02.09.2015 02:57Спасибо за статью. Приятно знать, что кто-то еще использует кокос))
Было бы интересно прочитать (хотя бы вкратце) про следующие аспекты и то как автор с ними борется:
-поддержка разных размеров экранов на разных платформах
-поддержка нативных штук (как вы обходитесь без JNI обвязок?) )
-были ли автотесты?
-был ли какой-то нестандартный ui, которого нет в кокосе?
-были ли какие-то нестандартные тразишины, которые не сделать (сложно сделать) штатными методами кокоса?Rivers
02.09.2015 09:34+1Спасибо автору! Мы тоже писали игру на cocos2d-x (кратко о ней megamozg.ru/company/whisperarts/blog/18730)
Из трудностей/особенностей могу поделиться:
— GoogleAnalytics, AdMob и Facebook пришлось пробрасывать мостами и использовать нативные sdk под каждую платформу
— Для внутренних платежей использовали soomla (https://github.com/soomla/cocos2dx-store). Было много проблем с интеграцией, но в итоге всё завелось. Его нет под Win8, поэтому там в итоге запустили платную версию с демо-режимом
— WindowsPhone не поддерживал проигрывание mp3, поэтому специально для этой платформы для сборки мы использовали wav
— cocos2d-x неплохо поддерживает работу с сетью — в том числе веб-сокеты, которые мы использовали для создания сетевого режима игры
—Движок не поддерживал пути с кириллицей. Если имя пользователя windows написано по русски, то игра вызовет ошибку и не запуститься.
это оказалось серьезной проблемой и для нас при использовании внутреннего конфига. Поэтому конкретно для этой платформы мы сделали сохранение в файл
— для некоторых элементов меню, анимаций и мультиков мы использовали CocosStudio (http://www.cocos2d-x.org/wiki/Cocos_Studio)darkwinddev
02.09.2015 14:55А можете пожалуйста рассказать подробнее как вы решили проблему с кириллицей?
Rivers
02.09.2015 16:44+1Я чуть ошибся, не в файл — а в стандартные winrt-настройки. У нас был свой класс, упрощающий доступ к настройкам. И в нем был такой макрос:
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
#include «SettingsWinRT.h»
#define USER_DEFAULTS SettingsWinRT::getInstance()
#else
#define USER_DEFAULTS UserDefault::getInstance()
#endif
Вот наши сеттинги для WINRT:
dl.dropboxusercontent.com/u/8086143/SettingsWinRT.h
darkwinddev
02.09.2015 14:52Спасибо.
-поддержка разных размеров экранов на разных платформах. Использовал разные ветки в гит для разных платформ. Для андроид использовал политики и дизаин-разрешение экрана ( setDesignResolutionSize(320, 480, kResolutionShowAll) ), а также для разных разрешений экрана разный арт вот Для winRT тоже политики, а для обычной win32 сделал один и тот же размер окна для всех экранов.
-поддержка нативных штук (как вы обходитесь без JNI обвязок?) ) Не использовал нативных штук, за исключением генерирования guid, и то взял готовую библиотеку, для андроид не понял как связываться с JNI, поэтому сделал один guid для всех. В итоге у меня в гугл аналитике по андроид новых пользователей 1.
-были ли автотесты? Нет.
-был ли какой-то нестандартный ui, которого нет в кокосе? В данном проекте нет. На одном проекте(cocos2d-iphone) понадобился контроллер магазина с табами, пришлось написать его самому с помощью CCScrollLayer.
-были ли какие-то нестандартные тразишины, которые не сделать (сложно сделать) штатными методами кокоса? Нет
stepanp
Даже не знаю что хуже, то что вы пишете гейм-логику на плюсах или то что запихали в проект MVC и половину содержимого gang of four в придачу. И почему не внесли в стаью время затраченное на штудирование книжки по паттернам?
Mixim333
Как я понял, автор — студент, поэтому неудивительно, что в проекте использованы всевозможные паттерны.
DarkWinDev, хочу поинтересоваться: откуда привычка давать имена аргументам методов с буквы 'a'? У самого на 2-3 курсе такой же бзик был, мне один из преподавателей сказал, что «это хороший тон», но в итоге я на практике еще ни разу не видел, чтобы в production'е кто-то так именовал параметры
darkwinddev
Уже месяц как не студент. Про 'a' перед именами аргументов сам не помню где прочитал, просто думаю что 'a' обозначает argument(аргумент функции).
darkwinddev
Выбрал с++ для гейм-логики т.к. его лучше знаю чем javascript и lua(эти языки тоже используются при работе с cocos2dx). Использовал MVC потому что мне показалось естественно отделить логику от её представления на экране. Первый класс отвечает за внешний вид, второй за предметную область(игровая логика), и третий за связь между первыми двумя. Понимал ооп года два от первого знакомства с книжкой банды четырёх, и до сих пор не понял до конца.
stepanp
Вредную литературу читаете потому что :)
Из-за использования MVC, у вас получается 50 файлов там где могло быть несколько строчек(условно). Лучше подумайте действительно ли вам в этом случае так необходимо отделять логику от представления?
Какой-нить lua осваивается в худшем случае за неделю(про js такое не могу сказать), зато потом экономит месяцы.
Suvitruf
Я бы ваши советы назвал вредными.
stepanp
Аргументируйте
Suvitruf
Вот это, например:
Плюс, если честно, не понимаю, в чём проблема с MVC? Количество файлов — не аргумент.
stepanp
А вы считаете, что разделять данные, логику, и представление нужно обязательно всегда и везде?
C MVC проблема в том что в данном случае он скорее всего просто не нужен. Аргументы — разбухаение кода, усложнение логики, уменьшение понятности и читаемости на пустом месте.
Лучше скажите какие плюсы даст MVC в данном случае
darkwinddev
Изначально я не задавался целью использовать какие-то шаблоны. Просто разделял программу на классы, каждый класс отвечает за что-то одно. Возможно было бы быстрее всё вместить в пару классов, но тогда бы мне было тяжелее изменять программу. Вот пример кода(Objective c), который я писал для самой первой моей игры, там смешано всё в одно, и мне сегодня сложно разобраться в этом классе.
stepanp
Я и не говорю что нужно запихивать весь код в один класс. Чрезмерное разделение на классы не на много лучше чем GodObject. А проблема приведенного примера больше в недостаточности разбиения на методы и плохом стиле кода.
domix32
А в чем проблема с гейм-логикой на плюсах?
stepanp
Ну это не проблема, а скорее лишний гемор. Вместо реализации собственно гейм-логики, придется очень много задумываться над деталями реализации там, где в скриптовых языках думать не надо вовсе.
stepanp
Вообщем плюсы банально сложнее. А зачем усложнять себе жизнь, если плюсы дают только выигрыш в производительности, который а гейм-логике не существенен, непонятно.
AxisPod
Где гемор? Давайте пример, мой опыт показывает противоположную ситуацию. Память в игровом цикле уже обычно вся выделена, с памятью работать не надо, на худой конец временное на стеке выделяется без напряга, думать не надо вообще. Дергай себе методы в машине состояний и не задумывайся не о чём. А вот во всяких скриптовых языках вы даже и не знаете когда будет выделяться память, вы не сможете в принципе создать пул объектов, чтобы память более не выделялась.
stepanp
Да на каждом шагу. Веселье с инклудами, гардами и forward declarations, там где в скриптах есть import\require, разбиение кода на h\cpp с иногда неочевидными синтаксическими приколами, монструозные конструкции из шаблонов, std::function, auto и лямбд, там где в сприптах есть first class функции, бесконечные касты и шаблоны там где в скриптах есть динамическая типизация, огромный ворох указателей, ссылок, constссылок\указателей, shared\weak\unique\raw указателей там где в скриптах только ссылочные типы(не считая примитивных) и т.п. Это не проблема когда нужно написать например движок, но при написании гейм-логики я тратить на это время не хочу.
Ну так скриптовые языки и придуманы за тем чтобы ускорить процесс разработки, путем скрытия таких деталей реализации от программиста, а не для того чтобы думать сколько раз объект скопируется при возвращении его из метода. К тому же никто не запрещает использовать пул для плюсовых объектов в скриптах.
AxisPod
Ну тут вы уехали не туда, разговор про игровую логику.
stepanp
Для игровой логики у плюсов существует какой-то специальный диалект без вышеописанных вещей? Или игровая логика по-вашему это только if/else?
domix32
Хм… а не гемор ли в таком случае пробрасывать игровые функции из нативного кода в скрипты и обратно? И задумываться над деталями реализации это же ведь не плохо, если конечно целью не стоит собрать что-то разовое из «говна и палок». Плюс скриптовую часть игры распотрошить на порядок проще, что не совсем хорошо. А может даже совсем не хорошо.
stepanp
Ради того чтобы экономить месяцы разработки, можно один раз забиндить скрипты. Да и всегда можно взять готовое решение(тот же cocos2d-x имеет готовые биндинги к lua и js). Нет, конечно, не плохо, вопрос в том сколько таких деталей реализации требуют внимния в плюсах и в сколько сприптах. Для скриптовых языков существуют компиляторы и обфускаторы.
bay73
Есть универсальное правило — писать быстрее всего на том языке, который знаешь.
Да, если стартует большой проект и заранее известно, что его трудоемкость будет в десятки человеко-лет, то надо сильно задуматься о выборе средств разработки и получении необходимой квалификации с целью минимизировать трудозатраты. Но для домашней поделки — выбор языка имеет совсем другие приоритеты.