terminalCode Example

NavMeshLink custom traversal — parabolic jump, ladder climb, and physics jump

calendar_today May 5, 2026 schedule ~1 min read person patrickjr verified 50
NavMeshLink connects two NavMesh points for agents to traverse (jumps, ladders, teleports). Default traversal linearly interpolates between start and end. Override OnLinkEntered for custom movement, and always call agent.CompleteLinkTraversal() when done.

Disable auto-traversal on the agent first:

CSHARP
agent.AutoTraverseLinks = false;

Parabolic Jump

CSHARP
public sealed class JumpLink : NavMeshLink
{
    protected override void OnLinkEntered( NavMeshAgent agent )
    {
        ParabolicJump( agent );
    }

    private async void ParabolicJump( NavMeshAgent agent )
    {
        var start = agent.CurrentLinkTraversal.Value.AgentInitialPosition;
        var end   = agent.CurrentLinkTraversal.Value.LinkExitPosition;

        var peakHeight = MathF.Abs( end.z - start.z ) + 25f;
        var mid = (start + end) / 2f;
        var duration = MathF.Max( 0.75f,
            ((mid.WithZ(peakHeight) - start) + (end - mid.WithZ(peakHeight))).Length / agent.MaxSpeed );

        TimeSince elapsed = 0;
        while ( elapsed < duration )
        {
            var t = elapsed / duration;
            var pos = Vector3.Lerp( start, end, t );
            pos.z = MathX.Lerp( start.z, end.z, t ) + 4f * peakHeight * t * (1f - t);
            agent.SetAgentPosition( pos );
            await Task.Frame();
        }

        agent.SetAgentPosition( end );
        agent.CompleteLinkTraversal();
    }
}

Ladder Climb

CSHARP
public sealed class LadderLink : NavMeshLink
{
    protected override void OnLinkEntered( NavMeshAgent agent )
    {
        ClimbLadder( agent );
    }

    private async void ClimbLadder( NavMeshAgent agent )
    {
        var initialPos  = agent.CurrentLinkTraversal.Value.AgentInitialPosition;
        var start       = agent.CurrentLinkTraversal.Value.LinkEnterPosition;
        var endVertical = start.WithZ( agent.CurrentLinkTraversal.Value.LinkExitPosition.z );
        var end         = agent.CurrentLinkTraversal.Value.LinkExitPosition;
        const float climbSpeed = 100f;

        var t0 = (start - initialPos).Length / climbSpeed;
        var t1 = (endVertical - start).Length / climbSpeed;
        var t2 = (end - endVertical).Length / climbSpeed;
        var total = t0 + t1 + t2;

        TimeSince elapsed = 0;
        while ( elapsed < total )
        {
            Vector3 pos;
            if      ( elapsed < t0 )      pos = Vector3.Lerp( initialPos,  start,       elapsed / t0 );
            else if ( elapsed < t0 + t1 ) pos = Vector3.Lerp( start,       endVertical, (elapsed - t0) / t1 );
            else                          pos = Vector3.Lerp( endVertical,  end,         (elapsed - t0 - t1) / t2 );
            agent.SetAgentPosition( pos );
            await Task.Frame();
        }

        agent.SetAgentPosition( end );
        agent.CompleteLinkTraversal();
    }
}

Physics-Based Jump (on the Agent)

Requires Rigidbody and a Collider on the same GameObject as the agent.

CSHARP
public sealed class NavigationLinkTraversal : Component
{
    [RequireComponent] NavMeshAgent Agent { get; set; }
    [RequireComponent] Rigidbody Body { get; set; }

    protected override void OnEnabled()
    {
        Agent.AutoTraverseLinks = false;
        Agent.LinkEnter += PhysicsJump;
    }

    protected override void OnDisabled() => Agent.LinkEnter -= PhysicsJump;

    private async void PhysicsJump()
    {
        var start = Agent.CurrentLinkTraversal.Value.AgentInitialPosition;
        var end   = Agent.CurrentLinkTraversal.Value.LinkExitPosition;

        Agent.UpdatePosition = false;
        Body.Velocity = Agent.MaxSpeed * (end.WithZ(0) - start.WithZ(0)).Normal * 1.25f
                      + Vector3.Up * MathF.Max( 500f, (end.z - start.z) * 8f );

        TimeSince t = 0;
        while ( true )
        {
            Agent.SetAgentPosition( WorldPosition );
            var tr = Scene.Trace
                .Ray( WorldPosition + Vector3.Up * 0.1f, WorldPosition + Vector3.Down * 1000 )
                .IgnoreGameObjectHierarchy( GameObject )
                .Run();
            if ( tr.Hit && t > 0.5f && tr.Distance < 4f ) break;
            await Task.Frame();
        }

        Agent.SetAgentPosition( WorldPosition );
        Agent.UpdatePosition = true;
        Agent.CompleteLinkTraversal();
    }
}
Was this helpful?