Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Code/Npcs/Tasks/MoveTo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ namespace Sandbox.Npcs.Tasks;
/// <summary>
/// Task that commands the NavigationLayer to move to a target position or GameObject.
/// When tracking a GameObject, re-evaluates the path periodically.
/// Does not override the NPC's look target — but will rotate the body to face the
/// movement direction when the angle would otherwise cause silly walking
/// If a look target is set (e.g. chasing a player), rotates the body to face that target
/// each frame. Otherwise, rotates to face the movement direction when the angle would
/// otherwise cause sideways walking.
/// </summary>
public class MoveTo : TaskBase
{
Expand Down Expand Up @@ -61,9 +62,18 @@ protected override TaskStatus OnUpdate()
var fwd = Npc.WorldRotation.Forward.WithZ( 0 ).Normal;
var angle = Vector3.GetAngle( fwd, moveDir );

if ( angle > LateralThreshold && !Npc.Animation.LookTarget.HasValue )
if ( Npc.Animation.LookTarget.HasValue )
{
// No look target — face the movement direction
// Has a look target (e.g. chasing a player) — rotate body to face the target
// directly so the NPC always looks at what it's chasing, not the path direction.
var toTarget = (Npc.Animation.LookTarget.Value.WithZ( 0 ) - Npc.WorldPosition.WithZ( 0 )).Normal;
var targetRot = Rotation.LookAt( toTarget, Vector3.Up );
Npc.GameObject.WorldRotation = Rotation.Lerp(
Npc.WorldRotation, targetRot, Npc.Animation.LookSpeed * Time.Delta );
}
else if ( angle > LateralThreshold )
{
// No look target — face the movement direction to avoid sideways walking
var targetRot = Rotation.LookAt( moveDir, Vector3.Up );
Npc.GameObject.WorldRotation = Rotation.Lerp(
Npc.WorldRotation, targetRot, Npc.Animation.LookSpeed * Time.Delta );
Expand Down
35 changes: 34 additions & 1 deletion code/Npcs/Layers/NavigationLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ public class NavigationLayer : BaseNpcLayer
/// </summary>
public float WishSpeed { get; set; } = 100f;

// Grace period after issuing a move before we allow failure checks,
// so the agent has time to start navigating.
private TimeSince _timeSinceLastMoveIssued;

protected override void OnStart()
{
Agent = Npc.GetComponent<NavMeshAgent>();

// We handle rotation ourselves (facing look target or movement direction),
// so prevent the agent from snapping the body to the path direction.
if ( Agent.IsValid() )
Agent.UpdateRotation = false;
}

/// <summary>
Expand All @@ -29,6 +38,7 @@ public void MoveTo( Vector3 target, float stopDistance = 10f )
{
MoveTarget = target;
StopDistance = stopDistance;
_timeSinceLastMoveIssued = 0;

if ( Agent.IsValid() )
{
Expand All @@ -49,7 +59,21 @@ protected override void OnUpdate()
if ( Agent.IsValid() )
{
Agent.MaxSpeed = WishSpeed;
Npc.Animation.SetMove( Agent.Velocity, Agent.WorldRotation );
// Use the NPC's actual body rotation (not the agent's path rotation) as the
// reference frame so that animation blend parameters (move_x / move_y) are
// computed relative to where the body is actually facing.
Npc.Animation.SetMove( Agent.Velocity, Npc.WorldRotation );

// If we have a pending move target but the agent isn't navigating,
// and the navmesh just finished building, re-issue the MoveTo so the
// agent can register on the freshly-built surface.
if ( MoveTarget.HasValue && !Agent.IsNavigating
&& !Npc.Scene.NavMesh.IsGenerating && !Npc.Scene.NavMesh.IsDirty
&& _timeSinceLastMoveIssued > 0.1f )
{
Agent.MoveTo( MoveTarget.Value );
_timeSinceLastMoveIssued = 0;
}
}
}

Expand All @@ -76,6 +100,15 @@ public TaskStatus GetStatus()
if ( distance <= StopDistance )
return TaskStatus.Success;

// If the navmesh is still building (e.g. after procedural geometry was spawned),
// keep waiting rather than failing immediately.
if ( Npc.Scene.NavMesh.IsGenerating || Npc.Scene.NavMesh.IsDirty )
return TaskStatus.Running;

// Give the agent a short grace period to start navigating after a move is issued.
if ( _timeSinceLastMoveIssued < 0.1f )
return TaskStatus.Running;

if ( Agent.IsValid() && !Agent.IsNavigating )
return TaskStatus.Failed;

Expand Down