什么是有限状态机

有限状态机的概念相信很多同学都清楚了,不清楚的可以参考一下书籍《游戏编程模式》中状态模式一节,里面讲得十分清楚。FSM在游戏中常用于玩家控制、怪物AI、UI状态、游戏流程控制等。

有限状态机的实现

结构

有限状态机的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是状态部分(FsmState),状态机部分(FsmBase、IFsm、Fsm)以及状态机管理器部分(IFsmManager、FsmManager)。

状态类FsmState

  • FsmState为有限状态机状态基类,所有用于有限状态机的状态都需要继承自此类,泛型参数T需要传入状态持有者类型。
  • OnInit、OnEnter、OnUpdate、OnLeave、OnDestroy为状态的生命周期方法,其中OnInit和OnDestroy分别在状态创建和销毁时调用,只会调用一次,而OnEnter、OnLeave分别在进入状态和离开状态时调用,可能会调用多次,而OnUpdate则是在进入该状态后每帧调用。
  • ChangeState用于切换到下一状态。ChangeState实际是用该方法传入的FSM对象调用FSM类里的ChangeState方法,正式执行状态切换逻辑。

状态机类Fsm

  • Fsm对象通过Create方法创建,需要传入状态机拥有者类型、状态机名字、状态列表3个参数,Create方法为静态方法,由FsmManager调用。参数状态列表将会保存在字段m_States中,并调用所有状态的OnInit方法。
  • 状态机通过Start方法启动,传入初始状态类型作为参数,方法内部会调用该状态的OnEnter。
  • Update方法会每帧调用当前状态的Update方法,且会计算当前状态机进行了的累计时间,可通过CurrentStateTime获取。
  • GetAllState和GetState方法可以获取注册进这个状态机的状态对象。
  • 状态机内通常不同状态之间是需要有数据交互的,GetData,SetData,HasData,RemoveData这四个接口则提供了不同状态间数据交互的功能,分别对应获取数据、设置数据、是否有数据、移除数据,数据以key-value形式存在于字典m_Datas中。
  • Shutdown方法会回收FSM对象,此方法由FsmManager的DestroyFsm方法调用。

状态机管理器FsmManager

  • 外部创建新的状态机统一通过FsmManager的CreateFsm接口创建,参数同FSM类中的静态方法Create,此方法会调用Fsm类的Create创建Fsm对象,然后以key-value的形式储存在字段m_Fsms中,注意m_Fsms是Dictionary<TypeNamePair, FsmBase>类型,以TypeNamePair为Key,TypeNamePair对象是结合状态机持有者类型和状态机名字字符串类型参数组成,为了保证Key的唯一性,对于同样类型的而不同实例的持有者,应该传入不同的状态机名字。
  • GetFsm、GetAllFsm、HasFsm,向外部提供某个状态机的查询、获取,需要传入持有者类型和状态机名字两个参数。
  • DestroyFsm可销毁特定状态机,会调用对应Fsm对象的Shutdown方法,并在FsmManager的m_Fsms字段中移除该状态机。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal override void Update(float elapseSeconds, float realElapseSeconds)
{
m_TempFsms.Clear();
if (m_Fsms.Count <= 0)
{
return;
}

foreach (KeyValuePair<TypeNamePair, FsmBase> fsm in m_Fsms)
{
m_TempFsms.Add(fsm.Value);
}

foreach (FsmBase fsm in m_TempFsms)
{
if (fsm.IsDestroyed)
{
continue;
}

fsm.Update(elapseSeconds, realElapseSeconds);
}
}
  • Update方法中会调用m_Fsms中的所有状态机的Update方法,值得注意的是这里并没有直接对m_Fsms进行foreach,而是添加到一个临时的列表中再进行循环调用,这样可以防止在迭代过程中,外部销毁某个状态机而从m_Fsms移除状态机对象时,造成迭代器失效。

示例

假设我们现在需要用状态来实现玩家的控制,其中包括空闲和移动状态,处于空闲状态下的玩家当检测到方向键按下时,会切换到移动状态,且根据方向键向某个方向进行移动,移动过程持续一秒。
我们需要3个类去实现这一需求,其中IdleState、MoveState两个类分别对应空闲状态、移动状态,Player则为状态机的持有者,也是状态机要控制的主体。

空闲状态类

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
41
42
43
44
45
46
47
using UnityEngine;
using GameFramework.Fsm;
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
using UnityGameFramework.Runtime;

public class IdleState : FsmState<Player>
{
//触发移动的指令列表
private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };

protected override void OnInit(ProcedureOwner fsm)
{
base.OnInit(fsm);
}

protected override void OnEnter(ProcedureOwner fsm)
{
base.OnEnter(fsm);
}

protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);

foreach (var command in MOVE_COMMANDS)
{
//触发任何一个移动指令时
if (Input.GetKeyDown(command))
{
//记录这个移动指令
fsm.SetData<VarInt32>("MoveCommand", (int)command);
//切换到移动状态
ChangeState<MoveState>(fsm);
}
}
}

protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
{
base.OnLeave(fsm, isShutdown);
}

protected override void OnDestroy(ProcedureOwner fsm)
{
base.OnDestroy(fsm);
}
}

移动状态类

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using UnityEngine;
using GameFramework.Fsm;
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
using UnityGameFramework.Runtime;

public class MoveState : FsmState<Player>
{
private static readonly float EXIT_TIME = 1f;
private float exitTimer;
private KeyCode moveCommand;

protected override void OnInit(ProcedureOwner fsm)
{
base.OnInit(fsm);
}

protected override void OnEnter(ProcedureOwner fsm)
{
base.OnEnter(fsm);

//进入移动状态时,获取移动指令数据
moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>("MoveCommand");
}

protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);

//计时器累计时间
exitTimer += elapseSeconds;

//switch(moveCommand)
//{
//根据移动方向指令向对应方向移动
//}

//达到指定时间后
if (exitTimer > EXIT_TIME)
{
//切换回空闲状态
ChangeState<IdleState>(fsm);
}
}

protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
{
base.OnLeave(fsm, isShutdown);

//推出移动状态时,把计时器清零
exitTimer = 0;
//清空移动指令
moveCommand = KeyCode.None;
fsm.RemoveData("MoveCommand");
}

protected override void OnDestroy(ProcedureOwner fsm)
{
base.OnDestroy(fsm);
}
}

玩家类

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
using System.Collections.Generic;
using UnityEngine;
using GameFramework.Fsm;
using StarForce;

public class Player : MonoBehaviour
{
//Player对象自增Id
private static int SERIAL_ID = 0;

private IFsm<Player> fsm;

// Start is called before the first frame update
void Start()
{
//创建状态列表
List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };
//创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复
fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);
//以IdleState为初始状态,启动状态机
fsm.Start<IdleState>();
}

// Update is called once per frame
void Update()
{

}

private void OnDestroy()
{
//销毁状态机
GameEntry.Fsm.DestroyFsm(fsm);
}
}

Inspector面板

FSM组件的Inspector面板可以实时看到所有正在运行的状态机,以及这些状态机当前处于的状态、运行时间。

最后

GameFramework解析 系列目录:GameFramework解析:开篇

个人原创,未经授权,谢绝转载!