前言

​ 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
    23
    public 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中不存在泛型组件)。

△后续进行流程图补充

基于ET实现的FSM状态机.drawio

具体使用:

以一个角色Unit作为例子:

  1. 在AfterOfflineUnitCreate的事件中加入FsmComponent,并设定其fsm类型名为FsmType.PlayerFSM,此时只会分发对应类型的FsmState

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public 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;
    }
    }
  2. 书写好对应的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
    42
    using 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
    41
    using 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()
    {

    }
    }
    }
  3. 在对应的Command命令的Execute函数中获取Unit的FsmComponent组件并切换状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace 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时赋值对其进行赋值

这里仅放上修改部分的流程图,其他和之前基本无变化

基于ET的FSM更改.drawio_

  • fsmName仅作为向FsmMgrComponent查询并分发对应生命周期函数的依据
  • allStates中存储状态机中所有可供转换的状态名
  • FsmComponent中的fsmName和allStates在AwakeSystem的Awake(…)中进行赋值