实现NavMeshLink的跳跃导航
解读
国内项目里“跳跃导航”通常指:角色在**离网(Off-Mesh)**的两块 NavMesh 之间做抛物线位移,例如跳台、断桥、爬梯、轻功飞跃。NavMeshLink 是 Unity 官方提供的“离线连接”组件,但默认只负责“瞬移”,不会帮你做抛物线运动、动画融合、落地检测。面试时,考官想确认三件事:
- 你是否真的用过 NavMeshLink,而不是只会用 NavMeshAgent;
- 能否把“瞬移”改造成“可见的跳跃”;
- 能否在移动端跑满 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 的主程级实现,分三步:配置、移动、性能。
- 配置阶段
在场景里两块不连续的 NavMesh 之间手动拉一个 GameObject,挂 NavMeshLink,设置:
- startTransform / endTransform 对准跳跃起点、落点
- costModifier = 2(让寻路把跳跃当成“高成本”边,避免 AI 没事就跳)
- bidirectional = true
- area = 3(User3,代表 JumpOnly,方便后端统计)
- 移动阶段
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(); // 回滚
- 性能阶段
- 把 cache 换成 NativeArray<Vector3>,用 Jobs 并行采样,Burst 编译后耗时可忽略。
- 跳跃过程关闭 NavMeshAgent 的更新(agent.enabled = false),落地再打开,减少内部插值开销。
- 对象池缓存 JumpLinkTraversal,避免战斗副本频繁 new。
拓展思考
- 多人同步:跳跃是“确定性”位移,帧同步方案下把 start、end、h、startTime 做帧编号广播,客户端用完全一致的公式计算位置,无需每帧同步坐标;状态同步方案则只在起跳、落地各发一次包,中间用插值。
- 动态 Link:跑酷游戏常需要在 Runtime 根据玩家“搭桥”生成 NavMeshLink,此时用 NavMesh.AddLink(),并给 link 一个 instanceID,服务器记录 ID 方便回滚。
- 手感调优:
- 起跳前 0.1 s 给角色一个“屈膝”动画事件,同时暂停 Agent 移动,解决“滑步起跳”手感问题;
- 落地后 0.15 s 内禁止再次起跳,防止玩家连跳穿模。
- GPU Instancing 场景:如果跳跃终点是大量动态生成的浮空平台,平台本身用 GPU Instancing 渲染,NavMeshSurface 需要每帧 Rebake 一小块,建议用 NavMeshBuildSource 的“体素裁剪”只 Rebake 半径 5 m 区域,耗时控制在 2 ms 以内。