GamePlay 3D Framework — лёгкий старт в кроссплатформенную разработку 3D игр

в 19:10, , рубрики: c++, game development, Gamedev, open source, метки: , ,

Доброе время суток, уважаемые читатели!

Прослушав курс по компьютерной графике в университете и вдоволь наигравшись с OpenGL, я решил, что пора бы уже двинуться дальше и попробовать себя в разработке игр. Писать с нуля свой движок, прямо скажем, не очень-то хотелось. Главной целью было скорее посмотреть как это делается, вынести уроки и может быть создать что-то на базе выбранного движка. Беглый поиск показал, что с открытыми кроссплатформенными движками немного туго. У одного проблемы с Linux, Windows или Mac OS, у другого со свежими версиями мобильных ОС, третий почти заброшен… Но я-таки наткнулся на один очень привлекательный экземпляр, о котором и хочу поведать в этой статье.

gameplay

Имя этому фреймворку — GamePlay 3D. Информации о нём на просторах интернета не очень много, чего уж говорить про рунет. Это open source фреймворк написанный на C++ для программирования игр на C++ со всеми вытекающими из этого достоинствами и недостатками. Авторы проекта позиционируют его как универсальный инструмент, эдакий аналог cocos2d для 3D игр. Чтобы начать писать на GamePlay 3D не нужно обладать глубокими знаниями OpenGL, GLSL или математики 3D графики, однако все мы понимаем, что для достижения хорошего результата от этого никуда не деться. Подробности и небольшой пример для старта под катом.

Начну, пожалуй, с краткого списка основных возможностей движка:

  • поддержка Linux, Windows, Mac OS X, Android, iOS и Blackberry OS
  • C++ API, игровые скрипты на Lua
  • OpenGL 3.2+ и OpenGL ES 2.0
  • сборка проекта под разные ОС в один клик*
  • простая и удобная конфигурация сцен (объекты, материалы, физика, анимация)
  • физический движок bullet (ну это вроде стандарт)
  • готовый инструмент для конвертирования 3D сцен и шрифтов в собственный бинарный формат
  • поддержка ввода с клавиатуры, мыши, джойстика, экранного джойстика и сенсоров
  • удобная работа со звуком (wav, ogg) в 3D
  • свой GUI с кнопками, чекбоксами, слайдерами и т.д.
  • и ещё много всего.

*ну почти в один...

Более полный список возможностей с сайта проекта

  • Full-featured OpenGL 3.2+ (desktop) and OpenGL ES 2.0 (mobile) based rendering system
  • Shader-based material system with built-in common shader library
  • Node-based scene graph system with support for lights, cameras, models, particle emitters, and physics collision objects
  • Heightmap based terrains with multiple surface layers and LOD
  • Declarative scene bindings (materials) and node attachments (particle emitters, physics collision objects, 3D audio sources)
  • Declarative particle system
  • Easy-to-use text and sprite rendering
  • Physics system (using Bullet physics)
    • vehicle physics
    • character physics
    • rigid body dynamics
    • constraints
    • ghost objects
  • Declarative UI system with support for themeable 2D and 3D forms. Includes the following built-in core controls and layouts:
    • button
    • label
    • text box
    • slider
    • check box
    • radio button
    • absolute layout
    • vertical layout
    • flow layout
  • Fully extensible animation system with skeletal character animation support
  • Complete 3D audio system with WAV and OGG support
  • Full vector math library with 2D/3D math and 3D object culling support
  • Mouse, keyboard, touch, gestures and gamepad support
  • Lua script bindings and binding generator tool
  • AI state machine

Мы же сейчас разберёмся как быстро и просто создать небольшое демо с крохотной сценой, несколькими объектами, физикой и базовым освещением, используя исключительно open source инструменты. Ссылки на использованные материалы и архив с готовым проектом разбросаны по тексту и собраны вместе в конце статьи.

Создавать наше первое демо мы будем в Linux. Нам понадобятся:

  • любимая IDE или текстовый редактор
  • cmake, make, gcc
  • Blender с поддержкой экспорта в COLLADA

Сборка движка заключается в выкачивании исходников с Github, установке зависимостей и выполнении cmake… && make в директории build (пользователи Windows и Mac OS X открывают имеющийся проект Visual Studio или XCode соответственно и компилируют одним кликом). После того как компиляция успешно завершена, мы приступем к созданию нашего проекта посредством исполнения скрипта gameplay-newproject.(sh|bat) в корневой директории фреймворка. Скрипт задаст несколько вопросов, ответив на которые мы получим готовую рыбу проекта в указанной нами директории. Назовем прокет Demo и в качестве пути укажем текущую директорию (корневую директорию движка).

Проект

Созданный проект уже содержит все необходимые файлы для компиляции и запуска игры, а также файлы проектов для Visual Studio и XCode. Пользователи Linux вынуждены сами заботиться о настройке проекта для своих IDE. Вся настройка заключается в указании директории с исходниками игры, добавлении библиотек движка в проект и т.п. Сейчас мы не будем на этом останавливаться, а просто воспользуемся обычным vim'ом, утилитами cmake и make. Скомпилируем проект, запустим и посмотрим, что мы имеем, а затем разберемся, из чего же он состоит и добавим немного драмы.

cd Demo/build
cmake ..
make

Запускаем получившийся бинарник:

cd ../bin/linux
./Demo

Если всё ок, то вы должны увидеть что-то вроде этого:
image

Итак, что же мы видим в папке с игрой за исключением файлов проектов VS, XCode и cmake?
game.config — базавоя конфигурация вроде разрешения, полный экран или окно и т.п.
res/ — директория с игровыми ресурсами
res/box.gpb — игровая сцена (просто кубик с камерой и источником света), закодированная в нативный формат
box.dae — игровая сцена в сыром виде, в данном случае в формате COLLADA
colored.(frag|vert) — шейдеры, использованные в материале куба
res/box.material — описание материала куба
src/ — исходники
src/Demo.(cpp|h) — базовый класс
android/ — проект для сборки игры под Android
bin/ — скомпилированные бинарники

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

Вот, что мы получим в итоге:

Для начала создадим сцену в Blender: кубики висят над некоей поверхностью, чуть выше пара-тройка шаров для полного счастья. Важно обратить внимание на то, что в игре высота определяется осью y, а не z, в отличие от Blender (на самом деле без разницы, но вроде как имеются какие-то проблемы, если высота определена не по оси y в GamePlay 3D, так что для чистоты эксперимента будем использовать y). Нам необходимо как-то назвать все объекты, чтобы мы могли обращаться к ним из кода игры и отличать их друг от друга. Поступим просто — все кубы будут зваться boxX, шары ballX, где X — любое число. Поверхность, над которой они парят, назовём floor. Камеру назовём camera и повернём её так, чтобы в объектив попало то, что надо. Сцена готова и её можно экспортировать в понимаемый утилитой gameplay-encoder формат. На выбор два варианта: FBX и COLLADA. Мы выбираем COLLADA, ибо на данном этапе так проще (подробности в конце статьи).

Результат выглядит примерно следующим образом:
Blender

Для ленивых и для тех, у кого нет под рукой блендера

Сцена для Blender: dl.dropbox.com/u/64710641/boxes.blend
Сцена в COLLADA: dl.dropbox.com/u/64710641/boxes.dae
Сцена конвертированная в GamePlayBundle: dl.dropbox.com/u/64710641/boxes.gpb

Тут в дело вступает gameplay-encoder. Эта утилита конвертирует различные неудобоваримые форматы файлов в родной бинарный формат фреймворка. Мы ведь не хотим тратить лишнее время и память на парсер сцены из XML или рендерить векторный шрифт в 3D? Лучше заранее распарсить такие объекты и сохранить их в оптимизированном бинарном виде, чтобы впоследствии просто загрузить в память. Итак, лёгким движением руки конвертируем экспортированный из Blender файл scene.dae в scene.gpb и кладём этот файл в поддиректорию res/ нашего проекта:

path/to/GamePlay/bin/linux/gameplay-encoder scene.dae

В отличие от стартового примера с вращающимся кубом, мы не будем вручную искать объекты в сцене и определять их свойства. Мы просто создадим файл, описывающий сцену, а дальше фремворк сделает всё сам. Итак, файл описания сцены game.scene:

scene boxes
{
	// Путь к файлу сцены
    path = res/boxes.gpb

	// Указываем активную камеру
    activeCamera = camera 
    
    // Свойства объекта с именем floor
    node floor 
    {
    	// Описание материала, посмотрим на него позже
    	material = res/game.material#floor 
    	
    	// Описание физических свойств, рассмотрим позже
    	collisionObject res/game.physics#floor 
    }

	// Свойства кубов
    node boxes 
    {
    	// Используем url, чтобы применить эти свойства ко всем объектам с именем boxX
    	url = box*
    	
    	// Материал
        material = res/game.material#box
        
        // Физика
        collisionObject = res/game.physics#box 
    }
    
    // Аналогично для шаров
    node ball 
    {
    	url = ball*
    	material = res/game.material#ball
        collisionObject = res/game.physics#ball
    }

	// Общие настройки физики в мире
    physics  
    {
    	// Устанавливаем гравитацию по оси y
        gravity = 0.0, -9.8, 0.0 
    }
}

