Показаны сообщения с ярлыком Bugfixing. Показать все сообщения
Показаны сообщения с ярлыком Bugfixing. Показать все сообщения

вторник, 25 декабря 2012 г.

Сделай паузу

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

Речь пойдет о реализации режима паузы. Я думаю, эта возможность будет полезна практически в любой игре. При активации паузы, весь игровой процесс должен на неопределенное время замирать, а при ее снятии, продолжаться с того-же места, где он остановился. В общем-то понятно, что на время паузы, анимированные спрайты должны приостановить всю обработку в методе update, но Дьявол, как всем известно, в деталях.

Начнем с интерфейсов. В метод IObject.update добавим флаг isPaused:

class IObject {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused) = 0;
};

В AbstractSpriteOwner добавиться аналогичный флаг и метод, который мы будем вызывать для корректировки таймстампов анимированных объектов при завершении паузы:

class AbstractSpriteOwner: public ISpriteOwner {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused);
        virtual void correctPauseDelta(uint64 pauseDelta);
};


Реализация метода correctPauseDelta тривиальна. Мы просто передаем продолжительность паузы в миллисекундах всем вложенным спрайтам:

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


В реализации AnimatedSprite мы добавляем полученную продолжительность паузы ко всем таймстампам текущих анимаций. Также мы добавляем досрочный выход из метода update в случае, если действует режим паузы:

void AnimatedSprite::addPauseDelta(uint64 pauseDelta) {
    for (CIter p = currentMessages.begin(); p != currentMessages.end(); ++p) {
        p->timestamp += pauseDelta;
    }
}

void AnimatedSprite::update(uint64 timestamp, bool isPaused) {
    if (isPaused && isPausable()) return;
         ...
}


О методе isPausable следует сказать особо. Мы должны иметь возможность разделять обычные объекты и объекты анимация которых (а также обработка ими событий) не останавливается в режиме паузы. К таким объектам, например, должно относиться меню, содержащее кнопку выхода из состояния паузы (в противном случае, включив паузу, мы никогда не сможем ее выключить). Определим реализацию метода isPausable по умолчанию в AbstractScreenObject и, при необходимости, будем переопределять ее в тех его наследниках, отключать анимацию которых не стоит:

class AbstractScreenObject: public IScreenObject {
    public:
                   ...
        virtual void addPauseDelta(uint64 pauseDelta) {}
        virtual bool isPausable() const {return true;}
};


Чтобы внести изменения в CompositeSprite, необходимо помнить, что он, с одной стороны, является контейнером спрайтов, в то время как с другой, сам является наследником AnimatedSprite и может обрабатывать правила анимации:

void CompositeSprite::addPauseDelta(uint64 pauseDelta) {
    AbstractSpriteOwner::correctPauseDelta(pauseDelta);
    AnimatedSprite::addPauseDelta(pauseDelta);
}

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


Основную работу по приостановке и возобновлению анимации возьмет на себя реализация Scene.

Scene.h:

class Scene: public AbstractSpriteOwner {
    protected:
                  ...
        bool IsPaused;
        uint64 pauseTimestamp;
    public:
                  ...
        virtual void update(uint64 timestamp, bool isPaused);
        bool isPaused() const {return IsPaused;}
        void suspend();
        void resume();
};

Scene.cpp:

void Scene::update(uint64 timestamp, bool) {
    if (IsPaused && (pauseTimestamp == 0)) {
        pauseTimestamp = pauseTimestamp;
    }
    if (!IsPaused && (pauseTimestamp != 0)) {
        uint64 pauseDelta = timestamp - pauseTimestamp;
        if (pauseDelta > 0) {
            correctPauseDelta(pauseDelta);
        }
        pauseTimestamp = 0;
    }
         ...
    AbstractSpriteOwner::update(timestamp, IsPaused);
}

bool Scene::sendMessage(int id, int x, int y) {
    if (AbstractSpriteOwner::sendMessage(id, x, y)) {
        return true;
    }
    if (IsPaused) return false;
    if (background != NULL) {
        return background->sendMessage(id, x, y);
    }
    return false;
}

void Scene::suspend() {
    desktop.suspend();
    IsPaused = true;
}

void Scene::resume() {
    desktop.resume();
    IsPaused = false;
}


Осталось добавить код приостановки фоновой музыки в класс Desktop.


Desktop.h:

class Desktop {
    private:
                   ...
        bool isMusicStarted;
    public:
        Desktop(): touches(), names(), currentScene(NULL), isMusicStarted(false) {}
                  ...
        void startMusic(const char* res);
        void stopMusic();
        void suspend();
        void resume();
};

Desktop.cpp:

void Desktop::startMusic(const char* res) {
    if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
        s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
        s3eAudioPlay(res, 0);
    isMusicStarted = true;
}

void Desktop::stopMusic() {
    isMusicStarted = false;
    s3eAudioStop();
}

void Desktop::suspend() {
    if (isMusicStarted) {
        s3eAudioPause();
    }
}

void Desktop::resume() {
    if (isMusicStarted) {
        s3eAudioResume();
    }
}


Теперь мы умеем останавливать время в нашем приложении :)

Проект на GitHub обновлен.

суббота, 10 ноября 2012 г.

Работа над ошибками

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

Я не сразу это заметил, а затем еще дольше искал, в чем ошибка. Ошибка здесь:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = 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 & emtTouchIdMask), 
                                              t->x, t->y);
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Как всегда, все произошло из за того, что "хотелось как лучше". Дело в том, что если не предпринимать каких-то дополнительных действий, при нажатии, Touchpad (имеется в виду модуль проекта) посылает целую последовательность событий "нажатие", там где кнопке хватило бы всего одного. В результате, кнопка начинала "дребезжать", быстро нажимаясь и отжимаясь. Чтобы этого избежать, была добавлена функция checkBounce, формирующая правильную последовательность событий, убирая лишние. В процессе поиска ошибки, ее код был перепроверен, наверное, раз десять. А ошибка была совсем не там.

Вот правильный код:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = 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)) {
                   if (currentScene != NULL) {
                       currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                                 t->x, t->y);
                   }
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Найдите десять отличий ;)

Для тех, кто не следил за циклом статей, как всегда, ссылка на исправленный проект.

P.S. В эти выходные закончился Evaluation период у Мармелада. Я решил его продлить, теперь уже за денежку :) Считаю, что $149 совсем не дорого за такое удовольствие.