Cocos2dx 3.2 横版过关游戏Brave学习笔记(四)

作者: douxt

前几天我把笔记写的差不多了,突然觉得可以发到论坛和大家分享一下,感觉学习cocos2d的新人很多,能相互学习一下还是挺好的。

在发第一个帖子的时候出了点问题,有些文字莫名其妙的成了斜体……我查了一下,原来是代码中有数组用了下标i,这个[ i],被论坛识别为斜体标记了....晕

刚发现上次写的代码存在问题,即在角色正在行走中,如果再次点击,PlayAnimationForever会再次运行,出现了多个动画同时播放的情况。如果想要播放的和正在进行的动画相同,不应该停止然后在重新开始,因为这样会导致不连贯。所以我加了检查,通过getActionByTag检查是否正在进行所需的action,如果正在进行,则不做任何操作。好像原版也有这个问题。

不同的帧动画不应该同时播放,所以在播放一个帧动画之前,可以通过stopActionByTag把所有帧动画都停止掉。

另外,我移除了私有变量_seq,增加了一个枚举用于标记动作Tag。在walkTo中,用检查Tag方法来防止动作的重复进行。之前用Tag停止动作失败的原因,很有可能是不同的动作用了相同的Tag。

接下来我提交一下代码,名为"fix animation superposition" 。哈哈,好像提交代码上瘾了啊。

游戏中的状态机设计

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类,感觉直接用字符串表示状态也是可行的。所以果断删了,直接用字符串。

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插入内容的方法……总算弄出一个能用的。

头文件如下:

#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<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里。

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中加入:

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

运行输出如下:


复制代码
  1. FSM::doEvent: doing event walk
  2. FSM::changeToState: idle -> walking
  3. Enter walking
  4. FSM::doEvent: doing event attack
  5. FSM::changeToState: walking -> attacking
  6. Enter attacking
  7. FSM::doEvent: cannot do event eat
  8. FSM::doEvent: doing event stop
  9. FSM::changeToState: attacking -> idle
  10. Enter idle
  11. FSM::doEvent: doing event die
  12. FSM::changeToState: idle -> dead
  13. Enter dead
  14. 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改变一下,让他用状态机来实现。

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得到的。
这个函数如下:

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基本一样除了:

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

这里的回调函数会使用状态机,将角色回到idle状态,而idle的回调函数会停止播放动画。
另外在上面的代码中有一句:
->addEvent("walk","walking","walking");
这个的作用是允许在从walking状态转换到walking状态,当点击屏幕时,walk的目的发生变化,即使在walking中也应该即刻改变目标。

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

然后 我做了一个名为Note 4 的commit.

标签: none

?>