如何基于FSM有限状态机实现Enemies AI

news2025/1/20 17:09:05

文章目录

  • 🍟 Preface
  • 🍕 巡逻状态
  • 🍿 寻路状态
  • 🌭 攻击状态
  • 🍗 完整代码


🍟 Preface

本文简单介绍如何基于FSM有限状态机实现Enemies AI,首先定义敌人的AI逻辑:默认状态下Enemy为巡逻状态,有若干巡逻点位,Enemy在这些点位之间来回巡逻走动,同时检测Player的位置,当Player进入一定范围内时,Enemy进入寻路状态,寻路到Player位置前,进入Attacking攻击状态,当Player离开一定距离时,Enemy重回巡逻状态进行巡逻。

  • Patrol State:巡逻状态
  • Path Finding State:寻路状态
  • Attacking State:攻击状态

🍕 巡逻状态

巡逻状态

如图所示,我们预设了三个巡逻点,Enemy会在这三个巡逻点之间来回移动巡逻,并且在到达一个巡逻点时,会随机休息几秒,首先在OnDrawGizmos函数中绘制出三个点的Position Handle,方便我们调试:

//巡逻点集合
[SerializeField] private Transform[] patrolPoints;

private void OnDrawGizmos()
{
    for (int i = 0; i < patrolPoints.Length; i++)
    {
        Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
        Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
    }
}

动画相关变量与参数如下:

//动画组件
[SerializeField] private Animator animator;

private class AnimatorParams
{
    public static readonly int Idle = Animator.StringToHash("Idle");
    public static readonly int Walk = Animator.StringToHash("Walk");
    public static readonly int Run = Animator.StringToHash("Run");
    public static readonly int Action = Animator.StringToHash("Action");
}

寻路功能使用Unity内部功能NavMeshAgent

//寻路代理
[SerializeField] private NavMeshAgent agent;

定义Patrol State

private class PatrolState : State
{
    //当前巡逻点的索引值
    public int index;
    //休息计时
    public float timer;
}

创建状态机并构建状态:

private void Start()
{
    var machine = StateMachine.Create("Enemy AI")
        .Build<PatrolState>("巡逻状态")
            .OnEnter(s =>
            {
                agent.isStopped = false;
                //StopDistance设为0
                agent.stoppingDistance = 0f;
                //设置速度
                agent.speed = 1f;
                //进入巡逻状态时 设置第一个巡逻点
                s.index = 0;
                agent.SetDestination(patrolPoints[s.index].position);
                //设置动画参数 进入Walk
                animator.SetBool(AnimatorParams.Idle, false);
                animator.SetBool(AnimatorParams.Walk, true);
            })
            .OnStay(s =>
            {
                //判断是否到达目标巡逻点
                if (Vector3.Distance(transform.position, patrolPoints[s.index].position) <= .1f)
                {
                    //设置动画参数 进入Idle
                    animator.SetBool(AnimatorParams.Walk, false);
                    animator.SetBool(AnimatorParams.Idle, true);
                    //到达后随机休息若干秒
                    s.timer += Time.deltaTime;
                    if (s.timer >= Random.Range(3f, 5f))
                    {
                        //重置计时器
                        s.timer = 0f;
                        //设置下一个巡逻点
                        s.index++;
                        s.index = s.index == patrolPoints.Length ? 0 : s.index;
                        agent.SetDestination(patrolPoints[s.index].position);
                        //设置动画参数 进入Walk
                        animator.SetBool(AnimatorParams.Idle, false);
                        animator.SetBool(AnimatorParams.Walk, true);
                    }
                }
            })
            .OnExit(s =>
            {
                agent.isStopped = true;
                animator.SetBool(AnimatorParams.Idle, false);
                animator.SetBool(AnimatorParams.Walk, false);
            })
        .Complete();

    //进入第一个状态
    machine.Switch2Next();
}

巡逻状态下,当Player进入到5米检测范围内时,进入寻路状态:

//当Player进入5米范围内时 Enemy进入寻路状态
SwitchWhen(() => Vector3.Distance(player.position, transform.position) <= 5f, "寻路状态")

