Pull to refresh

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

Development for iOS *Development for Android *
Tutorial
В последней статье цикла, посвященного разработке Marmalade Framework, мы добавим в проект анимированные и составные спрайты, а также построим небольшое демонстрационное приложение, иллюстрирующее принципы использования библиотеки.

Поскольку мы разрабатываем событийно-ориентированный Framework, выполнение любого действия (и анимации в том числе), будет производиться в результате обработки какого либо события. Набор базовых событий определим в модуле Desktop.

Desktop.h
#ifndef _DESKTOP_H_
#define _DESKTOP_H_

#include <set>
#include "s3eKeyboard.h"
#include "Scene.h"

using namespace std;

enum EMessageType {
    emtNothing                                                      = 0x00,

    emtHide                                                         = 0x01,
    emtShadow                                                       = 0x02,
    emtShow                                                         = 0x03,
    emtSwitch                                                       = 0x04,
    emtInit                                                         = 0x05,
    emtFix                                                          = 0x08,

    emtStartAnimation                                               = 0x06,
    emtStopAnimation                                                = 0x07,
    emtActivate                                                     = 0x09,

    emtSystemMessage                                                = 0x0F,

    emtTouchEvent                                                   = 0x10,
    emtTouchIdMask                                                  = 0x03,
    emtTouchMask                                                    = 0x78,
    emtMultiTouch                                                   = 0x14,
    emtTouchOut                                                     = 0x18,
    emtTouchDown                                                    = 0x30,
    emtTouchUp                                                      = 0x50,
    emtTouchOutUp                                                   = 0x58,
    emtTouchMove                                                    = 0x70,
    emtSingleTouchDown                                              = 0x30,
    emtSingleTouchUp                                                = 0x50,
    emtSingleTouchMove                                              = 0x70,
    emtMultiTouchDown                                               = 0x34,
    emtMultiTouchUp                                                 = 0x54,
    emtMultiTouchMove                                               = 0x74,

    emtKeyEvent                                                     = 0x80,
    emtKeyAction                                                    = 0x82,
    emtKeyDown                                                      = 0x81,
    emtKeyPressed                                                   = 0x83,
    emtKeyReleased                                                  = 0x82
};
...


Как мы увидим далее, мы сможем легко определять новые события, не изменяя модуль Desktop. Перечисление EMessageType не более чем удобный способ собрать все коды базовых событий вместе. Задачей анимированного спрайта является инкапсуляция обработки базовых событий. Интерфейс анимированного спрайта содержит всего два метода:

IAnimatedSprite.h
#ifndef _IANIMATEDSPRITE_H_
#define _IANIMATEDSPRITE_H_

#include <string>

#include "Desktop.h"

using namespace std;

class IAnimatedSprite {
    public:
      virtual bool isValidMessage(int msg)                                     = 0;
      virtual void doMessage(int msg, void* data = NULL, uint64 timestamp = 0) = 0;
};

#endif    // _IANIMATEDSPRITE_H_


Метод isValidMessage проверяет, может ли спрайт обработать событие заданного типа (если событие не может быть обработано спрайтом, оно передается содержащему его контейнеру — составному спрайту или сцене), а метод doMessage выполняет обработку. Помимо кода события — msg, в обработчик можно передать указатель на произвольные данные — data и метку времени — timestamp, вычисляемую на каждой итерации цикла в Main.cpp.

Реализация AnimatedSprite будет выглядеть следующим образом:

AnimatedSprite.h
#ifndef _ANIMATEDSPRITE_H_
#define _ANIMATEDSPRITE_H_

#include <map>
#include <vector>

#include "Sprite.h"
#include "IAnimatedSprite.h"
#include "AnimateMessage.h"
#include "ResourceManager.h"

#define REFRESH_CNT 2

using namespace std;

