怪物机制分析(有限状态机、编辑器可视化、巡逻机制)

所用的插件

AI Navigation

巡逻机制的建立

具体操作

1、在Window中Package Manager的unity register下载AI Register包,然后你就发现window中有AI选项了(详情请看

2、新建平面,勾选static(这里似乎只要勾选了就能激发AI的bake功能)

3、点开AI进行烘焙(bake)

4、给怪物加上自动寻路组件

视野可视化的建立

1、新建文件夹命名为Editor,然后编写脚本如下,用于可视化视野范围

2、编写该脚本用于处理视野机制的物理机制,因为其中有个算法取范围transform(而如果直接放在怪物模型上偏大),所以需要创建空物体挂载该脚本

3、给玩家Player设置合适的层级,并在"视野物理处理"脚本中挂载为"目标层级"

视野可视化脚本编写

这篇文章剖析了这个视野范围的机制:详情请看 https://blog.csdn.net/Plutogd/article/details/117636942
典型工作流程:

  1. OnSceneGUI() 中获取目标对象

  2. 使用 Handles.color 设置颜色

  3. 调用各种 Handles.DrawXXX 方法绘制图形

  4. (可选) 添加交互控件处理用户输入

  5. Unity 自动在 Scene 视图渲染结果

使用要求:

  1. 必须放在 Editor 文件夹

    任何使用 HandlesCustomEditor 的脚本必须放在项目 Assets/Editor 目录中

  2. 仅限编辑器模式

    复制代码
    csharp
    
    #if UNITY_EDITOR
    using UnityEditor;
    #endif
  3. 依赖场景视图回调

    • 主要在 OnSceneGUI() 方法中使用

    • 也可在 OnDrawGizmos() 中使用部分功能

复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

//自定义编辑器
[CustomEditor(typeof(emenyView))]
public class emenyViewEditor : Editor
{
    private void OnSceneGUI()
    {
        emenyView fow = (emenyView)target;
        //画的颜色为白色
        Handles.color = Color.white;
        //画一个线弧(圆的中心,圆的法线,开始的中心角度开始的地方,弧度、旋转的度数,圆的半径)
        Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
        //把视野角度的一般转为Vector3向量
        Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
        //把视野角度的一般转为Vector3向量并取反
        Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);

        //从玩家的位置到夹角的一条边画一条线(长度为视野的半径)
        Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
        //从玩家的位置到夹角的另一条边画一条线(长度为视野的半径)
        Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);

        Handles.color = Color.red;
        //遍历所有打到的敌人的位置
        foreach (Transform visibleTarget in fow.visibleTargets)
        {
            //画一条线从玩家的位置到敌人的位置
            Handles.DrawLine(fow.transform.position, visibleTarget.position);
        }


    }

}

Editor编辑器命名空间基本结构

  1. 命名空间导入

    复制代码
    csharp
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEditor;  // 关键:编辑器扩展所需命名空间
  2. 特性标记

    复制代码
    csharp
    
    [CustomEditor(typeof(emenyView))]  // 声明此编辑器作用于emenyView组件
  3. 类继承

    复制代码
    csharp
    
    public class emenyViewEditor : Editor  // 必须继承自Editor基类
  4. 核心方法

    复制代码
    csharp
    
    private void OnSceneGUI()  // 在Scene视图绘制的回调方法
  5. 关键对象

target:被编辑对象的引用(此处为emenyView实例)

复制代码
emenyView fow = (emenyView)target;

将当前编辑的目标对象(target)转换为 emenyView 类型,并将转换后的引用赋值给变量 fow

举个例子

可视化的内容编辑

流程原理

  1. 绘制视野范围 :使用Handles.DrawWireArc绘制一个圆,表示敌人的视野范围。

  2. 计算视野边界 :通过DirFromAngle方法计算视野角度的左右边界向量。

  3. 绘制视野边界线 :使用Handles.DrawLine绘制两条线,表示视野的左右边界。

  4. 绘制可见目标线 :遍历所有可见目标,使用Handles.DrawLine绘制从敌人位置到每个可见目标位置的线,以红色显示。