通过Handles类中的DrawWireArc方法将该范围绘制出来,方便调试:

Handles.color = Color.red;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);

如图所示,红色圈范围即为检测范围:

检测范围

🍿 寻路状态

寻路状态表示已经检测到Player,追击Player,不断寻路到Player前,设置AgentStop Distance属性为1.5,该寻路过程中的移动速度比巡逻状态时要快,因此调整Speed属性为2,当距离Player大于10时,重新回到巡逻状态,不再追击。

.Build<State>("寻路状态")
	.OnEnter(s =>
	{
		agent.isStopped = false;
		//StopDistance设为1
		agent.stoppingDistance = 1.5f;
		//加速移动
		agent.speed = 2f;
		//设置动画参数 进入Run
		animator.SetBool(AnimatorParams.Run, true);
	})
    .OnStay(s =>
    {
		//未到达Player前指定距离时 不断寻路
        if (Vector3.Distance(transform.position, player.position) > 1.5f)
        {
        	agent.SetDestination(player.position);
        }
        else
        {
        	//到达Player前指定距离 进入攻击状态
            s.machine.Switch("攻击状态");
        }
	})
    .OnExit(s =>
    {
    	animator.SetBool(AnimatorParams.Run, false);
    })
    //距离Player大于指定值时 重回巡逻状态
    .SwitchWhen(() => Vector3.Distance(transform.position, player.position) > 10f, "巡逻状态")
.Complete()

同样使用Handles类中的DrawWireArc方法绘制出追击范围:

private void OnDrawGizmos()
{
    for (int i = 0; i < patrolPoints.Length; i++)
    {
        Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
        Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
    }

    Handles.color = Color.red;
    Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);
    Handles.color = Color.cyan;
    Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 10f);
}

如图所示,青色圈范围即为追击范围:

追击范围

🌭 攻击状态

定义攻击状态:

private class AttackState : State
{
    //攻击CD
    public float attackCD = 2f;
}

构建攻击状态:

.Build<AttackState>("攻击状态")
	.OnEnter(s => agent.isStopped = true)
	.OnStay(s =>
    {
    	//朝向Player
        transform.rotation = Quaternion.LookRotation(player.position - transform.position);
        //Attack Action
        if (s.attackCD == 2f) animator.SetInteger(AnimatorParams.Action, 1);
        //攻击CD
        else
        {
        	s.attackCD -= Time.deltaTime;
            if (s.attackCD <= 0f) s.attackCD = 2f;
        }
        })
	.OnExit(s => animator.SetInteger(AnimatorParams.Action, 0))
    .SwitchWhen(() => Vector3.Distance(transform.position, player.position) >= 2f, "寻路状态")
.Complete();

这里使用一个Wolf的模型当做Player:

Player
Player进入巡逻检测范围:

进入巡逻检测范围

Player离开追击范围:

离开追击范围

🍗 完整代码

using UnityEngine;
using UnityEngine.AI;
using SK.Framework.FSM;

#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// 敌人单位
/// </summary>
public class EnemyUnit : MonoBehaviour
{
    //Player位置
    [SerializeField] private Transform player;
    //寻路代理
    [SerializeField] private NavMeshAgent agent;
    //动画组件
    [SerializeField] private Animator animator;
    //巡逻点集合
    [SerializeField] private Transform[] patrolPoints;

    private class PatrolState : State
    {
        //当前巡逻点的索引值
        public int index;
        //休息计时
        public float timer;
    }

    private class AttackState : State
    {
        public float attackCD = 2f;
    }

    private class AnimatorParams
    {
        public static readonly int Idle = Animator.StringToHash("Idle");
        public static readonly int Walk = Animator.StringToHash("Walk");
        public static readonly int Run = Animator.StringToHash("Run");
        public static readonly int Action = Animator.StringToHash("Action");
    }

