пятница, 28 сентября 2012 г.

4. Управляем ресурсами

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

Пока мы загружаем не много изображений, эти недостатки не очень заметны, но как только мы попытаемся сделать что-то серьезное, памяти может не хватить. Попробуем это исправить. Главным нашим оружием станет класс с гордым названием ResourceManager:

ResourceManager.h:

#ifndef _RESOURCEMANAGER_H_
#define _RESOURCEMANAGER_H_

#if defined IW_DEBUG
#define LOAD_ALL_GROUPS true
#else
#define LOAD_ALL_GROUPS false
#endif

#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, bool isUnloaded = false);

    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();
#ifdef IW_BUILD_RESOURCES
    IwGetResManager()->AddHandler(new CIwResHandlerWAV);
#endif
    IwGetResManager()->LoadGroup("sounds.group");
    if (LOAD_ALL_GROUPS || (Locale::getCurrentImageLocale() == elEnImage)) {
        IwGetResManager()->LoadGroup("locale_en.group");
    }
    if (LOAD_ALL_GROUPS || (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();
}

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

В методы init и release переносим весь код, связанный с загрузкой ресурсов из Main.cpp. Группы ресурсов, используемые для локализации интерфейса, загружаем выборочно, если собираем release. В debug-сборке, загружаем все группы, чтобы скомпилировать их для deployment-а. В методе load создаем объект-wrapper, задачей которого является управление хранением ресурса в оперативной памяти. В случае, если ресурс был ранее загружен, находим ResourceHolder и возвращаем его, для повторного использования. Реализация ResorceHolder предельно проста:

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;
}


В метод load, переносим код загрузки ресурса из Sprite. Метод getData будет загружать ресурс (если он еще не загружен) и возвращать указатель на него вызывающему. Также, добавляем метод unload, позволяющий выгружать неиспользуемый ресурс из оперативной памяти. Далее, вносим изменения в Sprite:

Sprite.h:

#ifndef _SPRITE_H_
#define _SPRITE_H_

#include "AbstractScreenObject.h"
#include "ISprite.h"
#include "ISpriteOwner.h"
#include "Locale.h"
#include "ResourceManager.h"

class Sprite: public AbstractScreenObject
            , public ISprite {
    protected:
        ISpriteOwner* owner;
        ResourceHolder* img;
        int capturedId;
    public:
        Sprite(ISpriteOwner* owner, int x, int y , int zOrder = 0);
        Sprite(ISpriteOwner* owner, const char* res, int x, int y, 
               int zOrder = 0, int locale = elImage);
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual bool sendMessage(int msg, int x, int y);
        virtual void update(uint64 timestamp) {}
        virtual void refresh();
        virtual void addImage(const char*res, int state = 0, int locale = 0);
        virtual CIw2DImage* getImage(int id = 0);
        virtual int  getState()  {return 0;}
        virtual int  getWidth();
        virtual int  getHeight();
        virtual void unload();
};

#endif    // _SPRITE_H_


Sprite.cpp:

...
void Sprite::addImage(const char*res, int state, int loc) {
    img = rm.load(res, loc);
}

CIw2DImage* Sprite::getImage(int id) {
    return img->getData();
}

void Sprite::unload() {
    if (img != NULL) {
        img->unload();
    }
}

Background.cpp:

#include "Background.h"

Background::Background(ISpriteOwner* owner, const char* res, int zOrder, 
           int locale): Sprite(owner, res, 0, 0, zOrder, locale) {}

void Background::refresh() {
    CIwMat2D m;
    m.SetRot(0);
    m.ScaleRot(IW_GEOM_ONE);
    m.SetTrans(CIwSVec2(0, 0));
    Iw2DSetTransformMatrix(m);
    Iw2DSetAlphaMode(alpha);
    Iw2DDrawImage(getImage(), CIwSVec2(0, 0), CIwSVec2(owner->getDesktopWidth(),
                  owner->getDesktopHeight()));
}


Деструктор убираем, поскольку вместо изображения, мы храним указатель на wrapper, хранением которого управляем ResourceManager. Реализация измененных методов тривиальна. В AbstractSpriteOwner добавляем код выгрузки ресурсов:

IObject.h:

#ifndef _IOBJECT_H_
#define _IOBJECT_H_

#include "s3e.h"

class IObject {
  public:
    virtual ~IObject() {}  
    virtual bool isBuzy()                                                      = 0;
    virtual int  getState()                                                    = 0;
    virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL) = 0;
    virtual bool sendMessage(int msg, int x, int y)                            = 0;
    virtual void update(uint64 timestamp)                                      = 0;
    virtual void refresh()                                                     = 0;
    virtual void unload()                                                      = 0;
};

#endif    // _IOBJECT_H_
AbstractSpriteOwner.h:

#ifndef _ABSTRACTSPRITEOWNER_H_
#define _ABSTRACTSPRITEOWNER_H_

#include <map>

#include "IObject.h"
#include "ISpriteOwner.h"
#include "AbstractScreenObject.h"

using namespace std;

class AbstractSpriteOwner: public ISpriteOwner {
         . . .
    public:
        AbstractSpriteOwner();
        virtual ~AbstractSpriteOwner();
                   . . .
        virtual void unload();
         . . .
};

#endif    // _ABSTRACTSPRITEOWNER_H_

AbstractSpriteOwner.cpp:

...
void AbstractSpriteOwner::unload() {
    for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
        p->second->unload();
    }
}


Здесь тоже все очевидно. Просто вызываем метод unload для каждого хранимого объекта. Осталось внести изменения в Main.cpp:

...
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();
}
...
Весь исходный код проекта, как обычно, можно получить здесь.

пятница, 21 сентября 2012 г.

3. Работаем со звуком

Как я и обещал ранее, сегодня мы будем учиться работать со звуком и, поскольку нам это понадобиться, немножко с ресурсами. Низкоуровневая работа со звуком (особенно кроссплатформенная) - дело не простое, поэтому, нам очень повезло, что разработчики Marmalade сделали ее за нас.
Для своих нужд мы используем SoundEngine, который, хотя и не является частью Marmalade, входит в комплект дистрибутива и легко может быть использован. У SoundEngine имеется ряд недостатков, о которых я скажу ниже, но его возможности, на текущий момент, нас вполне удовлетворяют.
Следует сразу сказать, что речь идет о двух принципиально различных подсистемах. s3eAudio - позволяет проигрывать фоновую музыку, поддерживает ряд кодеков (в том числе mp3). IwSound - позволяет воспроизводить несколько звуковых эффектов одновременно, но работает только с PCM-кодированными одноканальными wav-файлами. Подробное описание использования этих подсистем можно найти здесь, мы же, попытаемся встроить возможность воспроизведения звука в наш Framework. В файл настроек приложения app.icf добавляем строку, управляющую количеством звуковых эффектов, воспроизводить которые можно одновременно:

[SOUND]
MaxChannels=16


Файл проекта примет следующий вид:

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

subprojects
{
    iw2d
    iwresmanager
    SoundEngine
}

includepath
{
    ./source/Main
    ./source/Common
    ./source/Scene
}
files
{
    [Main]
    (source/Main)
    Main.cpp
    Main.h
    TouchPad.cpp
    TouchPad.h
    Desktop.cpp
    Desktop.h
    Locale.cpp
    Locale.h

    [Common]
    (source/Common)
    IObject.h
    IScreenObject.h
    ISprite.h
    ISpriteOwner.h
    AbstractScreenObject.h
    AbstractScreenObject.cpp
    AbstractSpriteOwner.h
    AbstractSpriteOwner.cpp

    [Scene]
    (source/Scene)
    Scene.cpp
    Scene.h
    Background.cpp
    Background.h
    Sprite.cpp
    Sprite.h

    [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
}


Мы видим, что наш файл проекта сильно вырос. Во первых, добавилась опция module_path, указывающая путь к исходникам SoundEngine. В раздел subprojets добавлены менеджер ресурсов (iwresmanager) и, собственно, SoundEngine. В папке Data, появились три новых файла, с расширением .group, о которых мы поговорим позже, а также файл music.mp3, содержащий фоновую мелодию. Наконец, в исходники добавлено описание класса Locale, который будет содержать зачатки системы локализации наших приложений.
Рассмотрим, что изменилось в наших исходниках. В класс Desktop мы добавляем методы для старта и завершения воспроизведения фоновой музыки:

...
void Desktop::startMusic(const char* res) {
    if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
        s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
        s3eAudioPlay(res, 0);
}

void Desktop::stopMusic() {
    s3eAudioStop();
}


Здесь все предельно просто. Мы проверяем наличие необходимых кодеков и запускаем воспроизведение, передавая имя файла (вместе с расширением). Второй параметр функции s3eAudioPlay предоставляет возможность циклически воспроизводить один и тот-же звук (значение по умолчанию - 1 (однократное воспроизведение), 0 - позволяет воспроизводить звук неограниченнное количество раз). Мы пока не будем оформлять подобным образом функцию воспроизведения звукового эффекта IwSound. В дальнейшем, эта возможность будет интегрирована со спрайтами (в то время как воспроизведение фоновой музыке останется в ведении Desktop). Пока добавим тестовый код непосредственно в функцию main:

#include "Main.h"

#include "s3e.h"
#include "Iw2D.h"
#include "IwGx.h"
#include "IwSound.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 rsource manager
    IwResManagerInit();
#ifdef IW_BUILD_RESOURCES
    // Tell resource system how to convert WAV files
    IwGetResManager()->AddHandler(new CIwResHandlerWAV);
#endif
    IwGetResManager()->LoadGroup("sounds.group");
    IwGetResManager()->LoadGroup("locale_en.group");
    IwGetResManager()->LoadGroup("locale_ru.group");

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

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

    // Shut down the resource manager
    IwResManagerTerminate();

    // Shutdown IwSound
    IwSoundTerminate();

    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);

        // DEBUG:
        uint64 musicTimeout = 5000;
        uint64 soundTimeout = 6000;

        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);

            // DEBUG:
            if ((musicTimeout > 0)&&(timestamp > musicTimeout)) {
                desktop.startMusic("music.mp3");
                musicTimeout = 0;
            }
            if ((soundTimeout > 0)&&(timestamp > soundTimeout)) {
                CIwResGroup* resGroup;
                const char* groupName = Locale::getGroupName(elSound);
                if (groupName != NULL) {
                    resGroup = IwGetResManager()->GetGroupNamed(groupName);
                    CIwSoundSpec* SoundSpec = 

                         (CIwSoundSpec*)resGroup->GetResNamed("sound", 
                          IW_SOUND_RESTYPE_SPEC);
                    CIwSoundInst* SoundInstance = SoundSpec->Play();
                }
                soundTimeout = 0;
            }

            // 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;
}



Мы добавили инициализацию менеджера ресурсов и SoundEngine в функцию init, освобождение этих ресурсов в release, а также вызов IwGetSoundManager()->Update() в главном цикле приложения (рекомендую помнить о необходимости периодического вызова метода update используемых подсистем, поскольку если этого случайно не сделать, ошибки могут быть очень не тривиальными). Помимо сказанного выше, в init добавлена загрузка групп ресурсов (о которых мы сейчас поговорим), и появились два временных фрагмента кода (помеченных коментарием DEBUG), задачей которых является воспроизведение фоновой музыки и однократное воспроизведение звукового эффекта через 5 и 6 секунд, начиная с момента начала работы, соответсвенно. Посмотрим на содержимое файла 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"
    }
}


Здесь описаны два звуковых ресурса menubutton (звук нажатия кнопки, который мы будем использовать в следующих статьях) и sound. Указаны относительные имена использованных файлов (начиная от каталога data) и для каждого эффекта создано описание CIwSoundSpec, содержащее имя ресурса, а также данные о громкости воспроизведения и признак циклического воспроизведения звука. Также, все эффекты описаны в CIwSoundGroup, в которой также указано количество эффектов, которые можно воспроризводить одновременно.
Компиляция группы ресурсов будет выполняться при загрузке группы методом LoadGroup. Поэтому, если мы забыли положить какой-то файл в нужное место, при выполнении программы, мы увидим следующую ошибку:


Добавив необходимые файлы и выполнив программу снова, мы увидим, что в каталоге data-ram, появился подкаталог data-gles1 содержащий скомпилированные ресурсы. Пока мы отлаживаем приложение, мы можем удалять этот каталог (он будет пересоздаваться), но он должен присутствовать на момент deployment-а, в противном случае, последний будет завершен с ошибкой. Снова обратим внимание на код активации звукового эффекта:

resGroup = IwGetResManager()->GetGroupNamed(groupName);
CIwSoundSpec* SoundSpec = (CIwSoundSpec*)resGroup->GetResNamed("sound", 

                           IW_SOUND_RESTYPE_SPEC);
CIwSoundInst* SoundInstance = SoundSpec->Play();


Мы ищем группу по имени и если ее нашли, получаем спецификацию звукового эффекта, используя который, можно воспроизвести звук. Метод Play возвращает SoundInstance, используя который можно управлять уже воспроизводимым звуком (например остановить воспроизведение).

Разумеется, звуки - не единственное, что можно поместить в группу ресурсов. Точно также, мы можем создать группу изображений, чем мы воспользуемся, чтобы локализовать графические ресурсы нашего приложения. Мы просто создаем две группы: locale_en и locale_ru, и помещаем в них рисунки с надписями на разных языках. Вот как будет выглядеть группа locale_ru, с ресурсами, которые понадобятся нам, когда мы будем разрабатывать пользовательский интерфейс:

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"
}

В locale_en изменятся только пути к каталогу с картинками. Разумеется, мы рассчитываем на то, что картинки на разных языках одинаковы по размеру и дизайну и различаются только языком надписей. В класс Sprite (и его интерфейс) внесем небольшое изменение:

ISprite.h:

#ifndef _ISPRITE_H_
#define _ISPRITE_H_

#include "Locale.h"

#include "Iw2D.h"
#include "IwGx.h"

class ISprite {
    public:
       virtual void addImage(const char* res, int state = 0, int locale = 0) = 0;
       virtual CIw2DImage* getImage(int state = 0)                           = 0;
};

#endif    // _ISPRITE_H_
Sprite.cpp:

#include "Sprite.h"
#include "Locale.h"
#include "Desktop.h"

Sprite::Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder, int locale): AbstractScreenObject(x, y)
                                                            , owner(owner)
                                                            , capturedId(-1)
                                                            , img(NULL) {
    addImage(res, 0, locale);
    owner->addSprite((AbstractScreenObject*)this, zOrder);
}

...
void Sprite::addImage(const char*res, int state, int loc) {
    CIwResGroup* resGroup;
    const char* groupName = Locale::getGroupName(loc);
    if (groupName != NULL) {
        resGroup = IwGetResManager()->GetGroupNamed(groupName);
        IwGetResManager()->SetCurrentGroup(resGroup);
        img = Iw2DCreateImageResource(res);
    } else {
        img = Iw2DCreateImage(res);
    }
}


Мы передаем в addImage код локали, по которому определяем имя группы и если такая группа существует, загружаем ресурс из нее по имени. В противном случае, мы, как и прежде, загружаем файл. Следует подчеркнуть, что когда м загружаем ресурс, мы используем имя ресурса, без расширения. Если мы загружаем файл, расширение обязательно должно быть указано.
Как легко догадаться из вышесказанного, кодами локали занимается класс Locale. В нашем случае, он имеет предельно простую реализацию:

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;
    }
}
Функция s3eDeviceGetInt позволяет нам получить информацию о языковых настройках устройства (разумеется кроссплатформенно). Вызывая getCurrentImageLocale мы получаем код локали, передавая который в getGroupName, получаем имя группы. Разумеется, точно также можно локализовать и звуковые ресурсы (просто эту возможность мы сейчас не используем).
Существует большое искушение - поместить в языконезависимую группу все графические ресурсы. Делать этого не стоит, поскольку в скомпилированной группе, графические ресурсы храняться в распакованном виде (и именно в таком виде они попадут в дистрибутив приложения). Если мы поместим background-изображения в группу ресурсов, то скорее всего, мы получим apk-файл размером > 10M, против одномегабайтного, использующего картинки в png-формате.
В следующей статье, мы рассмотрим анимированные и композитные спрайты.

Исходный код проекта можно получить здесь.