ExampleComponents/NavigationTargetWanderer.cs
using Sandbox;
using System;
public sealed class NavigationTargetWanderer : Component
{
[Property]
public List<GameObject> PotentialTargets { get; set; }
[RequireComponent]
NavMeshAgent Agent { get; set; }
[RequireComponent]
Rigidbody Body { get; set; }
[RequireComponent]
public SkinnedModelRenderer Model { get; set; }
private Vector3 _currentTarget = Vector3.Zero;
private TimeSince _timeSinceLastTargetChange = 0;
protected override void OnEnabled()
{
_currentTarget = PotentialTargets[Random.Shared.Next( 0, PotentialTargets.Count )].WorldPosition;
Agent.MoveTo( _currentTarget );
NavMeshAgent agent = GetComponent<NavMeshAgent>();
if ( agent == null )
return;
agent.MoveTo( _currentTarget );
}
protected override void OnUpdate()
{
var dir = Agent.Velocity;
var forward = WorldRotation.Forward.Dot( dir );
var sideward = WorldRotation.Right.Dot( dir );
var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();
Model.Set( "move_direction", angle );
Model.Set( "move_speed", Agent.Velocity.Length );
Model.Set( "move_groundspeed", Agent.Velocity.WithZ( 0 ).Length );
Model.Set( "move_y", sideward );
Model.Set( "move_x", forward );
Model.Set( "move_z", Agent.Velocity.z );
Model.Set( "wish_x", Agent.WishVelocity.x );
Model.Set( "wish_y", Agent.WishVelocity.y );
Model.Set( "wish_z", Agent.WishVelocity.z );
if ( _timeSinceLastTargetChange > 20f || WorldPosition.WithZ( 0 ).Distance( _currentTarget.WithZ( 0 ) ) < 16f )
{
_currentTarget = PotentialTargets[Random.Shared.Next( 0, PotentialTargets.Count )].WorldPosition;
Agent.MoveTo( _currentTarget );
_timeSinceLastTargetChange = 0;
}
}
}
public sealed class NavigationLinkTraversal : Component
{
[RequireComponent]
NavMeshAgent Agent { get; set; }
[RequireComponent]
Rigidbody Body { get; set; }
[RequireComponent]
public SkinnedModelRenderer Model { get; set; }
protected override void OnEnabled()
{
Agent.AutoTraverseLinks = false;
NavMeshAgent agent = GetComponent<NavMeshAgent>();
if ( agent == null )
return;
agent.LinkEnter += OnNavLinkEnter;
}
protected override void OnDisabled()
{
NavMeshAgent agent = GetComponent<NavMeshAgent>();
if ( agent == null )
return;
agent.LinkEnter -= OnNavLinkEnter;
}
private void OnNavLinkEnter()
{
// If link is a ladder and we are going up, climb it
if ( Agent.CurrentLinkTraversal.Value.LinkComponent.Tags.Has( "ladder" ) &&
Agent.CurrentLinkTraversal.Value.LinkExitPosition.z > WorldPosition.z )
{
ClimbLadder();
}
else
{
// 50/50 chance of physics jump or paraobolic jump
if ( Random.Shared.Next( 0, 2 ) == 0 )
PhysicsJump();
else
ParabolicJump();
}
}
private async void ClimbLadder()
{
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;
var climbSpeed = 100f;
var startDuration = (start - initialPos).Length / climbSpeed;
var climbDuration = (endVertical - start).Length / climbSpeed;
var endDuration = (end - endVertical).Length / climbSpeed;
var totalLadderTime = startDuration + climbDuration + endDuration;
TimeSince timeSinceStart = 0;
while ( timeSinceStart < totalLadderTime )
{
Vector3 newPosition = start;
// 1. Make sure we are positioned at the link start
if ( timeSinceStart < startDuration )
{
newPosition = Vector3.Lerp( initialPos, start, timeSinceStart / startDuration );
}
// 2. Vertical Movement
else if ( timeSinceStart < startDuration + climbDuration )
{
newPosition = Vector3.Lerp( start, endVertical, (timeSinceStart - startDuration) / climbDuration );
}
// 3. Move off ladder to link end position
else
{
newPosition = Vector3.Lerp( endVertical, end, (timeSinceStart - startDuration - climbDuration) / endDuration );
}
Agent.SetAgentPosition( newPosition );
await Task.Frame();
}
Agent.SetAgentPosition( end );
Agent.CompleteLinkTraversal();
}
private async void ParabolicJump()
{
Model.Set( "b_grounded", false );
Model.Set( "b_jump", true );
var start = Agent.CurrentLinkTraversal.Value.AgentInitialPosition;
var end = Agent.CurrentLinkTraversal.Value.LinkExitPosition;
// Calculate peak height for the parabolic arc
var heightDifference = end.z - start.z;
var peakHeight = MathF.Abs( heightDifference ) + 25f;
var mid = (start + end) / 2f;
// Estimate prabolic duraion size using a triangle /\ between start, mid, end
var startToMid = mid.WithZ( peakHeight ) - start;
var midToEnd = end - mid.WithZ( peakHeight );
var duration = ( startToMid + midToEnd ).Length / Agent.MaxSpeed;
duration = MathF.Max( 0.75f, duration ); // Ensure minimum duration
TimeSince timeSinceStart = 0;
while ( timeSinceStart < duration )
{
var t = timeSinceStart / duration;
// Linearly interpolate XY positions
var newPosition = Vector3.Lerp( start, end, t );
// Apply parabolic curve to Z position using a quadratic function
var yOffset = 4f * peakHeight * t * (1f - t);
newPosition.z = MathX.Lerp( start.z, end.z, t ) + yOffset;
Agent.SetAgentPosition( newPosition );
await Task.Frame();
}
Agent.SetAgentPosition( end );
Model.Set( "b_grounded", true );
Model.Set( "b_jump", false );
Agent.CompleteLinkTraversal();
}
private async void PhysicsJump()
{
Model.Set( "b_grounded", false );
Model.Set( "b_jump", true );
// Physiscs will drive our jump so disable game object position sync
Agent.UpdatePosition = false;
var start = Agent.CurrentLinkTraversal.Value.AgentInitialPosition;
var end = Agent.CurrentLinkTraversal.Value.LinkExitPosition;
var xyVelocity = Agent.MaxSpeed * (end.WithZ( 0 ) - start.WithZ( 0 )).Normal * 1.25f;
var velocity = xyVelocity + Vector3.Up * Math.Max( 500f, (end.z - start.z) * 8f );
// Launch the agent into the air
Body.Velocity = velocity;
TimeSince timeSinceStart = 0;
while ( true )
{
Agent.SetAgentPosition( WorldPosition );
// Try to find ground
var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * 0.1f, WorldPosition + Vector3.Down * 1000 )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
if ( tr.Hit && timeSinceStart > 0.5f && tr.Distance < 4f )
{
break;
}
await Task.Frame();
}
Agent.SetAgentPosition( WorldPosition );
// Hand back position control to the agent
Agent.UpdatePosition = true;
Model.Set( "b_grounded", true );
Model.Set( "b_jump", false );
Agent.CompleteLinkTraversal();
}
}