четверг, 11 октября 2012 г.

6. Двигаем картинками (часть 1)

Теперь, после того как мы проделали всю подготовительную работу, самое время заняться главным - реализацией анимации. Немного переформулируем требования, составленные нами в первой статье цикла, детализируя их, в контексте решаемой задачи:
  1. Интерфейс подсистемы анимации должен быть событийно-ориентированным. Для того чтобы инициировать анимационный эффект мы должны передать сообщение ответственному за этот эффект объекту.
  2. При выполнении равномерного прямолинейного перемещения спрайта, интерфейс подсистемы анимации должен принимать начальное и конечное положение перемещения и вычислять все промежуточные положения спрайта самостоятельно.
  3. Разрабатываемый интерфейс должен поддерживать возможность составления анимации из последовательности перемещений. Также, должна поддерживаться возможность синхронного перемещения нескольких объектов, в рамках одного анимационного эффекта.
  4. Реализация подсистемы анимации должна быть расширяемой, таким образом, чтобы, при необходимости мы могли добавить к списку базовых анимационных эффектов, например, поворот изображения, проигрывание звукового файла или что-то, что мы еще не придумали.
Основную нагрузку, по реализации анимационных эффектов, возьмет на себя класс AnimatedSprite. Поскольку CompositeSprite является его наследником, все сказанное ниже, в равной мере, будет относится и к нему. Построим диаграмму классов, иллюстрирующую архитектуру разрабатываемой нами подсистемы:



Мы видим, что в AnimatedSprite добавляется несколько коллекций. В rules будет содержаться соответствие числовых кодов сообщений некоторым укрупненным правилам выполнения анимации AnimateMessage. Список currentMessages будет содержать список правил, по которым производится анимация в настоящее время (одновременно может обрабатываться несколько правил), в messages будут содержаться сообщения, которые предстоит обработать.
В свою очередь, AnimateMessage будет содержать список базовых действий AnimateAction, для каждого из которых будут заданы время начала и завершения действия, относительно момента начала анимации (эти значения будут совпадать, для действий, выполняемых единомоментно). Наследуя от AnimateAction мы имеем возможность расширения списка базовых анимационных эффектов. В целях иллюстрации концепции, создадим два наследника: MoveAction - осуществляющий равномерное прямолинейное перемещение спрайта и SendMessageAction - выполняющий передачу произвольному объекту сообщения, в заданное время. Реализуем эти классы:

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


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

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


Реализация MoveAction выглядит несколько переусложненной, но именно такой вид она обрела в процессе отладки framework-а. Основная задача этого класса - вычисление промежуточных положений спрайта при выполнении его равномерного прямолинейного движения.

SendMessageAction.h:

#ifndef _SENDMESSAGEACTION_H_
#define _SENDMESSAGEACTION_H_

#include "AnimateAction.h"

class SendMessageAction: public AnimateAction {
    private:
        int msg;
        void* data;
    protected:
        virtual void doAction(int timeDelta);
    public:
        SendMessageAction(AbstractScreenObject* sprite, uint64 timeDelta, 
                          int msg, void* data = NULL);
};

#endif    // _SENDMESSAGEACTION_H_


SendMessageAction.cpp:

#include "SendMessageAction.h"

SendMessageAction::SendMessageAction(AbstractScreenObject* sprite, 
  uint64 timeDelta, int msg, 
  void* data): AnimateAction(sprite, timeDelta, timeDelta), msg(msg), data(data) {}

void SendMessageAction::doAction(int timeDelta) {
    sprite->sendMessage(msg, 0, data);
}


Реализация SendMessageAction прямолинейна. Заданному объекту передается сообщение, при достижении временной метки. В класс AbstractScreenObject внесены небольшие дополнения:

#ifndef _ABSTRACTSCREENOBJECT_H_
#define _ABSTRACTSCREENOBJECT_H_

...
class AbstractScreenObject: public IScreenObject {
    ...
    public:
        int getXDelta() const {return xDelta;}
        int getYDelta() const {return yDelta;}
        void setDeltaXY(int x = 0, int y = 0) {xDelta = x; yDelta = y;}
   ...
};

#endif    // _ABSTRACTSCREENOBJECT_H_

Осталось внести необходимые изменения в 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();
}


Здесь стоит обратить внимание на метод update, осуществляющий обработку правил анимации, а также на изменения в методах sendMessage и doMessage, благодоря которым заполняется очередь сообщений ожидающих обработку.

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

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

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