...

понедельник, 12 мая 2014 г.

Kosmos Arena — разработка игры

Привет, Хабр!

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


Проект, о котором я хочу вам рассказать, изначально придуман и реализован совершенно другим человеком — Виталием. Чтобы была мотивация читать статью далее, показываю скрины:


image



image



image



image

ПК-версия до сих пор доступна для скачивания и вы можете его оценить.


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий