笨木头  2012-12-20 12:57     Cocos2d-x,Cocos2d-x2.0     阅读(9022)     评论(22)
转载请注明,原文地址: http://www.benmutou.com/archives/43
文章来源:笨木头与游戏开发

Cocos2d-x 状态机篇】第04章--事件驱动,你想象不到的强大

 

笨木头花心贡献,啥?花心?不呢,是用心~

转载请注明,原文地址
 http://blog.csdn.net/musicvs/article/details/8349314

 

正文:

 

 

到现在为止,我们已经有能力实现简单的有限状态机了,但是,大家有没有发现一个问题?

(旁白:貌似我不止发现一个问题= =

 

那就是,我们必须主动地在update函数里检测状态的变化,但是,并不是每时每刻对象的状态都会改变的。这种主动检测状态改变的方式,其实十分不妥当,这会造成CUP的压力比较大。

(旁白:是CPU吧?拜托~!)

 

那么,现在我要向大家介绍一种十分美妙的技术,这无论是在应用开发还是游戏开发里,都是十分有用的。那就是,事件驱动。

很幸运,在Cocos2d-x里,已经集成了实现事件驱动的功能了,那就是CCNotificationCenter类,我更喜欢称之为“消息派发”。我建议对这方面不熟悉的朋友,先百度一下“观察者模式”、“订阅者”等关键词,以及了解关于CCNotificationCenter的使用。

那么,现在,我们要把我们第02章里的程序改为消息驱动的形式了~

(旁白:废话终于结束了。。。)

 

1. 彻底抛弃update函数,新的状态机

/*
    文件名:    MutouTFSM.h
    描 述:    木头对象的状态机,用来管理状态
    创建人:    笨木头 (CSDN博客:http://blog.csdn.net/musicvs)

  创建日期:   2012.12.19
    修改日期:   2012.12.20
*/
#ifndef __MUTOUT_FSM_H__
#define __MUTOUT_FSM_H__

#include "cocos2d.h"
USING_NS_CC;

class I_State;
class MutouT;

class MutouTFSM : public CCNode {
public:
    ~MutouTFSM();
        
    static MutouTFSM* createWithMutouT(MutouT* mutou);
    bool initWithMutouT(MutouT* mutou);
    
    void changeState(I_State* state);   /* 切换状态 */

private:
    void onRecvWantToRest(CCObject* obj);
    void onRecvWantToWriteCode(CCObject* obj);
    void onRecvWantToWriteArticle(CCObject* obj);

    /* 存放当前状态类 */
    I_State* mCurState;

    /* 木头对象 */
    MutouT* mMutou;
};

#endif

 

这是新的状态机类,发现什么不一样了吗?

(旁白:不就是update函数删掉了,然后多了一个析构函数么= =很特别吗?)

 

这其实不是重点(可恶的旁白竟然答对了),重点看看cpp文件:

#include "MutouTFSM.h"
#include "MutouT.h"
#include "I_State.h"
#include "EnumMsgType.h"

#define NOTIFY CCNotificationCenter::sharedNotificationCenter()

MutouTFSM::~MutouTFSM() {
    NOTIFY->removeObserver(this, "wantToRest");
    NOTIFY->removeObserver(this, "wantToWriteCode");
    NOTIFY->removeObserver(this, "wantToWriteArticle");
}

MutouTFSM* MutouTFSM::createWithMutouT( MutouT* mutou ) {
    MutouTFSM* fsm = new MutouTFSM();

    if(fsm && fsm->initWithMutouT(mutou)) {
        fsm->autorelease();
    }
    else {
        CC_SAFE_DELETE(fsm);
        fsm = NULL;
    }

    return fsm;
}

bool MutouTFSM::initWithMutouT( MutouT* mutou ) {
    this->mCurState = NULL;
    this->mMutou = mutou;
    mMutou->retain();

    /* 订阅消息 */
    NOTIFY->addObserver(this,callfuncO_selector(MutouTFSM::onRecvWantToRest) , "wantToRest", NULL);
    NOTIFY->addObserver(this,callfuncO_selector(MutouTFSM::onRecvWantToWriteCode) , "wantToWriteCode", NULL);
    NOTIFY->addObserver(this,callfuncO_selector(MutouTFSM::onRecvWantToWriteArticle) , "wantToWriteArticle", NULL);
    return true;
}

void MutouTFSM::changeState( I_State* state ) {
    CC_SAFE_DELETE(mCurState);

    this->mCurState = state;
    
}
void MutouTFSM::onRecvWantToRest( CCObject* obj ) {
    this->mCurState->execute(mMutou, en_Msg_WantToRest);
}

void MutouTFSM::onRecvWantToWriteCode( CCObject* obj ) {
    this->mCurState->execute(mMutou, en_Msg_WantToWriteCode);
}

void MutouTFSM::onRecvWantToWriteArticle( CCObject* obj ) {
    this->mCurState->execute(mMutou, en_Msg_WantToWriteArticle);
}

首先,为了避免代码过长,我做了一个宏定义:

#define NOTIFY CCNotificationCenter::sharedNotificationCenter()

我们来看看,状态机总共订阅了3个消息:wangToRestwantToWriteCodewantToWriteArticle。为了偷懒,我直接把字符串硬编码了。

再来看看收到消息后,状态机是怎么处理的:

void MutouTFSM::onRecvWantToRest( CCObject* obj ) {
    this->mCurState->execute(mMutou, en_Msg_WantToRest);
}

很简单,很以前一样,调用当前状态的execute方法,但这次多了一个参数,那就是消息类型。看看消息类型的枚举:

#ifndef __ENUM_MSG_TYPE_H__
#define __ENUM_MSG_TYPE_H__

enum EnumMsgType {
    en_Msg_WantToRest,
    en_Msg_WantToWriteCode,
    en_Msg_WantToWriteArticle,
};

#endif

(旁白:感觉好麻烦= =

 

 

大家没有觉得这样好麻烦吗?

(旁白:你就忽略我吧,我习惯了。。。)

 

其实我也觉得好麻烦,但是Cocos2d-xCCNotificationCenter只能这么用,它是使用函数指针的方式来回调进行消息发布,而不是使用接口。所以状态机每订阅一个消息就要多写一个函数来接收消息。

更可恶的是,订阅消息的时候,消息类型只能用字符串,不能用枚举值~!所以状态机在传递事件给状态类的时候,只能再建一套枚举类型了。如果大家有更好的方法记得和我分享~

 

2. 状态类做小改动

然后,状态类的execute函数也要增加一个参数,就是消息类型:

/*
    文件名:    State.h
    描 述:    状态基类
    创建人:    笨木头 (CSDN博客:http://blog.csdn.net/musicvs)

    创建日期:   2012.12.17
    修改日期:   2012.12.20
*/
#ifndef __I_STATE_H__
#define __I_STATE_H__

#include "EnumMsgType.h"

class MutouT;

class I_State {
public:
    virtual void execute(MutouT* mutou, EnumMsgType enMsgType) = 0;
};

#endif


 

很简单,不解释了,其它的状态子类也要做相应的修改。

然后,我们来看看3种状态类的execute函数的新实现:

void StateRest::execute( MutouT* mutou, EnumMsgType enMsgType ) {
    switch(enMsgType) {
    case en_Msg_WantToWriteCode:
        mutou->writeCode();
        mutou->getFSM()->changeState(new StateWirteCode());
        break;
    case en_Msg_WantToWriteArticle:
        mutou->writeArticle();
        mutou->getFSM()->changeState(new StateWriteArticle());
        break;
    }
}

void StateWriteArticle::execute( MutouT* mutou, EnumMsgType enMsgType ) {
    switch(enMsgType) {
    case en_Msg_WantToRest:
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
        break;
    }
}

void StateWirteCode::execute( MutouT* mutou, EnumMsgType enMsgType ) {
    switch(enMsgType) {
    case en_Msg_WantToRest:
        mutou->rest();
        mutou->getFSM()->changeState(new StateRest());
        break;
    }
}


 

看到了吗?

(旁白:看到了,看到了= =你快点说吧,别这么唠叨了。。。)

 

每个状态类的execute函数已经不需要再主动去判断木头当前在做什么或者想做什么或者是什么状况。它只要知道当前发生了什么事件,它只关心它所关心的事件,在特定的事件下改变木头的状态即可。

 

 

3. 测试新代码

是时候测试一下了,继续修改HelloWorldinit函数:

bool HelloWorld::init()
{
    bool bRet = false;
    do 
    {
        CC_BREAK_IF(! CCLayer::init());

        /* 新建木头2角色 */
        mMutou = MutouT::create();

        /* 初始化木头的状态为休息 */
        mMutou->getFSM()->changeState(new StateRest());

        /* 模拟事件的发生 */
        NOTIFY->postNotification("wantToWriteCode");
        NOTIFY->postNotification("wantToRest");
        NOTIFY->postNotification("wantToWriteArticle");
        NOTIFY->postNotification("wantToRest");
        NOTIFY->postNotification("wantToWriteArticle");
        NOTIFY->postNotification("wantToRest");
        NOTIFY->postNotification("wantToWriteCode");
        NOTIFY->postNotification("wantToRest");

        this->addChild(mMutou);
        bRet = true;
    } while (0);

    return bRet;
}


 

由于事件的发生是随机的,要产生这些随机的事件,可不是一两段代码就能搞定的,所以,我直接用最简单的方式,主动发送这些事件消息。

然后,用debug模式运行项目,我们将看到熟悉的日志:

mutou is wirting Code.

mutou is resting.

mutou is writing article.

mutou is resting.

mutou is writing article.

mutou is resting.

mutou is wirting Code.

mutou is resting.

 

4. 最后的最后

好了,我想,我对有限状态机的介绍就到这里了,其实我对有限状态机的理解也不是十分深刻,我也是现学现卖,一方面是巩固自己的知识,另一方面我确实很喜欢和大家分享知识。

也许通过这四篇文章的介绍大家还是无法感受到状态机倒底有多强大,但我可以肯定地告诉大家,它真的很强大。比如,我描述这样一个场景:

一个拥有超能力的木头,它所经过的地方周围的物体都会弹开,于是,它一遍走,周围的物体一遍弹。十分美妙的场景~

(旁白:美你个木头= =。。。)

这依旧可以使用有限状态机来实现,木头经过的时候可以发出“我来了,我的能力范围是方圆10米”,然后周围的物体订阅了这个消息,在接收到这个消息的时候,就由静止状态改变为弹射状态,然后物体就执行弹射动作。而这一切看起来就像是自动完成的。

好了,已经对状态机理解深刻的朋友,希望能给我一些建议,对状态机不了解的朋友,希望能帮到你们。好吧,旁白出来清场~

(旁白:清你可爱的妹纸的= =

(旁白:OK,大家排好队,一个接一个地离场...额,我为毛要帮他清场啊~!)

 

本章项目源码:http://download.csdn.net/detail/musicvs/4912216

 

22 条评论
  • 鞭程 2014-03-26 10:20:26

    博主,这有限状态机能理解为是面向对象的抽象运用吗?
    0回复
    • 博主 糟糕_树叶的mut 2014-03-26 11:08:15

      应该说是多态,贴切一些~
      0回复
  • CCNotificationCenter事件监听器|皂荚花 2014-03-11 13:35:56

    […] CCNotificationCenter事件监听器也好,观察者模式也罢,这个东西的用处就是用在俩个类通信的时候,一个类用来发送消息,一个类用来接收消息。其实这个东西的用处是非常大的,举个例子吧。比如一片草地,会在不同的季节反应出不同的状态,春天的时候草长了出来,都是绿的,夏天的时候草长的更高了,秋天的时候又会变颜色,在四季更换的不同状态下,草的颜色也会跟着发生改变,也就是会对应不同的状态,这个天气信息就可以看成发送过来的消息,而对这些消息的处理就是状态之间的相互切换。我也不知道说的正确不正确,但是推荐给大家一篇博客,不理解的可以看看。事件驱动,你想象不到的强大。下面我来说说怎么用这个东西,以及用的时候要注意的一些问题。 […]
    0回复
  • レイバン サングラスアウトドアーズマン2 2013-07-25 18:54:27

    レイバン サングラスアウトドアーズマン2...

    Master Who Is Concerned About men....
    0回复
  • musicvs 2012-12-21 08:16:00

    补上项目源码: http://download.csdn.net/detail/musicvs/4912216
    0回复
  • raaland 2012-12-24 01:20:00

    虽然是由浅入深,我还是被渐渐绕晕了
    嗯,要再看一遍
    0回复
  • musicvs 2012-12-24 08:05:00

    [reply]raaland[/reply]
    感谢支持, 那我还要继续改善我的表述能力才行~
    0回复
  • libgdx2 2013-01-08 12:13:00

    楼主请问为什么我用在主线程 add CCNotificationCenter
    然后在子线程post 消息 的时候 CCNotificationCenter回调的方法是在子线程的 但是在子线程切换scene 有会报错 ,请问楼主有什么解决方法吗
    0回复
  • musicvs 2013-01-08 12:21:00

    [reply]libgdx2[/reply]
    多线程的情况我没有试过呢...
    为什么要用多线程呢?我平时都不太敢用多线程,总是会有很多糟糕的问题发生,呵呵~
    0回复
  • libgdx2 2013-01-08 12:42:00

    [reply]musicvs[/reply]
    因为我这个是一个网游的客户端 要一直监听服务端发过来的消息,那如果不用多线程 请问你有什么其他的办法能一直监听服务器端发过来的消息吗
    0回复
  • libgdx2 2013-01-08 12:44:00

    [reply]libgdx2[/reply]
    我是想子线程收到服务器端的消息之后 然后做一些判断之后
    就切换scene
    0回复
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发