Сегодня хочу вам рассказать историю разработки мобильной (и не только) игры, а также интеграцию с популярным фреймворком cocos2d-x. Наверняка у многих из вас бывало желание написать свою, хоть и не большую, игру. Моя история начинается еще с 10-11 класса. Тогда небольшая демо-версия 2д проекта позволила мне выиграть конкурс Nokia N950 (помните такую?) и я оказался в числе 250 счастливчиков, которые получили девайс. С тех пор создание игр для меня является мечтой.Проект, о котором я хочу вам рассказать, изначально придуман и реализован совершенно другим человеком — Виталием. Чтобы была мотивация читать статью далее, показываю скрины:




ПК-версия до сих пор доступна для скачивания и вы можете его оценить.
Прошло немало времени, мы объединились с автором оригинальной игры и теперь трудимся над логическим продолжением проекта (включая версию для мобильных платформ). Как видите, проект требует достаточно большое количество эффектов и игровых компонент, поэтому первым и основным техническим требованием является использование C++ и минимального количества прослоек в выводе графики.
Я активно изучал cocos2d-x и пытался использовать его в небольших демо-проектах, на которых проверял актуальность геймплея моих идей.
Это довольно хорошая и богатая функционалом библиотека. Недавно разработчики выпустили 3ю версию, полностью переписав код отрисовки. Cocos2d-x берет на себя много рутинных дел: кроссплатформенные обертки над графическими объектами, сборка под разные платформы и т.д. Отказываться от всего этого было бы глупо, но использовать этот фреймворк по назначению тяжело. Вся эта система нодов и событий (actions) удобна только в теории и для небольших примеров. В действительно же это показало себя медленным и неудобным монстром.
В связи с этим, мы полностью оградили рендер-логику от кокоса и написали свою небольшую оболочку. Поэтому мы можем реализовывать алгоритмы и эффекты с минимальными затратами. Именно об этой оболочке я хочу рассказать вам далее. Код написан так, чтобы можно было расширять и дополнять список поддерживаемых платформ (сейчас все пишется параллельно под DirectX).
Ядром всего является интерфейс DeviceBase. Каждая платформа должна наследовать этот интерфейс и реализовывать некроссплатформенный функционал. Например — загрузка текстур.
namespace Graphics { class DeviceBase { public: // ... virtual void beginScene() = 0; virtual void endScene() = 0; virtual ShaderManager& shaders() const = 0; virtual TextureCache& textures() const = 0; virtual RenderTextureManager& renderTextures() const = 0; virtual TextureID loadTexture(const char* fileName, /* ... */) = 0; virtual float getDelta() = 0; virtual void renderBatch(Batch& batch) = 0; // ... }; }
Одна из самых важных функций, которую опишу позже, это renderBatch: непосредственный вывод треугольников на экран.
Следующим важным объектом системы является Batch. Это то, что может выводиться на экран: спрайт, текст (шрифт), графические примитивы.
У батча есть следующие характеристики:
- массив точек (из которых будут формироваться треугольники, например)
- прикрепленный шейдер
- рендер текстура (номер текстуры, в которую нужно рисовать батч)
- wrap/filter/blending — режимы текстуры
Этого уже достаточно для вывода примитивов на экран: создаем батч, который наполняет массив вершин нужными точками и передаем его в DeviceBase::renderBatch, который внутри использует прямые openGL вызовы:
void AndroidDevice::renderBatch(Batch& batch) { applyTarget(batch); applyTexture(batch); applyTextureWrapping(batch); applyTextureFilter(batch); applyShader(batch); applyBlending(batch); glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), &batch.data()[0].x); glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), &batch.data()[0].tx); glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), &batch.data()[0].col); if(batch.mode() == Mode::Strip) glDrawArrays(GL_TRIANGLE_STRIP, 0, batch.data().size()); else glDrawArrays(GL_TRIANGLES, 0, batch.data().size()); }
Как показывает практика, у новичков проблемы возникают именно здесь: в понятии того, как нужно связать opengl и cocos2d-x, как и где нужно использовать и применять шейдеры.
Рендер текстуры
Не буду описывать полный процесс создания обертки, но расскажу о проблеме, с которой мы столкнулись. Как видите выше, мы напрямую используем некоторые OpenGL-вызовы из кода. В связи с этим теряется местами связь между кокосом и OpenGL — кокос имеет обертки над некоторыми функциями, чтобы иметь возможность обрабатывать некоторые вызовы и действия. Пример тому — уход приложения в фон. После обратной активации, нужно пересоздать все текстуры и перезагрузить ресурсы (в том числе и рендер текстуры). Поэтому нам пришлось ловить сигнал ухода в фон. Для этих целей у класса AppDelegate есть два метода: applicationDidEnterBackground и applicationWillEnterForeground.
Рендер цикл
Следующий шаг — обойти систему нодов кокоса. Как вы знаете, у этого фреймворка нет update/render функций: все далается через ноды и события (actions). Как уже писал выше, этот подход совершенно не подходит нам. Выход такой: кокос требует создание минимум одного объекта типа cocos2d::CCScene, который есть точкой входа в логику игры. Этот объект уже имеет функцию draw, которую достаточно перегрузить. Осталось еще добавить функционал update:
this->schedule(schedule_selector(SCENE_CLASS::tick));
Внутри кокоса есть специальный schedule-класс, который позволяет вызывать функции с некоторыми интервалами времени (или на каждый тик игрового цикла).
Сигнатура tick метода имеет один аргумент: float delta time. В этой функции теперь можно просчитывать любую вашу игровую логику.
С рисованием сложнее: нам нужно подготовить кокос к тому, что в функции draw будет происходить рисование, причем рисование вручную через вызов OpenGL-функций.
void SCENE_CLASS::draw() { setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTextureColorAlphaTest)); CC_NODE_DRAW_SETUP(); // Рендер код }
Применяем на рендер стандартный кокосовый шейдер (в исходниках можете посмотреть его код).
CC_NODE_DRAW_SETUP — обычный макрос, внутри которого вызывается use шейдера и обновление состояния его юниформ-объектов.
Шейдеры
Особое внимание хочу обратить на шейдеры, это одна из самых сложных тем для начинающих, даже если вы не начинающих в геймдеве, вам будет сложно понять что и к чему в связке с кокосом. Создание шейдера:
const char* pixelFileName = "..."; CCGLProgram* program = new CCGLProgram(); program->initWithVertexShaderFilename("vert.h", pixelFileName); program->addAttribute(kCCAttributeNamePosition, kCCVertexAttrib_Position); program->addAttribute(kCCAttributeNameColor, kCCVertexAttrib_Color); program->addAttribute(kCCAttributeNameTexCoord, kCCVertexAttrib_TexCoords); program->link(); program->updateUniforms();
Как помните, выше я писал о том, что batch объект хранит id-шейдера. Достаточно связать в какой-то ассоциативном контейнере id -> CCGLProgram и передавать этот id в batch. Функция applyShader выглядит так:
void AndroidDevice::applyShader(const Batch& batch) { uint shaderTag = batch.getShader(); const auto floatUniforms = batch.floatUniforms(); const auto vec2Uniforms = batch.vec2Uniforms(); CCGLProgram* shaderHandle = ...; // Получаем из контейнера по id shaderHandle->use(); if(!vec2Uniforms.empty() || !floatUniforms.empty()) { for (auto it : vec2Uniforms) shaderHandle->setUniform2f(it.first, it.second); for (const auto it : floatUniforms) shaderHandle->setUniform1f(it.first, it.second); } }
Массивы юниформ это обычные std::map, которые хранят uniformName -> uniformValue.
Kosmos Arena
Достаточно тяжело в одной статье рассказать обо всем, поэтому на тему cocos2d-x писать далее не буду. В конце статьи архив на минимальный проект, который включает облегченную обертку которую мы используем. Если у кого-то будут вопросы — обращайтесь.
Kosmos Arena это Sci-Fi шутер в космосе с видом сверху. ПК-версия писалась для конкурса, поэтому особого геймплея или разнообразия миссий там нет. Сейчас мы разбили всю работу на этапы и собираемся разнообразить геймплей интересными компонентами. Например, на поверхности кристаллов будут передвигаться паукоподобные роботы, которыми можно будет управлять:

Интерьер игры выглядит в подобном стиле:

Физика
Как вы можете заметить, в игре много динамических объектов, которые одновременно находятся в кадре. Не смотря на это, даже android-версия стабильно держит 60 фпс. Чтобы добиться этого результата, в игре не исползьуется какой-то готовый 2d физический движок (box2d, например). Физический движок написан на основе интегрирования Верлета, что позволяет нам легко манипулировать физическими объектами: анимировать точки по времени и т.д. Виталий написал специальный редактор механизмов, где можно строить физические объекты и управлять их анимацией (автоматические переходы между разными стейтами, управление скоростью и т.д.). Выглядит это так:

Если будут желающие, в следующие статье Виталий может описать физический движок и проблемы, с которыми он столкнулся.
Процесс работы
Мы оба имеем постоянную работу, поэтому проектом занимаемся в свободное время почти каждый день. Для синхронизации используется git-репозиторий и trello доска.
Проект пишется с возможностью портирования на Win, MacOS, Android, iOS.
Да, нам жутко ! не хватает художника!, который сможет рисовать в «нашем» стиле и которому мы готовы отдавать процент от продаж.
Заключение
Если проект/статья заинтересует достаточное количество людей, мы продолжим писать. Возможные темы следующих статей: реализация конкретных эффектов из игры, физика, оптимизации в играх.
Windows prototype
Android APK (старое демо с частью возможностей Windows-версии)
Архив с минимальным проектом по ошибке пока недоступен, ближе к вечеру добавлю ссылку
Если вы обнаружите какие-то проблемы с Android-версией, пишите, пожалуйста, название вашего девайса.
ссылка на оригинал статьи http://habrahabr.ru/post/222349/
Добавить комментарий