前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何基于FSM有限状态机实现Enemies AI

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

作者头像
CoderZ
发布2022-12-26 21:07:23
5740
发布2022-12-26 21:07:23
举报

🍟 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,方便我们调试:

代码语言:javascript
复制
//巡逻点集合
[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));
    }
}

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

代码语言:javascript
复制
//动画组件
[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

代码语言:javascript
复制
//寻路代理
[SerializeField] private NavMeshAgent agent;

定义Patrol State

代码语言:javascript
复制
private class PatrolState : State
{
    //当前巡逻点的索引值
    public int index;
    //休息计时
    public float timer;
}

创建状态机并构建状态:

代码语言:javascript
复制
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米检测范围内时,进入寻路状态:

代码语言:javascript
复制
//当Player进入5米范围内时 Enemy进入寻路状态
SwitchWhen(() => Vector3.Distance(player.position, transform.position) <= 5f, "寻路状态")

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

代码语言:javascript
复制
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时,重新回到巡逻状态,不再追击。

代码语言:javascript
复制
.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方法绘制出追击范围:

代码语言:javascript
复制
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);
}

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

追击范围

🌭 攻击状态

定义攻击状态:

代码语言:javascript
复制
private class AttackState : State
{
    //攻击CD
    public float attackCD = 2f;
}

构建攻击状态:

代码语言:javascript
复制
.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离开追击范围:

离开追击范围

🍗 完整代码

代码语言:javascript
复制
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
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 当代野生程序猿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🍟 Preface
  • 🍕 巡逻状态
  • 🍿 寻路状态
  • 🌭 攻击状态
  • 🍗 完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档