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

发布于:2025-07-09 ⋅ 阅读:(14) ⋅ 点赞:(0)

所用的插件

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 文件夹

    任何使用 Handles 或 CustomEditor 的脚本必须放在项目 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. + 运算符
    向量加法:将目标位置和垂直偏移相加,得到最终标签位置


网站公告

今日签到

点亮在社区的每一天
去签到