Как стать автором
Обновить

Framework в Мармеладе (часть 3)

Время на прочтение 8 мин
Количество просмотров 3.2K
Сегодня мы продолжим описание разработки Marmalade Framework, начатой в 1 части статей этого цикла, усовершенствовав работу с графическими ресурсами, а также добавив работу со звуком и группами изображений, при помощи которых мы обеспечим локализацию приложений.

В первую очередь, внимательно посмотрим на то, как мы загружаем графические ресурсы в Sprite.cpp:

Sprite.cpp
void Sprite::addImage(const char*res, int state) {
    img = Iw2DCreateImage(res);
}


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

ResourceManager.h
#ifndef _RESOURCEMANAGER_H_
#define _RESOURCEMANAGER_H_

#include <map>
#include <string>

#include "s3e.h"
#include "IwResManager.h"
#include "IwSound.h"

#include "ResourceHolder.h"

using namespace std;

class ResourceManager {
    private:   
        map<string, ResourceHolder*> res;
    public:
        ResourceManager(): res() {}
        void init();
        void release();
        ResourceHolder* load(const char* name, int loc);

    typedef map<string, ResourceHolder*>::iterator RIter;
    typedef pair<string, ResourceHolder*> RPair;
};

extern ResourceManager rm;
       
#endif    // _RESOURCEMANAGER_H_


ResourceManager.cpp
#include "ResourceManager.h"
#include "Locale.h"

ResourceManager rm;

void ResourceManager::init() {
    IwResManagerInit();
}

void ResourceManager::release() {
    for (RIter p = res.begin(); p != res.end(); ++p) {
        delete p->second;
    }
    res.clear();
    IwResManagerTerminate();
}

ResourceHolder* ResourceManager::load(const char* name, int loc) {
    ResourceHolder* r = NULL;
    string nm(name);
    RIter p = res.find(nm);
    if (p == res.end()) {
        r = new ResourceHolder(name, loc);
        res.insert(RPair(nm, r));
    } else {
        r = p->second;
    }
    return r;
}


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

ResourceHolder.h
#ifndef _RESOURCEHOLDER_H_
#define _RESOURCEHOLDER_H_

#include <string>

#include "s3e.h"
#include "Iw2D.h"
#include "IwResManager.h"

using namespace std;

class ResourceHolder {
    private:
        string name;
        int loc;
        CIw2DImage* data;
    public:
        ResourceHolder(const char* name, int loc);
       ~ResourceHolder() {unload();}
        void load();
        void unload();
        CIw2DImage* getData();
};
       
#endif    // _RESOURCEHOLDER_H_
</spoiler>

<spoiler title="ResourceHolder.cpp">
<source lang="cpp">
#include "ResourceHolder.h"
#include "Locale.h"

ResourceHolder::ResourceHolder(const char* name, int loc): name(name)
                                               , loc(loc)
                                               , data(NULL) {
}

void ResourceHolder::load() {
    if (data == NULL) {
        CIwResGroup* resGroup;
        const char* groupName = Locale::getGroupName(loc);
        if (groupName != NULL) {
            resGroup = IwGetResManager()->GetGroupNamed(groupName);
            IwGetResManager()->SetCurrentGroup(resGroup);
            data = Iw2DCreateImageResource(name.c_str());
        } else {
            data = Iw2DCreateImage(name.c_str());
        }
    }
}

void ResourceHolder::unload() {
    if (data != NULL) {
        delete data;
        data = NULL;
    }
}

CIw2DImage* ResourceHolder::getData() {
    load();
    return data;
}


В методе ResourceHolder::load можно заметить, что получив имя, мы сначала пытаемся, загрузив какую-то группу (в зависимости от значения полученного в loc) найти ресурс в ней и, если это не удалось, используем имя для загрузки файла. Дело в том, что Marmalade позволяет размещать изображения (и прочие ресурсы) в так называемые группы, чтобы загружать их, как единое целое.

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