语法结构解析

  1. 设置绘制颜色

    复制代码
    Handles.color = Color.white;
    • Handles.color:设置Handles类绘制图形的颜色。

    • Color.white:表示白色。

  2. 绘制圆

    复制代码
    Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
    • Handles.DrawWireArc:绘制一个圆弧。

    • fow.transform.position:圆的中心位置。

    • Vector3.up:圆的法线方向,表示圆是水平放置的。

    • Vector3.forward:圆弧的起始方向。

    • 360:圆弧的角度,360度表示一个完整的圆。

    • fow.viewRadius:圆的半径。

  3. 计算视野角度的向量

    复制代码
    Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
    Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
    • fow.DirFromAngle:假设这是emenyView类中的一个方法,用于根据角度计算方向向量。

    • -fow.viewAngle / 2fow.viewAngle / 2:分别表示视野角度的一半,用于计算视野的左右边界。

  4. 绘制视野边界线

    复制代码
    Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
    Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);
    • Handles.DrawLine:绘制一条线。

    • fow.transform.position:线的起点。

    • fow.transform.position + viewAngleA * fow.viewRadiusfow.transform.position + viewAngleB * fow.viewRadius:线的终点,分别表示视野的左右边界。

  5. 设置绘制颜色为红色

    复制代码
    Handles.color = Color.red;
  6. 遍历可见目标并绘制线

    复制代码
    foreach (Transform visibleTarget in fow.visibleTargets)
    {
        Handles.DrawLine(fow.transform.position, visibleTarget.position);
    }
    • foreach:遍历fow.visibleTargets集合中的每一个元素。

    • Transform visibleTarget:表示当前遍历到的可见目标。

    • Handles.DrawLine:绘制一条从敌人位置到可见目标位置的线

Q1: Handles.color = Color.white; Handles.color = Color.red; 这两个颜色分别是给哪些图形内容上色?怎么个机制?
A1:

1、Handles.color = Color.white;

  • 作用对象 :这行代码设置的颜色为白色,它会影响接下来所有使用Handles类绘制的图形,直到颜色被再次更改。

  • 具体图形

    • Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);

      • 绘制的圆弧(代表敌人的视野范围)会使用白色。
    • Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);

      • 绘制的两条线(代表视野的左右边界)也会使用白色。

2、Handles.color = Color.red;

  • 作用对象 :这行代码将颜色更改为红色,之后所有使用Handles类绘制的图形都会使用红色,直到颜色再次被更改。

  • 具体图形

    • foreach (Transform visibleTarget in fow.visibleTargets) { Handles.DrawLine(fow.transform.position, visibleTarget.position); }

      • 这段代码中绘制的从敌人位置到每个可见目标位置的线会使用红色。

View脚本的编写

复制代码
using BehaviorDesigner.Runtime;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class emenyView : MonoBehaviour
{
    public float viewRadius = 5f;
    [Range(0, 360)] public float viewAngle = 90f;
    public List<Transform> visibleTargets = new List<Transform>();
    public LayerMask targetMask;    // 检测目标的层级
    public LayerMask obstacleMask; // 障碍物的层级

    //用于存储一些检测结果(比如是否检测到目标、当前目标位置)
    public bool hasTarget = false;
    public SharedTransform currentTarget;
    
    

    public float currentDistance; // 新增距离缓存
                                  // 因为这里检测了目标点和当前物体的位置,正好在计算视线中的dstToTarget 已经算出距离,
                                  // 后续再编写算出距离的脚本就有些不必要了
                                  // Start is called before the first frame update


    public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
    {
        if (!angleIsGlobal)
        {
            angleInDegrees += transform.eulerAngles.y;
        }
        return new Vector3(
            Mathf.Sin(angleInDegrees * Mathf.Deg2Rad),
            0,
            Mathf.Cos(angleInDegrees * Mathf.Deg2Rad)
        );
    }

    private void Update()
    {
        FindVisibleTargets();
    }

    // Update is called once per frame
    public void FindVisibleTargets()
    {
        visibleTargets.Clear();

        // 1. 球体检测:获取视野半径内的所有潜在目标
        Collider[] targetsInViewRadius = Physics.OverlapSphere(
            transform.position,
            viewRadius,
            targetMask
        );

        
        foreach (Collider targetCollider in targetsInViewRadius)
        {
            Transform target = targetCollider.transform;
            Vector3 dirToTarget = (target.position - transform.position).normalized;

            // 2. 角度检测:检查是否在视野角度内
            if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
            {
                float dstToTarget = Vector3.Distance(transform.position, target.position);

                // 3. 视线检测:检查是否有障碍物阻挡
                if (!Physics.Raycast(
                    transform.position,
                    dirToTarget,
                    dstToTarget,
                    obstacleMask
                ))
                {
                    // 真正检测到可见目标!
                    visibleTargets.Add(target);
                    hasTarget = true;
                    currentTarget.Value = target;
                    currentDistance = dstToTarget;
                    Debug.Log("发现敌人: " + target.name);
                }
            }
        }
    }
}

角度方法判断及其原理

功能原理:

该方法的功能是根据给定的角度(以度为单位)返回一个方向向量(即计算当前局部坐标下当前传入角度的全局向量)。具体原理如下

角度处理

如果angleIsGlobaltrue,则直接使用传入的角度作为全局角度。

如果angleIsGlobalfalse,表示传入的角度是相对于当前物体的局部角度,需要将其转换为全局角度------转换的方法是将当前物体的旋转角度(transform.eulerAngles.y)加到传入的角度上。

复制代码
    public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
    {
        if (!angleIsGlobal)
        {
            angleInDegrees += transform.eulerAngles.y;
        }
        return new Vector3(
            Mathf.Sin(angleInDegrees * Mathf.Deg2Rad),
            0,
            Mathf.Cos(angleInDegrees * Mathf.Deg2Rad)
        );
    }


作用在视野范围编辑器:
  private void OnSceneGUI()
    {
        emenyView fow = (emenyView)target;
         ......
        //把视野角度的一半转为Vector3向量
        Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
        //把视野角度的一半转为Vector3向量并取反
        Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
   }
  1. 方法签名

    • public:访问修饰符(可在其他类中访问)

    • Vector3:返回类型(Unity的3D向量)

    • DirFromAngle:方法名

    • (float angleInDegrees, bool angleIsGlobal):参数列表

      • angleInDegrees:浮点型角度值(单位:度)

      • angleIsGlobal:布尔值,标识角度是否为全局坐标系

  2. 逻辑分支

    • if (!angleIsGlobal):若角度不是全局坐标系

      • angleInDegrees += transform.eulerAngles.y:将当前物体的Y轴旋转角度叠加到输入角度上(局部→全局转换)

        • transform.eulerAngles.y(因为这里视野都是绕Y轴旋转的,主旋转轴是Y轴,所以要加上的是Y轴)

          表示当前物体绕世界坐标系Y轴的旋转角度(偏航角/Yaw)。

          例如:物体面朝正北时值为 ,面朝正东时值为 90°

        • angleInDegrees

          输入的参数,表示目标方向的角度(例如:30° 表示物体右侧30°方向)。

        • 叠加逻辑

          如果角度是局部的(相对于物体自身朝向),则将其转换为全局角度:
          全局角度 = 物体当前朝向 + 局部角度

          例如:

          • 物体当前朝东(90°),输入局部角度 30°(右侧)。

          • 转换后全局角度 = 90° + 30° = 120°(东南方向)。

  3. 向量计算

    这里所求出的参数,即是该角度在空间向量上的坐标系数(有三个轴要各自求其分量)

    • Mathf.Deg2Rad:将角度转换为弧度(Unity三角函数需弧度制)

    • Mathf.Sin(angle):计算X分量(水平方向)

    • Mathf.Cos(angle):计算Z分量(前后方向)

    • Y = 0:固定垂直分量为0(水平面方向)

  4. 返回值

    • new Vector3(x, 0, z):构造并返回单位方向向量

Q1:视野范围编辑器中使用fow.DirFromAngle(-fow.viewAngle / 2, false);的作用是什么

A1:作用是求出它们的视角边界线的向量,以便后续可视化:将设定的视野范围角度分为两半(fow.viewAngle是视野的角度);如果当前传入的角度是局部角度,那么还需要在算出全局角度,再根据数学公式分别求出该角度在各个轴方向上的向量数值,合并成一个三元坐标返回即是向量线条