    private void Start()
    {
        var machine = StateMachine.Create("Enemy AI")
            .Build<PatrolState>("巡逻状态")
                .OnEnter(s =>
                {
                    agent.isStopped = false;
                    //StopDistance设为0
                    agent.stoppingDistance = 0f;
                    //设置速度
                    agent.speed = 1f;
                    //进入巡逻状态时 设置第一个巡逻点
                    s.index = 0;
                    agent.SetDestination(patrolPoints[s.index].position);
                    //设置动画参数 进入Walk
                    animator.SetBool(AnimatorParams.Idle, false);
                    animator.SetBool(AnimatorParams.Walk, true);
                })
                .OnStay(s =>
                {
                    //判断是否到达目标巡逻点
                    if (Vector3.Distance(transform.position, patrolPoints[s.index].position) <= .1f)
                    {
                        //设置动画参数 进入Idle
                        animator.SetBool(AnimatorParams.Walk, false);
                        animator.SetBool(AnimatorParams.Idle, true);
                        //到达后随机休息若干秒
                        s.timer += Time.deltaTime;
                        if (s.timer >= Random.Range(3f, 5f))
                        {
                            //重置计时器
                            s.timer = 0f;
                            //设置下一个巡逻点
                            s.index++;
                            s.index = s.index == patrolPoints.Length ? 0 : s.index;
                            agent.SetDestination(patrolPoints[s.index].position);
                            //设置动画参数 进入Walk
                            animator.SetBool(AnimatorParams.Idle, false);
                            animator.SetBool(AnimatorParams.Walk, true);
                        }
                    }
                })
                .OnExit(s =>
                {
                    agent.isStopped = true;
                    animator.SetBool(AnimatorParams.Idle, false);
                    animator.SetBool(AnimatorParams.Walk, false);
                })
                //当Player进入5米范围内时 Enemy进入寻路状态
                .SwitchWhen(() => Vector3.Distance(player.position, transform.position) <= 5f, "寻路状态")
            .Complete()
            .Build<State>("寻路状态")
                .OnEnter(s =>
                {
                    agent.isStopped = false;
                    //StopDistance设为1
                    agent.stoppingDistance = 1.5f;
                    //加速移动
                    agent.speed = 2f;
                    //设置动画参数 进入Run
                    animator.SetBool(AnimatorParams.Run, true);
                })
                .OnStay(s =>
                {
                    //未到达Player前指定距离时 不断寻路
                    if (Vector3.Distance(transform.position, player.position) > 1.5f)
                    {
                        agent.SetDestination(player.position);
                    }
                    else
                    {
                        //到达Player前指定距离 进入攻击状态
                        s.machine.Switch("攻击状态");
                    }
                })
                .OnExit(s =>
                {
                    animator.SetBool(AnimatorParams.Run, false);
                })
                //距离Player大于指定值时 重回巡逻状态
                .SwitchWhen(() => Vector3.Distance(transform.position, player.position) > 10f, "巡逻状态")
            .Complete()
            .Build<AttackState>("攻击状态")
                .OnEnter(s => agent.isStopped = true)
                .OnStay(s =>
                {
                    //朝向Player
                    transform.rotation = Quaternion.LookRotation(player.position - transform.position);
                    //Attack Action
                    if (s.attackCD == 2f) animator.SetInteger(AnimatorParams.Action, 1);
                    //攻击CD
                    else
                    {
                        s.attackCD -= Time.deltaTime;
                        if (s.attackCD <= 0f) s.attackCD = 2f;
                    }
                })
                .OnExit(s => animator.SetInteger(AnimatorParams.Action, 0))
                .SwitchWhen(() => Vector3.Distance(transform.position, player.position) >= 2f, "寻路状态")
            .Complete();

        //进入第一个状态
        machine.Switch2Next();
    }

#if UNITY_EDITOR
    private void OnDrawGizmos()
    {
        for (int i = 0; i < patrolPoints.Length; i++)
        {
            Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
            Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
        }

        Handles.color = Color.red;
        Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);
        Handles.color = Color.cyan;
        Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 10f);
    }
#endif
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/40930.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

刷爆力扣之等价多米诺骨牌对的数量

刷爆力扣之等价多米诺骨牌对的数量 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&…

