第四讲:游戏中的状态机

来源:互联网 发布:朋友圈链接制作软件 编辑:程序博客网 时间:2024/06/06 05:12

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


游戏中的状态机设计

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<string> _states; 用这个保存所有的状态,这里不应该有两个状态名字相同。
map<string, map<string, string>> _events; 用于保存所有的事件,形式为<eventName, <from, to>>
map<string, function<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<std::string> _states;
    std::unordered_map<std::string,std::unordered_map<std::string,std::string>> _events;
    std::unordered_map<std::string,std::function<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搭建一个横版过关游戏

0 0
原创粉丝点击