目标检测机制及返回检测结果

复制代码
public void FindVisibleTargets()
    {
        visibleTargets.Clear();

        // 1. 球体检测:获取视野半径内的所有潜在目标
        Collider[] targetsInViewRadius = Physics.OverlapSphere(
            transform.position,
            viewRadius,
            targetMask
        );

        
        foreach (Collider targetCollider in targetsInViewRadius)
        {
            //检测碰撞体位置,并且将碰撞体与当前敌人位置单位化,求出敌人与目标方向向量
            Transform target = targetCollider.transform;
            Vector3 dirToTarget = (target.position - transform.position).normalized;

            // 2. 角度检测:检查是否在视野角度内
            if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
            {
            //   计算三元位置点的距离长度
                float dstToTarget = Vector3.Distance(transform.position, target.position);

                // 3. 视线检测:检查是否有障碍物阻挡
                if (!Physics.Raycast(
                    transform.position,
                    dirToTarget,
                    dstToTarget,
                    obstacleMask
                ))
                {
                    // 真正检测到可见目标!
                    visibleTargets.Add(target);
                    hasTarget = true;
                    currentTarget.Value = target;
                    currentDistance = dstToTarget;
                    Debug.Log("发现敌人: " + target.name);
                }
            }
        }
    }
}

1、球体检测

Physics.OverlapSphere 是 Unity 物理引擎提供的一个核心方法,用于在球形区域内检测碰撞体。

复制代码
Collider[] targetsInViewRadius = Physics.OverlapSphere(
    transform.position,  // 检测中心点(当前物体位置)
    viewRadius,          // 检测半径(视野范围)
    targetMask           // 目标层级掩码(只检测特定层)
);

数组特性:

  • 动态长度 :返回的 Collider[] 数组长度取决于检测到的碰撞体数量

  • 零检测处理:如果没有碰撞体,返回空数组(不是 null)

  • 内存分配:每次调用都会在堆内存中新建数组

2、单位化距离向量

复制代码
Vector3 dirToTarget = (target.position - transform.position).normalized;
  • target.position - transform.position:通过向量减法,计算出从当前对象指向目标对象的向量,即距离向量。这个向量的方向是从当前对象指向目标对象,其大小(模长)表示两个对象之间的距离。

  • .normalized :是 Unity 中 Vector3 类型的一个属性,用于获取该向量的单位向量。单位向量是指模长为 1 的向量,它保留了原向量的方向信息,但将其长度缩放到 1。

3、判断是否碰撞到障碍物

复制代码
if (!Physics.Raycast( // 条件开始
    transform.position, // 参数1:射线起点
    dirToTarget,        // 参数2:射线方向
    dstToTarget,        // 参数3:射线长度
    obstacleMask        // 参数4:检测层级
))                      // 条件结束
{
    // 条件成立时执行的代码块
}
  1. 核心函数Physics.Raycast()

    • Unity的物理系统函数,用于检测射线碰撞

    • 返回值bool类型(true表示碰到障碍物,false表示无碰撞)

  2. 逻辑运算符!(非运算符)

    • 对Raycast结果取反

含义:若是没有射线未能接触到障碍物,则执行获取接下来的条件。

怪物行为状态的切换(有限状态机的基础使用)

复制代码
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;

public enum EnemyState { Idle, Patrol, Chase, Attack }

public class EnemyController : MonoBehaviour
{
    [Header("References")]
    public List<Transform> waypoints;
    private NavMeshAgent _agent;
    private Animator _animator;
    private FieldOfView _fov;

    [Header("State Settings")]
    private EnemyState _currentState;
    [SerializeField] private float _idleDuration = 3f;
    private float _idleTimer;
    private int _currentWaypointIndex;

    [Header("Detection")]
    [SerializeField] private float _detectionInterval = 0.2f;
    private float _detectionTimer;
    private Transform _playerTarget;

    [Header("Combat")]
   
    [SerializeField] private float _attackRange = 10f;
    [SerializeField] private float _attackRate = 1.5f;
    private float _attackCooldown;
   
