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

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

В этом цикле статей я опишу разработку небольшого Framework-а, предназначенного для создания 2D-игр, с использованием Marmalade. Marmalade предоставляет API для разработки кросс-платформенных приложений, позволяя собирать их, в том числе, под Android и iOS. Работа в Marmalade довольно комфортна, а его справочная система сопровождена большим количеством примеров, но сам процесс разработки носит довольно низкоуровневый характер. Использование готового Framework-а может сильно облегчить жизнь начинающему разработчику.

Чтобы лучше представлять себе, что мы собираемся делать, начнем со списка требований. Итак, разрабатываемый нами Framework должен:

  • Быть событийно-ориентированным и, в том числе, поддерживать отложенные события (выполняющиеся через заданное количество миллисекунд)
  • Поддерживать анимацию 2D-спрайтов, включающую их плавное перемещение, масштабирование и циклическое изменение картинки
  • Поддерживать одновременную анимацию нескольких спрайтов
  • Обрабатывать события touchPad-а (с корректной обработкой MultiTouch) и клавиатуры
  • Поддерживать управление проигрыванием фоновой музыки и звуковыми эффектами (в том числе с проигрыванием нескольких звуков одновременно)

Сам процесс разработки будет последовательно описан в этой и последующих статьях. Ниже, представлена диаграмма классов:

image

Основные интерфейсы и классы:

  • IObject — обеспечивает обработку событий и базовую функциональность отображения и обновления объекта
  • IScreenObject — интерфейс объекта имеющего экранное отображение
  • ISprite — интерфейс спрайта, позволяющий отобразить произвольный графический ресурс
  • IAnimatedSprite — интерфейс анимированного спрайта, обеспечивающий доступ к нескольким графическим ресурсам, связанным с различными состояниями объекта
  • ISpriteOwner — интерфейс контейнера спрайтов (сцены или составного спрайта)
  • IAbstractSpriteOwner — абстрактная реализация экранного объекта, позволяющая отображать его, перемещать по экрану, масштабировать и т.п.
  • Sprite — реализация спрайта
  • Background — фоновое изображение
  • AnimatedSprite — реализация анимированного спрайта, обеспечивающая обработку базовых событий (скрыть/показать объект, включить анимацию и.т.п.)
  • CompositeSprite — реализация составного спрайта

Начнем разработку с построения mkb-файла, представляющего собой описание, по которому Marmalade построит C++ проект.

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-файлов, расположив их в соответствующих папках проекта, после чего, запустим MKB-файл на исполнение. Если все сделано правильно, откроется Microsoft Visual Studio, в которой мы увидим наш проект:

image

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

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 обеспечит нам взаимодействие с экраном устройства:

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_

Размеры экрана получаются с использованием вызовов Iw2DGetSurfaceWidth и Iw2DGetSurfaceHeight.

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

Создадим необходимые интерфейсы, в соответствии с разработанной нами ранее архитектурой.

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;} 
        virtual int  getHeight()    {return 0;}
        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.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 является наследником Sprite и переопределяет метод refresh. Экранный размер изображения не вычисляется на основании данных, предоставленных владельцем, а берется непосредственно из размеров desktop

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

AbstractSpriteOwner управляет хранением спрайтов и ведет Z-последовательность их отображения на экране.

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 является 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;
}

Все готово. Запускаем наше приложение на выполнение… и получаем ошибку:

image

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

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

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

Автор: GlukKazan

Источник

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


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