基于ET框架的FSM状态机设计
前言
FSM状态机可以说是RPG、ACT等类型游戏的必备架构,而因为ET框架中并不带有基于ECS组件式思想所设计的状态机,因此萌生了自己设计的想法。而在参考了花花的GameFramwork解析:有限状态机(FSM)后想根据GF的思路来进行设计。
但在设计的过程中也踩了不少坑,首先GF的状态机采取了传统OOP思路,由FSM(状态机管理器,其继承自FsmBase并实现IFSM接口)管理其中的所有FSMState(状态部分),在其生命周期中调用状态中对应的函数(OnEnter、OnUpdate、OnLeave等)。因此在根据GF第一次设计出FSMComponent组件时发现还是离不开OOP的思想,甚至变成了OOP+ECS这种奇怪的组合,ET中Component存储数据与System做逻辑。
后续考虑后根据ECS的思想采用了和ET中UIEvent分发较为相似的思路。采用Attribute标记状态生命周期函数(OnInit、OnEnter、OnUpdate等)+全局FsmMgrComponent进行事件派发的方法。
基础结构:
FsmComponent中存储当前状态机名fsmName以及状态名stateName作为标识,同时也可记录下前一个状态名lastStateName,且保留有GF中用于在FSM中传递的数据流data。
全局具有FsmComponent,其在进入战斗场景阶段就加入到zoneScene中,在其组件的Awake函数中会搜索所有带有FsmAttribute的类,并将其对应的AFsmState加入到存储字典中,用于后续派发,避免了一个FsmComponent存储一个Fsm状态机且需实例化多个FsmState的情况,实现了FsmState的共用。(此时FsmState中仅存有对应的派发函数,不能在其类中设置变量,会造成多个同fsmName的FsmComponent的混用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public override void Awake(FsmMgrComponent self)
{
//将dll中具有Fsm标签的class加入到对应fsm的字典中
FsmMgrComponent.Instance = self;
self.allFsm = new Dictionary<string, Fsm>();
foreach (Type type in Game.EventSystem.GetTypes(typeof(FsmAttribute)))
{
object[] attrs = type.GetCustomAttributes(typeof (FsmAttribute), false);
if(attrs.Length==0) continue;
FsmAttribute fsmAttribute = attrs[0] as FsmAttribute;
AFsmState aFsmState=Activator.CreateInstance(type) as AFsmState;
if (!self.allFsm.ContainsKey(fsmAttribute.fsmType))
{
self.allFsm.Add(fsmAttribute.fsmType,new Fsm());
}
self.allFsm[fsmAttribute.fsmType].allEvents.Add(type.Name.Split('_')[1],aFsmState);
}
}AFsmState抽象类中具有OnEnter、Onupdate、OnExit、Ondestroy的对应状态派发函数,其传入值为当前调用的FsmComponent,同时使用data数据流进行不同生命周期函数间的交互。
保留了ECS思想中Component的通用性,也维护了GF的状态机中类似于FSMState<Player>的泛型标记,转而用stateName来代替(ET中不存在泛型组件)。
△后续进行流程图补充
具体使用:
以一个角色Unit作为例子:
在AfterOfflineUnitCreate的事件中加入FsmComponent,并设定其fsm类型名为FsmType.PlayerFSM,此时只会分发对应类型的FsmState
1
2
3
4
5
6
7
8
9
10
11
12
13public class AfterOfflineUnitCreate_CreateUnitView:AEvent<EventType.AfterOfflineUnitCreate>
{
protected override async ETTask Run(AfterOfflineUnitCreate args)
{
......
//每个Unit中加入FsmComponent用于管理状态机
FsmComponent fsm=args.unit.AddComponent<FsmComponent,string>(FsmType.PlayerFSM);
......
await ETTask.CompletedTask;
}
}书写好对应的IdleState、WalkState
PlayerFSM_IdleState: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
42using System;
using UnityEngine;
namespace ET
{
[Fsm(FsmType.PlayerFSM)]
public class PlayerFSM_IdleState:AFsmState
{
public override void OnEnter(FsmComponent fsmComponent)
{
Debug.Log("Enter idle");
AnimatorComponent animatorComponent = fsmComponent.Parent.GetComponent<AnimatorComponent>();
if (animatorComponent != null)
{
animatorComponent.MotionType = MotionType.Idle;
}
}
public override void OnUpdate(FsmComponent fsmComponent)
{
Debug.Log("Update idle");
}
public override void OnExit(FsmComponent fsmComponent)
{
Debug.Log("Exit idle");
AnimatorComponent animatorComponent = fsmComponent.Parent.GetComponent<AnimatorComponent>();
if (animatorComponent != null)
{
animatorComponent.ResetMotionType();
}
}
public override void OnDestroy()
{
}
}
}PlayerFSM_WalkState:
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
41using System;
using UnityEngine;
namespace ET
{
[Fsm(FsmType.PlayerFSM)]
public class PlayerFSM_WalkState:AFsmState
{
public override void OnEnter(FsmComponent uiComponent)
{
Debug.Log("Enter Walk");
AnimatorComponent animatorComponent = uiComponent.Parent.GetComponent<AnimatorComponent>();
if (animatorComponent != null)
{
animatorComponent.MotionType = MotionType.Run;
}
}
public override void OnUpdate(FsmComponent uiComponent)
{
Debug.Log("Update Walk");
}
public override void OnExit(FsmComponent uiComponent)
{
Debug.Log("Leave Walk");
AnimatorComponent animatorComponent = uiComponent.Parent.GetComponent<AnimatorComponent>();
if (animatorComponent != null)
{
animatorComponent.ResetMotionType();
}
}
public override void OnDestroy()
{
}
}
}在对应的Command命令的Execute函数中获取Unit的FsmComponent组件并切换状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21namespace ET.Demo.Command
{
[Command(CommandType.MoveCommand)]
public class MoveCommand:ACommand
{
public override void OnInit(CommandComponent commandComponent)
{
}
public override void Execute(CommandComponent commandComponent, TypeEntityPair entityPair,params object[] param)
{
Entity entity = entityPair.entity;
FsmComponent fsm = entity.GetComponent<FsmComponent>();
if(fsm==null) return;
if (!fsm.isRunning) fsm.Start("WalkState");
else fsm.ChangeState("WalkState");
}
}
}
后期拓展:
增加类似baseState状态的复用机制:
在实际使用的过程发现无法实现状态机通用状态的复用,比如某个Enemy的攻击state具有特殊定义,也必须重写其他states并分离出一个新的FSM类型,因此对当前的ET状态机的部分逻辑进行更改。
以下是新版的FsmComponent,主要是加入了allStates来存储当前Fsm状态机中的所有状态,无需再将FsmType作为一个类型导入,而是仅仅作为分发具体声明周期的查询参数。并在Awake时赋值对其进行赋值
这里仅放上修改部分的流程图,其他和之前基本无变化
- fsmName仅作为向FsmMgrComponent查询并分发对应生命周期函数的依据
- allStates中存储状态机中所有可供转换的状态名
- FsmComponent中的fsmName和allStates在AwakeSystem的Awake(…)中进行赋值