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

2. Обрабатываем события

В прошлой статье, мы начали писать простой Framework, предназначенный для разработки 2D-игр, с использованием Marmalade. Сегодня мы продолжим это начинание, добавив обработку событий TouchPad-а и клавиатуры. 
TouchPad является основным нашим средством ввода и, по возможности, должен поддерживать широко используемый, в настоящее время, Multitouch. Обработка событий клавиатуры, на мобильных устройствах используется реже, но есть у нее одно важное применение, используемое практически в любом приложении - кнопка s3eKeyAbsBSK ("Назад") должна возвращать приложение к предыдущей активности (в терминологии Android) и закрывать приложение, если мы находимся в начальном экране.
Как обычно, начнем доработку с mkb-файла. В секцию files добавим новые файлы:

#!/usr/bin/env mkb
...
files
{
    [Main]
    (source/Main)
    ...
    TouchPad.cpp
    TouchPad.h
}
...

Содержимое этих файлов будет следующим:

TouchPad.h:

#ifndef _TOUCHPAD_H_
#define _TOUCHPAD_H_

#include "IwGeom.h"
#include "s3ePointer.h"

#define MAX_TOUCHES    11

struct Touch {
    public:
        int        x, y;
        bool       isActive, isPressed, isMoved;
        int        id;   
};

class TouchPad {
    private:
        bool        IsAvailable;
        bool        IsMultiTouch;
        Touch       Touches[MAX_TOUCHES];
    public:
        static bool isTouchDown(int eventCode);
        static bool isTouchUp(int eventCode);
        bool        isAvailable() const { return IsAvailable; }
        bool        isMultiTouch() const { return IsMultiTouch; }
        Touch*      getTouchByID(int id);
        Touch*      getTouch(int index) { return &Touches[index]; }   
        Touch*      findTouch(int id);                               
        int         getTouchCount() const;
        bool        init();
        void        release();
        void        update();
        void        clear();
};

extern TouchPad touchPad;

#endif    // _TOUCHPAD_H_

TouchPad.cpp:

#include "TouchPad.h"
#include "Desktop.h"

TouchPad touchPad;

bool TouchPad::isTouchDown(int eventCode) {
    return (eventCode & emtTouchMask) == emtTouchDown;
}
 
bool TouchPad::isTouchUp(int eventCode) {
    return (eventCode & emtTouchMask) == emtTouchUp;
}

void HandleMultiTouchButton(s3ePointerTouchEvent* event) {
    Touch* touch = touchPad.findTouch(event->m_TouchID);
    if (touch != NULL) {
        touch->isPressed = event->m_Pressed != 0;
        touch->isActive  = true;
        touch->x  = event->m_x;
        touch->y  = event->m_y;
        touch->id = event->m_TouchID;
    }
}

void HandleMultiTouchMotion(s3ePointerTouchMotionEvent* event) {
    Touch* touch = touchPad.findTouch(event->m_TouchID);
    if (touch != NULL) {
        if (touch->isActive) {
            touch->isMoved = true;
        }
        touch->isActive  = true;
        touch->x = event->m_x;
        touch->y = event->m_y;
    }
}

void HandleSingleTouchButton(s3ePointerEvent* event) {
    Touch* touch = touchPad.getTouch(0);
    touch->isPressed = event->m_Pressed != 0;
    touch->isActive  = true;
    touch->x  = event->m_x;
    touch->y  = event->m_y;
    touch->id = 0;
}

void HandleSingleTouchMotion(s3ePointerMotionEvent* event) {
    Touch* touch = touchPad.getTouch(0);
    if (touch->isActive) {
        touch->isMoved = true;
    }
    touch->isActive  = true;
    touch->x = event->m_x;
    touch->y = event->m_y;
}

Touch* TouchPad::getTouchByID(int id) {
    for (int i = 0; i < MAX_TOUCHES; i++) {
        if (Touches[i].isActive && Touches[i].id == id)
            return &Touches[i];
    }
    return NULL;
}

Touch* TouchPad::findTouch(int id) {
    if (!IsAvailable)
        return NULL;
    for (int i = 0; i < MAX_TOUCHES; i++) {
        if (Touches[i].id == id)
            return &Touches[i];
        if (!Touches[i].isActive)    {
            Touches[i].id = id;
            return &Touches[i];
        }
    }
    return NULL;
}

int    TouchPad::getTouchCount() const {
    if (!IsAvailable)
        return 0;
    int r = 0;
    for (int i = 0; i < MAX_TOUCHES; i++) {
        if (Touches[i].isActive) {
            r++;
        }
    }
    return r;
}

