Minor A* optimisations (#1335)

* Add some extra comments
* Remove the redundant closedTiles variable
* Rename some variables to better match the common naming schemes

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2020-07-11 04:30:33 +10:00
committed by GitHub
parent 405a610009
commit a77f219515
4 changed files with 36 additions and 55 deletions

View File

@@ -103,8 +103,8 @@ namespace Content.Client.GameObjects.EntitySystems.AI
var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1); var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" + label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
$"Nodes traversed: {message.ClosedTiles.Count}\n" + $"Nodes traversed: {message.CameFrom.Count}\n" +
$"Nodes per ms: {message.ClosedTiles.Count / (message.TimeTaken * 1000)}"; $"Nodes per ms: {message.CameFrom.Count / (message.TimeTaken * 1000)}";
} }
} }

View File

@@ -46,70 +46,73 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
return null; return null;
} }
var openTiles = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer()); var frontier = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
var gScores = new Dictionary<PathfindingNode, float>(); var costSoFar = new Dictionary<PathfindingNode, float>();
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>(); var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
var closedTiles = new HashSet<PathfindingNode>();
PathfindingNode currentNode = null; PathfindingNode currentNode = null;
openTiles.Add((0.0f, _startNode)); frontier.Add((0.0f, _startNode));
gScores[_startNode] = 0.0f; costSoFar[_startNode] = 0.0f;
var routeFound = false; var routeFound = false;
var count = 0; var count = 0;
while (openTiles.Count > 0) while (frontier.Count > 0)
{ {
// Handle whether we need to pause if we've taken too long
count++; count++;
if (count % 20 == 0 && count > 0) if (count % 20 == 0 && count > 0)
{ {
await SuspendIfOutOfTime(); await SuspendIfOutOfTime();
}
if (_startNode == null || _endNode == null) if (_startNode == null || _endNode == null)
{ {
return null; return null;
} }
}
(_, currentNode) = openTiles.Take(); // Actual pathfinding here
(_, currentNode) = frontier.Take();
if (currentNode.Equals(_endNode)) if (currentNode.Equals(_endNode))
{ {
routeFound = true; routeFound = true;
break; break;
} }
closedTiles.Add(currentNode);
foreach (var nextNode in currentNode.GetNeighbors()) foreach (var nextNode in currentNode.GetNeighbors())
{ {
if (closedTiles.Contains(nextNode))
{
continue;
}
// If tile is untraversable it'll be null // If tile is untraversable it'll be null
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode); var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode);
var direction = PathfindingHelpers.RelativeDirection(nextNode, currentNode); if (tileCost == null)
if (tileCost == null || !PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction))
{ {
continue; continue;
} }
var gScore = gScores[currentNode] + tileCost.Value; // So if we're going NE then that means either N or E needs to be free to actually get there
var direction = PathfindingHelpers.RelativeDirection(nextNode, currentNode);
if (!PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction))
{
continue;
}
if (gScores.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue) // f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = costSoFar[currentNode] + tileCost.Value;
if (costSoFar.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue)
{ {
continue; continue;
} }
cameFrom[nextNode] = currentNode; cameFrom[nextNode] = currentNode;
gScores[nextNode] = gScore; costSoFar[nextNode] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal. // pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration // There's other ways to do it but future consideration
var fScore = gScores[nextNode] + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f); // The closer the fScore is to the actual distance then the better the pathfinder will be
openTiles.Add((fScore, nextNode)); // (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
var fScore = gScore + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f);
frontier.Add((fScore, nextNode));
} }
} }
@@ -130,30 +133,22 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
if (DebugRoute != null && route.Count > 0) if (DebugRoute != null && route.Count > 0)
{ {
var debugCameFrom = new Dictionary<TileRef, TileRef>(cameFrom.Count); var debugCameFrom = new Dictionary<TileRef, TileRef>(cameFrom.Count);
var debugGScores = new Dictionary<TileRef, float>(gScores.Count); var debugGScores = new Dictionary<TileRef, float>(costSoFar.Count);
var debugClosedTiles = new HashSet<TileRef>(closedTiles.Count);
foreach (var (node, parent) in cameFrom) foreach (var (node, parent) in cameFrom)
{ {
debugCameFrom.Add(node.TileRef, parent.TileRef); debugCameFrom.Add(node.TileRef, parent.TileRef);
} }
foreach (var (node, score) in gScores) foreach (var (node, score) in costSoFar)
{ {
debugGScores.Add(node.TileRef, score); debugGScores.Add(node.TileRef, score);
} }
foreach (var node in closedTiles)
{
debugClosedTiles.Add(node.TileRef);
}
var debugRoute = new SharedAiDebug.AStarRouteDebug( var debugRoute = new SharedAiDebug.AStarRouteDebug(
_pathfindingArgs.Uid, _pathfindingArgs.Uid,
route, route,
debugCameFrom, debugCameFrom,
debugGScores, debugGScores,
debugClosedTiles,
DebugTime); DebugTime);
DebugRoute.Invoke(debugRoute); DebugRoute.Invoke(debugRoute);

View File

@@ -55,19 +55,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
gScores.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position, score); gScores.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position, score);
} }
var closedTiles = new List<Vector2>();
foreach (var tile in routeDebug.ClosedTiles)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
closedTiles.Add(mapManager.GetGrid(tile.GridIndex).LocalToWorld(tileGrid).Position);
}
var systemMessage = new SharedAiDebug.AStarRouteMessage( var systemMessage = new SharedAiDebug.AStarRouteMessage(
routeDebug.EntityUid, routeDebug.EntityUid,
route, route,
cameFrom, cameFrom,
gScores, gScores,
closedTiles,
routeDebug.TimeTaken routeDebug.TimeTaken
); );