class AnimatedSprite: public Sprite,
                      public IAnimatedSprite {
    protected:
        struct Message {
            Message(int id, uint64 timestamp, void* data = NULL): 
                     id(id), timestamp(timestamp), data(data) {}
            Message(const Message& m): 
                     id(m.id), timestamp(m.timestamp), data(m.data) {}
            int id;
            void* data;
            uint64 timestamp;
        };
        struct CurrentMessage {
            CurrentMessage(AnimateMessage* message, uint64 timestamp):
                     message(message), timestamp(timestamp), 
                     lastTimeDelta(0), isEmpty(false) {}
            CurrentMessage(const CurrentMessage& m): 
                     message(m.message), timestamp(m.timestamp),
                     lastTimeDelta(m.lastTimeDelta), isEmpty(m.isEmpty) {}
            AnimateMessage* message;
            uint64 timestamp;
            uint64 lastTimeDelta;
            bool isEmpty;
        };
        int state;
        map<int, ResourceHolder*> images;
        map<int, AnimateMessage*> rules;
        uint64 lastTimestamp;
        vector<Message> messages;
        vector<CurrentMessage> currentMessages;
        bool isAnimated;
        int refreshCnt;
    public:
        AnimatedSprite(ISpriteOwner* scene, int x, int y, int zOrder = 0);
        AnimatedSprite(ISpriteOwner* scene, const char* res, int x, 
                       int y, int zOrder = 0, int loc = elNothing);
       ~AnimatedSprite();
        void clearMessageRules() {rules.clear();}
        void addMessageRule(int msg, AnimateMessage* rule);
        virtual void addImage(const char*res, int id = 0, int loc = 0);
        virtual CIw2DImage* getImage(int id = 0);
        virtual int  getState();
        virtual bool setState(int newState);
        virtual void update(uint64 timestamp);
        virtual void refresh();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual bool isBuzy() {return false;}
        virtual bool isValidMessage(int msg) {return (msg <= emtSystemMessage);}
        virtual void doMessage(int msg, void* data = NULL, uint64 timestamp = 0);
        virtual void unload();

    typedef map<int,  ResourceHolder*>::iterator IIter;
    typedef pair<int, ResourceHolder*> IPair;
    typedef map<int,  AnimateMessage*>::iterator RIter;
    typedef pair<int, AnimateMessage*> RPair;
    typedef vector<Message>::iterator MIter;
    typedef vector<CurrentMessage>::iterator CIter;
};

#endif    // _ANIMATEDSPRITE_H_


AnimatedSprite.cpp
#include "AnimatedSprite.h"
#include "Desktop.h"
#include "Locale.h"

AnimatedSprite::AnimatedSprite(ISpriteOwner* scene, int x, int y, 
                               int zOrder): Sprite(scene, x, y, zOrder)
                                            , state(0)
                                            , images()
                                            , lastTimestamp(0)
                                            , messages()
                                            , currentMessages()
                                            , isAnimated(false)
                                            , refreshCnt(REFRESH_CNT)
                                            , rules() {}

AnimatedSprite::AnimatedSprite(ISpriteOwner* scene, const char* res, int x, int y,
                               int zOrder, int loc): Sprite(scene, x, y, zOrder)
                                            , state(0)
                                            , images()
                                            , lastTimestamp(0)
                                            , messages()
                                            , currentMessages()
                                            , isAnimated(false)
                                            , refreshCnt(REFRESH_CNT)
                                            , rules() {
    AnimatedSprite::addImage(res, 0, loc);
}

AnimatedSprite::~AnimatedSprite() {
    for (RIter p = rules.begin(); p != rules.end(); ++p) {
        delete p->second;
    }
}

void AnimatedSprite::unload() {
    for (IIter p = images.begin(); p != images.end(); ++p) {
        p->second->unload();
    }
}

void AnimatedSprite::addMessageRule(int msg, AnimateMessage* rule) {
    RIter p = rules.find(msg);
    if (p != rules.end()) {
        return;
    }
    rules.insert(RPair(msg, rule));
}

void AnimatedSprite::addImage(const char*res, int id, int loc) {
    ResourceHolder* img = rm.load(res, loc);
    images.insert(IPair(id, img));
}

bool AnimatedSprite::setState(int newState) {
    IIter p = images.find(newState);
    if (p == images.end()) {
        return false;
    }
    state = newState;
    return true;
}
       
CIw2DImage* AnimatedSprite::getImage(int id) {
    IIter p = images.find(id);
    if (p == images.end()) {
        return NULL;
    }
    return p->second->getData();
}

int AnimatedSprite::getState() {
    return state;
}

