NPC steering blending (#25666)

* NPC steering blending

Significantly more stable than using LastSteerDirection and also AntiStuck never got tripped locally when I was running around. I also left future notes for me to cleanup the pathfinder in future.

* Remove index
This commit is contained in:
metalgearsloth
2024-02-28 17:41:15 +11:00
committed by GitHub
parent 1c015d1fd5
commit f819404f6d
3 changed files with 56 additions and 48 deletions

View File

@@ -28,11 +28,11 @@ public sealed partial class NPCSteeringComponent : Component
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public float Radius = 0.35f; public float Radius = 0.35f;
[ViewVariables] [ViewVariables, DataField]
public readonly float[] Interest = new float[SharedNPCSteeringSystem.InterestDirections]; public float[] Interest = new float[SharedNPCSteeringSystem.InterestDirections];
[ViewVariables] [ViewVariables, DataField]
public readonly float[] Danger = new float[SharedNPCSteeringSystem.InterestDirections]; public float[] Danger = new float[SharedNPCSteeringSystem.InterestDirections];
// TODO: Update radius, also danger points debug only // TODO: Update radius, also danger points debug only
public readonly List<Vector2> DangerPoints = new(); public readonly List<Vector2> DangerPoints = new();
@@ -45,21 +45,9 @@ public sealed partial class NPCSteeringComponent : Component
[DataField("forceMove")] [DataField("forceMove")]
public bool ForceMove = false; public bool ForceMove = false;
/// <summary>
/// Next time we can change our steering direction.
/// </summary>
[DataField("nextSteer", customTypeSerializer:typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextSteer = TimeSpan.Zero;
[DataField("lastSteerIndex")]
public int LastSteerIndex = -1;
[DataField("lastSteerDirection")] [DataField("lastSteerDirection")]
public Vector2 LastSteerDirection = Vector2.Zero; public Vector2 LastSteerDirection = Vector2.Zero;
public const int SteeringFrequency = 5;
/// <summary> /// <summary>
/// Last position we considered for being stuck. /// Last position we considered for being stuck.
/// </summary> /// </summary>

View File

@@ -9,6 +9,7 @@ using Content.Shared.Movement.Components;
using Content.Shared.NPC; using Content.Shared.NPC;
using Content.Shared.Physics; using Content.Shared.Physics;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent; using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
@@ -16,7 +17,7 @@ namespace Content.Server.NPC.Systems;
public sealed partial class NPCSteeringSystem public sealed partial class NPCSteeringSystem
{ {
private void ApplySeek(float[] interest, Vector2 direction, float weight) private void ApplySeek(Span<float> interest, Vector2 direction, float weight)
{ {
if (weight == 0f || direction == Vector2.Zero) if (weight == 0f || direction == Vector2.Zero)
return; return;
@@ -25,13 +26,10 @@ public sealed partial class NPCSteeringSystem
for (var i = 0; i < InterestDirections; i++) for (var i = 0; i < InterestDirections; i++)
{ {
if (interest[i].Equals(-1f))
continue;
var angle = i * InterestRadians; var angle = i * InterestRadians;
var dot = MathF.Cos(directionAngle - angle); var dot = MathF.Cos(directionAngle - angle);
dot = (dot + 1) * 0.5f; dot = (dot + 1f) * 0.5f;
interest[i] += dot * weight; interest[i] = Math.Clamp(interest[i] + dot * weight, 0f, 1f);
} }
} }
@@ -72,7 +70,7 @@ public sealed partial class NPCSteeringSystem
TransformComponent xform, TransformComponent xform,
Angle offsetRot, Angle offsetRot,
float moveSpeed, float moveSpeed,
float[] interest, Span<float> interest,
float frameTime, float frameTime,
ref bool forceSteer) ref bool forceSteer)
{ {
@@ -274,7 +272,8 @@ public sealed partial class NPCSteeringSystem
} }
// If not in LOS and no path then get a new one fam. // If not in LOS and no path then get a new one fam.
if (!inLos && steering.CurrentPath.Count == 0) if ((!inLos && steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0) ||
(!steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0))
{ {
needsPath = true; needsPath = true;
} }
@@ -465,12 +464,12 @@ public sealed partial class NPCSteeringSystem
int layer, int layer,
int mask, int mask,
TransformComponent xform, TransformComponent xform,
float[] danger) Span<float> danger)
{ {
var objectRadius = 0.15f; var objectRadius = 0.25f;
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius); var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
var ents = _entSetPool.Get(); var ents = _entSetPool.Get();
_lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Static); _lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic | LookupFlags.Static);
foreach (var ent in ents) foreach (var ent in ents)
{ {
@@ -478,6 +477,7 @@ public sealed partial class NPCSteeringSystem
if (!_physicsQuery.TryGetComponent(ent, out var otherBody) || if (!_physicsQuery.TryGetComponent(ent, out var otherBody) ||
!otherBody.Hard || !otherBody.Hard ||
!otherBody.CanCollide || !otherBody.CanCollide ||
otherBody.BodyType == BodyType.KinematicController ||
(mask & otherBody.CollisionLayer) == 0x0 && (mask & otherBody.CollisionLayer) == 0x0 &&
(layer & otherBody.CollisionMask) == 0x0) (layer & otherBody.CollisionMask) == 0x0)
{ {
@@ -506,7 +506,7 @@ public sealed partial class NPCSteeringSystem
} }
else else
{ {
weight = distance / detectionRadius; weight = (detectionRadius - distance) / detectionRadius;
} }
if (obstacleDirection == Vector2.Zero) if (obstacleDirection == Vector2.Zero)
@@ -541,7 +541,7 @@ public sealed partial class NPCSteeringSystem
int mask, int mask,
PhysicsComponent body, PhysicsComponent body,
TransformComponent xform, TransformComponent xform,
float[] danger) Span<float> danger)
{ {
var objectRadius = 0.25f; var objectRadius = 0.25f;
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius); var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
@@ -614,4 +614,35 @@ public sealed partial class NPCSteeringSystem
// TODO: Alignment // TODO: Alignment
// TODO: Cohesion // TODO: Cohesion
private void Blend(NPCSteeringComponent steering, float frameTime, Span<float> interest, Span<float> danger)
{
/*
* Future sloth notes:
* Pathfinder cleanup:
- Cleanup whatever the fuck is happening in pathfinder
- Use Flee for melee behavior / actions and get the seek direction from that rather than bulldozing
- Must always have a path
- Path should return the full version + the snipped version
- Pathfinder needs to do diagonals
- Next node is either <current node + 1> or <nearest node + 1> (on the full path)
- If greater than <1.5m distance> repath
*/
// IDK why I didn't do this sooner but blending is a lot better than lastdir for fixing stuttering.
const float BlendWeight = 10f;
var blendValue = Math.Min(1f, frameTime * BlendWeight);
for (var i = 0; i < InterestDirections; i++)
{
var currentInterest = interest[i];
var lastInterest = steering.Interest[i];
var interestDiff = (currentInterest - lastInterest) * blendValue;
steering.Interest[i] = lastInterest + interestDiff;
var currentDanger = danger[i];
var lastDanger = steering.Danger[i];
var dangerDiff = (currentDanger - lastDanger) * blendValue;
steering.Danger[i] = lastDanger + dangerDiff;
}
}
} }