View File

@@ -58,7 +58,6 @@ namespace Content.Shared.AI
public Queue<TileRef> Route { get; } public Queue<TileRef> Route { get; }
public Dictionary<TileRef, TileRef> CameFrom { get; } public Dictionary<TileRef, TileRef> CameFrom { get; }
public Dictionary<TileRef, float> GScores { get; } public Dictionary<TileRef, float> GScores { get; }
public HashSet<TileRef> ClosedTiles { get; }
public double TimeTaken { get; } public double TimeTaken { get; }
public AStarRouteDebug( public AStarRouteDebug(
@@ -66,14 +65,12 @@ namespace Content.Shared.AI
Queue<TileRef> route, Queue<TileRef> route,
Dictionary<TileRef, TileRef> cameFrom, Dictionary<TileRef, TileRef> cameFrom,
Dictionary<TileRef, float> gScores, Dictionary<TileRef, float> gScores,
HashSet<TileRef> closedTiles,
double timeTaken) double timeTaken)
{ {
EntityUid = uid; EntityUid = uid;
Route = route; Route = route;
CameFrom = cameFrom; CameFrom = cameFrom;
GScores = gScores; GScores = gScores;
ClosedTiles = closedTiles;
TimeTaken = timeTaken; TimeTaken = timeTaken;
} }
} }
@@ -105,7 +102,6 @@ namespace Content.Shared.AI
public readonly IEnumerable<Vector2> Route; public readonly IEnumerable<Vector2> Route;
public readonly Dictionary<Vector2, Vector2> CameFrom; public readonly Dictionary<Vector2, Vector2> CameFrom;
public readonly Dictionary<Vector2, float> GScores; public readonly Dictionary<Vector2, float> GScores;
public readonly List<Vector2> ClosedTiles;
public double TimeTaken; public double TimeTaken;
public AStarRouteMessage( public AStarRouteMessage(
@@ -113,14 +109,12 @@ namespace Content.Shared.AI
IEnumerable<Vector2> route, IEnumerable<Vector2> route,
Dictionary<Vector2, Vector2> cameFrom, Dictionary<Vector2, Vector2> cameFrom,
Dictionary<Vector2, float> gScores, Dictionary<Vector2, float> gScores,
List<Vector2> closedTiles,
double timeTaken) double timeTaken)
{ {
EntityUid = uid; EntityUid = uid;
Route = route; Route = route;
CameFrom = cameFrom; CameFrom = cameFrom;
GScores = gScores; GScores = gScores;
ClosedTiles = closedTiles;
TimeTaken = timeTaken; TimeTaken = timeTaken;
} }
} }