locale_ru.group
CIwResGroup
{
    name "locale_ru"

    "./locale_ru/play.png"
    "./locale_ru/setup.png"
    "./locale_ru/musicoff.png"
    "./locale_ru/musicon.png"
    "./locale_ru/soundoff.png"
    "./locale_ru/soundon.png"
}


Говоря о группах, следует дать две важных рекомендации:

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

Текущую локаль на устройстве мы можем определять кроссплатформенно, используя следующий код.

Locale.h
#ifndef _LOCALE_H_
#define _LOCALE_H_

enum ELocale {
    elNothing       = 0x0,
    elImage         = 0x1,
    elSound         = 0x2,
    elEnImage       = 0x5,
    elRuImage       = 0x9,
    elEnSound       = 0x6,
    elRuSound       = 0xA
};

class Locale {
    public:
        static int getCurrentImageLocale();
        static int getCurrentSoundLocale();
        static int getCommonImageLocale() {return elImage;}
        static int getCommonSoundLocale() {return elSound;}
        static const char* getGroupName(int locale);
};
       
#endif    // _LOCALE_H_


Locale.cpp
#include "Locale.h"
#include "s3e.h"

const char* Locale::getGroupName(int locale) {
    switch (locale) {
        case   elImage: return "images";
        case elEnSound:
        case elRuSound:
        case   elSound: return "sounds";
        case elEnImage: return "locale_en";
        case elRuImage: return "locale_ru";
               default: return NULL;
    }
}

int Locale::getCurrentImageLocale() {
    int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
    switch (lang) {
        case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuImage;
        default: return elEnImage;
    }
}

int Locale::getCurrentSoundLocale() {
    int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
    switch (lang) {
        case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuSound;
        default: return elEnSound;
    }
}


Осталось добавить в наш проект поддержку работы со звуком. Мы будем использовать две подсистемы:

  • S3E Audio — для проигрывания фоновой музыки (поддерживает ряд кодеков и стерео)
  • S3E Sound — для проигрывания звуковых эффектов (поддерживает возможность одновременного проигрывания нескольких звуков)

Принципы работы с этими подсистемами хорошо описаны в этой статье и я не буду подробно на них останавливаться. Опишу лишь изменения, которые необходимо внести в проект.

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

app.icf
[SOUND]
MaxChannels=16


В файл проекта добавляем следующие описания.

mf.mkb
#!/usr/bin/env mkb
options
{
    module_path="$MARMALADE_ROOT/examples"
}

subprojects
{
    iw2d
    iwresmanager
    SoundEngine
}
...
files
{
    ...
    [Data]
    (data)
    locale_en.group
    locale_ru.group
    sounds.group
}

assets
{
    (data)
    background.png
    sprite.png
    music.mp3

    (data-ram/data-gles1, data)
    locale_en.group.bin
    locale_ru.group.bin
    sounds.group.bin
}


Описание группы звуковых ресурсов будет выглядеть следующим образом.

sounds.group
CIwResGroup
{
    name "sounds"

    "./sounds/menubutton.wav"
    "./sounds/sound.wav"

    CIwSoundSpec
    {
        name        "menubutton"
        data        "menubutton"
        vol         0.9
        loop        false
    }

    CIwSoundSpec
    {
        name        "sound"
        data        "sound"
        vol         0.9
        loop        false
    }

    CIwSoundGroup
    {
        name        "sound_effects"
        maxPolyphony     8
        killOldest    true
        addSpec        "menubutton"
        addSpec        "sound"
    }
}


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

ResourceManager.cpp
void ResourceManager::init() {
    IwResManagerInit();
#ifdef IW_BUILD_RESOURCES
    IwGetResManager()->AddHandler(new CIwResHandlerWAV);
#endif
    IwGetResManager()->LoadGroup("sounds.group");
    if (Locale::getCurrentImageLocale() == elEnImage) {
        IwGetResManager()->LoadGroup("locale_en.group");
    }
    if (Locale::getCurrentImageLocale() == elRuImage) {
        IwGetResManager()->LoadGroup("locale_ru.group");
    }
}