Указанные материалы и физические свойства берутся не с потолка, а из соответствующих файлов game.material и game.physics.

game.material:

// Базовый материал для простых цветных объектов
material colored
{
    technique
    {
    	// Первый и единственный проход рендеринга
        pass 0
        {
		   // Присваеваем переменным в шейдерах значения, определенные
		   // фреймворком в процессе построения сцены
		    u_worldViewProjectionMatrix = WORLD_VIEW_PROJECTION_MATRIX
		    u_inverseTransposeWorldViewMatrix = INVERSE_TRANSPOSE_WORLD_VIEW_MATRIX
		
		    // В нашем случае можно было обойтись и без оптимизаций...
		    // но оставим для полноты картины
		    renderState
		    {
		        cullFace = true
		        depthTest = true
		    }
		    
            // И наконец указываем какие шейдеры будут использоваться
            vertexShader = res/shaders/colored.vert
            fragmentShader = res/shaders/colored.frag
        }
    }
}

// Материал для кубов. Наследует все свойства материала colored
material box : colored
{
	// Определяем ещё одну переменную для шейдера
    u_diffuseColor = 0.8, 0.2, 0.2, 1.0
}

// Аналогично для шаров
material ball : colored
{
    u_diffuseColor = 0.2, 0.2, 0.8, 1.0
}

// И поверхности
material floor : colored
{
	u_diffuseColor = 0.2, 0.8, 0.2, 1.0
}

Стоит отметить, что переменные в шейдерах не появляются сами собой из ниоткуда. Необходимо обращать внимание на то, что написано в коде шейдеров и определять соответствующие переменные в материалах, если необходимо. С фреймворком поставляется небольшая библиотека готовых шейдеров, реализующих освещение разных типов, текстурирование и прочее. Мы использовали простой вариант раскрашивания объектов в один цвет. Чтобы это дело работало, просто скопируем всю библиотеку шейдеров из path/to/GamePlay/gameplay/res/shaders к нам в проект в res/shaders.

game.physics:

// Физические свойства для кубов
collisionObject box
{
	// Тип объекта - твердое тело
    type = RIGID_BODY
    
    // Форма bounding box - параллелепипед
    shape = BOX
    
    // Масса объекта
    mass = 2.0
}

// Физические свойства для шаров
collisionObject ball
{
    type = RIGID_BODY
    // Форма bounding box - сфера
    shape = SPHERE
    mass = 1.0
}


// Физические свойства для поверхности, на которую они упадут
collisionObject floor
{
    type = RIGID_BODY
    shape = BOX
    mass = 0
}

Значения возможных физических параметров подробно описаны в документации.

Дело за мылым — загрузить scene.gpb в коде игры.
Мы имеем базовый класс Demo, наследующий класс Game. Жизненный цикл игры выглядит следующим образом:

  1. инициализация игры в методе initialize();
  2. обновление состояния мира перед каждым кадром в методе update(float elapsedTime);
  3. рендеринг сцены в методе render(float elapsedTime);
  4. завершение приложения в методе finalize();

Мы лишь изменим метод initialize() и опустошим метод update(float elapsedTime).

Обычно в методе initialize() мы загружаем файлы сцены, скрипты, и устанавливаем начальное состояние игры. Так и поступим:

void Demo::initialize()
{
	// Для загрузки сцены воспользуемся написанным ранее файлом game.scene,
	// так что всю остальную загрузку фреймворк произведет самостоятельно
    _scene = Scene::load("res/game.scene");

	// Создаем объект-источник света. Можно было сделать это прямо на сцене в Blender,
	// но на данный момент нам будет проще управиться с ним вручную
    _lightNode = Node::create("directionalLight1");
    
    // Направляем свет в ту же сторону, что и камера
	Light* light = Light::createDirectional(_scene->getActiveCamera()->getNode()->getForwardVector());
	
	// Устанавливаем цвет освещения
	light->setColor(1.0, 1.0, 1.0);
	// Сохраняем указатель на источник света в объекте
	_lightNode->setLight(light);

    // Устанавливаем параметры камеры исходя из текущего размера окна
    _scene->getActiveCamera()->setAspectRatio((float)getWidth() / (float)getHeight());

	// Обходим все объекты на сцене, чтобы задать параметры освещения
    _scene->visit(this, &Demo::initializeScene);
}

