所用的插件
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
典型工作流程:
在
OnSceneGUI()
中获取目标对象使用
Handles.color
设置颜色调用各种
Handles.DrawXXX
方法绘制图形(可选) 添加交互控件处理用户输入
Unity 自动在 Scene 视图渲染结果
使用要求:
必须放在 Editor 文件夹:
任何使用Handles
或CustomEditor
的脚本必须放在项目Assets/Editor
目录中仅限编辑器模式:
csharp #if UNITY_EDITOR using UnityEditor; #endif
依赖场景视图回调:
主要在
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编辑器命名空间基本结构
命名空间导入:
csharp using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; // 关键:编辑器扩展所需命名空间
特性标记:
csharp [CustomEditor(typeof(emenyView))] // 声明此编辑器作用于emenyView组件
类继承:
csharp public class emenyViewEditor : Editor // 必须继承自Editor基类
核心方法:
csharp private void OnSceneGUI() // 在Scene视图绘制的回调方法
关键对象:
target
:被编辑对象的引用(此处为emenyView实例)emenyView fow = (emenyView)target;
将当前编辑的目标对象(target)转换为
emenyView
类型,并将转换后的引用赋值给变量fow
举个例子:
可视化的内容编辑
流程原理
绘制视野范围:使用
Handles.DrawWireArc
绘制一个圆,表示敌人的视野范围。计算视野边界:通过
DirFromAngle
方法计算视野角度的左右边界向量。绘制视野边界线:使用
Handles.DrawLine
绘制两条线,表示视野的左右边界。绘制可见目标线:遍历所有可见目标,使用
Handles.DrawLine
绘制从敌人位置到每个可见目标位置的线,以红色显示。
语法结构解析
设置绘制颜色:
Handles.color = Color.white;
Handles.color
:设置Handles
类绘制图形的颜色。
Color.white
:表示白色。
绘制圆
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
:圆的半径。
计算视野角度的向量
Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false); Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
fow.DirFromAngle
:假设这是emenyView
类中的一个方法,用于根据角度计算方向向量。
-fow.viewAngle / 2
和fow.viewAngle / 2
:分别表示视野角度的一半,用于计算视野的左右边界。
绘制视野边界线
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.viewRadius
和fow.transform.position + viewAngleB * fow.viewRadius
:线的终点,分别表示视野的左右边界。
设置绘制颜色为红色:
Handles.color = Color.red;
遍历可见目标并绘制线:
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);
}
}
}
}
}
角度方法判断及其原理
功能原理:
该方法的功能是根据给定的角度(以度为单位)返回一个方向向量(即计算当前局部坐标下当前传入角度的全局向量)。具体原理如下
角度处理:
如果
angleIsGlobal
为true
,则直接使用传入的角度作为全局角度。如果
angleIsGlobal
为false
,表示传入的角度是相对于当前物体的局部角度,需要将其转换为全局角度——转换的方法是将当前物体的旋转角度(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);
}
方法签名:
public
:访问修饰符(可在其他类中访问)
Vector3
:返回类型(Unity的3D向量)
DirFromAngle
:方法名
(float angleInDegrees, bool angleIsGlobal)
:参数列表
angleInDegrees
:浮点型角度值(单位:度)
angleIsGlobal
:布尔值,标识角度是否为全局坐标系逻辑分支:
if (!angleIsGlobal)
:若角度不是全局坐标系
angleInDegrees += transform.eulerAngles.y
:将当前物体的Y轴旋转角度叠加到输入角度上(局部→全局转换)
transform.eulerAngles.y(因为这里视野都是绕Y轴旋转的,主旋转轴是Y轴,所以要加上的是Y轴)
表示当前物体绕世界坐标系Y轴的旋转角度(偏航角/Yaw)。
例如:物体面朝正北时值为0°
,面朝正东时值为90°
。
angleInDegrees
输入的参数,表示目标方向的角度(例如:30°
表示物体右侧30°方向)。叠加逻辑
如果角度是局部的(相对于物体自身朝向),则将其转换为全局角度:
全局角度 = 物体当前朝向 + 局部角度
例如:
物体当前朝东(
90°
),输入局部角度30°
(右侧)。转换后全局角度 =
90° + 30° = 120°
(东南方向)。向量计算:
这里所求出的参数,即是该角度在空间向量上的坐标系数(有三个轴要各自求其分量)
Mathf.Deg2Rad
:将角度转换为弧度(Unity三角函数需弧度制)
Mathf.Sin(angle)
:计算X分量(水平方向)
Mathf.Cos(angle)
:计算Z分量(前后方向)
Y = 0
:固定垂直分量为0(水平面方向)返回值:
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:检测层级 )) // 条件结束 { // 条件成立时执行的代码块 }
核心函数:
Physics.Raycast()
Unity的物理系统函数,用于检测射线碰撞
返回值:
bool
类型(true表示碰到障碍物,false表示无碰撞)逻辑运算符:
!
(非运算符)
对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
}
枚举类型状态流程
枚举定义状态标识:
emEnemyState
是一个枚举类型,它定义了四种状态,每种状态对应一个枚举值。csharp public enum emEnemyState { Stand, // → 对应Standing()方法 Patrol, // → 对应Patroling()方法 Trace, // → 对应Tracing()方法 Attack // → 对应Attacking()方法 }
状态变量存储当前状态:
csharp private emEnemyState _state; // 存储当前状态值
Switch语句驱动行为:
在switch
语句中,通过比较_state
的值与枚举值来匹配相应的case
分支。例如:当_state
的值为emEnemyState.Stand
时,执行Standing()
方法。csharp void Update() { switch (_state) // 根据当前状态值选择执行分支 { case emEnemyState.Stand: Standing(); // 执行站立行为 break; case emEnemyState.Patrol: Patroling(); // 执行巡逻行为 break; // ...其他状态类似 } }
计算表达式的值:首先计算
_state
的值。匹配
case
标签:将_state
的值与每个case
标签后的枚举值进行比较。执行匹配的代码块:如果找到匹配的
case
标签,执行该case
下的代码块,直到遇到break
语句或switch
语句结束。退出
switch
语句:执行完匹配的代码块后,退出switch
语句,继续执行switch
语句后面的代码。
状态切换改变行为:
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 的停止机制
isStopped
的作用:
当设置
isStopped = true
时,NavMeshAgent 会停止寻路,即不会再沿着当前路径继续移动。但是,它不会立即清除当前的移动速度,Agent 会因为惯性继续滑动一段距离。
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)
计算方向向量
csharp Vector3 lookPos = _playerTarget.position - transform.position;
获取从当前物体指向目标物体的向量(三维方向)。
锁定水平方向
csharp lookPos.y = 0;
将方向向量的
y
分量设为0,相当于将方向投影到水平面(XZ平面),忽略高度差。
例如:如果目标在正上方,此操作后方向会变为零向量(需额外处理)。应用水平旋转
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); } }
优点:
强大交互:
支持手柄工具(如
RadiusHandle
、PositionHandle
)可直接在场景中拖拽调整参数
定制Inspector:
可重写
OnInspectorGUI()
添加自定义UI支持序列化属性高级操作
代码分离:编辑器代码独立于游戏逻辑
缺点:
复杂度高:需额外创建编辑器类
路径限制:必须放在
Assets/Editor
文件夹手动关联:需通过
[CustomEditor]
属性指定目标组件
代码解析
Vector3 labelPosition = _playerTarget.position + Vector3.up * 2f;
_playerTarget.position
_playerTarget
:对目标玩家对象的引用(可能是 Transform 或 GameObject)
.position
:获取该对象在世界空间中的坐标(Vector3)
Vector3.up
Unity 预定义的常量:(0, 1, 0),表示世界空间中的向上方向
* 2f
2f
:浮点数 2.0(f 表示 float 类型),将向上向量乘以标量值,得到长度为 2 米的垂直偏移
+
运算符
向量加法:将目标位置和垂直偏移相加,得到最终标签位置