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

本文详细介绍了如何使用有限状态机(FSM)在Unity中实现敌人AI。敌人有三种状态:巡逻、寻路和攻击。在巡逻状态中,敌人会在预设的多个巡逻点间移动;当检测到玩家进入一定范围,切换到寻路状态,加速追赶玩家;接近玩家后进入攻击状态,攻击后若与玩家保持一定距离,又将返回巡逻状态。整个AI逻辑通过 Animator 和 NavMeshAgent 进行控制,并利用 Handles 绘制调试辅助线来展示不同状态的检测范围。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


🍟 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
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CoderZ1010

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值