Unity有限状态机实现怪物AI(代码框架思路)

2024-06-04 05:20

本文主要是介绍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(代码框架思路)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1029176

相关文章

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分

一文详解如何从零构建Spring Boot Starter并实现整合

《一文详解如何从零构建SpringBootStarter并实现整合》SpringBoot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,:本文主要介绍如何从... 目录一、Spring Boot Starter的核心价值二、Starter项目创建全流程2.1 项目初始化(

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

MySQL INSERT语句实现当记录不存在时插入的几种方法

《MySQLINSERT语句实现当记录不存在时插入的几种方法》MySQL的INSERT语句是用于向数据库表中插入新记录的关键命令,下面:本文主要介绍MySQLINSERT语句实现当记录不存在时... 目录使用 INSERT IGNORE使用 ON DUPLICATE KEY UPDATE使用 REPLACE