建议阅读缩放比例:150%
什么是有限状态机(Finite-state machine)
- 在有限个状态和有限的条件下,程序只会处于一个状态之下。
- 通过条件的触发,使得程序从一个状态切换到对应条件触发的状态。
- 同一个条件不能同时对应多个切换状态。
举个栗子
为什么要使用FSM
简单来说,有限状态机就是在你写好的状态下,根据你写好的特定条件来进行切换状态的模式。
想想看,是不是和我们在Unity中使用的Animator动画状态机很像!
在我没了解过FSM之前,我制作怪物的AI时只会用bool、trigger、float来进行简单的移动攻击逻辑,并且写完之后总感觉逻辑不够清晰,当我做一些简单的行为时没什么问题,但当我后期想添加更多动画和功能的时候,整个代码的编写就会很艰难
例如当我想实现这些功能时:
- 敌人在待机状态,没有发现玩家的情况下,有概率会进行巡逻状态
- 当敌人发现玩家进入追踪状态,但还未到达近战可攻击距离时,可选择冲刺位移攻击,或者切换远程攻击模式,也可能因为自身血量过低且没有队友的情况下而选择逃跑
- 在打到可近战攻击距离进入攻击状态时,依然有可能选择向后或左右位置闪避,或者改变攻击方式,例如可使玩家击倒的踢技
当上述这些功能分开时都没有什么困难的地方,但当她们放在一起时就会变得格外混乱,
所以我们可以利用FSM搭配Animator来制作怪物AI,让整个代码逻辑更加清晰且更易于编写。
如何使用FSM制作怪物AI
在写具体的代码之前,我们先了解一下制作FSM都需要哪些功能
当前状态 | 条件 | 目标状态 |
---|---|---|
待机 | 发现目标 | 追击 |
追击 | 进入攻击范围 | 攻击 |
攻击 | 玩家离开攻击范围 | 追逐 |
巡逻 | 生命为0 | 死亡 |
死亡 | 无 | 无 |
所以我们的运行逻辑应该是:
状态机检测当前状态的条件 -> 如果某个条件达成 -> 状态机切换当前状态
为此,我们一共需要三个类
分别为:状态机类、状态类、条件类
同时我们还需要两个枚举类:状态枚举类、条件枚举类
条件类(FSMTrigger)
我们先从最简单的条件类(FSMTrigger)说起
在这个类中需要有两个抽象方法,需要由子类去实现
- void Init()初始化方法,要求子类必须初始化条件,为编号赋值
- bool HandleTrigger(FSMBase fsm);//逻辑处理方法,例如判断血量是否为0,返回bool
在HandleTrigger()里,要用FSMBase类里给出的值来判断是否满足切换条件
在Init()里,我们需要将TriggerID这个字段赋予当前类的状态枚举
如在NoHealthTrigger类里要定义TriggerID = FSMTriggerID.NoHealth
这个ID的作用是为了在之后的状态类(FSMState)里去调用
利用TriggerID在映射表中找到对应state,让状态类告诉状态机要切换哪一个状态
状态类(FSMState)
状态类的作用是检测当前状态的条件是否满足,如果满足就会告诉状态机去切换哪一个状态
所以我们需要创建一个字典,让条件与状态对应,如果需要增加新的映射,就在状态机里去调用添加方法,需要注意的是,同一个条件仅能对应一个状态,而不同的状态可以对应同一个条件。
例如:
玩家血量为0只能变成死亡状态,而在待机、移动、攻击时都可以通过血量为0到达死亡状态。
- AddMap(FSMTriggerID triggerID, FSMStateID stateID)
由状态机调用(为映射表和条件列表赋值) - void Init()
同样,子类也必须去初始化状态 - Reason(FSMBase fsm)
在状态机中,每帧去调用来判断是否满足条件,若满足则切换当前状态
同时我们也给子类留出三个备选事件
- EnterState(FSMBase fsm) 进入状态
- ActionState(FSMBase fsm)在状态时
- ExitState(FSMBase fsm) 退出状态
状态机 (FSMBase)
在这个类中,我们就需要去使用之前我们写好的方法,来让整个框架动起来
为此,我们需要先配置状态机,也就是我们字典里的映射,设置让某个条件会触发指定的状态
- ConfigFSM() 配置状态机
之后,我们在让当前状态为我们设置好的初始状态
- InitDefaultState() 查找默认状态并赋给当前状态
以及,给状态类与条件类提供会用到的字段
- InitCompoinent();提供游戏运行的一些参数
这三个函数就是在Start的生命周期内需要执行的方法
这之后就需要让脚本不断的去检测,当前是否满足条件,若满足则切换下一状态,若不满足则继续执行当前状态的方法。
所以要在Update下去使用State里的方法
- currentState.Reason(this); 判断当前状态条件
- currentState.ActionState(this);执行当前状态逻辑
至此,有限状态机的基本框架已经全部搭建完毕。
代码部分
条件基类
public abstract class FSMTrigger : MonoBehaviour
{
public FSMTriggerID TriggerID { get; set; } //编号
public FSMTrigger()
{
Init();
}
public abstract void Init();//要求子类必须初始化条件,为编号赋值
public abstract bool HandleTrigger(FSMBase fsm);//逻辑处理
}
状态基类
public abstract class FSMState
{
public FSMStateID stateID { get; set; }
public Dictionary<FSMTriggerID, FSMStateID> map;//映射表
private List<FSMTrigger> Triggers;//条件列表
public FSMState()
{
Init();
map = new Dictionary<FSMTriggerID, FSMStateID>();
Triggers = new List<FSMTrigger>();
}
//检测当前的条件是否满足
public void Reason(FSMBase fsm)
{
for (int i = 0; i < Triggers.Count; i++)
{
//发现条件满足
if (Triggers[i].HandleTrigger(fsm))
{
//从映射表中获取输出状态
FSMStateID stateID = map[Triggers[i].TriggerID];
//切换状态
fsm.ChangeActiveState(stateID);
return;
}
}
}
//要求实现类必须初始化类,为编号赋值
public abstract void Init();
//由状态机调用(为映射表和条件列表赋值)
public void AddMap(FSMTriggerID triggerID, FSMStateID stateID)
{
//添加映射
map.Add(triggerID, stateID);
//创建条件对象
CreateTrigger(triggerID);
}
private void CreateTrigger(FSMTriggerID triggerID)
{
//创建条件对象
//命名规范:AI.FSM.+条件枚举+Trigger
Type type = Type.GetType("AI.FSM." + triggerID + "Trigger");
FSMTrigger trigger = Activator.CreateInstance(type) as FSMTrigger;
Triggers.Add(trigger);
}
//为具体状态类提供备选事件
public virtual void EnterState(FSMBase fsm) { }
public virtual void ActionState(FSMBase fsm) { }
public virtual void ExitState(FSMBase fsm) { }
}
}
状态机
public class FSMBase : MonoBehaviour
{
#region 脚本生命周期
private void Start()
{
InitCompoinent();
ConfigFSM();
InitDefaultState();
}
//每帧处理的逻辑
private void Update()
{
//判断当前状态条件
currentState.Reason(this);
//执行当前状态逻辑
currentState.ActionState(this);
}
#endregion
#region 状态机自身成员
//状态列表
public List<FSMState> states;
[Tooltip("默认状态编号")]
public FSMStateID defaultStateID;
public FSMState currentState; //当前状态
private FSMState defaultState;//默认状态
private void InitDefaultState()
{
//查找默认状态
defaultState = states.Find(s => s.stateID == defaultStateID);
currentState = defaultState;
currentState.EnterState(this);//进入状态;
}
//配置状态机
private void ConfigFSM()
{
states = new List<FSMState>();
//创建状态对象
IdleState idle = new IdleState();
//设置状态(添加映射)(AddMap)
idle.AddMap(FSMTriggerID.NoHealth, FSMStateID.Death);
//加入状态机
states.Add(idle);
DeathState death = new DeathState();
states.Add(death);
}
//切换状态
public void ChangeActiveState(FSMStateID stateID)
{
//离开上一个状态
currentState.ExitState(this);
//如果需要切换Default,则直接返回默认状态
if(stateID==FSMStateID.Default)
{
currentState = defaultState;
}
else
{
currentState = states.Find(s => s.stateID == stateID); //设置当前状态
}
//currentState = stateID == FSMStateID.Default ? currentState = defaultState : currentState = states.Find(s => s.stateID == stateID);
//进入下一个状态
currentState.EnterState(this);
}
#endregion
#region 为状态与条件提供的成员
[HideInInspector]
public Animator anim;
[HideInInspector]
public CharacterBase chStatus;
public void InitCompoinent()
{
anim = GetComponent<Animator>();
chStatus = GetComponent<CharacterBase>();
}
#endregion
写在最后
由于本人刚接触FSM,所以解释的不是很好
非常推荐和我一样的萌新,看一看这个老师讲的FSM课程
https://www.bilibili.com/video/BV1464y1u79N?p=1