索引篇:《第一讲:创建项目&给游戏添加角色》
游戏中的状态机设计
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
map
map |
这个函数做什么用呢?当然是状态转换后的行为控制了。例如_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::unordered_map
std::unordered_map
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搭建一个横版过关游戏