View File

@@ -3,7 +3,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.DoAfter; using Content.Server.DoAfter;
using Content.Server.Doors.Systems;
using Content.Server.NPC.Components; using Content.Server.NPC.Components;
using Content.Server.NPC.Events; using Content.Server.NPC.Events;
using Content.Server.NPC.Pathfinding; using Content.Server.NPC.Pathfinding;
@@ -28,7 +27,6 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Shared.Prying.Systems; using Content.Shared.Prying.Systems;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Threading;
namespace Content.Server.NPC.Systems; namespace Content.Server.NPC.Systems;
@@ -315,8 +313,6 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
return; return;
} }
var interest = steering.Interest;
var danger = steering.Danger;
var agentRadius = steering.Radius; var agentRadius = steering.Radius;
var worldPos = _transform.GetWorldPosition(xform); var worldPos = _transform.GetWorldPosition(xform);
var (layer, mask) = _physics.GetHardCollision(uid); var (layer, mask) = _physics.GetHardCollision(uid);
@@ -328,13 +324,10 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
var body = _physicsQuery.GetComponent(uid); var body = _physicsQuery.GetComponent(uid);
var dangerPoints = steering.DangerPoints; var dangerPoints = steering.DangerPoints;
dangerPoints.Clear(); dangerPoints.Clear();
Span<float> interest = stackalloc float[InterestDirections];
Span<float> danger = stackalloc float[InterestDirections];
for (var i = 0; i < InterestDirections; i++) // TODO: This should be fly
{
steering.Interest[i] = 0f;
steering.Danger[i] = 0f;
}
steering.CanSeek = true; steering.CanSeek = true;
var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot); var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot);
@@ -347,6 +340,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
SetDirection(mover, steering, Vector2.Zero); SetDirection(mover, steering, Vector2.Zero);
return; return;
} }
DebugTools.Assert(!float.IsNaN(interest[0])); DebugTools.Assert(!float.IsNaN(interest[0]));
// Don't steer too frequently to avoid twitchiness. // Don't steer too frequently to avoid twitchiness.
@@ -354,7 +348,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
// I think doing this after all the ops above is best? // I think doing this after all the ops above is best?
// Originally I had it way above but sometimes mobs would overshoot their tile targets. // Originally I had it way above but sometimes mobs would overshoot their tile targets.
if (!forceSteer && steering.NextSteer > curTime) if (!forceSteer)
{ {
SetDirection(mover, steering, steering.LastSteerDirection, false); SetDirection(mover, steering, steering.LastSteerDirection, false);
return; return;
@@ -366,11 +360,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger); Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger);
// Prioritise whichever direction we went last tick if it's a tie-breaker. // Blend last and current tick
if (steering.LastSteerIndex != -1) Blend(steering, frameTime, interest, danger);
{
interest[steering.LastSteerIndex] *= 1.1f;
}
// Remove the danger map from the interest map. // Remove the danger map from the interest map.
var desiredDirection = -1; var desiredDirection = -1;
@@ -378,7 +369,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
for (var i = 0; i < InterestDirections; i++) for (var i = 0; i < InterestDirections; i++)
{ {
var adjustedValue = Math.Clamp(interest[i] - danger[i], 0f, 1f); var adjustedValue = Math.Clamp(steering.Interest[i] - steering.Danger[i], 0f, 1f);
if (adjustedValue > desiredValue) if (adjustedValue > desiredValue)
{ {
@@ -394,9 +385,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
resultDirection = new Angle(desiredDirection * InterestRadians).ToVec(); resultDirection = new Angle(desiredDirection * InterestRadians).ToVec();
} }
steering.NextSteer = curTime + TimeSpan.FromSeconds(1f / NPCSteeringComponent.SteeringFrequency);
steering.LastSteerDirection = resultDirection; steering.LastSteerDirection = resultDirection;
steering.LastSteerIndex = desiredDirection;
DebugTools.Assert(!float.IsNaN(resultDirection.X)); DebugTools.Assert(!float.IsNaN(resultDirection.X));
SetDirection(mover, steering, resultDirection, false); SetDirection(mover, steering, resultDirection, false);
} }