Significantly improve NPC steering (#17931)
This commit is contained in:
@@ -45,6 +45,9 @@ public sealed class NPCSteeringComponent : Component
|
|||||||
[DataField("nextSteer", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
[DataField("nextSteer", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||||
public TimeSpan NextSteer = TimeSpan.Zero;
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public sealed class MoveToOperator : HTNOperator
|
|||||||
_steering.PrunePath(uid, mapCoords, targetCoordinates.ToMapPos(_entManager, _transform) - mapCoords.Position, result.Path);
|
_steering.PrunePath(uid, mapCoords, targetCoordinates.ToMapPos(_entManager, _transform) - mapCoords.Position, result.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
comp.CurrentPath = result.Path;
|
comp.CurrentPath = new Queue<PathPoly>(result.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public abstract class PathRequest
|
|||||||
public Task<PathResult> Task => Tcs.Task;
|
public Task<PathResult> Task => Tcs.Task;
|
||||||
public readonly TaskCompletionSource<PathResult> Tcs;
|
public readonly TaskCompletionSource<PathResult> Tcs;
|
||||||
|
|
||||||
public Queue<PathPoly> Polys = new();
|
public List<PathPoly> Polys = new();
|
||||||
|
|
||||||
public bool Started = false;
|
public bool Started = false;
|
||||||
|
|
||||||
@@ -103,9 +103,9 @@ public sealed class BFSPathRequest : PathRequest
|
|||||||
public sealed class PathResultEvent
|
public sealed class PathResultEvent
|
||||||
{
|
{
|
||||||
public PathResult Result;
|
public PathResult Result;
|
||||||
public readonly Queue<PathPoly> Path;
|
public readonly List<PathPoly> Path;
|
||||||
|
|
||||||
public PathResultEvent(PathResult result, Queue<PathPoly> path)
|
public PathResultEvent(PathResult result, List<PathPoly> path)
|
||||||
{
|
{
|
||||||
Result = result;
|
Result = result;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed partial class PathfindingSystem
|
|||||||
|
|
||||||
private static readonly PathComparer PathPolyComparer = new();
|
private static readonly PathComparer PathPolyComparer = new();
|
||||||
|
|
||||||
private Queue<PathPoly> ReconstructPath(Dictionary<PathPoly, PathPoly> path, PathPoly currentNodeRef)
|
private List<PathPoly> ReconstructPath(Dictionary<PathPoly, PathPoly> path, PathPoly currentNodeRef)
|
||||||
{
|
{
|
||||||
var running = new List<PathPoly> { currentNodeRef };
|
var running = new List<PathPoly> { currentNodeRef };
|
||||||
while (path.ContainsKey(currentNodeRef))
|
while (path.ContainsKey(currentNodeRef))
|
||||||
@@ -35,10 +35,8 @@ public sealed partial class PathfindingSystem
|
|||||||
running.Add(currentNodeRef);
|
running.Add(currentNodeRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
running = Simplify(running);
|
|
||||||
running.Reverse();
|
running.Reverse();
|
||||||
var result = new Queue<PathPoly>(running);
|
return running;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float GetTileCost(PathRequest request, PathPoly start, PathPoly end)
|
private float GetTileCost(PathRequest request, PathPoly start, PathPoly end)
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
PathFlags flags = PathFlags.None)
|
PathFlags flags = PathFlags.None)
|
||||||
{
|
{
|
||||||
if (!TryComp<TransformComponent>(entity, out var start))
|
if (!TryComp<TransformComponent>(entity, out var start))
|
||||||
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
|
||||||
|
|
||||||
var layer = 0;
|
var layer = 0;
|
||||||
var mask = 0;
|
var mask = 0;
|
||||||
@@ -252,7 +252,7 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
var path = await GetPath(request);
|
var path = await GetPath(request);
|
||||||
|
|
||||||
if (path.Result != PathResult.Path)
|
if (path.Result != PathResult.Path)
|
||||||
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
|
||||||
|
|
||||||
return new PathResultEvent(PathResult.Path, path.Path);
|
return new PathResultEvent(PathResult.Path, path.Path);
|
||||||
}
|
}
|
||||||
@@ -280,14 +280,13 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
return 0f;
|
return 0f;
|
||||||
|
|
||||||
var distance = 0f;
|
var distance = 0f;
|
||||||
var node = path.Path.Dequeue();
|
var lastNode = path.Path[0];
|
||||||
var lastNode = node;
|
|
||||||
|
|
||||||
do
|
for (var i = 1; i < path.Path.Count; i++)
|
||||||
{
|
{
|
||||||
|
var node = path.Path[i];
|
||||||
distance += GetTileCost(request, lastNode, node);
|
distance += GetTileCost(request, lastNode, node);
|
||||||
lastNode = node;
|
}
|
||||||
} while (path.Path.TryDequeue(out node));
|
|
||||||
|
|
||||||
return distance;
|
return distance;
|
||||||
}
|
}
|
||||||
@@ -301,7 +300,7 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
{
|
{
|
||||||
if (!TryComp<TransformComponent>(entity, out var xform) ||
|
if (!TryComp<TransformComponent>(entity, out var xform) ||
|
||||||
!TryComp<TransformComponent>(target, out var targetXform))
|
!TryComp<TransformComponent>(target, out var targetXform))
|
||||||
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
|
||||||
|
|
||||||
var request = GetRequest(entity, xform.Coordinates, targetXform.Coordinates, range, cancelToken, flags);
|
var request = GetRequest(entity, xform.Coordinates, targetXform.Coordinates, range, cancelToken, flags);
|
||||||
return await GetPath(request);
|
return await GetPath(request);
|
||||||
@@ -471,7 +470,7 @@ namespace Content.Server.NPC.Pathfinding
|
|||||||
|
|
||||||
if (!request.Task.IsCompletedSuccessfully)
|
if (!request.Task.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
return new PathResultEvent(PathResult.NoPath, new Queue<PathPoly>());
|
return new PathResultEvent(PathResult.NoPath, new List<PathPoly>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same context as do_after and not synchronously blocking soooo
|
// Same context as do_after and not synchronously blocking soooo
|
||||||
|
|||||||
@@ -292,13 +292,38 @@ public sealed partial class NPCSteeringSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
|
/// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PrunePath(EntityUid uid, MapCoordinates mapCoordinates, Vector2 direction, Queue<PathPoly> nodes)
|
public void PrunePath(EntityUid uid, MapCoordinates mapCoordinates, Vector2 direction, List<PathPoly> nodes)
|
||||||
{
|
{
|
||||||
if (nodes.Count <= 1)
|
if (nodes.Count <= 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Prune the first node as it's irrelevant (normally it is our node so we don't want to backtrack).
|
// Work out if we're inside any nodes, then use the next one as the starting point.
|
||||||
nodes.Dequeue();
|
var index = 0;
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
var node = nodes[i];
|
||||||
|
var matrix = _transform.GetWorldMatrix(node.GraphUid);
|
||||||
|
|
||||||
|
// Always want to prune the poly itself so we point to the next poly and don't backtrack.
|
||||||
|
if (matrix.TransformBox(node.Box).Contains(mapCoordinates.Position))
|
||||||
|
{
|
||||||
|
index = i + 1;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
nodes.RemoveRange(0, index);
|
||||||
|
_pathfindingSystem.Simplify(nodes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, take the node after the nearest node.
|
||||||
|
|
||||||
// TODO: Really need layer support
|
// TODO: Really need layer support
|
||||||
CollisionGroup mask = 0;
|
CollisionGroup mask = 0;
|
||||||
|
|
||||||
@@ -310,11 +335,11 @@ public sealed partial class NPCSteeringSystem
|
|||||||
// If we have to backtrack (for example, we're behind a table and the target is on the other side)
|
// If we have to backtrack (for example, we're behind a table and the target is on the other side)
|
||||||
// Then don't consider pruning.
|
// Then don't consider pruning.
|
||||||
var goal = nodes.Last().Coordinates.ToMap(EntityManager, _transform);
|
var goal = nodes.Last().Coordinates.ToMap(EntityManager, _transform);
|
||||||
var canPrune =
|
|
||||||
_interaction.InRangeUnobstructed(mapCoordinates, goal, (goal.Position - mapCoordinates.Position).Length() + 0.1f, mask);
|
|
||||||
|
|
||||||
while (nodes.TryPeek(out var node))
|
for (var i = 0; i < nodes.Count; i++)
|
||||||
{
|
{
|
||||||
|
var node = nodes[i];
|
||||||
|
|
||||||
if (!node.Data.IsFreeSpace)
|
if (!node.Data.IsFreeSpace)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -322,16 +347,17 @@ public sealed partial class NPCSteeringSystem
|
|||||||
|
|
||||||
// If any nodes are 'behind us' relative to the target we'll prune them.
|
// If any nodes are 'behind us' relative to the target we'll prune them.
|
||||||
// This isn't perfect but should fix most cases of stutter stepping.
|
// This isn't perfect but should fix most cases of stutter stepping.
|
||||||
if (canPrune &&
|
if (nodeMap.MapId == mapCoordinates.MapId &&
|
||||||
nodeMap.MapId == mapCoordinates.MapId &&
|
|
||||||
Vector2.Dot(direction, nodeMap.Position - mapCoordinates.Position) < 0f)
|
Vector2.Dot(direction, nodeMap.Position - mapCoordinates.Position) < 0f)
|
||||||
{
|
{
|
||||||
nodes.Dequeue();
|
nodes.RemoveAt(i);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pathfindingSystem.Simplify(nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -382,7 +408,7 @@ public sealed partial class NPCSteeringSystem
|
|||||||
TransformComponent xform,
|
TransformComponent xform,
|
||||||
float[] danger)
|
float[] danger)
|
||||||
{
|
{
|
||||||
var objectRadius = 0.10f;
|
var objectRadius = 0.15f;
|
||||||
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
|
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
|
||||||
|
|
||||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, detectionRadius, LookupFlags.Static))
|
foreach (var ent in _lookup.GetEntitiesInRange(uid, detectionRadius, LookupFlags.Static))
|
||||||
@@ -430,7 +456,7 @@ public sealed partial class NPCSteeringSystem
|
|||||||
for (var i = 0; i < InterestDirections; i++)
|
for (var i = 0; i < InterestDirections; i++)
|
||||||
{
|
{
|
||||||
var dot = Vector2.Dot(norm, Directions[i]);
|
var dot = Vector2.Dot(norm, Directions[i]);
|
||||||
danger[i] = MathF.Max(dot * weight * 0.9f, danger[i]);
|
danger[i] = MathF.Max(dot * weight, danger[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -368,6 +368,12 @@ 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.
|
||||||
|
if (steering.LastSteerIndex != -1)
|
||||||
|
{
|
||||||
|
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;
|
||||||
var desiredValue = 0f;
|
var desiredValue = 0f;
|
||||||
@@ -392,6 +398,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
|||||||
|
|
||||||
steering.NextSteer = curTime + TimeSpan.FromSeconds(1f / NPCSteeringComponent.SteeringFrequency);
|
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);
|
||||||
}
|
}
|
||||||
@@ -425,14 +432,6 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
|||||||
_interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask))
|
_interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask))
|
||||||
{
|
{
|
||||||
steering.CurrentPath.Clear();
|
steering.CurrentPath.Clear();
|
||||||
// Enqueue our poly as it will be pruned later.
|
|
||||||
var ourPoly = _pathfindingSystem.GetPoly(xform.Coordinates);
|
|
||||||
|
|
||||||
if (ourPoly != null)
|
|
||||||
{
|
|
||||||
steering.CurrentPath.Enqueue(ourPoly);
|
|
||||||
}
|
|
||||||
|
|
||||||
steering.CurrentPath.Enqueue(targetPoly);
|
steering.CurrentPath.Enqueue(targetPoly);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -468,7 +467,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
|
|||||||
var ourPos = xform.MapPosition;
|
var ourPos = xform.MapPosition;
|
||||||
|
|
||||||
PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path);
|
PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path);
|
||||||
steering.CurrentPath = result.Path;
|
steering.CurrentPath = new Queue<PathPoly>(result.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move these to movercontroller
|
// TODO: Move these to movercontroller
|
||||||
|
|||||||
Reference in New Issue
Block a user