使用 nlohmann 解析 json 文件

使用 nlohmann 解析 json 文件nlohmann/json的配置json基本数据结构json文件的读取、构造与输出C对象与nlohmann::json对象的转换C对象转换成nlohmann::json对象nlohmann::json对象转换成C对象序列化反序列化序列化nlohmann 是德国工程师&#xff0c;以其名字为工程名的 nlohm…

springboot项目的打包发布部署,jar和war的区别

简介&#xff1a; 1.Spring Boot使用了内嵌容器&#xff0c;因此它的部署方式也变得非常简单灵活&#xff0c;可以将Spring Boot项目打包成JAR包来独立运行&#xff0c;也可以打包成WAR包部署到Tomcat容器中运行&#xff0c;如果涉及大规模的部署&#xff0c;Jenkins成为最佳选…

【HCIP-Datacom】 IS-IS基础 ISIS动态路由协议配置(ISIS思维导图在底部)

目录 ISIS配置方法&#xff1a; 路由计算&#xff1a; ATT置位条件&#xff1a; 路由渗透&#xff1a; ISIS的认证&#xff1a; ISIS配置命令&#xff1a; ISIS的开销类型&#xff1a; ISIS配置方法&#xff1a; 进入ISIS进程 isis 1 //创建isis进程 设置实体名 network-entit…

.NET 升级发布后,IIS出现了System.IO.DirectoryNotFoundException

最近计划升级项目到.NET6, 在使用Release发布后发现IIS不能发现wwwroot目录,什么错误? 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:无尽的折腾后,终于又回到了起点,工控,我来了 !1 发布的一…

《统计学习方法》 第十六章 主成分分析PCA

主成分分析(PCA) 假设xxx为mmm 维随机变量&#xff0c;其均值为μ\muμ&#xff0c;协方差矩阵为Σ\SigmaΣ 考虑由mmm维随机变量xxx到mmm维随机变量yyy的线性变换 yiαiTx∑k1mαkixk,i1,2,⋯,my _ { i } \alpha _ { i } ^ { T } x \sum _ { k 1 } ^ { m } \alpha _ { k …

计算点在线上的投影坐标

如题 计算点到线上的垂点&#xff0c;首先明确&#xff1a; 该线段必须给出确切的起始点和终点&#xff0c; 而不是一个向量&#xff0c;因为一个向量并不能代表一个线段。 所以参数列表如下&#xff1a; Vector3 VerticalPoint(Vector3 point, Vector3 lStart, Vector3 lEnd…

【论文翻译】增强复制状态机的两阶段提交协议

Enhancing Two Phase-Commit Protocol for Replicated State Machines Halit Uyanık and Tolga Ovatman Department of Computer Engineering Istanbul Technical University 34469 Istanbul, Turkey Email目录1 介绍2 设计和实现2.1 事件类型2.2 在状态机上执行事件2.3 具有优…

8、常用基本命令(重要)

文章目录8、常用基本命令&#xff08;重要&#xff09;8.1 帮助命令8.1.1 man 获得帮助信息8.1.2 help 获得 shell 内置命令的帮助信息8.1.3 常用快捷键8.2 文件目录类8.2.1 pwd 显示当前工作目录的绝对路径8.2.2 ls 列出目录的内容8.2.3 cd 切换目录8.2.4 mkdir 创建一个新的目…

Linux驱动入门

一、驱动简介 Linux的驱动在本质上就是一种软件程序&#xff0c;上层软件可以在不了解硬件特性的情况下&#xff0c;通过驱动提供的接口&#xff0c;和计算机硬件进行通信。 系统调用是内核和应用程序之间的接口&#xff0c;而驱动程序是内核和硬件之间的接口。它为应用程序屏蔽…

缓存穿透、缓存击穿、缓存雪崩及其解决方案

缓存&#xff08;cache&#xff09;&#xff0c;大家都非常熟悉&#xff0c;几乎每个系统乃至整个计算机体系中都会用到。在分布式系统架构中&#xff0c;主要用于减轻数据库的压力&#xff0c;提高系统的响应速度和并发吞吐&#xff0c;即空间(内存)换时间。当大量的读、写请求…