    private void Awake()
    {
        _animator = GetComponent<Animator>();
        _agent = GetComponent<NavMeshAgent>();
        _fov = GetComponentInChildren<FieldOfView>();

        if (_fov == null)
            Debug.LogError("Missing FieldOfView component in children");

        ChangeState(EnemyState.Idle);
    }

    void Update()
    {
        _fov.FindVisibleTargets();
        CheckForTargets();

        _detectionTimer = 0;

        

        switch (_currentState)
        {
            case EnemyState.Idle:  UpdateIdle(); break;
            case EnemyState.Patrol: UpdatePatrol(); break;
            case EnemyState.Chase: UpdateChase(); break;
            case EnemyState.Attack: UpdateAttack(); break;
        }
    }

    private void CheckForTargets()
    {
        
        if (_fov.HasTarget())
        {
            
            _playerTarget = _fov.GetCurrentTarget();
            /*Vector3 toPlayer = _playerTarget.position - transform.position;
            float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;*/
            float distance = Vector3.Distance(transform.position, _playerTarget.position);
            if (distance <= _attackRange)
            {
                ChangeState(EnemyState.Attack);
            }
            else 
            {
                
                ChangeState(EnemyState.Chase);
            }
        }
        else if (_currentState == EnemyState.Chase ||
                _currentState == EnemyState.Attack)
        {
            ChangeState(EnemyState.Patrol);
        }
    }

    private void ChangeState(EnemyState newState)
    {
        // 退出当前状态
        switch (_currentState)
        {
            case EnemyState.Attack:
                _animator.SetBool("BasicATK", false);
                _agent.isStopped = false;
                break;

            case EnemyState.Chase:
                _animator.SetBool("IsRun", false);
                break;

            case EnemyState.Patrol:
                _animator.SetBool("IsWalk", false);
                break;
        }

        // 进入新状态
        switch (newState)
        {
            case EnemyState.Idle:
                _animator.SetBool("IsWalk", false);
                _idleTimer = 0;
                break;

            case EnemyState.Patrol:               
                _animator.SetBool("IsWalk", true);
                _agent.speed = 10f; // 走路速度
                SetNextWaypoint();
                break;

            case EnemyState.Chase:
                _animator.SetBool("IsRun", true);
                _agent.speed = 15f; // 跑步速度
                break;

            case EnemyState.Attack:
                Vector3 toPlayer = _playerTarget.position - transform.position;
                float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;
                _agent.velocity = Vector3.zero;
                _agent.isStopped = true;
                _animator.SetBool("BasicATK", true);
                _attackCooldown = 0;
                break;
        }

        _currentState = newState;
    }

    private void UpdateIdle()
    {
        _idleTimer += Time.deltaTime;
        if (_idleTimer > _idleDuration)
        {
            ChangeState(EnemyState.Patrol);
        }
    }

    private void UpdatePatrol()
    {
        if (_agent.remainingDistance < 0.5f)
        {
            ChangeState(EnemyState.Idle);
        }
    }

    private void SetNextWaypoint()
    {
        if (waypoints.Count == 0) return;

        _agent.SetDestination(waypoints[_currentWaypointIndex].position);
        _currentWaypointIndex = (_currentWaypointIndex + 1) % waypoints.Count;
        
    }

    private void UpdateChase()
    {
        Debug.Log("跑");
        if (_playerTarget == null) return;

        _agent.SetDestination(_playerTarget.position);

        // 面向目标
        Vector3 lookPos = _playerTarget.position - transform.position;
        lookPos.y = 0;
        transform.rotation = Quaternion.LookRotation(lookPos);

        // 实时检查距离:如果进入攻击范围,立即切换到攻击状态
        Vector3 toPlayer = _playerTarget.position - transform.position;
        float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;
        Debug.Log($"{distance}");
        if (distance <= _attackRange)
        {
            ChangeState(EnemyState.Attack);
        }
    }