void AnimatedSprite::doMessage(int msg, void* data, uint64 timestamp) {
    init();
    int s = getState();
    switch (msg) {
        case emtStartAnimation:
            isAnimated = true;
            break;
        case emtStopAnimation:
            isAnimated = false;
            break;
        case emtSwitch:
            s++;
            if (getImage(s) == NULL) {
                s = 0;
            }
            setState(s);
            return;
        case emtHide:
            isVisible = false;
            return;
        case emtShadow:
            isVisible = true;
            alpha = IW_2D_ALPHA_HALF;
            return;
        case emtShow:
            isVisible = true;
            alpha = IW_2D_ALPHA_NONE;
            return;
    };
    if (timestamp == 0) {
        timestamp = s3eTimerGetMs();
    }
    RIter p = rules.find(msg);
    if (p != rules.end()) {
        for (CIter q = currentMessages.begin(); q != currentMessages.end(); ++q) {
            if (q->isEmpty) {
                q->isEmpty       = false;
                q->message       = p->second;
                q->timestamp     = timestamp;
                q->lastTimeDelta = 0;
                return;
            }
        }
        currentMessages.push_back(CurrentMessage(p->second, timestamp));
    }
}

bool AnimatedSprite::sendMessage(int msg, uint64 timestamp, void* data) {
    if (!isValidMessage(msg)) {
        return false;
    }
    if (timestamp <= lastTimestamp) {
        doMessage(msg, data);
        return true;
    }
    messages.push_back(Message(msg, timestamp, data));
    return true;
}
 
void AnimatedSprite::update(uint64 timestamp) {
    bool isEmpty = true;
    for (MIter p = messages.begin(); p != messages.end(); ++p) {
        if (p->timestamp <= lastTimestamp) continue;
        if (p->timestamp <= timestamp) {
            doMessage(p->id, p->data, p->timestamp);
            continue;
        }
        isEmpty = false;
    }
    if (isEmpty) {
        messages.clear();
    }
    isEmpty = true;
    for (CIter p = currentMessages.begin(); p != currentMessages.end(); ++p) {
        if (p->isEmpty) continue;
        uint64 timeDelta = timestamp - p->timestamp;
        if (!p->message->update(timeDelta, p->lastTimeDelta)) {
            p->isEmpty = true;
            continue;
        }
        p->lastTimeDelta = timeDelta;
        isEmpty = false;
    }
    if (isEmpty) {
        currentMessages.clear();
    }
    lastTimestamp = timestamp;
}

void AnimatedSprite::refresh() {
    if (isAnimated) {
        if (--refreshCnt <= 0) {
            refreshCnt = REFRESH_CNT;
            doMessage(emtSwitch);
        }
    }
    Sprite::refresh();
}


В отличии от обычного спрайта, анимированный может загрузить несколько изображений, связывая их с некоторыми числовыми значениями. Эти значения мы будем называть состояниями спрайта. Обработка событий будет выполняться в doMessage.

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

CompositeSprite.h
#ifndef _COMPOSITESPRITE_H_
#define _COMPOSITESPRITE_H_

#include "AnimatedSprite.h"
#include "AbstractSpriteOwner.h"
#include "Scene.h"

class CompositeSprite: public AnimatedSprite
                     , public AbstractSpriteOwner {
    protected:
        ISpriteOwner* owner;   
    public:
        CompositeSprite(ISpriteOwner* scene, int x, int y, int zOrder);
        virtual int  getXSize(int xSize);
        virtual int  getYSize(int ySize);
        virtual int  getXPos(int x);
        virtual int  getYPos(int y);
        virtual bool setState(int newState);
        virtual void refresh();
        virtual void update(uint64 timestamp);
        virtual bool isBuzy();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual bool sendMessage(int msg, int x, int y);
        virtual void unload();
};

#endif    // _COMPOSITESPRITE_H_


CompositeSprite.cpp
#include "CompositeSprite.h"

CompositeSprite::CompositeSprite(ISpriteOwner* scene, int x, int y, int zOrder): AnimatedSprite(scene, x, y, zOrder), owner(scene), AbstractSpriteOwner() {}

int CompositeSprite::getXSize(int xSize) {
    return owner->getXSize(xSize);
}
       
int CompositeSprite::getYSize(int ySize) {
    return owner->getYSize(ySize);
}