【模型推理加速系列】06: 基于resnet18加速方案评测

简介 花雪随风不厌看&#xff0c;更多还肯失林峦。愁人正在书窗下&#xff0c;一片飞来一片寒。小伙伴们好&#xff0c;我是微信公众号小窗幽记机器学习的首席称重师&#xff1a;卖麻辣烫的小男孩。今天这篇文章以resnet18模型为例&#xff0c;对比Pytorch、ONNX、TorchScript…

cmdline(二):uboot cmdline怎么传?cmdline kernel怎么用?

前面我们知道了cmdline是什么&#xff0c;已经在哪里添加cmdline&#xff1f;现在我们来看看在哪里传输cmdline&#xff0c;以及传输收到后怎么用&#xff1f; 参考内容来自前辈&#xff0c;感激&#xff1a; https://blog.csdn.net/weixin_42031299/article/details/12123950…

Spring Boot JPA EntityManager实体管理器示例

在本教程中&#xff0c;您将了解如何在 Spring Boot 示例中使用 JPA EntityManager&#xff08;使用 CRUD 操作和查询方法&#xff09;。我将向您展示&#xff1a; 在 Spring 引导中访问 JPA 实体管理器的方法如何使用实体管理器方法&#xff1a;执行SQL查询使用和CRUD操作cre…

【Android App】实现在线语音合成功能(使用云知声平台和WebSocket 超详细 附源码)

需要源码和Jar包请点赞关注收藏后评论区留下QQ~~~ 一、在线语音合成 虽然国产智能机大多集成了中文语音引擎&#xff0c;但是系统自带的语音工具无法满足商用要求&#xff0c;功能单一&#xff0c;所以势必引入第三方的语音引擎&#xff0c;依靠第三方提供的开发包统一支撑语音…

【新知实验室】——腾讯云音视频TRTC体验

腾讯实时音视频 TRTC 是什么&#xff1f; 腾讯实时音视频&#xff08;Tencent Real-Time Communication&#xff0c;TRTC&#xff09;将腾讯21年来在网络与音视频技术上的深度积累&#xff0c;以多人音视频通话和低延时互动直播两大场景化方案&#xff0c;通过腾讯云服务向开发…

clickHouse基础语法

clichouse数据类型 整形 lnt8 8bit,1字节 &#xff08;-128-127&#xff09;lnt16 16bitlnt32 32bitlnt64 64bit 无符号整型 相比于上面&#xff0c;就是把负数部分挪到正数部分 Ulnt8 &#xff08;0-255&#xff09;Ulnt16Ulnt32Ulnt64 浮点型 Float32 也就是floatFloa…

Instant Neural Graphics Primitives with a Multiresolution Hash Encoding以及源码浅析

背景 现存的一些新视图合成的训练过程和渲染速度都比较慢&#xff0c;其原因是因为query point需要使用MLP编码&#xff0c;而且在一个采样空间中&#xff0c;存在很多无效的query point也要计算其density和color&#xff0c;从而出现很多冗余计算。 作者针对这个问题&#x…

MAUI 中使用 DI 及 MVVM

MAUI 中使用 DI 及 MVVM为什么要使用 依赖注入 和 MVVM如何在 MAUI 中使用依赖注入如何使用 MVVM不使用框架或组件定义一个 BaseViewModelMainViewModel 的实现MainPage 中进行 Binding使用组件优化前面的 ViewModel 代码基项目的效果为什么要使用 依赖注入 和 MVVM MVVM 和 依…

1535_TriCore编译器Tasking使用_汇编分区、内置函数以及伪指令

全部学习汇总&#xff1a; GreyZhang/TriCore_Tasking_Compiler_Skills: Some skills for Tasking compiler on AURIX platform. Happy hacking! (github.com) 看了一下这个章节的内容&#xff0c;原本看着页数很多拆分成了两次学习。后面发现剩下的这部分内容主要并不是框架性…