4、动态障碍物:NavMesh Obstacle组件、动态切割NavMesh、性能优化

好,咱们接着聊寻路系统里一个很头疼的问题——动态障碍物。

你想想看,游戏世界里不可能全是静止不动的墙和柱子。门会开、箱子会被推走、甚至会有从天而降的巨石。如果寻路网格(NavMesh)是死的,那AI角色就会傻乎乎地往墙上撞。我早期做项目时就吃过这个亏,一个NPC愣是冲着刚关上的门走,穿模穿得那叫一个尴尬。

所以,这一章咱们就专门解决这个问题。我会把NavMesh Obstacle组件、动态切割技术,还有性能优化的坑,一次性给你讲透。

4.1 NavMesh Obstacle 组件:轻量级的“挡路牌”

Unity提供了一个现成的组件,叫 NavMesh Obstacle。说白了,它就是个“挡路牌”。

你把它挂在一个动态物体上(比如一个滚动的油桶),AI角色就会自动绕开它。不需要重新烘焙整个地图,性能开销很小。

核心原理: Obstacle组件不会修改NavMesh本身,它只是在运行时告诉寻路系统:“这里暂时不能走”。AI会通过“避障”算法绕开它,而不是重新寻路。

怎么用?很简单。给物体添加组件,然后配置几个关键参数:

参数 作用 我的建议
Shape 形状(Box/Capsule) 根据物体外形选,别偷懒
Size 碰撞体大小 稍微比模型大一圈,留点余量
Carve 是否切割NavMesh 静态物体关掉,动态物体看情况
Move Threshold 移动阈值 默认0.1,别改太小,否则频繁触发

小技巧: 如果物体只是原地旋转(比如风扇),把Carve关掉。只开Obstacle就够了,性能更好。我曾经见过一个项目,所有风扇都开了Carve,结果每帧都在切割网格,帧率直接掉到20。

4.2 动态切割 NavMesh:真正的“地形改造”

Obstacle组件虽然好用,但它有个局限——它只是“绕开”,不是“挖掉”。

什么意思?比如一扇吊桥升起来了,AI应该认为那里是悬崖,而不是绕到桥头再跳过去。这时候,就需要动态切割了。

动态切割,就是运行时真的把NavMesh挖掉一块。Unity里通过开启Obstacle组件的 Carve 属性来实现。

// 开启切割的代码示例
using UnityEngine;
using UnityEngine.AI;

public class DynamicObstacle : MonoBehaviour
{
    private NavMeshObstacle obstacle;

    void Start()
    {
        obstacle = GetComponent<NavMeshObstacle>();
        // 开启切割
        obstacle.carving = true;
        // 设置切割形状为盒子
        obstacle.shape = NavMeshObstacleShape.Box;
        // 设置大小,比实际模型略大
        obstacle.size = new Vector3(2f, 3f, 2f);
    }

    // 当物体移动时,切割区域会自动更新
    void Update()
    {
        // 比如让障碍物左右移动
        transform.position = new Vector3(Mathf.Sin(Time.time) * 5f, 0, 0);
    }
}

嗯,这里要注意:切割是有代价的。每次切割,Unity都要重新计算受影响区域的寻路数据。如果场景里同时有十几个动态切割物体,性能就会明显下降。

避坑指南: 我曾经在一个塔防项目里,给每个可建造的炮塔都开了Carve。结果20个炮塔一建起来,游戏直接卡成PPT。后来我改成只在炮塔建造和拆除时触发切割,平时用Obstacle模式,问题就解决了。

4.3 性能优化:别让动态障碍物拖垮你的帧率

动态障碍物用得好是神器,用不好就是性能杀手。我总结了几条实战经验,你直接拿去用:

4.3.1 减少切割频率

不是所有移动都需要切割。只有改变寻路拓扑结构时才需要。比如:

  • 门打开/关闭时 → 需要切割
  • 箱子被推动时 → 不需要切割,用Obstacle就行
  • 角色死亡倒下 → 不需要切割,尸体用Obstacle

4.3.2 使用 Move Threshold

这个参数很多人忽略。它控制物体移动多少距离后才重新计算切割。默认0.1米,如果你的物体移动很频繁(比如巡逻的无人机),建议改成0.5或1.0。

// 优化后的配置
obstacle.carvingMoveThreshold = 0.5f; // 移动超过0.5米才重新切割

4.3.3 分层管理障碍物

我习惯把障碍物分成三类:

类型 示例 推荐方案
静态障碍物 墙壁、柱子 直接烘焙进NavMesh
半动态障碍物 门、升降平台 用Carve,只在状态变化时触发
全动态障碍物 移动的敌人、滚石 只用Obstacle,不开Carve

4.3.4 限制同时切割的数量

如果场景里确实需要多个动态切割,可以写一个管理器,控制同时切割的物体数量。比如:

public class ObstacleManager : MonoBehaviour
{
    public int maxCarvingObstacles = 5;
    private List<NavMeshObstacle> activeCarvers = new List<NavMeshObstacle>();

    public void RequestCarve(NavMeshObstacle obstacle)
    {
        if (activeCarvers.Count >= maxCarvingObstacles)
        {
            // 关闭最久没用的那个
            activeCarvers[0].carving = false;
            activeCarvers.RemoveAt(0);
        }
        obstacle.carving = true;
        activeCarvers.Add(obstacle);
    }
}

个人经验: 我做过一个吃鸡类游戏,毒圈边缘会有随机掉落的巨石。一开始所有巨石都开Carve,帧率直接崩了。后来改成只对距离玩家最近的3块巨石开Carve,其他用Obstacle,帧率瞬间回到60。你想想看,玩家根本注意不到远处的石头有没有切割,对吧?

4.4 总结与最佳实践

好,咱们把这一章的核心点捋一捋:

  • NavMesh Obstacle 是轻量级方案,适合频繁移动的物体
  • 动态切割(Carve) 是重量级方案,适合改变地形的物体
  • 性能优化 的关键是:减少切割频率、合理设置阈值、分层管理

最后送你一句话:能用Obstacle解决的,就别用Carve。 这是我在无数次掉帧后悟出来的道理。下一章咱们会讲如何让AI角色“聪明地”绕过这些障碍物,而不是傻傻地原地转圈。到时候见!