int CompositeSprite::getXPos(int x) {
    return AbstractScreenObject::getXPos() + owner->getXPos(x);
}
   
int CompositeSprite::getYPos(int y) {
    return AbstractScreenObject::getYPos() + owner->getYPos(y);
}

void CompositeSprite::refresh() {
    if (isVisible) {
        init();
        AbstractSpriteOwner::refresh();
    }
}

void CompositeSprite::update(uint64 timestamp) {
    AnimatedSprite::update(timestamp);
    AbstractSpriteOwner::update(timestamp);
}

bool CompositeSprite::isBuzy() {
    return AnimatedSprite::isBuzy();
}

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

bool CompositeSprite::sendMessage(int msg, int x, int y) {
    if (!isVisible) return false;
    return AbstractSpriteOwner::sendMessage(msg, x, y);
}

bool CompositeSprite::setState(int newState) {
    state = newState;
    return true;
}

void CompositeSprite::unload() {
    AbstractSpriteOwner::unload();
}


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

image

В AnimatedSprite добавляется несколько коллекций. В rules будет содержаться соответствие числовых кодов сообщений некоторым укрупненным правилам выполнения анимации AnimateMessage. Список currentMessages будет содержать список правил, по которым производится анимация в настоящее время (одновременно может обрабатываться несколько правил), в messages будут содержаться сообщения, которые предстоит обработать.

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

AnimateMessage.h
#ifndef _ANIMATEMESSAGE_H_
#define _ANIMATEMESSAGE_H_

#include <set>
#include "s3eTypes.h"

#include "AnimateAction.h"

using namespace std;

class AnimateMessage {
    private:
        set<AnimateAction*> actions;
    public:
        AnimateMessage();
       ~AnimateMessage();
        bool update(uint64 newDelta, uint64 oldDelta);
        void addAction(AnimateAction* action) {actions.insert(action);}

    typedef set<AnimateAction*>::iterator AIter;
};

#endif    // _ANIMATEMESSAGE_H_


AnimateMessage.cpp
#include "AnimateMessage.h"

AnimateMessage::AnimateMessage(): actions() {}

AnimateMessage::~AnimateMessage() {
    for (AIter p = actions.begin(); p != actions.end(); ++p) {
        delete *p;
    }
}

bool AnimateMessage::update(uint64 newDelta, uint64 oldDelta) {
    bool r = false;
    for (AIter p = actions.begin(); p != actions.end(); ++p) {
        if ((*p)->isSheduled(oldDelta)) {
            r = true;
            (*p)->update(newDelta);
        } else {
            (*p)->clear();
        }
    }
    return r;
}


Метод update принимает два timestamp-а: текущее и предыдущее значения. Его реализация тривиальна. Производится перебор всех AnimateAction и, в случае, если его анимация не завершена, вызывается метод update. В противном случае, состояние AnimateAction сбрасывается в исходное. Если найден хотя-бы один элемент, анимация которого не завершена, метод update возвращает true.

AnimateAction.h
#ifndef _ANIMATEACTION_H_
#define _ANIMATEACTION_H_

#include "s3eTypes.h"

#include "AbstractScreenObject.h"

class AnimateAction {
    private:
        uint64 startDelta;
        uint64 stopDelta;
    protected:
        AbstractScreenObject* sprite;
        virtual void doAction(int timeDelta) = 0;
        virtual int getTimeInterval() {return (int)(stopDelta - startDelta);}
    public:
        AnimateAction(AbstractScreenObject* sprite, uint64 startDelta, 
                      uint64 stopDelta);
        virtual ~AnimateAction() {}
        virtual bool isSheduled(uint64 timeDelta);
        virtual void update(uint64 timeDelta);
        virtual void clear() {}
};

#endif    // _ANIMATEACTION_H_


AnimateAction.cpp
#include "AnimateAction.h"

AnimateAction::AnimateAction(AbstractScreenObject* sprite, uint64 startDelta,
                             uint64   stopDelta): sprite(sprite)
                                                , startDelta(startDelta)
                                                , stopDelta(stopDelta) {
}

bool AnimateAction::isSheduled(uint64 timeDelta) {
    return timeDelta < stopDelta;
}

