В своей статье я бы хотел поделиться технической частью игры, которую сделали два человека. Будут рассмотрены основные архитектурные шаблоны (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 с фильтрами (каталогами для исходных файлов) здесь.
В конечном итоге я выпустил свою игру и получил много опыта и удовольствия при работе над проектом.

Всем спасибо за внимание.

Комментарии (37)


  1. stepanp
    31.08.2015 18:32
    -5

    Даже не знаю что хуже, то что вы пишете гейм-логику на плюсах или то что запихали в проект MVC и половину содержимого gang of four в придачу. И почему не внесли в стаью время затраченное на штудирование книжки по паттернам?


    1. Mixim333
      31.08.2015 18:56
      +1

      Как я понял, автор — студент, поэтому неудивительно, что в проекте использованы всевозможные паттерны.

      DarkWinDev, хочу поинтересоваться: откуда привычка давать имена аргументам методов с буквы 'a'? У самого на 2-3 курсе такой же бзик был, мне один из преподавателей сказал, что «это хороший тон», но в итоге я на практике еще ни разу не видел, чтобы в production'е кто-то так именовал параметры


      1. darkwinddev
        31.08.2015 19:08
        -2

        Уже месяц как не студент. Про 'a' перед именами аргументов сам не помню где прочитал, просто думаю что 'a' обозначает argument(аргумент функции).


    1. darkwinddev
      31.08.2015 18:57
      -1

      Выбрал с++ для гейм-логики т.к. его лучше знаю чем javascript и lua(эти языки тоже используются при работе с cocos2dx). Использовал MVC потому что мне показалось естественно отделить логику от её представления на экране. Первый класс отвечает за внешний вид, второй за предметную область(игровая логика), и третий за связь между первыми двумя. Понимал ооп года два от первого знакомства с книжкой банды четырёх, и до сих пор не понял до конца.


      1. stepanp
        31.08.2015 19:07
        +1

        Вредную литературу читаете потому что :)

        Из-за использования MVC, у вас получается 50 файлов там где могло быть несколько строчек(условно). Лучше подумайте действительно ли вам в этом случае так необходимо отделять логику от представления?

        Какой-нить lua осваивается в худшем случае за неделю(про js такое не могу сказать), зато потом экономит месяцы.


        1. Suvitruf
          31.08.2015 19:24
          +3

          Я бы ваши советы назвал вредными.


          1. stepanp
            31.08.2015 19:25
            +3

            Аргументируйте


            1. Suvitruf
              31.08.2015 19:29

              Вот это, например:

              действительно ли вам так необходимо отделять логику от представления?

              Плюс, если честно, не понимаю, в чём проблема с MVC? Количество файлов — не аргумент.


              1. stepanp
                31.08.2015 19:38
                +6

                А вы считаете, что разделять данные, логику, и представление нужно обязательно всегда и везде?

                C MVC проблема в том что в данном случае он скорее всего просто не нужен. Аргументы — разбухаение кода, усложнение логики, уменьшение понятности и читаемости на пустом месте.

                Лучше скажите какие плюсы даст MVC в данном случае


        1. darkwinddev
          31.08.2015 19:45

          Изначально я не задавался целью использовать какие-то шаблоны. Просто разделял программу на классы, каждый класс отвечает за что-то одно. Возможно было бы быстрее всё вместить в пару классов, но тогда бы мне было тяжелее изменять программу. Вот пример кода(Objective c), который я писал для самой первой моей игры, там смешано всё в одно, и мне сегодня сложно разобраться в этом классе.


          1. stepanp
            31.08.2015 19:56
            +1

            Я и не говорю что нужно запихивать весь код в один класс. Чрезмерное разделение на классы не на много лучше чем GodObject. А проблема приведенного примера больше в недостаточности разбиения на методы и плохом стиле кода.


    1. domix32
      31.08.2015 23:08
      +1

      А в чем проблема с гейм-логикой на плюсах?


      1. stepanp
        31.08.2015 23:31
        -1

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


        1. stepanp
          31.08.2015 23:40
          -1

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


        1. AxisPod
          01.09.2015 07:08
          +1

          Где гемор? Давайте пример, мой опыт показывает противоположную ситуацию. Память в игровом цикле уже обычно вся выделена, с памятью работать не надо, на худой конец временное на стеке выделяется без напряга, думать не надо вообще. Дергай себе методы в машине состояний и не задумывайся не о чём. А вот во всяких скриптовых языках вы даже и не знаете когда будет выделяться память, вы не сможете в принципе создать пул объектов, чтобы память более не выделялась.


          1. stepanp
            01.09.2015 15:09
            -2

            Да на каждом шагу. Веселье с инклудами, гардами и forward declarations, там где в скриптах есть import\require, разбиение кода на h\cpp с иногда неочевидными синтаксическими приколами, монструозные конструкции из шаблонов, std::function, auto и лямбд, там где в сприптах есть first class функции, бесконечные касты и шаблоны там где в скриптах есть динамическая типизация, огромный ворох указателей, ссылок, constссылок\указателей, shared\weak\unique\raw указателей там где в скриптах только ссылочные типы(не считая примитивных) и т.п. Это не проблема когда нужно написать например движок, но при написании гейм-логики я тратить на это время не хочу.

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


            1. AxisPod
              01.09.2015 16:52

              Ну тут вы уехали не туда, разговор про игровую логику.


              1. stepanp
                01.09.2015 19:19

                Для игровой логики у плюсов существует какой-то специальный диалект без вышеописанных вещей? Или игровая логика по-вашему это только if/else?


        1. domix32
          01.09.2015 12:46

          Хм… а не гемор ли в таком случае пробрасывать игровые функции из нативного кода в скрипты и обратно? И задумываться над деталями реализации это же ведь не плохо, если конечно целью не стоит собрать что-то разовое из «говна и палок». Плюс скриптовую часть игры распотрошить на порядок проще, что не совсем хорошо. А может даже совсем не хорошо.


          1. stepanp
            01.09.2015 14:38
            -1

            Ради того чтобы экономить месяцы разработки, можно один раз забиндить скрипты. Да и всегда можно взять готовое решение(тот же cocos2d-x имеет готовые биндинги к lua и js). Нет, конечно, не плохо, вопрос в том сколько таких деталей реализации требуют внимния в плюсах и в сколько сприптах. Для скриптовых языков существуют компиляторы и обфускаторы.


            1. bay73
              01.09.2015 17:34

              Есть универсальное правило — писать быстрее всего на том языке, который знаешь.

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


  1. AtomKrieg
    31.08.2015 19:26
    +9

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


  1. piromanlynx
    31.08.2015 20:40
    +10

    Как сделать что то странное и долго


  1. Voley
    01.09.2015 08:48
    -2

    >месяц на продумывание архитектуры и написание классов-моделей,

    wat? Это можно ну за неделю сделать с головой


    1. darkwinddev
      01.09.2015 09:35
      -1

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


      1. Voley
        01.09.2015 09:46
        -3

        Значит вы медленно работаете. Я за 2 месяца сделал игру с нехилой бизнес логикой, разными механиками, под 3 платформы, работая по 2-3 часа в день. Включая рисование всего.


        1. bay73
          01.09.2015 12:00
          +1

          Полгода на тетрис — реально многовато. Помнится был тут цикл статей «по игре в неделю» — это была нормальная производительность. Но если считать, что полгода потрачено на изучение языка и шаблонов проектирования, то нормально.


  1. skor
    01.09.2015 20:00

    Почти образцовый, очень аккуратный проект.
    Удивляюсь, конечно, как он не падает при таком довольно старомодном подходе к программированию на C++.


    1. darkwinddev
      01.09.2015 20:52

      Спасибо. А что значит старомодный подход к программированию на C++.? В моём коде есть несколько проблем:
      1) Не использовал умные указатели, т.к. поздно про них прочитал(когда уже половина кода была написана). Я попытался их внедрить но почему-то получал утечки памяти, возможно я плохо следил за слабыми ссылками.
      2) Почти не ставил идентификатор const перед переменными.
      3) Иногда передавал по значению, где можно передать по ссылке или указателю.
      При этом я активно использовал лямбда-выражения, и один раз потоки из с++11.


      1. skor
        01.09.2015 21:23

        Проблем много, лень всё описывать тут, но главное, что они все решаемы. Перебор контейнеров, например.


        1. darkwinddev
          01.09.2015 21:48

          вы правы). Было бы неплохо если вы предложите ресурсы(книги, ссылки) по их улучшению.


          1. 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


  1. sova
    02.09.2015 02:57

    Спасибо за статью. Приятно знать, что кто-то еще использует кокос))

    Было бы интересно прочитать (хотя бы вкратце) про следующие аспекты и то как автор с ними борется:
    -поддержка разных размеров экранов на разных платформах
    -поддержка нативных штук (как вы обходитесь без JNI обвязок?) )
    -были ли автотесты?
    -был ли какой-то нестандартный ui, которого нет в кокосе?
    -были ли какие-то нестандартные тразишины, которые не сделать (сложно сделать) штатными методами кокоса?


    1. 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)


      1. darkwinddev
        02.09.2015 14:55

        А можете пожалуйста рассказать подробнее как вы решили проблему с кириллицей?


        1. 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


    1. darkwinddev
      02.09.2015 14:52

      Спасибо.
      -поддержка разных размеров экранов на разных платформах. Использовал разные ветки в гит для разных платформ. Для андроид использовал политики и дизаин-разрешение экрана ( setDesignResolutionSize(320, 480, kResolutionShowAll) ), а также для разных разрешений экрана разный арт вот Для winRT тоже политики, а для обычной win32 сделал один и тот же размер окна для всех экранов.
      -поддержка нативных штук (как вы обходитесь без JNI обвязок?) ) Не использовал нативных штук, за исключением генерирования guid, и то взял готовую библиотеку, для андроид не понял как связываться с JNI, поэтому сделал один guid для всех. В итоге у меня в гугл аналитике по андроид новых пользователей 1.
      -были ли автотесты? Нет.
      -был ли какой-то нестандартный ui, которого нет в кокосе? В данном проекте нет. На одном проекте(cocos2d-iphone) понадобился контроллер магазина с табами, пришлось написать его самому с помощью CCScrollLayer.
      -были ли какие-то нестандартные тразишины, которые не сделать (сложно сделать) штатными методами кокоса? Нет