    private void UpdateAttack()
    {
        if (_playerTarget == null) return;

        // 保持面向目标
        Vector3 lookPos = _playerTarget.position - transform.position;
        lookPos.y = 0;
        transform.rotation = Quaternion.LookRotation(lookPos);

        // 实时检查距离:如果目标超出攻击范围,立即切回追逐
        Vector3 toPlayer = _playerTarget.position - transform.position;
        float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;

        if (distance > _attackRange)
        {
            ChangeState(EnemyState.Chase);
            return; // 立即退出,不再执行攻击逻辑
        }

        // 攻击冷却逻辑(仅在有效攻击范围内执行)
        if (_attackCooldown <= 0)
        {
            _animator.SetTrigger("Attack");
            _attackCooldown = _attackRate;
        }
        else
        {
            _attackCooldown -= Time.deltaTime;
        }
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        // 绘制检测范围
        UnityEditor.Handles.color = new Color(0, 1, 0, 0.1f);



        // 绘制攻击范围
        UnityEditor.Handles.color = new Color(1, 0, 0, 0.2f);
        UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, _attackRange);

        // 绘制到目标的距离
        if (_playerTarget != null)
        {
            // 绘制从物体到目标的线
            UnityEditor.Handles.color = Color.yellow;
            UnityEditor.Handles.DrawLine(transform.position, _playerTarget.position);

            // 计算距离
            Vector3 toPlayer = _playerTarget.position - transform.position;
            float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;

            // 在目标位置上方显示距离数值
            Vector3 labelPosition = _playerTarget.position + Vector3.up * 2f;
            UnityEditor.Handles.Label(labelPosition, $"Distance: {distance:F2}");
        }
    }


#endif
}

枚举类型状态流程

  1. 枚举定义状态标识
    emEnemyState 是一个枚举类型,它定义了四种状态,每种状态对应一个枚举值。

    复制代码
    csharp
    
    public enum emEnemyState
    {
        Stand,    // → 对应Standing()方法
        Patrol,   // → 对应Patroling()方法
        Trace,    // → 对应Tracing()方法
        Attack    // → 对应Attacking()方法
    }
  2. 状态变量存储当前状态

    复制代码
    csharp
    
    private emEnemyState _state; // 存储当前状态值
  3. Switch语句驱动行为

    switch 语句中,通过比较 _state 的值与枚举值来匹配相应的 case 分支。例如:当 _state 的值为 emEnemyState.Stand 时,执行 Standing() 方法。

    复制代码
    csharp
    
    void Update()
    {
        switch (_state) // 根据当前状态值选择执行分支
        {
            case emEnemyState.Stand: 
                Standing();  // 执行站立行为
                break;
                
            case emEnemyState.Patrol:
                Patroling(); // 执行巡逻行为
                break;
                
            // ...其他状态类似
        }
    }
    1. 计算表达式的值 :首先计算 _state 的值。

    2. 匹配 case 标签 :将 _state 的值与每个 case 标签后的枚举值进行比较。

    3. 执行匹配的代码块 :如果找到匹配的 case 标签,执行该 case 下的代码块,直到遇到 break 语句或 switch 语句结束。

    4. 退出 switch 语句 :执行完匹配的代码块后,退出 switch 语句,继续执行 switch 语句后面的代码。

  4. 状态切换改变行为

    复制代码
    csharp
    
    private void ChangeState(emEnemyState newState)
    {
        _state = newState; // 改变状态值
        // 下一帧Update()将自动执行新状态对应的行为
    }

状态持续以及状态切换

这里需要额外注意下:case类型的状态都是按照顺序依次执行的,直到break结束执行

复制代码
//状态持续更新逻辑
switch (_currentState)
{
    case EnemyState.Idle:  UpdateIdle(); break;
    case EnemyState.Patrol: UpdatePatrol(); break;
    case EnemyState.Chase: UpdateChase(); break;
    case EnemyState.Attack: UpdateAttack(); break;
}
  • 状态更新逻辑 :在每一帧中,根据当前状态调用对应的方法(如 UpdateIdle()UpdatePatrol()UpdateChase()UpdateAttack()),执行该状态下的具体行为。
