层次状态机HFSM

背景

  层次状态机 即 HFSM。在开发过程中,角色和系统的行为复杂性不断增加。传统状态机在处理复杂逻辑时,可能导致状态间的逻辑层次混乱。尤其是当一个状态需要包含多个子状态时,维护这些状态变得非常困难。引入层次状态机HFSM可以很好地解决这些问题。
  层次状态机的核心思想是,将一个状态机也作为状态,这样就能够在状态机内部嵌套多个子状态机,实现更复杂的状态切换逻辑,特别是互斥状态间的隔离,与包含状态间的共存。例如,角色可以有一个 移动状态 ,但这个状态下还可以细分为 行走 和 奔跑 子状态,明显的移动状态与行走和奔跑状态属于包含关系。角色还可以有一个 闲置状态 ,明显的闲置状态与移动状态属于互斥关系。
  层次状态机可以帮助我们清晰地管理这种复杂逻辑,使代码的组织结构更加合理,满足更多更复杂的需求。

使用

私有HFSM

例子说明:
  角色Player有行为状态机 fsm ,行为状态机下有 移动状态 Move 、 闲置状态 Idle 。移动状态下细分为 行走状态 Walk 、 奔跑状态 Run 。所以我们将移动状态定义为层次状态机 moveHfsm 。
结构如下:

1
2
3
4
5
6
7
8
// 状态关系图:
fsm{
Idle,
Move{
Walk,
Run,
}
}

创建状态定义枚举

避免成环详见下文注意事项

1
2
3
4
5
6
7
8
9
public enum Def
{
Idle,
Move,
Walk,
Run,
}
// 设计上,使用同一个状态定义枚举类型,可能出现成环现象。
// 可以不同层次的状态,使用不同的状态定义类型,这样即可在初始化状态字典时进行语法检测。

声明层次状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 声明移动层次状态机 MoveHfsm
// class 层次状态机名 : HfsmComponent<持有者,状态定义类型>
public class MoveHfsm : HfsmComponent<Player, Def>
{
// 实现接口函数 -> 构造函数
public MoveHfsm(Player owner) : base(owner)
{
}

// 可以重写层次状态机的生命周期函数
public override void OnEnter(Player owner, params object[] objs)
{
base.OnEnter(owner,objs);
}
public override void OnUpdate(Player owner, params object[] objs)
{
base.OnUpdate(owner,objs);
}
public override void OnExit(Player owner)
{
base.OnExit(owner);
}
}

声明状态类

同状态机模板的状态声明部分。状态机模板FSM

1
2
3
4
5
6
7
8
9
10
11
12
public class IdleState : IFsmComponentState<Player>
{
// ……实现接口函数
}
public class WalkState : IFsmComponentState<Player>
{
// ……实现接口函数
}
public class RunState : IFsmComponentState<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
public class Player
{
// 创建状态机和层次状态机
public FsmComponent<Player, Def> fsm; // 行为状态机(主状态机)
public MoveHfsm moveHfsm; // 移动层次状态机

// 由下至上初始化,因为上层状态机需要引用到下层Hfsm

// 先初始化层次状态机
moveHfsm = new MoveHfsm(this);
// 状态字典dict: <状态定义类型,IFsmComponentState<持有者类>>
moveHfsm.SetFsm(new Dictionary<Def, IFsmComponentState<Player>>()
{
// 状态键值对项: {状态定义,状态对象}
{Def.Walk, new WalkState()},
{Def.Run, new RunState()},
});

// 再初始化主状态机
fsm = new FsmComponent<Player, Def>(this);
fsm.SetFsm(new Dictionary<Def, IFsmComponentState<Player>>()
{
{Def.Idle, new IdleState()},
// 层次状态机 作为 状态 添加进状态字典
// 键值对项: {状态定义,层次状态机对象}
{Def.Move, moveHfsm},
});
}

实现私有状态机的生命周期函数

1
2
3
4
5
6
7
8
public class Player
{
private void Update()
{
//只需要实现主状态机的即可,层次状态机的生命周期函数 由 主状态机进行转发
fsm.OnUpdate();
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// fsm初始状态为 Null
// 设置初始状态为闲置状态 Idle,fsm当前状态为 Idle
fsm.ChangeFsmState(Def.Idle);
// 切换为移动状态 Move,fsm当前状态为 Move
fsm.ChangeFsmState(Def.Move);
// 再细切换为移动状态下的奔跑状态 Run,fsm当前状态为 Move->Run
moveFsm.ChangeFsmState(Def.Run);

// 判断当前状态
// 包含关系的状态相同,互斥关系的状态相反,优先判断互斥关系
// Move与Run、Walk为包含关系都为True,优先Run与Walk为互斥,所以Walk为False,Idle与Move互斥
if(fsm.IsState(Def.Run)) // -> True
if(fsm.IsState(Def.Move)) // -> True
if(fsm.IsState(Def.Idle)) // -> False
if(fsm.IsState(Def.Walk)) // -> False

公共层次状态机

使用方法与私有HFSM基本相同
声明层次状态机所继承的类名有区别:

1
2
// HFSM继承自BaseHfsm
public class MyHFSM : BaseHfsm

注意事项

  • 设置了fsm或hfsm的状态字典后,fsm或hfsm的当前状态为空。与私有状态机的规则保持一致。
  • 初始化状态机时需要由下至上进行初始化,因为顶层fsm或hfsm,需要引用到 下层的fsm或hfsm实例对象,进行赋值和数据交换,交换初始化顺序会出现报空。
  • 初始化状态字典时,需要留意下层状态不能包含上层状态,否则会成环。(在IsState()中不做环检测和跳环算法,应该在源头避免成环,即初始化状态字典时避免成环)。可以不同层次的状态,使用不同的状态定义类型,这样即可在初始化状态字典时进行语法检测。
  • 切换状态时,应该一层再一层按顺序进行切换,否则跳层切换状态,会找不到hfsm所持有的状态。
  • 判断当前状态时,如果状态不相符,会进入到下一层hfsm的状态判断,最坏情况下查找的时间复杂度为O(n)。(鉴于层次状态机总体上为树形结构,该时间复杂度明显超出预期,后续需要进行查找操作的优化)

内部实现

  1. 层次状态机机制的核心思想,是将状态机也视为一种状态,状态机持有状态机,即可完成状态机的嵌套,即达到状态间分层次的目的。所以层次状态机首先应该是一个状态机,应该继承FsmComponent类,同时,也被视为状态,应该继承状态的接口:
    1
    public class HfsmComponent<Owner, Key> : FsmComponent<Owner, Key>, IFsmComponentState<Owner>