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

相关文章

python实现svg图片转换为png和gif

《python实现svg图片转换为png和gif》这篇文章主要为大家详细介绍了python如何实现将svg图片格式转换为png和gif,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录python实现svg图片转换为png和gifpython实现图片格式之间的相互转换延展:基于Py

Python利用ElementTree实现快速解析XML文件

《Python利用ElementTree实现快速解析XML文件》ElementTree是Python标准库的一部分,而且是Python标准库中用于解析和操作XML数据的模块,下面小编就来和大家详细讲讲... 目录一、XML文件解析到底有多重要二、ElementTree快速入门1. 加载XML的两种方式2.

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

C# foreach 循环中获取索引的实现方式

《C#foreach循环中获取索引的实现方式》:本文主要介绍C#foreach循环中获取索引的实现方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、手动维护索引变量二、LINQ Select + 元组解构三、扩展方法封装索引四、使用 for 循环替代

Spring Security+JWT如何实现前后端分离权限控制

《SpringSecurity+JWT如何实现前后端分离权限控制》本篇将手把手教你用SpringSecurity+JWT搭建一套完整的登录认证与权限控制体系,具有很好的参考价值,希望对大家... 目录Spring Security+JWT实现前后端分离权限控制实战一、为什么要用 JWT?二、JWT 基本结构

Java实现优雅日期处理的方案详解

《Java实现优雅日期处理的方案详解》在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间,下面我们就来看看如何使用java处理这样的日期问题吧,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言一、日期的坑1.1 日期格式化陷阱1.2 时区转换二、优雅方案的进阶之路2.1 线程安全重构2

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络