Добрый день! В предыдущей статье была рассмотрена общая часть процесса создания игр жанра HOPA (Hidden Object Puzzle Game или «поиск предметов»). В этой статье мы рассмотрим принцип двухуровневой программной абстракции, который является парадигмой основных платформозависимых компонентов нашего движка, и общую структуру нижнего уровня движка. Такой подход позволил нам добиться гибкости в портировании как движка на новые платформы, так и самих игр с одной платформы на другую. А также нам удалось создать:
- трехуровневую структуру игры;
- унифицированную подсистему 2D графики;
- универсальную организацию исходного кода.
В основу программной части Alawar Engine входят 2 библиотеки: SF (Stargaze Framework library) и QE (Stargaze Quest Engine library). SF является ядром всей системы и содержит почти всю платформозависимую реализацию игры. При этом библиотека имеет одну общую ветку исходников для всех платформ. На данный момент SF функционирует под шестью платформами: Windows (XP, Vista, Windows 7), Mac OS X, iOS, Android, PS3 и Windows 8 (в разработке).
Quest Engine является надстройкой над Stargaze Framework, реализующая логику работы игры, созданную в Quest Editor, и не содержит платформозависимого кода. Изначально QE был ориентирован на игры жанра HOPA со статическим набором статических объектов, но на данный момент он активно развивается и позволяет реализовать игры с динамическим набором динамических объектов, например жанров Time Management (Resource management) и Tower Defense. Большая часть платформозависимых программных модулей внутри SF имеет два уровня абстракции. Благодаря вышеперечисленным особенностям нам удалось добиться достаточно высокой портируемости нашего движка.
В общем случае проект игры имеет трехуровневую структуру и состоит из пользовательского кода, библиотеки Quest Engine, библиотеки Stargaze Framework и дополнительных библиотек, например, библиотеки покупок. При правильном подходе к реализации пользовательский код может быть запущен в первоначальном виде, без дополнительных изменений и усилий на всех платформах, о которых я писал в начале. К слову сказать, наши библиотеки могут быть подключены к проекту игры как на уровне исходников, так и на уровне скомпилированных библиотек. На примере архитектуры библиотеки Stargaze Framework рассмотрим, как можно организовать кроссплатформенную реализацию игры, чтобы понять, за счет чего достигнута такая возможность.
На самом нижнем уровне Stargaze Framework находятся модули системной интеграции (платформозависимый код), которые располагаются в отдельных директориях (win, android и т.д.). Директории с названием платформ содержат исключительно платформозависимый исходный код, уникальный для конкретной платформы и не содержат исходного кода, который может быть общим для разных платформ. Например, реализация обращений к графической или аудио подсистеме текущей ОС или оптимизированные под конкретную архитектуру алгоритмы преобразования пространства цветов для декодирования видео. Помимо этого платформозависимый код в крайних случаях встраивается в основной код через директивы условной компиляции. Это делается в тех случаях, когда выделение отдельной программной абстракции несоизмеримо с размером кода, который заключается в директивы условной компиляции, например, при подключении заголовочных файлов:
#if defined(__SF_WINDOWS)
#include <Objbase.h>
#elif defined(__SF_MAC) || defined(__SF_IPHONE)
#include <uuid/uuid.h>
#endif
За счет этого Stargaze Framework имеет единую развивающуюся ветку и единые принципы работы одновременно для всех платформ. При этом портирование на новую платформу начинается в отдельной ветке, а затем все изменения сливаются в текущую активную ветку.
Уровнем выше располагаются следующие модули:
- подсистема ресурсов и настроек позволяет загружать и сохранять файлы конфигурации, загружать ресурсы из файлов, предоставляет остальным компонентам удобный интерфейс доступа к этим настройкам и ресурсам; все обращения к ресурсам и настройкам происходят через строковые идентификаторы;
- подсистема логов и диагностики – позволяет вести логи, отображать диагностические сообщения и сообщения об ошибках, останавливать игру при критических ошибках, собирать информацию о производительности и т.д.;
- подсистема управления временем – включает в себя менеджер времени и таймеры, используемые для отслеживания различного времени в игре: время от начала запуска игры, время между кадрами игры и т.д.;
- набор вспомогательных компонентов (misc) – включает в себя средства декодирования изображений различного формата (png, jpg и т.д.), средства для работы с различными алгоритмами сжатия данных, математический инструментарий, собственные структуры данных для работы со стоками и массивами и т.д.;
- подсистема многозадачности (MT) – относительно новая для SF подсистема, представляющая собой обертку системной многозадачности.
На следующем уровне находятся собственные подсистемы 2D графики, аудио и видео, которые активно используют как подсистемы предыдущего уровня, так и модули системной интеграции:
- подсистема 2D графики — позволяет выводить геометрические примитивы: линии, прямоугольники, полигоны и т.д., рисовать текстуры (битовые изображения), применять матрицы трансформации (аффинные преобразования), а также области обрезки вывода (clipping);
- аудио подсистема – отвечает за вывод звуков и музыки в играх; при проигрывании звуков позволяет задавать громкость, баланс, скорость воспроизведения. Система позволяет организовывать звуки в группы, что позволяет применять операции сразу к набору звуков;
- подсистема видео – эта подсистема отвечает за вывод видео как внутри игровых сцен, так и в полноэкранном режиме.
Помимо вышеперечисленных подсистем можно выделить ряд промежуточных:
- подсистема шрифтов – отвечает за управление шрифтами и вывод текста, является частью подсистемы 2D графики;
- подсистема частиц – позволяет анимировать изображения (математическая и физическая анимация), создавая при этом различные визуальные эффекты;
- подсистема анимации – также позволяет создавать анимацию (клипы), но может использовать для этого множество объектов (анимация покадровая и логическая);
- механизм эффектов – не является законченной подсистемой, но предоставляет разработчику свободу в программировании различных визуальных эффектов применяемых к GUI.
На самом верхнем уровне располагается подсистема GUI – собственная библиотека виджетов, содержащая в себе набор готовых классических примитивов, таких как окна, кнопки, поля ввода, чекбоксы, радиокнопки и т.д. Интегрирующим звеном является менеджер виджетов, основные задачи которого — это передача сообщений пользовательского ввода виджетам, обновление состояния виджетов и их отрисовка. Кроме того, менеджер виджетов содержит модуль эмуляции жестов. У этого модуля два основных назначения: обработка тех жестов, которые не реализованы на уровне системы и обработка пользовательских жестов.
Рассмотрим более подробно механизмы работы самого приложения, 2D графики, аудио и видео. В нашем фреймворке выделен базовый класс приложения CApplication, который практически не содержит платформозависимого кода. Данный класс описывает базовую логику работы приложения, при этом разработчик игры создает наследника от этого класса и наполняет его требуемым функционалом. Платформозависимая реализация механизма работы приложения (инициализация приложения, создание основного окна, обработка событий и т.п.) скрыта в классах, наследуемых от CSystemIntegration:
class CSystemIntegration
{
public:
CSystemIntegration();
virtual ~CSystemIntegration();
virtual bool Init() = 0;
virtual void Run() = 0;
virtual void Stop() = 0;
virtual void Shutdown() = 0;
virtual bool EnsureSingleInstance() = 0;
virtual bool ChangeScreenMode(bool _fullscreen, bool _32bpp, size_t _width, size_t _height) = 0;
virtual bool GetOriginalDesktopDimentions(size_t &_width, size_t &_height) = 0;
virtual EventInformation &GetCurrentEvent() = 0;
virtual void DefaultWindowProc() = 0;
virtual void GetWindowClientRect(misc::IntRect &_rc) = 0;
virtual void AdjustClientRectToWindow(misc::IntRect &_rc) = 0;
virtual void GetDesktopWindowedSpace(misc::IntRect &_rc) = 0;
virtual void ScreenCoordsIntoClient(misc::IntVector& _pos) = 0;
virtual void ClientCoordsIntoScreen(misc::IntVector& _pos) = 0;
virtual void EnableSystemGestureRecognizer(int _recognizerType, bool _enable) {};
virtual void SetMouseCursorPos(const misc::IntVector& _pos) = 0;
virtual void GetMouseCursorPos(misc::IntVector& _pos) = 0;
virtual void SetSysCursor(gui::SysCursor _cursor, bool _show_now = true) = 0;
virtual gui::SysCursor GetSysCursor() = 0;
virtual void ShowSysCursor(bool _show = true) = 0;
virtual bool IsSysCursorShown() = 0;
protected:
void AppUpdate();
void AppDraw();
void ActivateApp(bool _activate = true);
void MinimizeApp(bool _minimized = true);
};
В большинстве случаев программист игры имеет дело только с высокоуровневой абстракцией CApplication, который в свою очередь использует CSystemIntegration. Интерфейс данного класса описывает общую модель взаимодействия с системной частью приложения. Модель предполагает, что у приложения есть некоторая область вывода (окно), очередь системных сообщений (клавиатурные события, жесты и т.п.) и основной рабочий цикл. Хотя непосредственная реализация методов классов, наследуемых от CSystemIntegration, не стандартизована, есть несколько соглашений, например, основной цикл работы приложения на любой платформе должен вызывать последовательно методы AppUpdate() и AppDraw(). Например, для платформ Windows и PS3 реализации основных циклов выглядят следующим образом:
void CStandaloneApplicationWindows::MessageCycle()
{
MSG msg;
while (!m_EndModal)
{
while (!m_Stop && PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (m_Stop) break;
AppUpdate();
AppDraw();
}
m_EndModal = false;
}
void CStandaloneApplicationPS3::MessageCycle()
{
while (!m_EndModal)
{
if (g_quitRequested == false)
{
cellSysutilCheckCallback();
if ( cellPadUtilUpdate() )
{
input::PointerApp::main();
_updatePad();
}
if (m_Stop) break;
AppUpdate();
AppDraw();
}else break;
}
m_EndModal = false;
}
Таким образом, для запуска приложения программисту игры достаточно организовать точку входа (на некоторых платформах она вынесена из SF) и написать следующий код (пример для iOS):
bool SFIPhoneMain()
{
static game::CGameApplication app;
if (!app.Init())
return false;
else
return true;
}
void StartLoadGame()
{
sf::core::CApplication * app = sf::core::g_Application;
app->SetMainWindow(new game::CMainMenuWindow());
app->Run();
}
Где CGameApplication наследник от CApplication, а CMainMenuWindow наследник от CWindow определенные программистом в коде самой игры. Код иллюстрирует, что данная модель двухуровневой программной абстракции позволяет достичь минимальных затрат на организацию приложения, и как следствие, минимальной головной боли, связанной с переносом игры на другую платформу. А в контексте нескольких приложений это позволяет выделить точку входа приложения в отдельную библиотеку решений.
Унифицированная подсистема 2D-графики позволяет портировать Stargaze Framework без особых проблем на платформы с различными render-машинами, такими как D3D9, D3D11, OpenGL ES (в том числе 2.0), GCM. Такая универсальность достигнута благодаря классам CRenderer и CRenderDevice. Класс CRenderer реализует API верхнего уровня – единый набор методов работы с 2D-графикой, который полностью покрывает требования к казуальным 2D-играм. Например:
-
void RenderString(const CFont* _font, const SF_WSTRING& _string, float _x, float _y, int _justify_h = -1, int _justify_v = -1, float _scale=1.f, const Color& _color=0xffffffff, const Color& _bk_color=0) – отрисовка текста;
-
void RenderTexture(const CTexture* _texture, const misc::FloatRect& _dest, size_t _frame = -1, const Color& _color = 0xffffffffu) – отрисовка текстуры;
-
void PushState() – сохранить состояние рендера;
-
void PopState() – восстановить состояние рендера;
-
void ApplyMatrix(const misc::FloatMatrix& _matrix) – применить аффинное преобразование.
А также хранит стек состояний render-машины: цвет смешивания, матрицы преобразований, текущую текстуру и режим смешивания. Вся платформозависимая реализация скрыта в классах CRenderDevice (API нижнего уровня), имеющих однотипный интерфейс:
class CRenderDevice
{
public:
CRenderDevice();
bool Init();
void Reset();
bool BeginScene();
bool EndScene();
void Render(RenderPrimitives _primitive, const RENDERVERTEX* const _verts, size_t _verts_count);
void Render(RenderPrimitives _primitive, const void* const _verts, size_t _verts_count, DWORD _verts_fvf, DWORD _vertex_size);
void Flush();
void SetTexture(DWORD _stage, IDirect3DTexture9* _texture);
void SetTextureStageState(DWORD _stage, DWORD _state, DWORD _val);
DWORD GetTextureStageState(DWORD _stage, DWORD _state) const;
void SetBlendMode(BlendModes _blend_mode);
void SetPixelShader(IDirect3DPixelShader9* _shader);
void SetRenderTarget(IDirect3DTexture9* _texture);
bool GetAvailableResolutions(std::list<Resolution> &_container);
bool ClearRenderTarget(const Color& _color = 0);
void ToggleHeavyRenderProfile();
private:
…
};
При вызове API верхнего уровня (CRenderer), например, для отрисовки текстуры CRenderer применяет текущее состояние render-машины, самостоятельно пересчитывает массив вершин текстуры и вызывает функцию CRenderDevice::Render. При любой смене состояния render-машины внутри CRenderDevice вызывается функцияFlush(). Для удобства есть потенциальные возможности использовать шейдеры и рисовать в текстуру, но в HOPA-играх это используется крайне редко.
Для вывода звука используется класс CAudioManager, который в свою очередь использует какую-либо библиотеку, имеющую реализацию на конкретной платформе (Bass, OpenAL, MultiStream, XAudio2 и т.п.). В рамках различных платформ данный класс может иметь как двухуровневую реализацию, например, Windows и MAC OS X, так и одноуровневую, например iOS. Такое допущение сделано в силу того, что на некоторых платформах есть удобный набор API для проигрывания звуков. Данный класс полностью скрывает подробности воспроизведения звуков, предоставляя доступ к звукам только по их идентификаторам и идентификаторам групп звуков. Это позволяет свести к минимуму время для «прикручивания» звуков к игре. Например, запуск определенного трека выглядит следующим образом: sf::core::g_AudioManager::Instance().Play(“some_music”).
Пожалуй, самой проблемной подсистемой (но в тоже время самой универсальной в плане кроссплатформенной реализации) является подсистема видео. Это обусловлено контекстом использования видео в игре. Например, на игровой сцене может присутствовать четыре различных видеообъекта, в т. ч. с альфа-каналом, что приводит к снижению fps в игре и повышенному расходу памяти. На данный момент для Windows, Android, MAC OS X и iOS применяется две кроссплатформенных реализации на основе Theora и WebM декодеров. Последний более предпочтителен. API верхнего уровня данной подсистемы интегрирует класс CVideo, интерфейс которого также как и у CAudioManager, достаточно прост.
Исключение составляют только два метода Update и Draw, которые надо вызывать для обновления декодера и отрисовки декодированной текстуры соответственно. Это позволяет нам отображать несколько различных видео на одной сцене по слоям. API нижнего уровня реализованы классами, которые наследованы от CVideo. Данные классы скрывают методы работы с конкретным декодером, а также смешивания обычного видео и альфа-канала. Данный подход позволил нам минимизировать затраты на перенос видео с одной платформы на другую.
В процессе портирования библиотеки Stargaze Framework на различные платформы нами был сделан вывод, что двухуровневая модель абстракции платформозависимых подсистем является более гибкой. Она позволяет нивелировать специфику разных платформ, предоставляя единый принцип разработки и портирования игры.
Автор: EgorGutorov