void TouchPad::update() {
    for (int i = 0; i < MAX_TOUCHES; i++) {
        Touches[i].isMoved = false;
    }
    if (IsAvailable) {
        s3ePointerUpdate();
    }
}

void TouchPad::clear() {
    for (int i = 0; i < MAX_TOUCHES; i++) {
        if (!Touches[i].isPressed) {
            Touches[i].isActive = false;
        }
        Touches[i].isMoved = false;
    }
}

bool TouchPad::init() {
    IsAvailable = s3ePointerGetInt(S3E_POINTER_AVAILABLE) ? true : false;
    if (!IsAvailable) return false;
    for (int i = 0; i < MAX_TOUCHES; i++) {
        Touches[i].isPressed = false;
        Touches[i].isActive = false;
        Touches[i].isMoved = false;
        Touches[i].id = 0;
    }
    IsMultiTouch = s3ePointerGetInt(S3E_POINTER_MULTI_TOUCH_AVAILABLE) 
                                    ? true : false;
    if (IsMultiTouch) {
        s3ePointerRegister(S3E_POINTER_TOUCH_EVENT, 
             (s3eCallback)HandleMultiTouchButton, NULL);
        s3ePointerRegister(S3E_POINTER_TOUCH_MOTION_EVENT, 
             (s3eCallback)HandleMultiTouchMotion, NULL);
    } else {
        s3ePointerRegister(S3E_POINTER_BUTTON_EVENT, 
             (s3eCallback)HandleSingleTouchButton, NULL);
        s3ePointerRegister(S3E_POINTER_MOTION_EVENT, 
             (s3eCallback)HandleSingleTouchMotion, NULL);
    }
    return true;
}

void TouchPad::release() {
    if (IsAvailable) {
        if (IsMultiTouch) {
            s3ePointerUnRegister(S3E_POINTER_TOUCH_EVENT, 
                (s3eCallback)HandleMultiTouchButton);
            s3ePointerUnRegister(S3E_POINTER_TOUCH_MOTION_EVENT, 
                (s3eCallback)HandleMultiTouchMotion);
        } else {
            s3ePointerUnRegister(S3E_POINTER_BUTTON_EVENT, 
                (s3eCallback)HandleSingleTouchButton);
            s3ePointerUnRegister(S3E_POINTER_MOTION_EVENT, 
                (s3eCallback)HandleSingleTouchMotion);
        }
    }
}

За основу этого кода была взята следующая статья. Здесь следует обратить внимание на код регистрации и разрегистрации Callback-вызовов в методах init и release. Сама по себе, логика обработки событий довольно тривиальна. Далее, внесем необходимые изменения в класс Desktop.

Desktop.h:

#ifndef _DESKTOP_H_
#define _DESKTOP_H_

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

using namespace std;

enum EMessageType {
    emtNothing                                                      = 0x00,

    emtTouchEvent                                                   = 0x10,
    emtTouchIdMask                                                  = 0x07,
    emtTouchMask                                                    = 0x70,
    emtMultiTouch                                                   = 0x14,
    emtTouchDown                                                    = 0x30,
    emtTouchUp                                                      = 0x50,
    emtTouchMove                                                    = 0x70,
    emtSingleTouchDown                                              = 0x30,
    emtSingleTouchUp                                                = 0x50,
    emtSingleTouchMove                                              = 0x70,
    emtMultiTouchDown                                               = 0x34,
    emtMultiTouchUp                                                 = 0x54,
    emtMultiTouchMove                                               = 0x74,

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

class Desktop {
    private:
        int width, height;
        bool isChanged;
        Scene* currentScene;
        bool isKeyAvailable;
        bool IsQuitMessageReceived;
        bool checkBounce(int id, int msg);
        void getScreenSizes();
        static int32 ScreenSizeChangeCallback(void* systemData, void* userData);
    public:
        Desktop(): touches() {}
        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);
        void sendQuitMessage() {IsQuitMessageReceived = true;}
        bool isQuitMessageReceived();
        set<int> touches;
    typedef set<int>::iterator TIter;
};
       
extern Desktop desktop;

#endif    // _DESKTOP_H_

Desktop.cpp:

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

#include "TouchPad.h"

Desktop desktop;