Добавим метод initializeScene(), который будет назначать параметры освещения для каждого объекта сцены используя созданный нами источник света. Думаю, комментарии тут излишни:

bool Demo::initializeScene(Node* node)
{
    Model* model = node->getModel();
    if (model)
    {
        Material* material = model->getMaterial();
        if (material && material->getTechnique()->getPassByIndex(0)->getEffect()->getUniform("u_lightDirection"))
        {
            material->getParameter("u_ambientColor")->setValue(_scene->getAmbientColor());
            material->getParameter("u_lightColor")->setValue(_lightNode->getLight()->getColor());
            material->getParameter("u_lightDirection")->setValue(_lightNode->getForwardVectorView());
        }
    }

    return true;
}

Не забудем добавить сигнатуру метода initializeScene() и private переменную Node* _lightNode в Demo.h.
Метод update(float elapsedTime) оставим пустым, т.к. менять состояние мира в этом примере мы не будем, за нас всё сделает физика.

Полный листинг Demo.h и Demo.cpp

#ifndef TEMPLATEGAME_H_
#define TEMPLATEGAME_H_

#include "gameplay.h"

using namespace gameplay;

/**
 * Main game class.
 */
class Demo: public Game
{
public:

    /**
     * Constructor.
     */
    Demo();

    /**
     * @see Game::keyEvent
     */
	void keyEvent(Keyboard::KeyEvent evt, int key);
	
    /**
     * @see Game::touchEvent
     */
    void touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex);

protected:

    /**
     * @see Game::initialize
     */
    void initialize();

    /**
     * @see Game::finalize
     */
    void finalize();

    /**
     * @see Game::update
     */
    void update(float elapsedTime);

    /**
     * @see Game::render
     */
    void render(float elapsedTime);

private:

    /**
     * Draws the scene each frame.
     */
    bool drawScene(Node* node);
    bool initializeScene(Node* node);

    Scene* _scene;
    Node* _lightNode;
};

#endif


#include "Demo.h"

// Declare our game instance
Demo game;

Demo::Demo()
    : _scene(NULL), _lightNode(NULL)
{
}

void Demo::initialize()
{
    _scene = Scene::load("res/game.scene");

    _lightNode = Node::create("directionalLight1");
	Light* light = Light::createDirectional(_scene->getActiveCamera()->getNode()->getForwardVector());
	light->setColor(1.0, 1.0, 1.0);
	_lightNode->setLight(light);

    // Set the aspect ratio for the scene's camera to match the current resolution
    _scene->getActiveCamera()->setAspectRatio((float)getWidth() / (float)getHeight());

    _scene->visit(this, &Demo::initializeScene);
}

void Demo::finalize()
{
	SAFE_RELEASE(_lightNode);
    SAFE_RELEASE(_scene);
}

void Demo::update(float elapsedTime)
{
}

void Demo::render(float elapsedTime)
{
    // Clear the color and depth buffers
    clear(CLEAR_COLOR_DEPTH, Vector4::zero(), 1.0f, 0);

    // Visit all the nodes in the scene for drawing
    _scene->visit(this, &Demo::drawScene);
}

bool Demo::initializeScene(Node* node)
{
    Model* model = node->getModel();
    if (model)
    {
        Material* material = model->getMaterial();
        if (material && material->getTechnique()->getPassByIndex(0)->getEffect()->getUniform("u_lightDirection"))
        {
            material->getParameter("u_ambientColor")->setValue(_scene->getAmbientColor());
            material->getParameter("u_lightColor")->setValue(_lightNode->getLight()->getColor());
            material->getParameter("u_lightDirection")->setValue(_lightNode->getForwardVectorView());
        }
    }

    return true;
}

bool Demo::drawScene(Node* node)
{
    // If the node visited contains a model, draw it
    Model* model = node->getModel(); 
    if (model)
    {
        model->draw();
    }
    return true;
}

void Demo::keyEvent(Keyboard::KeyEvent evt, int key)
{
    if (evt == Keyboard::KEY_PRESS)
    {
        switch (key)
        {
        case Keyboard::KEY_ESCAPE:
            exit();
            break;
        }
    }
}

