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

в 13:03, , рубрики: android, iOS, marmalade, Разработка под android, разработка под iOS, метки: , ,

Сегодня мы продолжим описание разработки 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_

ResourceHolder.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. Реализацию менеджера хранимых данных я приводить не буду, поскольку она тривиальна. Желающие могут самостоятельно посмотреть ее здесь.

В следующей статье мы рассмотрим вопрос создания анимированных и составных спрайтов.

При разработке Marmalade Framework использовались следующие материалы

Автор: GlukKazan

Источник

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


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