void AnimateAction::update(uint64 timeDelta) {
    if (timeDelta >= startDelta) {
        uint64 delta = timeDelta - startDelta;
        if (timeDelta > stopDelta) {
            delta = stopDelta - startDelta;
        }
        doAction((int)delta);
    }
}


Метод isSheduled возвращает true, в случае если полученная временная отметка меньше времени завершения действия. Если полученная временная отметка больше или равна времени начала действия, в метод doAction передается значение времени, прошедшего с момента начала действия (но не более общей продолжительности выполнения действия). Поскольку в метод isSheduled передается предыдущее значение timestamp-а, гарантируется по крайней мере однократное выполнение doAction.

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

MoveAction.h
#ifndef _MOVEACTION_H_
#define _MOVEACTION_H_

#include "AnimateAction.h"

class MoveAction: public AnimateAction {
    private:
        int x, y;
        int startX, startY;
        bool isCleared;
    protected:
        virtual void doAction(int timeDelta);
        virtual void clear() {isCleared = true;}
    public:
        MoveAction(AbstractScreenObject* sprite, uint64 startDelta, 
                   uint64 stopDelta, int x, int y);
        MoveAction(AbstractScreenObject* sprite, uint64 delta, int x, int y);
};

#endif    // _MOVEACTION_H_


MoveAction.cpp
#include "MoveAction.h"

MoveAction::MoveAction(AbstractScreenObject* sprite, uint64 startDelta, 
                       uint64 stopDelta,
                       int x, int y): AnimateAction(sprite, startDelta, stopDelta)
                       , x(x), y(y), isCleared(true) {}

MoveAction::MoveAction(AbstractScreenObject* sprite, uint64 delta,
                       int x, int y): AnimateAction(sprite, delta, delta)
                       , x(x), y(y), isCleared(true) {}

void MoveAction::doAction(int timeDelta) {
    if (isCleared) {
        startX = sprite->getXDelta();
        startY = sprite->getYDelta();
        isCleared = false;
    }
    int timeInterval = getTimeInterval();
    if (timeInterval <= 0) {
        sprite->setDeltaXY(x, y);
    } else if (timeDelta > timeInterval) {
        sprite->setDeltaXY(x, y);
    } else {
        int xInterval = x - startX;
        int yInterval = y - startY;
        int xDelta = (xInterval * timeDelta) / timeInterval;
        int yDelta = (yInterval * timeDelta) / timeInterval;
        sprite->setDeltaXY(startX + xDelta, startY + yDelta);
    }
}


SoundAction.h
#ifndef _SOUNDACTION_H_
#define _SOUNDACTION_H_

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

#include "AnimateAction.h"
#include "Locale.h"

using namespace std;

class SoundAction: public AnimateAction {
    private:
        string res;
        int loc;
        bool checkSound();
    protected:
        virtual void doAction(int timeDelta);
    public:
        SoundAction(AbstractScreenObject* sprite, uint64 timeDelta, const char* r,
                    int loc = elSound);
};

#endif    // _SOUNDACTION_H_


SoundAction.cpp
#include "SoundAction.h"
#include "Desktop.h"

SoundAction::SoundAction(AbstractScreenObject* sprite, uint64 timeDelta,
         const char* r, int loc): AnimateAction(sprite, timeDelta, timeDelta)
         , res(r), loc(loc) {
}

void SoundAction::doAction(int timeDelta) {
    CIwResGroup* resGroup;
    const char* groupName = Locale::getGroupName(loc);
    if (checkSound() &&(groupName != NULL)) {
        resGroup = IwGetResManager()->GetGroupNamed(groupName);
        CIwSoundSpec* SoundSpec = (CIwSoundSpec*)resGroup->GetResNamed(res.c_str(),
                                  IW_SOUND_RESTYPE_SPEC);
        CIwSoundInst* SoundInstance = SoundSpec->Play();
    }
}

bool SoundAction::checkSound() {
    IObject* o = (IObject*)desktop.getName("soundon");
    if (o != NULL) {
        return (o->getState() != 0);
    }
    return false;
}


Как мы видим, набор AnimateAction может легко расширяться.

Используя описанные выше механизмы анимации создадим небольшой пользовательский интерфейс, для управления настройками типичного Android-приложения. Для начала, нам понадобятся кнопки. Кнопки должны реагировать на нажатие, выполняя простую анимацию:

