本文主要是介绍Unity有限状态机实现怪物AI(代码框架思路),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
状态的枚举
状态基类
接口(规范不同对象的同一行为)
状态机类(作为媒介用于管理各个状态之间的转换)
附带一个攻击状态的子类脚本作为示例:
状态的枚举
首先最容易想到的是状态的枚举,比如说攻击状态、巡逻状态、追击状态等等,用枚举进行表示
public enum E_AI_State
{/// <summary>/// 睡眠状态/// </summary>Sleep,/// <summary>/// 巡逻状态/// </summary>Patrol,/// <summary>/// 聊天状态/// </summary>Chat,/// <summary>/// 逃跑状态/// </summary>Run,/// <summary>/// 追逐玩家状态/// </summary>Chase,/// <summary>/// 攻击玩家的状态/// </summary>Atk,/// <summary>/// 警觉状态/// </summary>Alertness,
}
状态基类
再就是所有怪物 对应的状态都会有一个 脚本去控制该状态下该执行什么,而这些状态肯定会有一部分的逻辑是相同的,所以可以提取出一个抽象类,即状态基类
public abstract class BaseState
{//有限状态机实现的AI中的 这些 状态类 它的本质 对于我们来说 是在做什么?//逻辑处理(不仅仅是做AI,不管你用代码做什么样的事情 都是在进行逻辑处理)//AI状态的切换 //切换这个词 就意味着// 状态1 ——> 状态2//在这个基类中 可以去实现所有状态共有的 进入、离开、处于状态的行为(函数、方法)//但是这些方法中 由于是基类,没有明确是哪种状态,也就意味着方法中不会写内容//那么 不能写内容的函数 你能联想到什么?//1.如果是接口 ,那么直接声明//2.如果是类,那么可以考虑抽象方法——一定是抽象类//管理自己的有限状态机对象protected StateMachine stateMachine;/// <summary>/// 初始化状态类时 将管理者传入 进行记录/// </summary>/// <param name="machine"></param>public BaseState(StateMachine machine){stateMachine = machine;}//当前状态的类型public virtual E_AI_State AIState{get;}// 1.离开状态时 做什么public abstract void QuitState();// 2.进入状态时 做什么public abstract void EnterState();// 3.处于状态时 做什么(核心逻辑处理)public abstract void UpdateState();
}
接口(规范不同对象的同一行为)
public interface IAIObj
{//所有AI对象都应该可以获取到它的Transform信息public Transform objTransform{get;}//所有AI对象都应该有一个当前的位置public Vector3 nowPos{get;}//AI对象的目标对象所在的位置public Vector3 targetObjPos{get;}//所有AI对象都应该有一个攻击范围的概念//好用于判断 什么时候开始攻击玩家public float atkRange{get;}//出生位置 需要继承它的AI对象提供public Vector3 bornPos {get;set;}//AI对象中 应该有 移动相关的方法public void Move(Vector3 dirOrPos);//AI对象中 应该有 停止移动相关的方法public void StopMove();//AI对象中 应该有 攻击相关的方法public void Atk();//AI对象中 可能想要单独 切换指定动作//切换动作 应该传递一些相关参数 才能够指定切换哪个动作吧public void ChangeAction(E_Action action);//我们应该根据AI不同的状态 去提取出他们的行为合集
}
状态机类(作为媒介用于管理各个状态之间的转换)
public class StateMachine
{//他要管理AI的所有状态//所以我们通过一个容器去存储这些状态//这些状态会随时的取出来进行切换 因此我们要选用一个方便查找获取的容器存储//key —— 状态类型(是有限的状态类型,那么就可以是一开始定死的,// 即使以后策划天马行空 有了新状态需求 ,我们改代码即可,因为我们有了热更新技术 所以也没有太大的影响)//value —— 代表的是处理状态的逻辑对象private Dictionary<E_AI_State, BaseState> stateDic = new Dictionary<E_AI_State, BaseState>();//表示当前有限状态 处于的状态(也就是对应的怪物或玩家当前处于的AI状态)private BaseState nowState;//这个就是ai有限状态机 管理的 ai对象 会去通过ai状态命令该对象 执行对应的行为public IAIObj aiObj;//回归的判断临界距离 现在我们写死 以后可能是从AI表中读取public float backDis = 15;//我们的有限状态机制作的AI 里面有很多的AI状态//那么这些AI状态逻辑当中,最终要去针对什么处理对应的状态逻辑//处理的其实是 游戏当中需要AI的对象 比如 怪物、玩家、宠物、NPC等等//虽然这些对象都是不一样的对象 但是 他们理论上来说需要具备共同的行为//这样在处理AI逻辑时 才更方便进行一些行为的调用//我们其实可以尝试 在AI模块把这些内容提取出来 作为接口 让这些需要AI的对象 必须要实现这个接口 才行/// <summary>/// 初始化有限状态机类 /// </summary>/// <param name="aiObj">传入 ai对象 用于之后的行为控制</param>public void Init(IAIObj aiObj){this.aiObj = aiObj;}/// <summary>/// 添加AI状态/// </summary>public void AddState(E_AI_State state){switch (state){case E_AI_State.Patrol:stateDic.Add(state, new PatrolState(this));break;case E_AI_State.Run:stateDic.Add(state, new RunState(this));break;case E_AI_State.Chase:stateDic.Add(state, new ChaseState(this));break;case E_AI_State.Atk:stateDic.Add(state, new AtkState(this));break;}}/// <summary>/// 改变状态/// </summary>public void ChangeState(E_AI_State state){//如果当前处于另一个状态 就退出该状态if (nowState != null)nowState.QuitState();//如果存在该状态的逻辑出来对象 那么就进入该状态if(stateDic.ContainsKey(state)){nowState = stateDic[state];nowState.EnterState();}}/// <summary>/// 更新当前状态逻辑处理/// </summary>public void UpdateState(){if (nowState != null)nowState.UpdateState();}/// <summary>/// 检测是否切换到回归状态/// </summary>public void CheckChangeRun(){//在追逐过程中 发现超出了 我们的最大距离 就应该切换到回归的状态//目前我们处理的是利用ai对象和自己的出生点距离 进行最大距离判断//达到的效果是 ai对象一定要跑到边界 才甘心//其实还可以利用 目标对象和自己的出生点距离 + 自己攻击距离 来进行距离判断//达到的效果就是 目标达不到了 就没有必要追了if (Vector3.Distance(this.aiObj.nowPos, this.aiObj.bornPos) >= this.backDis){ChangeState(E_AI_State.Run);}}
}
在状态机类中的AddState方法中,向字典中添加了对应的状态(把自己这个状态机类传进去供初始化),也就是说,当我在另一个脚本中调用状态机类的中的AddState,他就会在BaseState中自动关联上了状态机类 这个脚本,参考代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;public class Monster : MonoBehaviour, IAIObj
{public GameObject bullet;//网格寻路组件private NavMeshAgent navMeshAgent;//在需要使用AI模块的对象当中 声明一个 AI状态机对象 用于开启AI功能private StateMachine aiStateMachine;private Vector3 nowObjPos;//对象当前的位置public Vector3 nowPos {get{nowObjPos = this.transform.position;//为了和我们AI模块的定位规则相同 没有考虑 Y上的位置 主要是在xz平面进行位移nowObjPos.y = 0;return nowObjPos;}}//出生位置public Vector3 bornPos{get;set;}//AI对象必须能够被AI模块获取到Transform 方便我们进行相关处理public Transform objTransform => this.transform;//自己的攻击范围(目前我们可以写死,以后 一般是通过配置表进行数据初始化//如果还有其他规则,自己实现对应的攻击范围规则即可)public float atkRange => 2;//用于测试用的目标对象//正常情况下,应该通过代码动态的再场景中寻找满足条件的目标 我们这里仅仅是测试//所以直接通过拖拽进行关联public Transform targetTransform;//由于我们现在还不用去考虑 目标 所以随便给一个目标位置public Vector3 targetObjPos{get{//注意:这里减去y方向的0.5 是因为我们用立方体举例子,它的y往上升了0.5//为了贴合地面 所以我们减去0.5return targetTransform.position - Vector3.up * 0.5f;}}private void Start(){navMeshAgent = this.GetComponent<NavMeshAgent>();//之所以把AI的重要初始化 放到对象类当中 主要原因//是因为不同对象 可能会存在不同的AI状态,不同的起始状态//这些往往在游戏中 都是配置表当中配置的 所以一般写在怪物创建处//注意://大多数情况下 会放在 怪物管理器中的创建怪物的方法中,但是我们目前没有设计怪物管理器//因此,我们把这一块代码 放在了 怪物出生的生命周期函数中 也就是Start中(也可以放在Awake)//初始化AI模块的有限状态机对象aiStateMachine = new StateMachine();//把ai对象自己 传入其中进行初始化aiStateMachine.Init(this);//你需要什么AI状态 就动态添加(以后一般情况下 是通过配置表的配置去添加)//为AI添加巡逻状态aiStateMachine.AddState(E_AI_State.Patrol);aiStateMachine.AddState(E_AI_State.Chase);aiStateMachine.AddState(E_AI_State.Atk);aiStateMachine.AddState(E_AI_State.Run);//初始化完所有AI状态后 那就需要一个当前的AI状态//目前一开始就让对象时一个巡逻状态aiStateMachine.ChangeState(E_AI_State.Patrol);//出生位置 就是对象一开始所在的位置bornPos = this.transform.position;}private void Update(){//ai相关的更新 是由 ai对象的 帧更新函数 发起的 aiStateMachine.UpdateState();}public void Atk(){//暂时不写 之后写到攻击AI时 再去写它print("攻击");//动态创建自动 发射即可GameObject obj = Instantiate(bullet, this.transform.position + this.transform.forward + Vector3.up * 0.5f, this.transform.rotation);Destroy(obj, 5f);}public void ChangeAction(E_Action action){print(action);}public void Move(Vector3 dirOrPos){//结束停止移动navMeshAgent.isStopped = false;navMeshAgent.SetDestination(dirOrPos);}public void StopMove(){//该方法过时了//navMeshAgent.Stop();//停止移动navMeshAgent.isStopped = true;}
}
附带一个攻击状态的子类脚本作为示例:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class AtkState : BaseState
{public override E_AI_State AIState => E_AI_State.Atk;//下一次攻击的时间private float nextAtkTime;//下次攻击等待的时间private float waitTime = 2f;public AtkState(StateMachine machine):base(machine){}public override void EnterState(){Debug.Log("进入攻击状态了");//进入攻击状态时 认为此时此刻就要攻击nextAtkTime = Time.time;}public override void QuitState(){}public override void UpdateState(){//进入AI状态后 不停的让ai对象去攻击即可if (Time.time >= nextAtkTime){stateMachine.aiObj.Atk();nextAtkTime = Time.time + waitTime;}//如果目标和我的距离过远了,我们应该去切换到追逐状态 ,追到了再继续打它if (Vector3.Distance(stateMachine.aiObj.nowPos, stateMachine.aiObj.targetObjPos) > stateMachine.aiObj.atkRange){stateMachine.ChangeState(E_AI_State.Chase);}//我们可以利用向量和四元数相关知识 让ai对象看向目标对象 也可以简单粗暴的用LookAt//我们在这里只是举例子 就使用LookAt来节约一些事件 之后 大家可以根据自己的需求去进行制作stateMachine.aiObj.objTransform.LookAt(stateMachine.aiObj.targetObjPos + Vector3.up * 0.5f);//在追逐过程中 发现超出了 我们的最大距离 就应该切换到回归的状态stateMachine.CheckChangeRun();}
}
这篇关于Unity有限状态机实现怪物AI(代码框架思路)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!