🧭 NavMesh 寻路与状态控制指南
本文档详细阐述 Project Vampirefall 中敌人如何利用 Unity NavMesh 系统进行寻路,重点解析异常状态 (CC) 下的寻路控制,以及动态阻挡 (塔防) 的实现细节。1. 核心组件架构 (Core Components)
在 Unity 中,寻路不仅仅是SetDestination。对于复杂的 ARPG + TD 游戏,我们需要精细控制 NavMeshAgent。
1.1 Agent 配置的最佳实践
- Acceleration (加速度): 设为极高 (如 60-100)。防止怪物起步“滑步”或转弯“漂移”。我们需要响应灵敏的移动。
- Angular Speed (转速): 设为 360 或更高。怪物应该能瞬间转身,而不是像坦克一样慢慢转。
- Auto Braking (自动刹车):
- 近战怪:
True。防止冲过头。 - 远程怪:
False。它们需要平滑地移动到射程边缘并停止。
- 近战怪:
- Avoidance Priority (避让优先级):
- Boss/Elite: 0-20 (优先级高,推着别人走)。
- Minion: 50-99 (优先级低,会被挤开)。
- 技巧: 动态调整优先级。当怪物进入攻击状态时,提高优先级,防止被路过的队友挤歪。
2. 状态控制与寻路交互 (CC & Pathfinding)
这是 ARPG 开发中最容易出 Bug 的地方:如何让NavMeshAgent 正确响应“冰冻”、“击退”等物理效果?
❄️ 2.1 减速 (Slow)
- 实现: 直接修改
agent.speed。 - 叠加规则:
- 建议使用 乘法叠加:
FinalSpeed = BaseSpeed * (1 - SlowA) * (1 - SlowB)。 - 设置下限:
Mathf.Max(FinalSpeed, 0.5f),防止速度减为 0 导致动画播放异常。
- 建议使用 乘法叠加:
🧊 2.2 定身/冰冻 (Root / Freeze)
- 错误做法:
agent.enabled = false。这会导致怪物失去碰撞体积,或者瞬间重置路径。 - 正确做法:
agent.isStopped = true。- 保留路径数据 (Path Pending),只是暂停移动。
- 同时禁用
Animator的移动参数,防止原地太空步。
- 恢复:
agent.isStopped = false。
💫 2.3 眩晕 (Stun)
- 区别: 定身时怪物还能攻击/转头,眩晕时完全瘫痪。
- 实现:
-
agent.isStopped = true。 -
agent.updateRotation = false(禁止转头)。 -
逻辑层禁用 AI 状态机 (
FSM.Pause())。
-
🥊 2.4 击退 (Knockback) —— 最难点
NavMeshAgent 和 Rigidbody 是死对头。Agent 试图吸附在地面,Rigidbody 试图飞出去。
- 标准流程:
-
Disable Agent:
agent.enabled = false(必须彻底关掉,否则它会强制把坐标拉回 NavMesh 上)。 -
Add Force:
rb.isKinematic = false;rb.AddForce(ExplosionForce). - Wait: 等待击退时间(如 0.5秒)或等待速度接近 0。
-
Re-enable Agent:
rb.isKinematic = true.agent.Warp(transform.position)(关键!告诉 Agent 我瞬移到了新位置).agent.enabled = true.agent.SetDestination(Target)(重新寻路).
-
Disable Agent:
3. 实用案例大全 (Practical Use Cases)
⚔️ Case A: 到达攻击距离 (Reaching Attack Range)
当怪物进入射程时,如何优雅地停下来攻击,而不是推着玩家走?- 核心逻辑: 不要依赖
StoppingDistance,自己手动控制。 - 流程:
-
Check: 每帧检测
Distance(Self, Target) <= AttackRange。 -
Stop: 满足条件时,立即
agent.isStopped = true。 -
Rotate: 攻击时手动调用
transform.LookAt(Target)(或者插值旋转),保证朝向正确。 -
Resume: 如果目标跑出射程
Distance > AttackRange * 1.2(迟滞阈值),则agent.isStopped = false继续追击。
-
Check: 每帧检测
体积修正 (Size Adjustment)
在塔防中,塔通常有很大的碰撞体积(如Radius = 2.0m),而小怪只有 Radius = 0.5m。直接计算 Vector3.Distance (中心点距离) 会导致小怪贴着塔站,或者打不到塔。
- 公式修正:
EffectiveRange = AttackRange + TargetRadius + SelfRadius。- Center-to-Center: 默认距离计算是中心对中心。
- Edge-to-Edge: 我们希望的是“边缘对边缘”。
- 通过加上双方半径,我们算出的是“为了让武器碰到对方边缘,两个中心点需要的最大距离”。
- 近战判定:
- 对于近战攻击,不需要真实的物理碰撞(武器碰到塔)。
- Hitbox Lag: 依赖物理碰撞会导致因为动画延迟、帧率波动而造成的判定丢失。
- 最佳实践: 只要在
AttackAnimation的关键帧 (Impact Frame) 触发时,检测Distance <= EffectiveRange即可判定伤害。这是最稳定、手感最好的做法。
🧱 Case B: 动态阻挡与堵路检测 (Dynamic Blocking)
在塔防模式下,玩家放塔不能把怪物的路彻底堵死 (Maze Blocking Rule)。- 组件: 塔预制体上挂
NavMeshObstacle。 - 设置:
Carve = true(必须勾选,否则怪物会穿过去或被挤开,而不是绕路)。 - 放置前检测算法:
- 虚拟放置一个 Obstacle (不显示,只参与计算)。
-
调用
NavMesh.CalculatePath(SpawnPoint, NexusPoint, AreaMask, pathResult)。 -
检查
pathResult.status == NavMeshPathStatus.PathComplete。 -
如果是
Partial或Invalid,说明路被堵死了,禁止玩家建造。
🕷️ Case C: 爬墙/跳跃 (Off-Mesh Links)
让怪物像蜘蛛一样翻越城墙,或者跳过沟壑。- 场景: 僵尸从地面跳上 2楼平台攻击玩家。
- 实现:
-
在地图关键连接点放置
OffMeshLink组件。 -
当 Agent 走到 Link 入口时,
agent.isOnOffMeshLink变为true。 -
接管控制:
agent.autoTraverseOffMeshLink = false(禁止自动瞬移)。- 播放“跳跃”动画。
- 使用
Coroutine平滑移动 Agent 的transform到 Link 的另一端。 - 到达后,
agent.CompleteOffMeshLink(),交还控制权。
-
在地图关键连接点放置
🎭 Case D: 游击/风筝 (Kiting)
弓箭手不仅要追玩家,还要保持距离。- 笨办法: 只要距离 < 5米,就向后跑。容易卡墙角。
- NavMesh 办法 (SamplePosition):
-
计算反向向量:
Dir = (SelfPos - TargetPos).normalized. -
目标点:
FleePos = SelfPos + Dir * 5.0f. -
采样有效性:
NavMesh.SamplePosition(FleePos, out hit, 2.0f, AreaMask). -
如果采样成功,
SetDestination(hit.position)。 - 如果采样失败(背后是墙),则向侧面寻找向量(叉乘)重试。
-
计算反向向量:
🐜 Case E: 拥挤处理 (Crowd Separation)
几百个僵尸挤在一起,互相推挤导致卡顿。- RVO (Reciprocal Velocity Obstacles): Unity 内置的避障。
- 缺点: 消耗 CPU,且容易让怪物看起来像醉汉乱晃。
- 优化方案:
- 降低 RVO Quality: 在 Project Settings 里把 Agent 的避障质量调低。
- 关闭部分 RVO: 只有前排怪物开启 Obstacle Avoidance,后排怪物关闭(反正它们只要跟着前排走)。
-
流场 (Flow Field) 替代: 如果同屏超过 200 个单位,放弃 NavMeshAgent。
- 使用
NavMesh.CalculatePath算出一口路径。 - 所有怪物只根据路径上的“向量场”移动
transform.Translate。 - 仅在攻击最后阶段启用 Agent 进行精确定位。
- 使用
4. 代码片段:安全的击退处理
5. 常见坑点速查 (Troubleshooting)
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 怪物原地转圈 | 目标点在脚下,但因为 StoppingDistance 没停住。 | 增大 StoppingDistance 或检测 agent.remainingDistance < Threshold 手动 Stop。 |
| 怪物穿墙 | 速度太快,或 NavMeshObstacle 没有开 Carve。 | 开启 Carve;对于极快单位,改用 Raycast 检测前方障碍。 |
| 浮空/陷入地下 | Agent 的 BaseOffset 设置不对,或模型原点不在脚底。 | 调整 BaseOffset;确保美术模型的 Pivot 在脚底中心。 |
| 性能暴跌 | 每一帧都对 100 个怪调用 SetDestination。 | 必须限制频率!使用协程每 0.2s 更新一次路径,或仅当目标移动超过 1米时更新。 |