Button.h
#ifndef _BUTTON_H_
#define _BUTTON_H_

#include "AnimatedSprite.h"
#include "AbstractSpriteOwner.h"

enum EButtonMessage {
    ebmDown             = 0x0100,
    ebmUp               = 0x0101,
    ebmOutUp            = 0x0111,
    ebmPressed          = 0x0102
};

class Button: public AnimatedSprite {
    protected:
        AnimateMessage* msgDown;
        AnimateMessage* msgUp;
        int message;
        AbstractSpriteOwner* receiver;
        void configure();
    public:
        Button(ISpriteOwner* scene, const char* res, int x, int y, 
               int zOrder = 0, int loc = elNothing);
        Button(ISpriteOwner* scene, int x, int y, int zOrder = 0);
        virtual bool isValidMessage(int msg);
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual void doMessage(int msg, void* data = NULL, uint64 timestamp = 0);
        virtual bool isPausable() const {return false;}
        void addReceiver(int m, AbstractSpriteOwner* r);
};

#endif    // _BUTTON_H_


Button.cpp
#include "Button.h"
#include "Desktop.h"
#include "MoveAction.h"
#include "SendMessageAction.h"
#include "SoundAction.h"

Button::Button(ISpriteOwner* scene, const char* res, int x, int y, 
             int zOrder, int loc): AnimatedSprite(scene, res, x, y, zOrder, loc),
                                   receiver(NULL) {
    Button::configure();
}

Button::Button(ISpriteOwner* scene, int x, int y, int zOrder): 
               AnimatedSprite(scene, x, y, zOrder), 
               receiver(NULL) {
    Button::configure();
}

void Button::configure() {
    msgDown = new AnimateMessage();
    msgDown->addAction(new MoveAction(this, 0, 50, 10, 10));
    msgDown->addAction(new SoundAction(this, 50, "menubutton"));
    addMessageRule(ebmDown, msgDown);
    msgUp = new AnimateMessage();
    msgUp->addAction(new MoveAction(this, 100, 150, 0, 0));
    addMessageRule(ebmOutUp, msgUp);
    msgUp = new AnimateMessage();
    msgUp->addAction(new MoveAction(this, 100, 150, 0, 0));
    msgUp->addAction(new SendMessageAction(this, 100, ebmPressed));
    msgUp->addAction(new SendMessageAction(this, 110, emtInit));
    addMessageRule(ebmUp, msgUp);
}

bool Button::isValidMessage(int msg) {
    switch (msg) {
        case emtTouchDown:
        case   emtTouchUp:
        case      ebmDown:
        case        ebmUp:
        case     ebmOutUp:
        case   ebmPressed: return true;
        default: return AnimatedSprite::isValidMessage(msg);
    }
}

void Button::doMessage(int msg, void* data, uint64 timestamp) {
    if (msg == ebmPressed) {
        if (receiver != NULL) {
            receiver->sendMessage(message, 0, (IObject*)this);
        }
        return;
    }
    AnimatedSprite::doMessage(msg, data, timestamp);
}

bool Button::sendMessage(int msg, uint64 timestamp, void* data) {
    if ((msg & emtTouchEvent) != 0) {
        switch (msg & emtTouchMask) {
            case emtTouchDown:
                sendMessage(ebmDown, desktop.getCurrentTimestamp());
                break;
            case emtTouchUp:
                sendMessage(ebmUp, desktop.getCurrentTimestamp());
                break;
            case emtTouchOutUp:
                sendMessage(ebmOutUp, desktop.getCurrentTimestamp());
                break;
        }
        return true;
    }
    return AnimatedSprite::sendMessage(msg, timestamp, data);
}

void Button::addReceiver(int m, AbstractSpriteOwner* r) {
    message  = m;
    receiver = r;
}


В методе sendMessage мы обрабатываем коды событий Touchpad, формируя внутренние события, с которыми работает кнопка. В configure с этими событиями связывается анимация, при нажатии, кнопка перемещается на 10 единиц вниз и вправо, при отпускании, возвращается на место. В случае, если при отпускании точка касания не ушла с кнопки, формируется событие ebmPressed, с которым мы можем связать произвольный обработчик.

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