复制代码
private void ChangeState(EnemyState newState)
    {
        // 退出当前状态
        switch (_currentState)
        {
            case EnemyState.Attack:
                _animator.SetBool("BasicATK", false);
                _agent.isStopped = false;
                break;

            case EnemyState.Chase:
                _animator.SetBool("IsRun", false);
                break;

            case EnemyState.Patrol:
                _animator.SetBool("IsWalk", false);
                break;
        }

        // 进入新状态
        switch (newState)
        {
            case EnemyState.Idle:
                _animator.SetBool("IsWalk", false);
                _idleTimer = 0;
                break;

            case EnemyState.Patrol:               
                _animator.SetBool("IsWalk", true);
                _agent.speed = 10f; // 走路速度
                SetNextWaypoint();
                break;

            case EnemyState.Chase:
                _animator.SetBool("IsRun", true);
                _agent.speed = 15f; // 跑步速度
                break;

            case EnemyState.Attack:
                Vector3 toPlayer = _playerTarget.position - transform.position;
                float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;
                _agent.velocity = Vector3.zero;
                _agent.isStopped = true;
                _animator.SetBool("BasicATK", true);
                _attackCooldown = 0;
                break;
        }

        _currentState = newState;
    }

1、将动画状态的转换以及代理人状态的转换,全部放在该方法中集合转换,避免了到处随便取用动画及代理人状态转换而导致的修改不易 。

2、这个还有意思的一个点是只用一个全局参数_currentState,就完成了状态切换。并且将新导入的状态参数赋值给它。

代理人系统误解

Q1:Unity NavMesh Agent 的停止机制

  1. isStopped 的作用

    • 当设置 isStopped = true 时,NavMeshAgent 会停止寻路,即不会再沿着当前路径继续移动。

    • 但是,它不会立即清除当前的移动速度,Agent 会因为惯性继续滑动一段距离。

  2. velocity 的作用

    • velocity 表示 Agent 当前的移动速度向量。

    • 当你手动设置 _agent.velocity = Vector3.zero 时,相当于强制清除当前的移动速度,让 Agent 立即停止移动。

所以需要同时设置两个:

  • 停止寻路isStopped = true 负责停止寻路系统,告诉 Agent 不要再继续移动。

  • 清除惯性velocity = Vector3.zero 负责清除 Agent 当前的物理速度,防止它因为惯性继续滑动。

物体之间距离算法的误解

float distance = Vector3.Distance(transform.position, _playerTarget.position);这个距离是两个物体中心点之间的空间距离

而此时要对比的是物体与敌人之间的水平距离和敌人攻击范围,应该用如下方法:

复制代码
 Vector3 toPlayer = _playerTarget.position - transform.position;
float distance = new Vector3(toPlayer.x, 0, toPlayer.z).magnitude;

题外话:

因为位置变量不断变化,所以distance变量也需要定帧更新,加上使用次数较多,想放在update函数中进行集中管理,不过当时想岔了,想直接使用distance局部变量传递到其他方法去,应当使用全局变量才对。

面向目标函数的区别

锁定Y轴的面向目标算法

复制代码
// 面向目标
        Vector3 lookPos = _playerTarget.position - transform.position;
        lookPos.y = 0;
        transform.rotation = Quaternion.LookRotation(lookPos)
  1. 计算方向向量

    复制代码
    csharp
    
    Vector3 lookPos = _playerTarget.position - transform.position;

    获取从当前物体指向目标物体的向量(三维方向)。

  2. 锁定水平方向

    复制代码
    csharp
    
    lookPos.y = 0;

    将方向向量的 y 分量设为0,相当于将方向投影到水平面(XZ平面) ,忽略高度差。
    例如:如果目标在正上方,此操作后方向会变为零向量(需额外处理)。

  3. 应用水平旋转

    复制代码
    csharp
    
    transform.rotation = Quaternion.LookRotation(lookPos);

    Quaternion.LookRotation() 根据方向向量生成旋转:

    • 物体的 前方向(Z轴) 对齐 lookPos

    • 物体的 上方向(Y轴) 保持世界坐标系的上方向(Vector3.up),因此不会倾斜或俯仰

LookAt函数

它会同时修改物体在水平(yaw)和垂直(pitch)方向的旋转 ,导致物体完全"盯住"目标点(包括抬头/低头)

