第四讲:游戏中的状态机

索引篇:《第一讲:创建项目&给游戏添加角色》


游戏中的状态机设计

Quick-Cocos2d-x内置了对状态机的支持,所以这里的状态机就要自己想办法了,初步的想法是设计一个状态机对象,然后让Player类持有一个状态机对象。当然也可以让Player继承状态机对。不过我们先考虑用组合的方法把。


状态机的必备构件:

1. 状态(State)

这里的状态有  idle,walking,attacking, dead 等。

先假设他们是互斥的。虽然一边walking一边attacking也是可能的。


2. 事件(Event)

可以理解为指令,即要求满足一定条件的状态机改变状态到指定态。

例如:

{name="walk", from="idle", to="walking"}

如果令状态机执行这个事件,则当其处于idle状态时,会变化至walking态。

所以状态机对象需要保存所有状态,以及所有的事件,以供使用。


3.动作(Action)

例如在进入dead状态后,角色需要播放dead动画,并移除自身。

每个状态都要提供一个函数如onIdleEnter,在进入这个态时调用,当然也可为空。

按理说退出一个状态也应该调用一个函数,如onIdleExit,不过我们暂时可以不用这个。


状态和事件是否需要单独设计class?如果是class是否要继承Ref?纠结了半天,也写了下Event类和State类,感觉直接用字符串表示状态也是可行的。所以果断删了,直接用字符串。

1
2
3
set _states; 用这个保存所有的状态,这里不应该有两个状态名字相同。
map> _events; 用于保存所有的事件,形式为>
map void ()>> _onEnters;  保存每个态的回调函数,如果不为空就在进入状态时调用这个函数。

这个函数做什么用呢?当然是状态转换后的行为控制了。例如_onEnters["idle"]可以负责停止所有帧动画的播放。

_onEnters["dead"]让角色播放死亡动画,然后处理后事等等。

然后还需要保存当前状态,前一个状态。


折腾了半天,看了网上的资料,发现状态机也可以挺复杂,也参考了别人的简易状态机,还有状态机的数学语言定义等等。又发现了C++里的map容器可以用unordered_map,他的性能测试,set容器用法,map插入内容的方法。总算弄出一个能用的。


头文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifndef __FSM__
#define __FSM__
  
#include "cocos2d.h"
  
class  FSM : public  cocos2d::Ref
{
public :
  
     bool  init();
     //Create FSM with a initial state name and optional callback function
     static  FSM* create(std::string state, std::function< void ()> onEnter = nullptr);
      
     FSM(std::string state, std::function< void ()> onEnter = nullptr);
     //add state into FSM
     FSM* addState(std::string state, std::function< void ()> onEnter = nullptr);
     //add Event into FSM
     FSM* addEvent(std::string eventName, std::string from, std::string to);
     //check if state is already in FSM
     bool  isContainState(std::string stateName);
     //print a list of states
     void  printState();
     //do the event
     void  doEvent(std::string eventName);
     //check if the event can change state
     bool  canDoEvent(std::string eventName);
     //set the onEnter callback for a specified state
     void  setOnEnter(std::string state, std::function< void ()> onEnter);
private :
     //change state and run callback.
     void  changeToState(std::string state);
private :
     std::set _states;
     std::unordered_map> _events;
     std::unordered_map void ()>> _onEnters;
     std::string _currentState;
     std::string _previousState;
};
  
#endif


现在不妨做个测试,可以先写到init里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bool  FSM::init()
{
     this ->addState( "walking" ,[](){cocos2d:: log ( "Enter walking" );})
         ->addState( "attacking" ,[](){cocos2d:: log ( "Enter attacking" );})
         ->addState( "dead" ,[](){cocos2d:: log ( "Enter dead" );});
  
     this ->addEvent( "walk" , "idle" , "walking" )
         ->addEvent( "walk" , "attacking" , "walking" )
         ->addEvent( "attack" , "idle" , "attacking" )
         ->addEvent( "attack" , "walking" "attacking" )
         ->addEvent( "die" , "idle" , "dead" )
         ->addEvent( "die" , "walking" , "dead" )
         ->addEvent( "die" , "attacking" , "dead" )
         ->addEvent( "stop" , "walking" , "idle" )
         ->addEvent( "stop" , "attacking" , "idle" )
         ->addEvent( "walk" , "walking" , "walking" );
  
     this ->doEvent( "walk" );
     this ->doEvent( "attack" );
     this ->doEvent( "eat" );
     this ->doEvent( "stop" );
     this ->doEvent( "die" );
     this ->doEvent( "walk" );
     return  true ;
}

在MainScene::init中加入:

1
auto fsm = FSM::create( "idle" ,[](){cocos2d:: log ( "Enter idle" );});


运行输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FSM::doEvent: doing event walk
FSM::changeToState: idle -> walking
Enter walking
FSM::doEvent: doing event attack
FSM::changeToState: walking -> attacking
Enter attacking
FSM::doEvent: cannot  do  event eat
FSM::doEvent: doing event stop
FSM::changeToState: attacking -> idle
Enter idle
FSM::doEvent: doing event die
FSM::changeToState: idle -> dead
Enter dead
FSM::doEvent: cannot  do  event walk
  • 第一个walk Event成功,idle -> walking

  • 第二个attack Event成功,walking -> attacking

  • 第三个eat Event失败,因为我们没有定义eat Event

  • 第四个stop Event成功,attacking -> idle 

  • 第五个die Event 成功,idle -> dead 

  • 第六个walk Event失败,这也是我们期望的,因为死了之后不应该还能行走。


下面应该考虑在player中使用FSM, 可以新建一个私有成员持有一个实例。在尝试过程中出了点故障,好久才搞定,原来是FSM create之后我没有retain,访问出问题了。既然要retain,那就别忘了release。


我们先把以前的walkTo改变一下,让他用状态机来实现。

1
2
3
4
5
6
void  Player::walkTo(Vec2 dest)
{
     std::function< void ()> onWalk = CC_CALLBACK_0(Player::onWalk,  this , dest);
     _fsm->setOnEnter( "walking" , onWalk);
     _fsm->doEvent( "walk" );
}

即现在是委托"walking"状态的回调函数来进行动作,回调函数是由另一个函数Player::onWalk bind得到的。这个函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void  Player::onWalk(Vec2 dest)
{
     log ( "onIdle: Enter walk" );
     this ->stopActionByTag(WALKTO_TAG);
     auto curPos =  this ->getPosition();
  
     if (curPos.x > dest.x)
         this ->setFlippedX( true );
     else
         this ->setFlippedX( false );
  
     auto diff = dest - curPos;
     auto  time  = diff.getLength()/_speed;
     auto move = MoveTo::create( time , dest);
     auto func = [&]()
     {
         this ->_fsm->doEvent( "stop" );
     };
     auto callback = CallFunc::create(func);
     auto seq = Sequence::create(move, callback, nullptr);
     seq->setTag(WALKTO_TAG);
     this ->runAction(seq);
     this ->playAnimationForever(0);
}


这个函数和原来的walkTo基本一样除了:

1
2
3
4
auto func = [&]()
{
     this ->_fsm->doEvent( "stop" );
};

这里的回调函数会使用状态机,将角色回到idle状态,而idle的回调函数会停止播放动画。


另外在上面的代码中有一句: 

1
->addEvent( "walk" , "walking" , "walking" );

这个的作用是允许在从walking状态转换到walking状态,当点击屏幕时,walk的目的发生变化,即使在walking中也应该即刻改变目标。


现在的情况好像和之前一样,不一样的是现在用的是状态机。


推荐阅读:

【系列原创教程】使用Quick-Cocos2d-x搭建一个横版过关游戏

你可能感兴趣的:(cocos2dx)