建议阅读缩放比例:150%

什么是有限状态机(Finite-state machine)

  • 在有限个状态和有限的条件下,程序只会处于一个状态之下。
  • 通过条件的触发,使得程序从一个状态切换到对应条件触发的状态。
  • 同一个条件不能同时对应多个切换状态。

举个栗子
Coding.png

为什么要使用FSM

简单来说,有限状态机就是在你写好的状态下,根据你写好的特定条件来进行切换状态的模式。

想想看,是不是和我们在Unity中使用的Animator动画状态机很像!

在我没了解过FSM之前,我制作怪物的AI时只会用bool、trigger、float来进行简单的移动攻击逻辑,并且写完之后总感觉逻辑不够清晰,当我做一些简单的行为时没什么问题,但当我后期想添加更多动画和功能的时候,整个代码的编写就会很艰难

例如当我想实现这些功能时:

  • 敌人在待机状态,没有发现玩家的情况下,有概率会进行巡逻状态
  • 当敌人发现玩家进入追踪状态,但还未到达近战可攻击距离时,可选择冲刺位移攻击,或者切换远程攻击模式,也可能因为自身血量过低且没有队友的情况下而选择逃跑
  • 在打到可近战攻击距离进入攻击状态时,依然有可能选择向后或左右位置闪避,或者改变攻击方式,例如可使玩家击倒的踢技

当上述这些功能分开时都没有什么困难的地方,但当她们放在一起时就会变得格外混乱,
所以我们可以利用FSM搭配Animator来制作怪物AI,让整个代码逻辑更加清晰且更易于编写。

如何使用FSM制作怪物AI

在写具体的代码之前,我们先了解一下制作FSM都需要哪些功能

当前状态条件目标状态
待机发现目标追击
追击进入攻击范围攻击
攻击玩家离开攻击范围追逐
巡逻生命为0死亡
死亡

所以我们的运行逻辑应该是:
状态机检测当前状态的条件 -> 如果某个条件达成 -> 状态机切换当前状态

为此,我们一共需要三个类
分别为:状态机类、状态类、条件类
(1TK`YJ3_JHJ[JU)7Y(3}9Y.png

同时我们还需要两个枚举类:状态枚举类、条件枚举类

条件类(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