void Desktop::init() {
    IsQuitMessageReceived = false;
    getScreenSizes();
    setScene(NULL);
    isKeyAvailable = (s3eKeyboardGetInt(S3E_KEYBOARD_HAS_KEYPAD) 
                                     || s3eKeyboardGetInt(S3E_KEYBOARD_HAS_ALPHA));
    s3eSurfaceRegister(S3E_SURFACE_SCREENSIZE, ScreenSizeChangeCallback, NULL);
}

void Desktop::release() {
    s3eSurfaceUnRegister(S3E_SURFACE_SCREENSIZE, ScreenSizeChangeCallback);
    touches.clear();
}

int32 Desktop::ScreenSizeChangeCallback(void* systemData, void* userData) {
    desktop.isChanged = true;
    return 0;
}

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

void Desktop::getScreenSizes() {
    width = Iw2DGetSurfaceWidth();
    height = Iw2DGetSurfaceHeight();
    isChanged = false;
}

bool Desktop::checkBounce(int id, int msg) {
    TIter p = touches.find(id);
    if (TouchPad::isTouchDown(msg)) {
        if (p != touches.end()) return true;
        touches.insert(id);
    } else {
        if (p == touches.end()) return true;
        if (TouchPad::isTouchUp(msg)) {
            touches.erase(p);
        }
    }
    return false;
}

void Desktop::update(uint64 timestamp) {
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (checkBounce(t->id, msg)) return;
                if (currentScene != NULL) {
                    currentScene->sendMessage(msg | t->id, t->x, t->y);
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}

void Desktop::refresh() {
    if (currentScene != NULL) {
        currentScene->refresh();
    }
}
bool Desktop::isQuitMessageReceived() {
    if (s3eDeviceCheckQuitRequest()) {
        return true;
    }
    return IsQuitMessageReceived;
}


Здесь мы добавили перечисление EMessageType, определив коды системных событий и добавили обработку событий в метод update. Стоит обратить внимание на метод checkBounce, с помощью которого мы избавляемся от "дребезга" при обработке событий TouchPad-а. Также, не следует забывать о необходимости вызова s3eKeyboardUpdate, в том случае если клавиатура доступна в используемой нами аппаратной конфигурации. Помимо сказанного, в Desktop добавлен обработчик событий изменения размеров экрана (например в результате изменения его ориентации). Файл Main.cpp изменяется незначительно:

#include "Main.h"

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

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

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

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

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

void release() {
    desktop.release();
    touchPad.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 (!desktop.isQuitMessageReceived()) {
            // Update keyboard system
            s3eKeyboardUpdate();
            // Update
            touchPad.update();
            desktop.update(s3eTimerGetMs());
            // Clear the screen
            IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);
            touchPad.clear();
            // Refresh
            desktop.refresh();
            // Show the surface
            Iw2DSurfaceShow();
            // Yield to the opearting system
            s3eDeviceYield(duration);
        }
    }
    release();
    return 0;
}


Здесь добавлены вызовы инициализации и деинициализации TouchPad и изменена логика обработки завершения работы приложения. Мы убрали непосредственную проверку нажатия кнопки s3eKeyAbsBSK и заменили вызов s3eDeviceCheckQuitRequest в условии цикла на вызов метода Desktop::isQuitMessageReceived. Также в основной цикл приложения добавлены вызовы методов Touchpad::update и clear. Важную роль, в обработке событий клавиатуры, будет играть измененный класс Scene:

Scene.h:

#ifndef _SCENE_H_
#define _SCENE_H_

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

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

using namespace std;

class Scene: public AbstractSpriteOwner {
    private:
        AbstractScreenObject* background;
        map<s3eKey, int> keys;
        bool isInitialized;
        uint64 lastTime;
    protected:
        virtual bool doKeyMessage(int msg, s3eKey key) {return false;}
        virtual void regKey(s3eKey key);
    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);

    typedef map<s3eKey, int>::iterator KIter;
    typedef pair<s3eKey, int> KPair;
};

#endif    // _SCENE_H_

Scene.cpp:

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

Scene::Scene(): AbstractSpriteOwner()
              , isInitialized(false)
              , background(NULL)
              , keys()
              , lastTime(0) {
//  regKey(s3eKeyLSK);
    regKey(s3eKeyBack);
    regKey(s3eKeyAbsBSK);
}

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::regKey(s3eKey key) {
    keys.insert(KPair(key, 0));
}

