实现NavMeshLink的跳跃导航

解读

国内项目里“跳跃导航”通常指:角色在**离网(Off-Mesh)**的两块 NavMesh 之间做抛物线位移,例如跳台、断桥、爬梯、轻功飞跃。NavMeshLink 是 Unity 官方提供的“离线连接”组件,但默认只负责“瞬移”,不会帮你做抛物线运动、动画融合、落地检测。面试时,考官想确认三件事:

  1. 你是否真的用过 NavMeshLink,而不是只会用 NavMeshAgent;
  2. 能否把“瞬移”改造成“可见的跳跃”;
  3. 能否在移动端跑满 60 FPS,且不因跳跃产生 GC 抖动。
    因此,回答必须给出可落地的框架级代码,并解释性能、手感、多人同步三点。

知识点

  • NavMeshLink 与 Off-Mesh Link 区别:前者 Component 化,支持 Runtime 动态增删,后者已过时。
  • Agent 的 autoTraverseOffMeshLink=false 必须手动关闭,否则 Unity 会帮你瞬移。
  • Parabola 计算:给定起点、终点、重力加速度 g,用公式 y = h − 4h·x²/L² 可得到最大高度 h 的抛物线;移动端建议预采样 16 段,缓存进 NativeArray,避免每帧 new Vector3[]。
  • 动画融合:在 StartJump() 时把 Agent.speed 设为 0,把 Animator.applyRootMotion = false,用 Animator.CrossFade("Jump", 0.1f, 0, 0) 保证脚底不滑。
  • 落地检测:用 NavMesh.Raycast 检测落点下方是否有 NavMesh,防止美术把终点摆到悬崖外;若检测失败,则回滚到上一个有效路径点。
  • 热更新兼容:跳跃逻辑写在继承自 MonoBehaviour 的“跳跃状态机”里,不直接改 NavMeshLink 源码,方便 Lua 热补丁替换。
  • 性能:跳跃过程每帧只改 transform.position,不激活 NavMeshObstacle;移动端用 Jobs 并行计算抛物线采样,Burst 后耗时 < 0.05 ms。

答案

下面给出一份可直接拷进公司 Demo 的主程级实现,分三步:配置、移动、性能。

  1. 配置阶段
    在场景里两块不连续的 NavMesh 之间手动拉一个 GameObject,挂 NavMeshLink,设置:
  • startTransform / endTransform 对准跳跃起点、落点
  • costModifier = 2(让寻路把跳跃当成“高成本”边,避免 AI 没事就跳)
  • bidirectional = true
  • area = 3(User3,代表 JumpOnly,方便后端统计)
  1. 移动阶段
public class JumpLinkTraversal : MonoBehaviour
{
    [SerializeField] float g = 9.8f;
    [SerializeField] int sample = 16;
    NavMeshAgent agent;
    Animator anim;
    bool inJump;
    float t;
    Vector3[] cache;          // 预采样
    NavMeshLink link;

    void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
        anim  = GetComponent<Animator>();
        agent.autoTraverseOffMeshLink = false;
    }

    void Update()
    {
        if (!inJump && agent.isOnOffMeshLink)
        {
            link = agent.currentOffMeshLinkData.offMeshLink as NavMeshLink;
            if (link != null && link.area == 3)   // 只处理 JumpOnly
                StartJump();
        }

        if (inJump)
        {
            t += Time.deltaTime;
            int idx = Mathf.Clamp((int)(t * sample), 0, sample - 1);
            transform.position = cache[idx];
            if (t >= 1f)
            {
                inJump = false;
                agent.CompleteOffMeshLink();      // 通知导航系统“我跳完了”
                anim.CrossFade("Land", 0.1f);
            }
        }
    }

    void StartJump()
    {
        inJump = true;
        t = 0;
        var data = agent.currentOffMeshLinkData;
        Vector3 start = data.startPos;
        Vector3 end   = data.endPos;
        float L = Vector3.Distance(start, end);
        float h = L * 0.3f;                     // 可调曲线高度
        cache = new Vector3[sample];
        for (int i = 0; i < sample; i++)
        {
            float x = (float)i / (sample - 1);
            Vector3 p = Vector3.Lerp(start, end, x);
            p.y = start.y + h * (-4 * x * x + 4 * x); // 抛物线
            cache[i] = p;
        }
        anim.CrossFade("Jump", 0.1f, 0, 0);
        agent.velocity = Vector3.zero;
    }
}

落地检测可在 StartJump 里加

if (!NavMesh.SamplePosition(end, out NavMeshHit hit, 2f, NavMesh.AllAreas))
    agent.ResetPath();   // 回滚
  1. 性能阶段
  • 把 cache 换成 NativeArray<Vector3>,用 Jobs 并行采样,Burst 编译后耗时可忽略。
  • 跳跃过程关闭 NavMeshAgent 的更新(agent.enabled = false),落地再打开,减少内部插值开销。
  • 对象池缓存 JumpLinkTraversal,避免战斗副本频繁 new。

拓展思考

  1. 多人同步:跳跃是“确定性”位移,帧同步方案下把 start、end、h、startTime 做帧编号广播,客户端用完全一致的公式计算位置,无需每帧同步坐标;状态同步方案则只在起跳、落地各发一次包,中间用插值。
  2. 动态 Link:跑酷游戏常需要在 Runtime 根据玩家“搭桥”生成 NavMeshLink,此时用 NavMesh.AddLink(),并给 link 一个 instanceID,服务器记录 ID 方便回滚。
  3. 手感调优
    • 起跳前 0.1 s 给角色一个“屈膝”动画事件,同时暂停 Agent 移动,解决“滑步起跳”手感问题;
    • 落地后 0.15 s 内禁止再次起跳,防止玩家连跳穿模。
  4. GPU Instancing 场景:如果跳跃终点是大量动态生成的浮空平台,平台本身用 GPU Instancing 渲染,NavMeshSurface 需要每帧 Rebake 一小块,建议用 NavMeshBuildSource 的“体素裁剪”只 Rebake 半径 5 m 区域,耗时控制在 2 ms 以内。