目录
FSM状态机在Unity中的应用
非常感谢宝藏up主打工人小棋的框架和帮助,欢迎铁汁们给大哥点点关注。
在这篇文章中,我们会简单介绍一下有限状态机并且在Unity中利用FSM实现的敌人AI,也欢迎大佬们查缺补漏。
FSM状态机
先上定义:
有限状态自动机(Finite State Machine),表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
有限状态机包含三个特点:
1. 状态总数是有限的
2. 任一时刻只能处于一种状态
3. 在某些条件下可以从一种状态转换到另一种状态
生活中大多数事物都可以看作有限状态机,比如关闭状态的门在被推开后转变为打开状态。
蒸汽机的普及
第一次工业革命期间, 以蒸汽机为代表的一系列动力机使生产力得到突飞猛进的发展,人类从此进入了“蒸汽时代”。然而早期的蒸汽机由于效率低下,控制不便等问题并没有广泛普及。
为了让人们能够自由控制蒸汽机的速度,瓦特在蒸汽机上安装了叫做”离心调速器“的装置。这种装置能够在蒸汽机超过设定转速时,通过一系列的物理变化减小蒸汽机的速度,同样的,当蒸汽机速度过慢时,也会自动将速度调大。
离心调速器是瓦特改进蒸汽机的一个重要标志,也是蒸汽机得以广泛普及的关键因素。
我们可以把离心调速器看作一个有闲置、加速、减速, 三种状态的有限状态机,调速器的行为会在这三种状态中根据速度的大小切换:
状态设计模式
实现有限状态机,我们需要简单了解一下状态设计模式
状态模式的定义是:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类
说人话就是,如果你的代码有许多分支,使用了很多if else,为了遵循开闭原则,为了提高代码的逼格,可以用不同状态的互相转换代替冗长的条件判断。这样,一旦程序变得复杂,你就不必再担心维护和扩展一坨臃肿的的if-else语句
这个类图解释了状态模式的结构:
- 环境类Context:管理状态之间的切换,将状态的各种操作委托给状态对象执行
- 状态基类BaseState:抽象状态类,封装状态的行为
- 具体状态类:状态行为的实现
当我们要添加新状态时,只需要新创建一个具体状态类,在环境类管理好所有状态的切换和执行。代码层次分明,利于扩展。
在Unity实现敌人“哨兵飞船”的AI
未来的某一天,我们已经有了足够强大的动力系统,为了踏上太空文明之旅,一艘艘宇宙飞船从美丽的蓝星驶向太空 ,人类仿佛再一次迎来了工业革命的曙光。
为了让宇宙飞船有更广泛的用途,一些飞船被安装了更加智能的AI调速器在蓝星周围巡逻。这些“哨兵飞船”可以在闲置,巡逻,追击等复杂情况下自动调整合适的速度。
我们可以在在Unity的2D项目中设计一个“哨兵”作为敌人,如图是利用FSM状态机实现这样一个“哨兵”AI的框架,我们用状态机维护AI当前所处的状态,逻辑就会很清晰,后期扩展其它状态也十分容易。
敌人视野
如何让敌人在找到Player呢? 我们在敌人身上创建一个子物体,为子物体添加碰撞体模拟敌人的视野
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_View_Trigger : MonoBehaviour
{
public Transform playerGameobj;
public bool findPlayer = false;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
findPlayer = true;
playerGameobj = other.GetComponent<Transform>();
}
}
private void OnTriggerExit2D(Collider2D other)
{
if (other.tag == "Player")
{
findPlayer = false;
playerGameobj = null;
}
}
}
将这个脚本挂载到敌人视野的子物体上,调整Collider大小,别忘了设置为Trigger,检测敌人是否看到了玩家:
-
玩家进入触发器时,则findPlayer将被设置为true,并且playerGameobj也会获取玩家的信息
-
当玩家离开触发器时,则findPlayer将被设置为false,并且playerGameobj变量将被设置为null。
状态基类
我们需要抽象出一个状态基类,封装状态的行为。具体的状态需要继承自这个基类,你也可以使用接口实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//状态类型枚举
public enum Estate
{
None,
Idel,
Patrol,
Chase,
}
//状态基类
public class BaseState
{
//进入状态时执行
public virtual void OnEnter() { }
//状态中持续执行
public virtual void OnUpdate() { }
//退出状态时执行
public virtual void OnExit() { }
}
这段代码定义了状态基类的结构
Estate 枚举定义了不同的状态类型,包括了 None,Idle,Patrol和Chase 。
注意:在这个状态机框架中,Estate只是用于标识不同状态的枚举,并不是具体的状态。
当状态机需要切换到不同状态时,通过向FSM传入相应的Estate枚举值来指定切换到的具体状态类。状态机内部会根据Estate枚举值来实例化对应的状态类,并进行状态转换。这种映射关系使得状态机的实现更加清晰明了,并且方便后续的扩展和修改。
BaseState 类定义了状态的基类。
它包含了三个方法:
- OnEnter() 方法:当进入某个状态时调用。
- OnUpdate() 方法:处于状态中持续执行的方法。
- OnExit() 方法:状态退出时调用的方法。
这些方法是每个具体状态都应该实现的方法。需要在子类中覆盖这些方法来实现不同的状态行为。
FSM状态机类
接下来我们实现FSM状态机类,为了各个状态间可以共享数据,我们在这里定义一个参数类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//所有状态共享的参数
public class FSM_Parameter
{
public Estate currentEState;
}
public class FSM
{
//状态名枚举和具体状态类的索引
public Dictionary<Estate, BaseState> StateDict;
//每个状态共享的参数
public FSM_Parameter parameter;
//当前状态
public BaseState currentState;
public FSM(FSM_Parameter P)
{
parameter = P;
StateDict = new Dictionary<Estate, BaseState>();
}
//添加状态
public void AddState(Estate estate, BaseState baseState)
{
if (!StateDict.ContainsKey(estate))
{
StateDict.Add(estate, baseState);
}
}
//切换状态
public void SwitchState(Estate estate)
{
//目标状态是否已被添加
if (!StateDict.ContainsKey(estate))
{
return;
}
//退出当前状态
if (currentState != null)
{
currentState.OnExit();
}
//切换状态
currentState = StateDict[estate];
parameter.currentEState = estate;
currentState?.OnEnter();
}
public void OnUpdate()
{
currentState?.OnUpdate();
}
}
FSM_Parameter 类定义了所有状态共享的参数。这些参数在状态之间传递,确保状态之间能够共享数据,我们下文细说。
FSM 类定义了 FSM 状态机的具体实现。 它包含了三个成员变量:
- StateDict 字典:存储状态类型和状态对象之间的映射关系。
- parameter 成员变量:存储所有状态共享的参数。
- currentState 成员变量:存储当前状态对象。
它包含了三个方法:
-
AddState() 方法:用于向状态机中添加新的状态,传入状态类型 estate 和对应的状态对象baseState。如果已经存在相同类型的状态,则不会添加。
-
SwitchState() 方法:用于切换状态。传入目标状态类型 estate,如果状态机中存在该状态,则执行以下步骤:
- 调用当前状态的 OnExit() 方法退出当前状态。
- 更新 currentState 成员变量为目标状态对象。
- 更新 parameter.currentEState 成员变量为目标状态类型,这个变量只是为了方便观察使用。
- 调用新的状态的 OnEnter() 方法初始化新的状态。
你也可以在currentState使用Set属性实现
-
OnUpdate() 方法:用于执行当前状态的 OnUpdate() 方法。
关于参数类
参数类是一个存储共享数据的容器,状态类是用于实现具体 AI 行为的类。由于状态类需要使用一些共享的数据(例如敌人位置、速度等等),我们把这些共享数据存储在参数类中,以便状态类可以访问和更新它们。
不同种类的 AI 可能具有不同的属性和行为,因此需要创建不同的参数子类来存储这些属性和数据。 例如,在敌人 AI中需要存储空闲时间、移动速度、视野触发器等数据,而其他类型的 AI 可能需要不同的数据,因此需要创建不同的参数子类。
敌人的AI
这段代码需要挂载在敌人身上,实现敌人的AI
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//<summary>
//敌人参数
//<summary>
[System.Serializable]
public class Enemey_State_Paramter : FSM_Parameter
{
public float idelTime;//闲置时间
public float speed;//移动速度
public Enemy_View_Trigger viewTrigger;//视野触发器
public Transform transformP;//当前位置
}
//<summary>
//敌人AI
//<summary>
public class Enemey_State_AI : MonoBehaviour
{
FSM fsm;//状态机实例
public Enemey_State_Paramter p = new Enemey_State_Paramter();//敌人参数实例
Enemey_State_AI()
{
//初始化状态机
fsm = new FSM(p);
//添加状态
fsm.AddState(Estate.Idel, new Enemy_State_Idel(fsm));
fsm.AddState(Estate.Patrol, new Enemy_State_Patrol(fsm));
fsm.AddState(Estate.Chase, new Enemy_State_Chase(fsm));
//初始化状态
fsm.SwitchState(Estate.Idel);
}
private void Update()
{
fsm.OnUpdate();
}
}
Enemey_State_Paramter 类继承自参数类,存放特定于敌人状态共享的参数:
- idelTime(空闲时间)
- speed(移动速度)
- viewTrigger(视野触发器)
- transformP(当前位置)。
这些参数将在状态之间共享,确保状态能够获取和更新敌人的相关信息。
Enemey_State_AI 类用于实现敌人 AI。 它包含了两个成员变量:
- fsm 成员变量:存储 FSM 状态机实例。
- p 成员变量:存储 Enemey_State_Paramter 类型的参数。
在构造函数中:
- 首先创建敌人参数实例 p
- 然后创建状态机实例 fsm。
- 接着,将 Estate.Idel、Estate.Patrol 和 Estate.Chase 三种状态添加到状态机中,并传入对应的状态对象。
- 最后,使用将当前状态设置为 Estate.Idel。
这样一个敌人AI就初始化完成了。
在 Update() 方法中,每帧调用 fsm.OnUpdate() 方法,根据当前状态类型调用对应状态对象的 OnUpdate() 方法,实现不同的 AI 行为。
敌人具体的状态
恭喜,接下来我们把所有的状态一个个实现出来,你伟大的宇宙飞船就大功告成了!
你一定很清楚,所有的状态都继承自BaseState类,并且实现进入状态、状态中持续执行和退出状态时执行的方法。
在构造函数中,通过传递进来的FSM实例获取状态机对象的引用以及敌人的参数。
下面让我们实现这些状态
Idel(空闲状态)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_State_Idel : BaseState
{
Enemey_State_Paramter enemy_Parameter;
FSM fSM;
float IdelTimer;
public Enemy_State_Idel(FSM f)
{
enemy_Parameter = f.parameter as Enemey_State_Paramter;
fSM = f;
}
public override void OnEnter()
{
//重置计时器
IdelTimer = 0;
}
public override void OnUpdate()
{
//闲置计时
IdelTimer += Time.deltaTime;
//当player进入视野,切换至Chase
if (enemy_Parameter.viewTrigger.findPlayer)
fSM.SwitchState(Estate.Chase);
//闲置一定时间后切换至Patrol
if (IdelTimer >= enemy_Parameter.idelTime)
{
IdelTimer = 0;
fSM.SwitchState(Estate.Patrol);
}
}
public override void OnExit()
{
}
}
这段代码实现了敌人的闲置l状态:
-
在OnEnter方法中,将当前闲置的计时器重置为0。
-
在OnUpdate方法中,计时器不断计时,如果空闲计时达到了预设的时间,就切换到Patrol状态。如果在视野范围内找到了玩家,就切换到Chase状态;
-
在OnExit方法中,没有执行任何操作。
Patrol(巡逻状态)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_State_Patrol : BaseState
{
Enemey_State_Paramter enemy_Parameter;
FSM fSM;
public Enemy_State_Patrol(FSM f)
{
enemy_Parameter = f.parameter as Enemey_State_Paramter;
fSM = f;
}
Vector2 targetPos;
public override void OnEnter()
{
//生成附近的一个随机位置作为巡逻目标点
int x = Random.Range(-5, 5);
targetPos = new Vector2(enemy_Parameter.transformP.position.x + x, enemy_Parameter.transformP.position.y);
}
public override void OnUpdate()
{
//如果位于巡逻点,切换至Idel状态
if (Vector2.Distance(targetPos, enemy_Parameter.transformP.position) < 0.1f)
fSM.SwitchState(Estate.Idel);
//当player进入视野,切换至Chase
if (enemy_Parameter.viewTrigger.findPlayer)
fSM.SwitchState(Estate.Chase);
//向巡逻点移动
//enemy_Parameter.transformP.Translate(targetPos - new Vector2(enemy_Parameter.transformP.position.x, enemy_Parameter.transformP.position.y).normalized * Time.deltaTime);
enemy_Parameter.transformP.position = Vector2.MoveTowards(enemy_Parameter.transformP.position, targetPos, Time.deltaTime * enemy_Parameter.speed);
}
public override void OnExit()
{
}
}
这段代码实现了敌人的巡逻状态。在进入该状态时,会生成一个随机的目标点作为巡逻点。敌人会向目标点移动,如果到达目标点则会切换到闲置状态,如果看到了玩家则会切换到追逐状态。:
- 在 OnEnter() 方法中,生成附近的一个随机位置作为巡逻目标点。
- 在 OnUpdate() 方法中,首先判断是否到达了目标点,如果是,则切换至闲置状态。如果看到了玩家,则切换至追逐状态。否则,敌人会向目标点移动,
- 在 OnExit() 方法中,可以添加敌人离开该状态时需要执行的代码,这里没有添加。
参数和状态机实例实例获取方法与之前的状态类一样,在构造函数中获取。
Chase(追逐状态)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_State_Chase : BaseState
{
Enemey_State_Paramter enemy_Parameter;
FSM fSM;
public Enemy_State_Chase(FSM f)
{
enemy_Parameter = f.parameter as Enemey_State_Paramter;
fSM = f;
}
public override void OnEnter()
{
}
public override void OnUpdate()
{
//当player离开检测范围时,切换至Idel
if (!enemy_Parameter.viewTrigger.findPlayer)
{
fSM.SwitchState(Estate.Idel);
}
//向Player移动
if (enemy_Parameter.viewTrigger.playerGameobj != null)
enemy_Parameter.transformP.position = Vector2.MoveTowards(enemy_Parameter.transformP.position, enemy_Parameter.viewTrigger.playerGameobj.position, Time.deltaTime * enemy_Parameter.speed);
}
public override void OnExit()
{
}
}
这段代码实现了敌人的追逐状态。
在OnUpdate()方法中,如果玩家已经离开检测范围,则状态机将切换到闲置状态。在玩家处于检测范围内的情况下,敌人将移动向玩家。*
小结
现在,我们创建一个三角形作为player,正方形作为敌人,挂载上脚本,去欣赏你的宇宙飞船吧!
在右侧脚本中可以清晰的可以看到,当正方形闲置一段时间后会切换至巡逻状态,移动到附近一个地方
当玩家进入视野后会切换至追逐状态,追逐玩家直至玩家移出视野
当然,例如攻击状态,死亡状态等状态的扩展相信你也已经有了思路。
通过FSM,我们可以将敌人的行为分解成不同的状态,每个状态都对应着不同的行为。这样的架构使得我们的代码更加可读、易于维护和扩展。