From d6309ce9c100c8d05d85dc5c224c1cdc09e58d72 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Mon, 10 Oct 2022 11:31:23 +0200 Subject: [PATCH] Added the ability to find paths to one of multiple goals using AStar --- CHANGELOG.md | 3 ++- MLEM/Pathfinding/AStar.cs | 19 +++++++++++++------ Tests/PathfindingTests.cs | 11 +++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b1768..4ff0af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,14 @@ Jump to version: ### MLEM Additions - Added TokenizedString.Realign +- **Added the ability to find paths to one of multiple goals using AStar** Improvements - Improved EnumHelper.GetValues signature to return an array - Allow using external gesture handling alongside InputHandler through ExternalGestureHandling - Discard old data when updating a StaticSpriteBatch - Multi-target net452, making MLEM compatible with MonoGame for consoles -- **Allow retrieving the cost of a calculated path when using AStar** +- Allow retrieving the cost of a calculated path when using AStar - **Drastically improved StaticSpriteBatch batching performance** Fixes diff --git a/MLEM/Pathfinding/AStar.cs b/MLEM/Pathfinding/AStar.cs index ee22239..cc8d3f1 100644 --- a/MLEM/Pathfinding/AStar.cs +++ b/MLEM/Pathfinding/AStar.cs @@ -61,7 +61,7 @@ namespace MLEM.Pathfinding { /// A function that determines a set of additional neighbors to be considered for a given point. /// A stack of path points, where the top item is the first point to go to, or null if no path was found. public Stack FindPath(T start, T goal, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, CollectAdditionalNeighbors additionalNeighbors = null) { - this.TryFindPath(start, goal, out var path, out _, costFunction, defaultCost, maxTries, additionalNeighbors); + this.TryFindPath(start, new[] {goal}, out var path, out _, costFunction, defaultCost, maxTries, additionalNeighbors); return path; } @@ -69,7 +69,7 @@ namespace MLEM.Pathfinding { /// Tries to find a path between two points using this pathfinder's default settings or, alternatively, the supplied override settings. /// /// The point to start path finding at - /// The point to find a path to + /// The points to find a path to, one of which will be chosen as the closest or best destination /// The path that was found, or if no path was found. /// The total cost that was calculated for the path, or if no path was found. /// The function that determines the cost for each path point @@ -77,7 +77,7 @@ namespace MLEM.Pathfinding { /// The maximum amount of tries before path finding is aborted /// A function that determines a set of additional neighbors to be considered for a given point. /// Whether a path was found. - public bool TryFindPath(T start, T goal, out Stack path, out float totalCost, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, CollectAdditionalNeighbors additionalNeighbors = null) { + public bool TryFindPath(T start, ICollection goals, out Stack path, out float totalCost, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, CollectAdditionalNeighbors additionalNeighbors = null) { path = null; totalCost = float.PositiveInfinity; @@ -90,7 +90,7 @@ namespace MLEM.Pathfinding { var neighbors = new HashSet(); var open = new Dictionary>(); var closed = new Dictionary>(); - open.Add(start, new PathPoint(start, this.GetHeuristicDistance(start, goal), null, 0, defCost)); + open.Add(start, new PathPoint(start, this.GetMinHeuristicDistance(start, goals), null, 0, defCost)); var count = 0; while (open.Count > 0) { @@ -105,7 +105,7 @@ namespace MLEM.Pathfinding { open.Remove(current.Pos); closed.Add(current.Pos, current); - if (EqualityComparer.Default.Equals(current.Pos, goal)) { + if (goals.Contains(current.Pos)) { path = AStar.CompilePath(current); totalCost = current.F; break; @@ -118,7 +118,7 @@ namespace MLEM.Pathfinding { foreach (var neighborPos in neighbors) { var cost = getCost(current.Pos, neighborPos); if (!float.IsPositiveInfinity(cost) && cost < float.MaxValue && !closed.ContainsKey(neighborPos)) { - var neighbor = new PathPoint(neighborPos, this.GetHeuristicDistance(neighborPos, goal), current, cost, defCost); + var neighbor = new PathPoint(neighborPos, this.GetMinHeuristicDistance(neighborPos, goals), current, cost, defCost); // check if we already have a waypoint at this location with a worse path if (open.TryGetValue(neighborPos, out var alreadyNeighbor)) { if (neighbor.G < alreadyNeighbor.G) { @@ -160,6 +160,13 @@ namespace MLEM.Pathfinding { /// The set to populate with neighbors. protected abstract void CollectNeighbors(T position, ISet neighbors); + private float GetMinHeuristicDistance(T start, IEnumerable positions) { + var min = float.MaxValue; + foreach (var position in positions) + min = Math.Min(min, this.GetHeuristicDistance(start, position)); + return min; + } + private static Stack CompilePath(PathPoint current) { var path = new Stack(); while (current != null) { diff --git a/Tests/PathfindingTests.cs b/Tests/PathfindingTests.cs index 9fbc161..752107b 100644 --- a/Tests/PathfindingTests.cs +++ b/Tests/PathfindingTests.cs @@ -83,7 +83,7 @@ namespace Tests { } [Test] - public void TestCosts() { + public void TestCostsAndMultipleGoals() { var area = new[] { "XXXXXXXX", "X 2 X", @@ -95,12 +95,19 @@ namespace Tests { }; var pathfinder = PathfindingTests.CreatePathfinder(area, false); + // try to find paths to each goal individually var goals = new[] {new Point(1, 5), new Point(3, 5), new Point(5, 5)}; var goalCosts = new[] {19, float.PositiveInfinity, 9}; for (var i = 0; i < goals.Length; i++) { - pathfinder.TryFindPath(new Point(1, 1), goals[i], out _, out var cost); + pathfinder.TryFindPath(new Point(1, 1), new[] {goals[i]}, out _, out var cost); Assert.AreEqual(goalCosts[i], cost); } + + // try to find paths to the best goal + var expected = new[] {new Point(1, 1), new Point(2, 1), new Point(3, 1), new Point(4, 1), new Point(5, 1), new Point(5, 2), new Point(5, 3), new Point(5, 4), new Point(5, 5)}; + pathfinder.TryFindPath(new Point(1, 1), goals, out var path, out var bestCost); + Assert.AreEqual(bestCost, 9); + Assert.AreEqual(expected, path); } private static Stack FindPathInArea(Point start, Point end, IEnumerable area, bool allowDiagonals, AStar2.CollectAdditionalNeighbors collectAdditionalNeighbors = null) {