void ResourceManager::release() {
    for (RIter p = res.begin(); p != res.end(); ++p) {
        delete p->second;
    }
    res.clear();
    IwResManagerTerminate();
    IwSoundTerminate();
}


В Main.cpp не забываем добавить вызов метода update для звуковой подсистемы:

Main.cpp
#include "Main.h"

#include "s3e.h"
#include "Iw2D.h"
#include "IwGx.h"
#include "IwSound.h"

#include "ResourceManager.h"
#include "TouchPad.h"
#include "Desktop.h"
#include "Scene.h"
#include "Background.h"
#include "Sprite.h"

void init() {
	// Initialise Mamrlade graphics system and Iw2D module
	IwGxInit();
    Iw2DInit();

	// Init IwSound
	IwSoundInit();

	// Set the default background clear colour
	IwGxSetColClear(0x0, 0x0, 0x0, 0);

	// Initialise the resource manager
	rm.init();

	touchPad.init();
	desktop.init();
}

void release() {
	desktop.release();
	touchPad.release();

	// Shut down the resource manager
	rm.release();

	Iw2DTerminate();
	IwGxTerminate();
}

int main() {
    init();    {

        Scene scene;
        new Background(&scene, "background.png", 1, elNothing);
        new Sprite(&scene, "sprite.png", 122, 100, 2, elNothing);
        desktop.setScene(&scene);

        int32 duration = 1000 / 25;
        // Main Game Loop
        while (!desktop.isQuitMessageReceived()) {
            // Update keyboard system
            s3eKeyboardUpdate();
            // Update Iw Sound Manager
            IwGetSoundManager()->Update();
            // Update
            touchPad.update();
            uint64 timestamp = s3eTimerGetMs();
            desktop.update(timestamp);

            // Clear the screen
            IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);
            touchPad.clear();
            // Refresh
            desktop.refresh();
            // Show the surface
            Iw2DSurfaceShow();
            // Yield to the opearting system
            s3eDeviceYield(duration);
        }
    }
    release();
    return 0;
}


Группы ресурсов компилируются из описания при выполнении метода LoadGroup под отладчиком. Если мы что-то сделали неправильно, то получим сообщение об ошибке:

image

В результате компиляции, появляется каталог data-ram/data-gles1, содержащий двоичное представление загруженных групп. Работая под отладчиком, мы можем удалять содержимое этого каталога (оно будет пересоздано), но при выполнении сборки под мобильную платформу (iOS или Android) оно должно присутствовать. В противном случае, сборка завершиться с ошибкой.

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

app.icf
[S3E]
DataDirIsRAM=1


В результате, каталог data, ранее содержащий файлы ресурсов, доступных только на чтение, становится доступен на запись. Эта возможность кроссплатформенна и мы можем создавать файлы, для хранения настроек приложения, как на iOS, так и на Android. Реализацию менеджера хранимых данных я приводить не буду, поскольку она тривиальна. Желающие могут самостоятельно посмотреть ее здесь.

Следует отметить, что после установки флага DataDirIsRAM, Marmalade перемещает каталог data-gles1, содержащий скомпилированные ресурсы групп из data-ram в data. Это никак не влияет на работу под отладчиком, поскольку каталог создается автоматически при загрузке групп, но может привести к тому, что дистрибутив для мобильного устройства (Android или iPhone) может быть ошибочно собран с неактуальными ресурсами. Чтобы этого не случилось, необходимо внести в mkb-файл проекта простое изменение:

        ...
-	(data-ram/data-gles1, data)
+	(data-ram/data-gles1, data/data-gles1)
        locale_en.group.bin
        locale_ru.group.bin
        sounds.group.bin

Я хочу поблагодарить Mezomish, указавшего мне на этот недочет.

В следующей статье мы завершим разработку Framework-а и построим небольшое демонстрационное приложение.

При разработке Marmalade Framework использовались следующие материалы
Теги:
Хабы:
+2
Комментарии 2
Комментарии Комментарии 2

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн