понедельник, 10 сентября 2012 г.

1. Рисуем картинки

В предыдущей статье, мы разработали дизайн фреймворка для разработки простых 2D-игр, с использованием инструментальной среды Marmalade. Начнем разработку нашего фреймворка с построения mkb-файла, являющегося платформо-независимым описанием проекта.

mf.mkb:

#!/usr/bin/env mkb
options
{
}

subprojects
{
    iw2d
}

includepath
{
    ./source/Main
    ./source/Common
    ./source/Scene
}
files
{
    [Main]
    (source/Main)
    Main.cpp
    Main.h
    Desktop.cpp
    Desktop.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)

}

assets
{
    (data)
    background.png
    sprite.png

    (data-ram/data-gles1, data)
}

Файл получился довольно большой, но только за счет того, что я заранее описал в нем все файлы, которые понадобятся нам сегодня. На пустые разделы пока можно не обращать внимания (они понадобятся нам в последующем). В разделе subprojects описываются подпроекты, которые мы используем (в настоящее время это только подсистема iw2d Marmalade, которая позволит нам работать с 2D-графикой). В includepath, как это очевидно из названия, перечисляем имена каталогов, содержащих h-файлы. В разделе files описываются исходные файлы (имя в квадратных скобках определяет имя папки в проекте MSVC, а путь в круглых скобках показывает, где эта папка размещается на диске). В разделе assets описываются ресурсы, используемые приложением.
Далее, заранее создадим заготовки h- и cpp-файлов, расположив их в соответствующих папках проекта:

main.h:

#ifndef _MAIN_H_
#define _MAIN_H_

#endif    // _MAIN_H_

main.cpp:

#include "Main.h"


И запустим MKB-файл на исполнение. Если все сделано правильно, откроется Microsoft Visual Studio, в которой мы увидим наш проект:


Начнем наполнять его кодом. Главный цикл нашего приложения будет расположен в Main.cpp:


#include "Main.h"

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

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

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

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

    desktop.init();
}

void release() {
    desktop.release();

    Iw2DTerminate();
    IwGxTerminate();
}

int main() {
    init();    {

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

        int32 duration = 1000 / 25;
        // Main Game Loop
        while (!s3eDeviceCheckQuitRequest()) {
            // Update keyboard system
            s3eKeyboardUpdate();
            if ((s3eKeyboardGetState(s3eKeyAbsBSK) & S3E_KEY_STATE_DOWN) 
                == S3E_KEY_STATE_DOWN) break;
            // Update
            desktop.update(s3eTimerGetMs());
            // Clear the screen
            IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);
            // Refresh
            desktop.refresh();
            // Show the surface
            Iw2DSurfaceShow();
            // Yield to the opearting system
            s3eDeviceYield(duration);
        }
    }
    release();
    return 0;
}


Это достаточно шаблонный код для проектов Marmalade. В init инициализируются все подсистемы, с которыми мы будем работать, далее создается сцена и ее наполнение, которые мы будем отображать и передается в desktop в качестве главной сцены (это делается в операторном блоке, для того чтобы обеспечить удаление объекта scene до вызова функции release).
В главном цикле приложения мы проверяем условие выхода по нажатию кнопки "s3eKeyAbsBSK" (работу с клавиатурой мы рассмотрим в последующих статьях), после чего обновляем desktop, передавая ему текущее значение timestamp, очищаем экран, вызываем перерисовку desktop, отображаем изменения на экране вызовом Iw2DSurfaceShow, после чего передаем управление операционной системе, вызовом s3eDeviceYield. По завершении главного цикла, очищаем ресурсы в функции release.

desktop.h:

#ifndef _DESKTOP_H_
#define _DESKTOP_H_

#include "Scene.h"

class Desktop {
    private:
        int width, height;
        Scene* currentScene;
    public:
        void init();
        void release() {}
        void update(uint64 timestamp);
        void refresh();
        int getWidth() const {return width;}
        int getHeight() const {return height;}
        Scene* getScene() {return currentScene;}
        void setScene(Scene* scene);
};
       
extern Desktop desktop;

#endif    // _DESKTOP_H_


desktop.cpp:

#include "Desktop.h"
#include "Iw2D.h"

Desktop desktop;

void Desktop::init() {
    width = Iw2DGetSurfaceWidth();
    height = Iw2DGetSurfaceHeight();
    setScene(NULL);
}

void Desktop::setScene(Scene* scene) {
    if (scene != NULL) {
        scene->init();
    }
    currentScene = scene;
}

void Desktop::update(uint64 timestamp) {
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}

void Desktop::refresh() {
    if (currentScene != NULL) {
        currentScene->refresh();
    }
}


Подсистема desktop будет отслеживать изменение системных параметров и управлять обновлением и отрисовкой текущей сцены. Пока его реализация довольно примитивна (он получает размеры экрана устройства, используя вызовы Iw2DGetSurfaceWidth и Iw2DGetSurfaceHeight, возвращает их по требованию и вызывает методы update и refresh для currentScene. В следующих статьях, мы будем расширять функциональность desktop.
Далее, создадим необходимые интерфейсы, в соответствии с разработанной нами ранее архитектурой:

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

#endif    // _IOBJECT_H_


IScreenObject.h:

#ifndef _ISCREENOBJECT_H_
#define _ISCREENOBJECT_H_

#include "s3e.h"

#include "IObject.h"

class IScreenObject: public IObject {
    public:
        virtual int  getXPos()    = 0;
        virtual int  getYPos()    = 0;
        virtual int  getWidth()   = 0;
        virtual int  getHeight()  = 0;
};

#endif    // _ISCREENOBJECT_H_


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) = 0;
        virtual CIw2DImage* getImage(int state = 0)           = 0;
};

#endif    // _ISPRITE_H_


ISpriteOwner.h:

#ifndef _ISPRITEOWNER_H_
#define _ISPRITEOWNER_H_

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

class ISpriteOwner: public IObject {
    public:
        virtual void addSprite(AbstractScreenObject* sprite, int zOrder) = 0;
        virtual bool setZOrder(AbstractScreenObject* sprite, int z)      = 0;
        virtual int  getDesktopWidth()                                   = 0;
        virtual int  getDesktopHeight()                                  = 0;
        virtual int  getXSize(int xSize)                                 = 0;
        virtual int  getYSize(int ySize)                                 = 0;
        virtual int  getXPos(int x)                                      = 0;
        virtual int  getYPos(int y)                                      = 0;
};

#endif    // _ISPRITEOWNER_H_

Во вспомогательном классе AbstractScreenObject будем вести счетчик ссылок и хранить параметры расположения спрайта:

AbstractScreenObject.h:

#ifndef _ABSTRACTSCREENOBJECT_H_
#define _ABSTRACTSCREENOBJECT_H_

#include <string>
#include "Iw2D.h"

#include "IScreenObject.h"

using namespace std;

class AbstractScreenObject: public IScreenObject {
    private:
        static int idCounter;
        int id;
        int usageCounter;
    protected:
        virtual bool init();
        CIw2DAlphaMode alpha;
        int xPos, yPos, angle;
        int xDelta, yDelta;
        bool isVisible;
        bool isInitialized;
    public:
        AbstractScreenObject(int x, int y);
        virtual ~AbstractScreenObject() {}  
        int getId() const {return id;}
        void incrementUsage();
        bool decrementUsage();
        virtual int  getXPos()      {return xPos + xDelta;}
        virtual int  getYPos()      {return yPos + yDelta;}
        virtual int  getWidth()     {return 0;} // TODO:
        virtual int  getHeight()    {return 0;} // TODO:
        virtual bool isBackground() {return false;}
        virtual bool isBuzy()       {return false;}
        int  getAngle() const {return angle;}
        void move(int x = 0, int y = 0);
        void setXY(int x = 0, int y = 0);
        void clearXY();
        void setAngle(int a) {angle = a;}
        void setAlpha(CIw2DAlphaMode a) {alpha = a;}
        bool setState(int state) {return false;}
};

#endif    // _ABSTRACTSCREENOBJECT_H_

 
AbstractScreenObject.cpp:

#include "AbstractScreenObject.h"
#include "Desktop.h"

int AbstractScreenObject::idCounter = 0;

AbstractScreenObject::AbstractScreenObject(int x, int y): 
     xPos(x), alpha(IW_2D_ALPHA_NONE),yPos(y), angle(0), 
     xDelta(0), yDelta(0), isVisible(true), isInitialized(false), usageCounter(0) {
    id = ++idCounter;
}

bool AbstractScreenObject::init() {
    bool r = !isInitialized;
    isInitialized = true;
    return r;
}

void AbstractScreenObject::incrementUsage() {
    usageCounter++;
}

bool AbstractScreenObject::decrementUsage() {
    usageCounter--;
    return (usageCounter == 0);
}

void AbstractScreenObject::move(int x, int y) {
    xDelta += x;
    yDelta += y;
}

void AbstractScreenObject::setXY(int x, int y) {
    xPos = x;
    yPos = y;
}

void AbstractScreenObject::clearXY() {
    xDelta = 0;
    yDelta = 0;
}

Теперь, переходим к интересной части. Реализация Sprite:

Sprite.h:

#ifndef _SPRITE_H_
#define _SPRITE_H_

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

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

#endif    // _SPRITE_H_


Sprite.cpp:

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

Sprite::Sprite(ISpriteOwner* owner, int x, int y, int zOrder):
                                      AbstractScreenObject(x, y)
                                    , owner(owner)
                                    , img(NULL) {
    owner->addSprite((AbstractScreenObject*)this, zOrder);
}

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

Sprite::~Sprite() {
    if (img != NULL) {
        delete img;
    }
}

bool Sprite::sendMessage(int msg, uint64 timestamp, void* data) {
    return owner->sendMessage(msg, timestamp, data);
}

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

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

int Sprite::getWidth() {
    CIw2DImage* img = getImage(getState());
    if (img != NULL) {
        return img->GetWidth();
    } else {
        return 0;
    }
}

int Sprite::getHeight() {
    CIw2DImage* img = getImage(getState());
    if (img != NULL) {
        return img->GetHeight();
    } else {
        return 0;
    }
}

void Sprite::refresh() {
    init();
    CIw2DImage* img = getImage(getState());
    if (isVisible && (img != NULL)) {
        CIwMat2D m;
        m.SetRot(getAngle());
        m.ScaleRot(IW_GEOM_ONE);
        m.SetTrans(CIwSVec2(owner->getXSize(owner->getXPos(getXPos())), 
                         owner->getYSize(owner->getYPos(getYPos()))));
        Iw2DSetTransformMatrix(m);
        Iw2DSetAlphaMode(alpha);
        Iw2DDrawImage(img, CIwSVec2(0, 0), CIwSVec2(owner->getXSize(getWidth()),
                         owner->getYSize(getHeight())));
    }
}


Здесь стоит обратить внимание на методы addImage (загрузка рисунка из ресурса) и refresh (отображение рисунка на экране). В последнем, с последовательностью вызовов SetRot, ScaleRot, SetTrans лучше не экспериментировать (если вам не хочется долгой и мучительной отладки). Передавая в эти вызовы соответсвующие параметры, можно добиться поворота и масштабирования изображения, а также переноса его относительно начала координат.
В вызовах SetTrans и Iw2DrawImage, мы используем методы getXSize и getYSize для преобразования логических координат в экранные. Чуть позже, мы их рассмотрим. Следует отметить важное различие между ScaleRot и использованием третьего (необязательного параметра) Iw2DrawImage. Если первый из них позволяет выполнить афинное преобразование, масштабирующее изображение, то во втором случае, мы можем маштабировать исходное изображение независимо по осям X и Y (чтобы привести к требуемому aspect ratio).

Background.h:

#ifndef _BACKGROUND_H_
#define _BACKGROUND_H_

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

class Background: public Sprite {
    public:
        Background(ISpriteOwner* owner, const char* res, int zOrder);
        virtual bool isBackground() {return true;}
        virtual void refresh();
};

#endif    // _BACKGROUND_H_

Background.cpp:

#include "Background.h"

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

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


Класс Background является наследником Sprite и переопределяет метод refresh. Как легко заметить, экранный размер изображения не вычисляется на основании данных, предоставленных владельцем, а берется непосредственно из размеров desktop

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 {
    protected:
        multimap<int, AbstractScreenObject*> zOrder;
    public:
        AbstractSpriteOwner();
        virtual ~AbstractSpriteOwner();
        virtual void addSprite(AbstractScreenObject* sprite, int z);
        virtual bool setZOrder(AbstractScreenObject* sprite, int z);
        virtual int  getDesktopWidth();
        virtual int  getDesktopHeight();
        virtual int  getState() {return 0;}
        virtual void update(uint64 timestamp);
        virtual void refresh();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, 
                     void* data = NULL) {return false;}
        virtual bool sendMessage(int msg, int x, int y);

    typedef multimap<int, AbstractScreenObject*>::iterator ZIter;
    typedef multimap<int, AbstractScreenObject*>::reverse_iterator RIter;
    typedef pair<int, AbstractScreenObject*> ZPair;
};

#endif    // _ABSTRACTSPRITEOWNER_H_

AbstractSpriteOwner.cpp:

#include "AbstractSpriteOwner.h"
#include "Desktop.h"
#include "ISprite.h"

AbstractSpriteOwner::AbstractSpriteOwner(): zOrder() {}

AbstractSpriteOwner::~AbstractSpriteOwner() {
    for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
        if (p->second->decrementUsage()) {
            delete p->second;
        }
    }
}

void AbstractSpriteOwner::addSprite(AbstractScreenObject* sprite, int z) {
    sprite->incrementUsage();
    zOrder.insert(ZPair(z, sprite));
}

bool AbstractSpriteOwner::setZOrder(AbstractScreenObject* sprite, int z) {
    for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
        if (p->second == sprite) {
            zOrder.erase(p);
            zOrder.insert(ZPair(z, sprite));
            return true;
        }
    }
    return false;
}

int AbstractSpriteOwner::getDesktopWidth() {
    return desktop.getWidth();
}

int AbstractSpriteOwner::getDesktopHeight() {
    return desktop.getHeight();
}

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

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

bool AbstractSpriteOwner::sendMessage(int msg, int x, int y) {
    for (RIter p = zOrder.rbegin(); p != zOrder.rend(); ++p) {
        if (p->second->isBackground()) continue;
        if (p->second->sendMessage(msg, x, y)) {
            return true;
        }
    }
    return false;
}
AbstractSpriteOwner управляет хранением спрайтов и ведет Z-последовательность их отображения на экране. Наследником AbstractSpriteOwner является Scene:

Scene.h:

#ifndef _SCENE_H_
#define _SCENE_H_

#include "s3eKeyboard.h"

#include "AbstractSpriteOwner.h"
#include "AbstractScreenObject.h"

using namespace std;

class Scene: public AbstractSpriteOwner {
    private:
        AbstractScreenObject* background;
        bool isInitialized;
    public:
        Scene();
        virtual bool init();
        int getXSize(int xSize);
        int getYSize(int ySize);
        virtual int getXPos(int x) {return x;}
        virtual int getYPos(int y) {return y;}
        virtual void refresh();
        virtual void update(uint64 timestamp);
        virtual bool isBuzy() {return false;}
        virtual bool sendMessage(int id, int x, int y);
};

#endif    // _SCENE_H_

Scene.cpp:

#include "Scene.h"
#include "Desktop.h"

Scene::Scene(): AbstractSpriteOwner()
              , isInitialized(false)
              , background(NULL) {}

bool Scene::init() {
    bool r = !isInitialized;
    isInitialized = true;
    return r;
}

int Scene::getXSize(int xSize) {
    if (background != NULL) {
        return (getDesktopWidth() * xSize) / background->getWidth();
    }
    return xSize;
}

int Scene::getYSize(int ySize) {
    if (background != NULL) {
        return (getDesktopHeight() * ySize) / background->getHeight();
    }
    return ySize;
}

void Scene::refresh() {
    init();
    if (background == NULL) {
        for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
            if (p->second->isBackground()) {
                background = p->second;
                break;
            }
        }
    }
    AbstractSpriteOwner::refresh();
}

void Scene::update(uint64 timestamp) {
    AbstractSpriteOwner::update(timestamp);
}

bool Scene::sendMessage(int id, int x, int y) {
    if (AbstractSpriteOwner::sendMessage(id, x, y)) {
        return true;
    }
    if (background != NULL) {
        return background->sendMessage(id, x, y);
    }
    return false;
}
Уфф, вот и все :) Здесь стоит обратить внимание на реализацию методов getXSize и getYSize, вычисляющих экранные размеры на основании размеров фонового изображения и реальных размеров экрана устройства.
Запускаем наше приложение на выполнение ... и получаем ошибку:


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

[S3E]
DispFixRot=FixedLandscape
MemSize=70000000
MemSizeDebug=70000000


Теперь, запустив приложение, видим вполне ожидаемую картинку:


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

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

Комментариев нет:

Отправить комментарий