本文分享Unity中的AI算法和实现3-有限状态机FSM(下)
回家生孩子, 暂停了一个多月的更新, 今天重新续上, ^_^.
在上一篇文章中, 我们分享了状态机的基本理论, 然后结合Unity的Animator来熟悉了这些理论, 最后设计了我们自己的状态机并实现了框架部分.
这一篇文章, 我们将继续完成业务部分, 也就是怎么使用框架来实现我们的目标:
怪物在巡逻过程中发现玩家并追逐玩家, 并且在离玩家一定距离之后脱离追逐然后继续进行巡逻.
好了, 废话不多说, 马上开始今天的内容.
效果预览
怎么样, 是不是逐渐开始有点意思了?
设计我们自己的FSM示例
我们将在第一篇文章的基础之上, 扩充我们的示例.
需要保留和修改的是:
- 怪物对象: Monster
- Waypoints列表: Waypoints
- 地面: Plane
需要新增:
- 玩家: Gamer
- 数据管理器: DataMgr_FSM
- 怪物状态类: FSMState_Monster
- 怪物状态机类型: FSMStateType_Monster
- 怪物状态机控制器: MonsterContoller_FSM
场景搭建
创建相关类:
- DataMgr_FSM
- FSMState_Monster
- MonsterContoller_FSM
然后直接复制上一个场景, 创建GameObject代表Gamer, 为了简化, 这里使用简单的对象即可, 我们后续通过直接在Scene视图下拖动这个对象来代表玩家在场景中的移动.
创建GameObject代表DataMgr_FSM, 并将DataMgr_FSM附加上.
删除Monster身上的MonsterContoller, 然后附加MonsterContoller_FSM.
对应类的实现
代码实现部分的主体内容其实在上一篇文章里, 这个部分只是业务的实现, 内容不是很多. 我们一个个介绍.
DataMgr_FSM
DataMgr_FSM就是一个简单的数据管理器, 用于在多个类之间共享数据, 当然在实际的项目里没有这么简单, 我们在示例中一切以简化为主.
public class DataMgr_FSM : MonoBehaviour
{
/// <summary>
/// 单例
/// </summary>
public static DataMgr_FSM instance;
/// <summary>
/// 序列化的npc巡逻点
/// </summary>
public Transform[] monsterWaypoints;
/// <summary>
/// npc需要追踪的目标, 也就是Gamer
/// </summary>
public Transform target;
/// <summary>
/// npc移动速度
/// </summary>
public float monsterMoveSpeed = 10f;
/// <summary>
/// npc旋转速度
/// </summary>
public float monsterRotateSpeed = 10f;
/// <summary>
/// npc距离目标(跟踪目标和巡逻目标点)的最小距离, 达到后就不再接近以免乱跳
/// </summary>
public float minTargetDistance = 0.5f;
/// <summary>
/// 最大观测距离
/// </summary>
public float maxSeeGamerDistance = 10f;
/// <summary>
/// 最小观测距离
/// </summary>
public float minSeeGamerDistance = 5f;
/// <summary>
/// 玩家是否处于友好状态, 友好状态下的玩家不会被追击
/// </summary>
public bool IsGamerFriendly = false;
private void Awake()
{
instance = this;
}
}
FSMStateType_Monster
FSMStateType_Monster是怪物状态机类型, 定义了怪物的几种行为.
/// <summary>
/// 状态机类型 怪物
/// </summary>
public enum FSMStateType_Monster
{
None,
Patrol, // 巡逻
SeeGamer, // 看到玩家
LostGamer, // 丢失玩家
CatchGamer, // 抓到玩家
AttackGamer, // 攻击玩家
}
FSMState_Monster
FSMState_Monster是怪物状态类, 是怪物行为的核心实现, 这里只实现了Patrol和SeeGamer, 其它的状态基本上被揉在了两个状态中.
比如LostGamer就是SeeGamer超过可视距离之后需要切换的状态, 但是本身没有什么逻辑, 所以直接揉在了Patrol中.
public class FSMState_Monster_Patrol : FSMStateBase
{
private Transform m_SelfTrans;
private Transform m_GamerTrans;
/// <summary>
/// 当前wp索引
/// </summary>
private int m_CurrentWpIndex = 0;
public FSMState_Monster_Patrol(object owner) : base((int)FSMStateType_Monster.Patrol, owner)
{
m_SelfTrans = owner as Transform;
}
public override void OnEnter(FSMMgr mgr)
{
base.OnEnter(mgr);
if (DataMgr_FSM.instance.monsterWaypoints.Length <= 0)
Debug.LogError("Waypoint列表为空!");
}
public override string ToString()
{
return $"{(FSMStateType_Monster)StateType}状态";
}
private void RunningPatrol()
{
var targetPatrolTrans = DataMgr_FSM.instance.monsterWaypoints[m_CurrentWpIndex];
if (Vector3.Distance(targetPatrolTrans.position, m_SelfTrans.position) < DataMgr_FSM.instance.minTargetDistance)
{ // 已经靠近, 切换到下一个点
m_CurrentWpIndex++;
m_CurrentWpIndex %= DataMgr_FSM.instance.monsterWaypoints.Length; // 越界后从头开始
targetPatrolTrans = DataMgr_FSM.instance.monsterWaypoints[m_CurrentWpIndex];
}
var targetDir = (targetPatrolTrans.position - m_SelfTrans.position).normalized;
targetDir.y = 0;
// 移动和转向
m_SelfTrans.Translate(targetDir * Time.deltaTime * DataMgr_FSM.instance.monsterMoveSpeed, Space.World);
m_SelfTrans.rotation = Quaternion.Lerp(m_SelfTrans.rotation, Quaternion.LookRotation(targetDir), Time.deltaTime * DataMgr_FSM.instance.monsterRotateSpeed);
}
private void CheckSeeGamer()
{
m_GamerTrans = DataMgr_FSM.instance.target;
if (m_GamerTrans == null) return;
if (Vector3.Distance(m_GamerTrans.position, m_SelfTrans.position) <= DataMgr_FSM.instance.minSeeGamerDistance)
{
StateMgr.SetBool("SeeGamer", true);
}
}
public override void OnRunning()
{
RunningPatrol();
CheckSeeGamer();
}
}
public class FSMState_Monster_SeeGamer : FSMStateBase
{
private Transform m_SelfTrans;
private Transform m_GamerTrans;
public FSMState_Monster_SeeGamer(object owner) : base((int)FSMStateType_Monster.SeeGamer, owner)
{
m_SelfTrans = owner as Transform;
}
public override void OnEnter(FSMMgr mgr)
{
base.OnEnter(mgr);
}
public override string ToString()
{
return $"{(FSMStateType_Monster)StateType}状态";
}
public override void OnRunning()
{
m_GamerTrans = DataMgr_FSM.instance.target;
var targetPos = m_GamerTrans.position - m_SelfTrans.position;
var distance = Vector3.Distance(m_GamerTrans.position, m_SelfTrans.position);
if (distance > DataMgr_FSM.instance.maxSeeGamerDistance)
{ // 达到最大距离, 脱离目标
StateMgr.SetBool("SeeGamer", false);
return;
}
var targetDir = targetPos.normalized;
targetDir.y = 0;
if (targetDir.x != 0 && targetDir.z != 0)
m_SelfTrans.rotation = Quaternion.Lerp(m_SelfTrans.rotation, Quaternion.LookRotation(targetDir), Time.deltaTime * DataMgr_FSM.instance.monsterRotateSpeed);
if (distance > DataMgr_FSM.instance.minTargetDistance)
{ // 达到最近距离, 需要停止位移
m_SelfTrans.Translate(targetDir * Time.deltaTime * DataMgr_FSM.instance.monsterMoveSpeed, Space.World);
}
}
}
代码不是很复杂, 需要关注的是状态机的关系和切换.
MonsterContoller_FSM
MonsterContoller_FSM是怪物状态机控制器.
public class MonsterContoller_FSM : MonoBehaviour
{
[SerializeField] private Transform m_Target;
/// <summary>
/// 缓存Transform, 避免每帧使用属性获取
/// </summary>
private Transform m_SelfTrans;
private FSMMgr m_FsmMgr = new FSMMgr();
private bool m_IsGamerFriendly = false;
private void Awake()
{
m_SelfTrans = transform;
Application.targetFrameRate = 60;
m_FsmMgr.SetBool("SeeGamer", false);
m_FsmMgr.SetBool("IsGamerFriendly", false);
}
private void Start()
{
var patrolState = new FSMState_Monster_Patrol(m_SelfTrans);
var seeGamerState = new FSMState_Monster_SeeGamer(m_SelfTrans);
m_FsmMgr.RegisterState(patrolState);
m_FsmMgr.RegisterState(seeGamerState);
var patrolToSeeGamerTransition = new FSMTransitionBase(patrolState, seeGamerState);
var seeGamerToPatrolTransition = new FSMTransitionBase(seeGamerState, patrolState);
patrolToSeeGamerTransition.AddCondition("SeeGamer", FSMTransitionConditionMode.IfTrue);
patrolToSeeGamerTransition.AddCondition("IsGamerFriendly", FSMTransitionConditionMode.IfFalse);
seeGamerToPatrolTransition.AddCondition("SeeGamer", FSMTransitionConditionMode.IfFalse);
patrolState.RegisterTransition(patrolToSeeGamerTransition);
seeGamerState.RegisterTransition(seeGamerToPatrolTransition);
m_FsmMgr.ChangeState(patrolState);
}
private void Update()
{
if (m_IsGamerFriendly != DataMgr_FSM.instance.IsGamerFriendly)
{
m_IsGamerFriendly = DataMgr_FSM.instance.IsGamerFriendly;
m_FsmMgr.SetBool("IsGamerFriendly", m_IsGamerFriendly);
}
m_FsmMgr.OnRunning();
}
}
基本上是仿照Unity的Animator逻辑做的, 用过的应该很熟悉.
总结
今天我们使用上一篇文章设计的框架来实现了简单的怪物巡逻和追击玩家的行为, 目前市场上大部分游戏的类似逻辑实现基本上差不太多, 只是可能具体技术不一样, 比如我们这里使用的是状态机, 有些还可以使用行为树(后面会介绍), 目标导向的行为规划(GOAP, 听过还没研究过), 机器学习(简单研究过, 后面会介绍)等.
我们这里提供的专栏, 只是让大家对AI有一个基本的认识, 而不用说觉得AI有多么高大上, 其实日常游戏开发中使用的AI并不算很复杂(至少我目前遇到的不算), 复杂的是在量上而不是质上.
下篇文章可能是有限状态机的最后一篇, 可能会分享一下怪物如何避过路上遇到的障碍来追击玩家, 其实和有限状态机关系不大, 只是保持一个完整性.
但是也不太确定, 就想到哪里写哪里, AI相关的文章还有很多想要分享的, 比如之前提到的行为树, 机器学习, 还有比较常用的寻路算法之类的, 就是不知道有没有时间和心情了, 前几天遭了新冠, 这两天才稍微有点好转, 总之, 这世道, 麻烦多呢…
好啦, 不吐槽了, 今天就是这么多, 希望对大家有所帮助.
PS: 顺便对我家二少爷的到来表示热烈欢迎, 他老父亲还得继续加油呢~