SwitchButton.h
#ifndef _SWITCHBUTTON_H_
#define _SWITCHBUTTON_H_

#include "Button.h"

class SwitchButton: public Button {
    protected:
        void configure();
    public:
        SwitchButton(ISpriteOwner* scene, int x, int y, int zOrder = 0);
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
};

#endif    // _SWITCHBUTTON_H_


SwitchButton.cpp
#include "SwitchButton.h"
#include "Desktop.h"
#include "MoveAction.h"
#include "SendMessageAction.h"
#include "SoundAction.h"

SwitchButton::SwitchButton(ISpriteOwner* scene, int x, int y, int zOrder): 
                           Button(scene, x, y, zOrder) {
    SwitchButton::configure();
}

void SwitchButton::configure() {
    msgUp->addAction(new SendMessageAction(this, 50, emtSwitch));
}

bool SwitchButton::sendMessage(int msg, uint64 timestamp, void* data) {
    if (msg == emtSwitch) {
        doMessage(msg, 0, timestamp);
        if (receiver != NULL) {
            receiver->sendMessage(message, 0, (IObject*)this);
        }
        return true;
    }
    return Button::sendMessage(msg, timestamp, data);
}


Мы просто добавляем в анимацию отпускания кнопки формирование события emtSwitch, переключающее изображение для любого AnimatedSprite.

Теперь все готово для разработки пользовательского интерфейса. Класс Intro будет содержать два экрана — стартовый и экран настроек.

Intro.h
#ifndef _INTRO_H_
#define _INTRO_H_

#include "Scene.h"
#include "CompositeSprite.h"

enum EIntroMessage {
        eimPlay              = 0x100,
        eimSettings          = 0x101,
        eimBack              = 0x102,
        eimCheckMusic        = 0x103
};

enum EIntroStatus {
        eisMain              = 0,
        eisSettings          = 1
};

class Intro: public Scene {
    private:
        Sprite* background;
        CompositeSprite* title;
        CompositeSprite* menu;
        CompositeSprite* settings;
        int state;
        void checkMusic();
    protected:
        virtual bool doKeyMessage(int msg, s3eKey key);
        virtual int  getState() {return state;}
        void setState(int s) {state = s;}
    public:
        Intro();
        virtual bool init();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
};

extern Intro* introScene;

#endif    // _INTRO_H_


Intro.cpp
#include "Intro.h"
#include "Background.h"
#include "IntroTitle.h"
#include "IntroMenu.h"
#include "IntroSound.h"
#include "Desktop.h"

Intro* introScene = NULL;

Intro::Intro(): state(eisMain) {
    introScene = this;
}

bool Intro::init() {
    if (!Scene::init()) return false;
    regKey(s3eKeyBack);
    regKey(s3eKeyAbsBSK);
#if defined IW_DEBUG
    regKey(s3eKeyLSK);
#endif
    background = new Background(this, "background.png", 1);
    title = new IntroTitle(this, 2);
    menu = new IntroMenu(this, 3);
    settings = new IntroSound(this, 4);
    settings->doMessage(emtHide);
    checkMusic();
    return true;
}

bool Intro::doKeyMessage(int msg, s3eKey key) {
     if (msg == emtKeyPressed) {
        switch (state) {
            case eisSettings:
                sendMessage(eimBack);
                return true;
        }
    }
    return false;
}

bool Intro::sendMessage(int msg, uint64 timestamp, void* data) {
    switch (msg) {
        case eimPlay:
            // TODO:
            return true;
        case eimSettings:
            background->setAlpha(IW_2D_ALPHA_HALF);
            title->doMessage(emtHide);
            menu->doMessage(emtHide);
            settings->doMessage(emtShow);
            setState(eisSettings);
            return true;
        case eimBack:
            background->setAlpha(IW_2D_ALPHA_NONE);
            title->doMessage(emtShow);
            menu->doMessage(emtShow);
            settings->doMessage(emtHide);
            setState(eisMain);
            return true;
        case emtInit:
        case eimCheckMusic:
            checkMusic();
            return true;
    }
    return false;
}

void Intro::checkMusic() {
    bool f = false;
    IObject* o = (IObject*)desktop.getName("musicon");
    if (o == NULL) {
        desktop.stopMusic();
        return;
    }
    f = (o->getState() != 0);
    if (f) {
        desktop.startMusic("music.mp3");
    } else {
        desktop.stopMusic();
    }
}