(不过在此处用于怪物似乎也比较合适)

二者比较

巡逻点更新机制(重新更新机制)

复制代码
 private void SetNextWaypoint()
    {
        if (waypoints.Count == 0) return;

        _agent.SetDestination(waypoints[_currentWaypointIndex].position);
        _currentWaypointIndex = (_currentWaypointIndex + 1) % waypoints.Count;
        
    }

这里使用了取余机制巧妙地完成了巡逻点索引值更新:若是索引值1---3同4(此为列表总长度)取余就是其本身的值,而4对4取余就是0,意味着从起始巡逻点重新开始。

Gizmos可视化绘制方法

与继承Editor类的有什么区别和优劣

直接使用 OnDrawGizmos 的方式(当前代码)

  • 优点

    • 简单快捷:无需创建额外Editor类

    • 自动关联:直接写在MonoBehaviour脚本中,自动绑定目标对象

    • 轻量级:适合简单Gizmos绘制需求

    • 无路径要求:脚本可放在任意目录

  • 缺点

    • 功能有限

      • 只能使用Gizmos/Handles基础绘制API

      • 无法创建交互式手柄(如可拖拽的半径控制点)

    • 无法定制Inspector:不能修改Inspector面板的显示内容

继承 Editor 类的方式

复制代码
[CustomEditor(typeof(MyScript))]
public class MyScriptEditor : Editor 
{
    void OnSceneGUI()
    {
        MyScript script = (MyScript)target;
        // 可交互的手柄绘制
        Handles.color = Color.red;
        script._attackRange = Handles.RadiusHandle(script.transform.rotation, 
                                  script.transform.position, 
                                  script._attackRange);
    }
}
  • 优点

    • 强大交互

      • 支持手柄工具(如RadiusHandlePositionHandle

      • 可直接在场景中拖拽调整参数

    • 定制Inspector

      • 可重写OnInspectorGUI()添加自定义UI

      • 支持序列化属性高级操作

    • 代码分离:编辑器代码独立于游戏逻辑

  • 缺点

    • 复杂度高:需额外创建编辑器类

    • 路径限制 :必须放在Assets/Editor文件夹

    • 手动关联 :需通过[CustomEditor]属性指定目标组件

代码解析

复制代码
Vector3 labelPosition = _playerTarget.position + Vector3.up * 2f;
  1. _playerTarget.position

    • _playerTarget:对目标玩家对象的引用(可能是 Transform 或 GameObject)

    • .position:获取该对象在世界空间中的坐标(Vector3)

  2. Vector3.up

    Unity 预定义的常量:(0, 1, 0),表示世界空间中的向上方向

  3. * 2f

    • 2f:浮点数 2.0(f 表示 float 类型),将向上向量乘以标量值,得到长度为 2 米的垂直偏移
  4. + 运算符

    向量加法:将目标位置和垂直偏移相加,得到最终标签位置

相关推荐
武汉唯众智创23 分钟前
高职院校“赛岗课”一体化网络安全实战类人才培养方案
网络·安全·web安全·网络安全·“赛岗课”一体化·赛岗课
神的孩子都在歌唱2 小时前
常见的网络攻击方式及防御措施
运维·服务器·网络
电报号dapp1193 小时前
链游新纪元——链游平台开发引领游戏新潮流!
游戏·web3·去中心化·区块链
岑梓铭3 小时前
计算机网络第九章——数据链路层《局域网》
网络·笔记·计算机网络·考研·408
小白爱电脑5 小时前
什么是2.5G交换机?
运维·网络·5g·千兆宽带
温玉琳琅5 小时前
【UE5】虚幻引擎小百科
ue5·游戏引擎·虚幻
阿沁QWQ6 小时前
UDP的socket编程
网络·网络协议·udp
HXR_plume6 小时前
【计算机网络】王道考研笔记整理(1)计算机网络体系结构
网络·笔记·计算机网络
R_.L7 小时前
网络 :数据链路层
网络
cver1238 小时前
CSGO 训练数据集介绍-2,427 张图片 AI 游戏助手 游戏数据分析
人工智能·深度学习·yolo·目标检测·游戏·计算机视觉