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

来源:互联网 发布:cf手游刷枪软件ios 编辑:程序博客网 时间:2024/05/17 21:58
不知不觉竟然第4个笔记了,原因是我太懒了,完成一点点就弄一个笔记……
不过这也怪CSDN需要审核,如果一篇写太多,需要多次更改,那帖子可能一直处于审核阶段……而且作为一个博客竟然没有预览功能!另外博客中插入代码会自动帮你换行,如果你删掉换行,可能会导致提交的代码中混杂Html代码...

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

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

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

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


游戏中的状态机设计

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 functionstatic FSM* create(std::string state, std::function<void()> onEnter = nullptr);FSM(std::string state, std::function<void()> onEnter = nullptr);//add state into FSMFSM* addState(std::string state, std::function<void()> onEnter = nullptr);//add Event into FSMFSM* addEvent(std::string eventName, std::string from, std::string to);//check if state is already in FSMbool isContainState(std::string stateName);//print a list of statesvoid printState();//do the eventvoid doEvent(std::string eventName);//check if the event can change statebool canDoEvent(std::string eventName);//set the onEnter callback for a specified statevoid 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里。
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");});


输出如下:

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改变一下,让他用状态机来实现。
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);elsethis->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.


0 0
原创粉丝点击