Класс Intro представляет единственную в нашем приложении сцену, загружающую несколько CompositeSprite. В sendMessage определены обработчики событий. Далее мы определим несколько составных спрайтов, задачей которых будет вывод названия программы, а также меню, реализуемых как набор кнопок.

IntroTitle.h
#ifndef _INTROTITLE_H_
#define _INTROTITLE_H_

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

class IntroTitle: public CompositeSprite {
    public:
     IntroTitle(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder) {}
     virtual bool init();
     virtual void refresh();
};

#endif    // _INTROTITLE_H_


IntroTitle.cpp
#include "IntroTitle.h"
#include "Sprite.h"

bool IntroTitle::init() {
    if (!AbstractScreenObject::init()) return false;
    // Sprite settings
    setXY(122, 100);
    // Sprite components
    new Sprite(this, "sprite.png", 0, 0, 1);
    return true;
}

void IntroTitle::refresh() {
    CompositeSprite::refresh();
}


IntroMenu.h
#ifndef _INTROMENU_H_
#define _INTROMENU_H_

#include "CompositeSprite.h"
#include "Button.h"

class IntroMenu: public CompositeSprite {
    private:
        Scene* scene;
        Button* firstButton;
        Button* secondButton;
    public:
        IntroMenu(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder), 
                                             scene(scene) {}
        virtual bool init();
};

#endif    // _INTROMENU_H_


IntroMenu.cpp
#include "IntroMenu.h"
#include "Locale.h"
#include "Intro.h"
#include "Desktop.h"

bool IntroMenu::init() {
    if (!AbstractScreenObject::init()) return false;
    setXY(297, 384);
    firstButton = new Button(this, "play", 0, 0, 1, 
                             Locale::getCurrentImageLocale());
    firstButton->addReceiver(eimPlay, scene);
    secondButton = new Button(this, "setup", 0, 157, 2, 
                              Locale::getCurrentImageLocale());
    secondButton->addReceiver(eimSettings, scene);
    return true;
}


IntroSound.h
#ifndef _INTROSOUND_H_
#define _INTROSOUND_H_

#include "CompositeSprite.h"

class IntroSound: public CompositeSprite {
    private:
        Scene* scene;
    public:
       IntroSound(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder), 
                                             scene(scene) {}
      virtual bool init();
};

#endif    // _INTROSOUND_H_


IntroSound.cpp
#include "IntroSound.h"
#include "SwitchButton.h"
#include "Button.h"
#include "Intro.h"
#include "Locale.h"

bool IntroSound::init() {
    if (!AbstractScreenObject::init()) return false;
    setXY(346, 227);
    SwitchButton* s = new SwitchButton(this, 0, 0, 1);
        s->addImage("musicoff", 0, Locale::getCurrentImageLocale());
        s->addImage("musicon", 1, Locale::getCurrentImageLocale());
        s->setName("musicon");
        s->setState(1);
    s->addReceiver(eimCheckMusic, scene);
    s = new SwitchButton(this, 0, 157, 2);
        s->addImage("soundoff", 0, Locale::getCurrentImageLocale());
        s->addImage("soundon", 1, Locale::getCurrentImageLocale());
        s->setName("soundon");
        s->setState(1);
    Button* b = new Button(this, "back.png", -300, 350, 3, 
                           Locale::getCommonImageLocale());
    b->addReceiver(eimBack, scene);
    return true;
}


Запустив приложение, можно убедиться, что кнопки реагируют на нажатие, а также выполняют заданное переключение между экранами.

Итак, нам удалось разработать простейший Framework, попутно изучив некоторые особенности Marmalade API для разработки кроссплатформенных мобильных приложений. Конечно, разработанной библиотеке не хватает очень многого, для того чтобы считаться полноценным игровым Framework-ом (например в нем нет поддержки реализации физики), но лежащая в его основе гибкая архитектура позволит легко добавить любые необходимые расширения.

Исходные тексты проекта расположены по следующей ссылке.

При разработке Marmalade Framework использовались следующие материалы.
Tags:
Hubs:
Total votes 6: ↑5 and ↓1 +4
Views 2.8K
Comments Comments 3