diff --git a/CHANGELOG.md b/CHANGELOG.md index 325278e..7718cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,9 @@ 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 -- **Drastically improved StaticSpriteBatch batching performance** - Multi-target net452, making MLEM compatible with MonoGame for consoles +- Allow retrieving the cost of a calculated path when using AStar +- **Drastically improved StaticSpriteBatch batching performance** Fixes - Fixed TokenizedString handling trailing spaces incorrectly in the last line of non-left aligned text diff --git a/MLEM/Pathfinding/AStar.cs b/MLEM/Pathfinding/AStar.cs index 11f8ade..30ff7b1 100644 --- a/MLEM/Pathfinding/AStar.cs +++ b/MLEM/Pathfinding/AStar.cs @@ -78,7 +78,7 @@ namespace MLEM.Pathfinding { this.DefaultSpecialDirections = defaultSpecialDirections; } - /// + /// public Task> FindPathAsync(T start, T goal, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null) { return Task.Run(() => this.FindPath(start, goal, costFunction, defaultCost, maxTries, allowDiagonals)); } @@ -95,8 +95,59 @@ namespace MLEM.Pathfinding { /// An optional function that determines a set of additional directions (or offsets) that should be tested for walkability. /// 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, bool? allowDiagonals = null, GetSpecialDirections specialDirections = null) { - var stopwatch = Stopwatch.StartNew(); + this.TryFindPath(start, goal, out var path, out _, costFunction, defaultCost, maxTries, allowDiagonals, specialDirections); + return path; + } + /// + public Task> FindPathAsync(T start, IEnumerable goals, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null) { + return Task.Run(() => this.FindPath(start, goals, costFunction, defaultCost, maxTries, allowDiagonals)); + } + + /// + /// Tries to find paths between a position and a set of and returns the path that had the lowest overall cost. + /// Note that this method is only faster than a one-to-many pathfinding method like Dijkstra's algorithm in situations where the amount of possible is much lower than the total amount of possible positions, or the are relatively close to each other. + /// + /// The point to start path finding at + /// The set of points to try to find a path to + /// The function that determines the cost for each path point + /// The default cost for each path point + /// The maximum amount of tries before path finding is aborted + /// If diagonals should be looked at for path finding + /// An optional function that determines a set of additional directions (or offsets) that should be tested for walkability. + /// A stack of path points, where the top item is the first point to go to, or null if no paths were found. + public Stack FindPath(T start, IEnumerable goals, GetCost costFunction = null, float? defaultCost = null, int? maxTries = null, bool? allowDiagonals = null, GetSpecialDirections specialDirections = null) { + var lowestCost = float.PositiveInfinity; + Stack cheapestPath = null; + foreach (var goal in goals) { + if (!this.TryFindPath(start, goal, out var path, out var cost, costFunction, defaultCost, maxTries, allowDiagonals, specialDirections)) + continue; + if (cost < lowestCost) { + lowestCost = cost; + cheapestPath = path; + } + } + return cheapestPath; + } + + /// + /// 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 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 + /// The default cost for each path point + /// The maximum amount of tries before path finding is aborted + /// If diagonals should be looked at for path finding + /// An optional function that determines a set of additional directions (or offsets) that should be tested for walkability. + /// 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, bool? allowDiagonals = null, GetSpecialDirections specialDirections = null) { + path = null; + totalCost = float.PositiveInfinity; + + var stopwatch = Stopwatch.StartNew(); var getCost = costFunction ?? this.DefaultCostFunction; var diags = allowDiagonals ?? this.DefaultAllowDiagonals; var tries = maxTries ?? this.DefaultMaxTries; @@ -108,7 +159,6 @@ namespace MLEM.Pathfinding { open.Add(start, new PathPoint(start, this.GetManhattanDistance(start, goal), null, 0, defCost)); var count = 0; - Stack ret = null; while (open.Count > 0) { PathPoint current = null; foreach (var point in open.Values) { @@ -122,7 +172,8 @@ namespace MLEM.Pathfinding { closed.Add(current.Pos, current); if (current.Pos.Equals(goal)) { - ret = AStar.CompilePath(current); + path = AStar.CompilePath(current); + totalCost = current.F; break; } @@ -141,7 +192,7 @@ namespace MLEM.Pathfinding { stopwatch.Stop(); this.LastTriesNeeded = count; this.LastTimeNeeded = stopwatch.Elapsed; - return ret; + return path != null; void ExamineDirection(PathPoint current, T dir) { var neighborPos = this.AddPositions(current.Pos, dir); @@ -192,7 +243,7 @@ namespace MLEM.Pathfinding { public delegate float GetCost(T currPos, T nextPos); /// - /// A delegate used by and that determines a set of additional directions (or offsets) that should be tested for walkability. + /// A delegate used by and that determines a set of additional directions (or offsets) that should be tested for walkability. /// /// The current position in the path. /// A set of additional directions (or offsets) that should be checked for walkability. If the given has no special directions, an empty should be returned. diff --git a/Tests/PathfindingTests.cs b/Tests/PathfindingTests.cs index 688d4fe..ae0f0fc 100644 --- a/Tests/PathfindingTests.cs +++ b/Tests/PathfindingTests.cs @@ -80,14 +80,42 @@ namespace Tests { p => p.X == 2 && p.Y == 2 ? new[] {new Point(-1, 2)} : Enumerable.Empty())); } + [Test] + public void TestMultiplePaths() { + var area = new[] { + "XXXXXXXX", + "X 2 X", + "XXXXX X", + "X 53 X", + "X XXX X", + "X X X X", + "XXXXXXXX" + }; + var pathfinder = PathfindingTests.CreatePathfinder(area, false); + + 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); + Assert.AreEqual(goalCosts[i], cost); + } + + var path = pathfinder.FindPath(new Point(1, 1), goals).ToArray(); + 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)}; + Assert.AreEqual(path, expected); + } + private static Stack FindPathInArea(Point start, Point end, IEnumerable area, bool allowDiagonals, AStar2.GetSpecialDirections getSpecialDirections = null) { + return PathfindingTests.CreatePathfinder(area, allowDiagonals, getSpecialDirections).FindPath(start, end); + } + + private static AStar2 CreatePathfinder(IEnumerable area, bool allowDiagonals, AStar2.GetSpecialDirections getSpecialDirections = null) { var costs = area.Select(s => s.Select(c => c switch { ' ' => 1, 'X' => float.PositiveInfinity, _ => (float) char.GetNumericValue(c) }).ToArray()).ToArray(); - var pathFinder = new AStar2((_, p2) => costs[p2.Y][p2.X], allowDiagonals, 1, 64, getSpecialDirections); - return pathFinder.FindPath(start, end); + return new AStar2((_, p2) => costs[p2.Y][p2.X], allowDiagonals, 1, 64, getSpecialDirections); } }