void Demo::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex)
{
    switch (evt)
    {
    case Touch::TOUCH_PRESS:
        break;
    case Touch::TOUCH_RELEASE:
        break;
    case Touch::TOUCH_MOVE:
        break;
    };
}

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

Кроссплатформенность

Небольшой экскурс в сборку игры под другие платформы.

Android: необходимо скачать Android SDK и NDK, и установить Apache Ant. Пути к SDK и NDK должны быть в PATH. Переходим в директорию android нашего проекта, выполняем update project -t 1 -p. -s && ndk-build и получаем готовый apk.
Mac OS X/iOS: открываем файл проекта в XCode, собираем, запускаем.
Windows: Открываем файл проекта Visual Studio, собираем, запускаем.
Blackberry Playbook: смотрим описание на сайте. К сожалению, не являюсь обладателем Playbook и не могу прокомментировать ситуацию. Но т.к. авторы фреймворка изначально писали его под Playbook, думаю проблем с ним возникнуть не должно.

Недостатки

Куда же без них?

Самая большая проблема, с которой я столкнулся на данный момент — blender & gameplay-encoder. Первый не умеет экспортировать карты нормалей в COLLADA, по этому можно забыть про bump-mapping. Существует патч, написанный одним из пользователей фреймворка, но этот патч ещё не включен в код blender и его включение пока не планируется. Пересобрать blender с этим патчем задача не из простых, у меня пока не получилось. Конечно, можно использовать FBX вместо COLLADA. Но тут другая проблема — не так-то просто пересобрать gameplay-encoder. Он поставляется в виде готового бинарника и отлично работает без поддержки FBX, т.к. FBX проприетарен. FBX SDK можно бесплатно скачать с сайта Autodesk, но вот сборка gameplay-encoder с поддержкой FBX — это тоже большая головная боль. Где-то используется старая версия opencollada, отказывающаяся линковаться с новыми версиями libpcrecpp, где-то FBX SDK капризничает и т.д. И если на Ubuntu 12.04 это ещё возможно, то на более свежих линуксах — печаль. Решение — использовать Maya или 3DsMax там, где нужен bump-mapping или использовать другие способы. Или потратить время на сборку Blender или gameplay-encoder.

Документация — она прекрасна. Все классы и методы отлично задокументированны, в директории gameplay-api лежит сгенерированный doxygen'ом HTML. Но Tutorial скуден. С фреймворком поставляется несколько примеров игр в директории gameplay-samples. Создание некоторых из них разобрано в документации на сайте, но оно далеко не полное. Чтобы разобраться, что к чему в этих примерах и почему именно так, может потребоваться время.

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

Итоги

В данном примере мы коснулись только малой части того, что предлагает нам GamePlay 3D. Существует ещё множество всяческих плюшек и радостей, начиная от такой же простой симуляции транспортных средств и персонажей физическим движком, простого программирования анимации и звука, элементарного управления вводом с различных устройств и заканчивая удобным разделением конфигурации для различных платформ. Недавно на Хабре проскакивала статья о том, как тяжело бывает писать игры под Android, т.к., в числе прочего, в разных телефонах используются разные чипы аппаратного сжатия текстур. Для решения этой проблемы можно, например, создать разные файлы конфигурации с путями к соответствующим текстурам для разных чипов: game.atc.config для устройств с чипами atc, game.dxt.config для dxt, game.pvr.config для pvr, game.png.conf для PC, и движок подхватит нужный для конкретного девайса конфиг без лишних вопросов.

Надеюсь, статья вам понравилась и кто-то решит присоединиться к разработке этого чудесного фреймворка или написать на нём свой следующий (или первый) проект. Пост получился достаточно объемным, так что посмотрим поближе на материалы, добавим интерактивности в демо и прикрутим звуки в другой раз.

Сайт проекта: gameplay3d.org/

Архив с проектом и исходником сцены для Blender: dl.dropbox.com/u/64710641/Demo_complete.zip
Сцена для Blender: dl.dropbox.com/u/64710641/boxes.blend
Сцена в COLLADA: dl.dropbox.com/u/64710641/boxes.dae
Сцена конвертированная в GamePlayBundle: dl.dropbox.com/u/64710641/boxes.gpb

В завершение пара примеров от пользователей, опубликовавших свои разработки на форуме проекта.

Fluid dynamics experiments

Conduct's Feather Duster BB10 app

GamePlay 3D + Kinect

AR body suit using BBGamePlay + OpenNI

Unnamed Kinect game

Автор: wunderwaffel

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js