void Scene::update(uint64 timestamp) {
    for (KIter p = keys.begin(); p != keys.end(); ++p) {
        int msg = 0;
        int32 keyState = s3eKeyboardGetState(p->first);
        if (keyState & (S3E_KEY_STATE_DOWN | S3E_KEY_STATE_PRESSED)) {
            msg = emtKeyDown;
            if (p->second == 0) {
                msg = emtKeyPressed;
                p->second = 1;
            }
        }
        if (keyState == S3E_KEY_STATE_UP) {
            if (p->second == 1) {
                msg = emtKeyReleased;
                p->second = 0;
            }
        }
        if (msg != 0) {
            if (doKeyMessage(msg, p->first)) {
                lastTime = timestamp;
            } else {
                if (timestamp - lastTime >= 1000) {
                    lastTime = 0;
                }
                if ((lastTime == 0)&&(msg == emtKeyPressed)) {
                    switch (p->first) {
//                      case s3eKeyLSK:
                        case s3eKeyBack:
                        case s3eKeyAbsBSK:
                                desktop.sendQuitMessage();
                                break;
                    }
                }
            }
        }
    }
    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;
}


Можно видеть, что метод update сильно раззросся по сравнению с предыдущей версией. Для каждой клавиши, занесенной в список прослушиваемых клавиш keys, мы проверяем состояние, формируем события клавиатуры и передаем переопределяемому методу doKeyMessage. Если этот метод не обрабатывает полученные события, мы выполняем обработку по умолчанию для s3eKeyBack и s3eKeyAbsBSK, заключающуюся в вызове метода desktop.sendQuitMessage, приводящего к завершению приложения. С целью защиты от "дребезга" мы не выполняем обработку по умолчанию в случае, если сцена успешно обработала клавиатурное событие менее 1 секунды назад. 
Обработка s3eKeyLSK добавлена исключительно в отладочных целях. По неведомым мне причинам, разработчики Marmalade не предусмотрели в эмуляторе маппинг s3eKeyAbsBSK (либо я его не нашел). Возможно это будет исправлено в следующих версиях.
Для завершения статьи, нам осталось добавить обработку позиционных событий в класс 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;
        int capturedId;
    public:
        Sprite(ISpriteOwner* owner, int x, int y , int zOrder = 0);
        Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder = 0);
        virtual ~Sprite();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual bool sendMessage(int msg, int x, int y);
        virtual void update(uint64 timestamp) {}
        virtual void refresh();
        virtual void addImage(const char*res, int state = 0);
        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"
#include "Desktop.h"

Sprite::Sprite(ISpriteOwner* owner, int x, int y , int zOrder): 
                                                        AbstractScreenObject(x, y)
                                                        , owner(owner)
                                                        , capturedId(-1)
                                                        , 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)
                                                        , capturedId(-1)
                                                        , 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);
}

bool Sprite::sendMessage(int msg, int x, int y) {
    if ((msg & emtTouchEvent) != emtTouchEvent) return false;
    if (!isVisible) return false;
    int id = msg & emtTouchIdMask;
    msg &= emtTouchMask;
    if (capturedId >= 0) {
        if (id != capturedId) return false;
        if (msg == emtTouchDown) {
            capturedId = -1;
        }
    }
    if (capturedId < 0) {
        int X = owner->getXSize(owner->getXPos(getXPos()));
        int Y = owner->getYSize(owner->getYPos(getYPos()));
        if ((x < X)||(y < Y)) return false;
        X += owner->getXSize(getWidth());
        Y += owner->getYSize(getHeight());
        if ((x > X)||(y > Y)) return false;
    }
    switch (msg) {
        case emtTouchDown:
            capturedId = id;
            break;
        case emtTouchUp:
            capturedId = -1;
            break;
    }
    if (isBuzy()) {
        return true;
    }
    return sendMessage(msg) ||
           owner->sendMessage(msg);
}

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


В обработчике позиционного события sendMessage мы проверяем попадает ли точка касания в регион спрайта и если да, передаем непозиционное событие с тем-же кодом спрайту. Напомню, что в AbstractSpriteOwner::update мы поочередно вызываем sendMessage для всех спрайтов кроме Background-а, в порядке обратном Z-order (используемому для отрисовки спрайтов).
Для событий отпускания касания используется специальная обработка. Получив событие emtTouchDown. Sprite запоминает id-касания и передает последующие события с тем-же id в обработчик того-же спрайта, независимо от координат касания. Это позволяет корректно обработать следующий кейс:

1. Пользователь касается экранной кнопки
2. Пользовователь перемещает точку касания за пределы кнопки (не разрывая касания)
3. Пользователь разрывает касание за пределами экранной кнопки

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

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

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

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