diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index 1eb4a58c5..f64d27c97 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -7,6 +7,7 @@ namespace BossMod.AI; sealed class AIBehaviour(AIController ctrl, Autorotation autorot) : IDisposable { private readonly AIConfig _config = Service.Config.Get(); + private readonly NavigationDecision.Context _naviCtx = new(); private NavigationDecision _naviDecision; private bool _forbidMovement; private bool _forbidActions; @@ -104,9 +105,9 @@ private NavigationDecision BuildNavigationDecision(Actor player, Actor master, r if (_forbidMovement) return new() { LeewaySeconds = float.MaxValue }; if (_followMaster) - return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, master.Position, 1, new(), Positional.Any); + return NavigationDecision.Build(_naviCtx, autorot.WorldState, autorot.Hints, player, master.Position, 1, new(), Positional.Any); if (targeting.Target == null) - return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, null, 0, new(), Positional.Any); + return NavigationDecision.Build(_naviCtx, autorot.WorldState, autorot.Hints, player, null, 0, new(), Positional.Any); var adjRange = targeting.PreferredRange + player.HitboxRadius + targeting.Target.Actor.HitboxRadius; if (targeting.PreferTanking) @@ -117,12 +118,12 @@ private NavigationDecision BuildNavigationDecision(Actor player, Actor master, r if (desiredToTarget.LengthSq() > 4 /*&& (_autorot.ClassActions?.GetState().GCD ?? 0) > 0.5f*/) { var dest = autorot.Hints.ClampToBounds(targeting.Target.DesiredPosition - adjRange * desiredToTarget.Normalized()); - return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, dest, 0.5f, new(), Positional.Any); + return NavigationDecision.Build(_naviCtx, autorot.WorldState, autorot.Hints, player, dest, 0.5f, new(), Positional.Any); } } var adjRotation = targeting.PreferTanking ? targeting.Target.DesiredRotation : targeting.Target.Actor.Rotation; - return NavigationDecision.Build(autorot.WorldState, autorot.Hints, player, targeting.Target.Actor.Position, adjRange, adjRotation, targeting.PreferredPosition); + return NavigationDecision.Build(_naviCtx, autorot.WorldState, autorot.Hints, player, targeting.Target.Actor.Position, adjRange, adjRotation, targeting.PreferredPosition); } private void FocusMaster(Actor master) diff --git a/BossMod/BossModule/AIHintsVisualizer.cs b/BossMod/BossModule/AIHintsVisualizer.cs index 908b751ac..25848a19c 100644 --- a/BossMod/BossModule/AIHintsVisualizer.cs +++ b/BossMod/BossModule/AIHintsVisualizer.cs @@ -7,6 +7,7 @@ public class AIHintsVisualizer(AIHints hints, WorldState ws, Actor player, ulong { private readonly MapVisualizer?[] _zoneVisualizers = new MapVisualizer?[hints.ForbiddenZones.Count]; private MapVisualizer? _pathfindVisualizer; + private NavigationDecision.Context _naviCtx = new(); private NavigationDecision _navi; public void Draw(UITree tree) @@ -49,7 +50,8 @@ public void Draw(UITree tree) private MapVisualizer BuildZoneVisualizer(Func shape) { - var map = hints.Bounds.PathfindMap(hints.Center); + var map = new Map(); + hints.Bounds.PathfindMap(map, hints.Center); map.BlockPixelsInside(shape, 0, NavigationDecision.DefaultForbiddenZoneCushion); return new MapVisualizer(map, 0, player.Position); } @@ -61,7 +63,8 @@ private MapVisualizer BuildPathfindingVisualizer() _navi = BuildPathfind(targeting.enemy, targeting.range, targeting.pos, targeting.tank); if (_navi.Map == null) { - _navi.Map = hints.Bounds.PathfindMap(hints.Center); + _navi.Map = new(); + hints.Bounds.PathfindMap(_navi.Map, hints.Center); var imm = NavigationDecision.ImminentExplosionTime(ws.CurrentTime); foreach (var (shape, activation) in hints.ForbiddenZones) NavigationDecision.AddBlockerZone(_navi.Map, imm, activation, shape, NavigationDecision.DefaultForbiddenZoneCushion); @@ -74,7 +77,7 @@ private MapVisualizer BuildPathfindingVisualizer() private NavigationDecision BuildPathfind(AIHints.Enemy? target, float range, Positional positional, bool preferTanking) { if (target == null) - return NavigationDecision.Build(ws, hints, player, null, 0, new(), Positional.Any); + return NavigationDecision.Build(_naviCtx, ws, hints, player, null, 0, new(), Positional.Any); var adjRange = range + player.HitboxRadius + target.Actor.HitboxRadius; if (preferTanking) @@ -85,11 +88,11 @@ private NavigationDecision BuildPathfind(AIHints.Enemy? target, float range, Pos if (desiredToTarget.LengthSq() > 4 /* && gcd check*/) { var dest = target.DesiredPosition - adjRange * desiredToTarget.Normalized(); - return NavigationDecision.Build(ws, hints, player, dest, 0.5f, new(), Positional.Any); + return NavigationDecision.Build(_naviCtx, ws, hints, player, dest, 0.5f, new(), Positional.Any); } } var adjRotation = preferTanking ? target.DesiredRotation : target.Actor.Rotation; - return NavigationDecision.Build(ws, hints, player, target.Actor.Position, adjRange, adjRotation, positional); + return NavigationDecision.Build(_naviCtx, ws, hints, player, target.Actor.Position, adjRange, adjRotation, positional); } } diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 1e989c53b..c79136fcb 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -31,7 +31,7 @@ public float ScreenHalfSize } protected abstract PolygonClipper.Operand BuildClipPoly(); - public abstract Pathfinding.Map PathfindMap(WPos center); // if implementation caches a map, it should return a clone + public abstract void PathfindMap(Pathfinding.Map map, WPos center); public abstract bool Contains(WDir offset); public abstract float IntersectRay(WDir originOffset, WDir dir); public abstract WDir ClampToBounds(WDir offset); @@ -103,7 +103,7 @@ public record class ArenaBoundsCircle(float Radius, float MapResolution = 0.5f) private Pathfinding.Map? _cachedMap; protected override PolygonClipper.Operand BuildClipPoly() => new(CurveApprox.Circle(Radius, MaxApproxError)); - public override Pathfinding.Map PathfindMap(WPos center) => (_cachedMap ??= BuildMap()).Clone(center); + public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(_cachedMap ??= BuildMap(), center); public override bool Contains(WDir offset) => offset.LengthSq() <= Radius * Radius; public override float IntersectRay(WDir originOffset, WDir dir) => Intersect.RayCircle(originOffset, dir, Radius); @@ -127,7 +127,7 @@ public record class ArenaBoundsSquare(float Radius, float MapResolution = 0.5f) public float HalfWidth => Radius; protected override PolygonClipper.Operand BuildClipPoly() => new(CurveApprox.Rect(new(Radius, 0), new(0, Radius))); - public override Pathfinding.Map PathfindMap(WPos center) => new(MapResolution, center, Radius, Radius); + public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(MapResolution, center, Radius, Radius); public override bool Contains(WDir offset) => offset.AlmostZero(Radius); public override float IntersectRay(WDir originOffset, WDir dir) => Intersect.RayAABB(originOffset, dir, Radius, Radius); @@ -147,7 +147,7 @@ public record class ArenaBoundsRect(float HalfWidth, float HalfHeight, Angle Rot public readonly WDir Orientation = Rotation.ToDirection(); protected override PolygonClipper.Operand BuildClipPoly() => new(CurveApprox.Rect(Orientation, HalfWidth, HalfHeight)); - public override Pathfinding.Map PathfindMap(WPos center) => new(MapResolution, center, HalfWidth, HalfHeight, Rotation); + public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(MapResolution, center, HalfWidth, HalfHeight, Rotation); public override bool Contains(WDir offset) => offset.InRect(Orientation, HalfHeight, HalfHeight, HalfWidth); public override float IntersectRay(WDir originOffset, WDir dir) => Intersect.RayRect(originOffset, dir, Orientation, HalfWidth, HalfHeight); @@ -169,7 +169,7 @@ public record class ArenaBoundsCustom(float Radius, RelSimplifiedComplexPolygon private Pathfinding.Map? _cachedMap; protected override PolygonClipper.Operand BuildClipPoly() => new(Poly); - public override Pathfinding.Map PathfindMap(WPos center) => (_cachedMap ??= BuildMap()).Clone(center); + public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(_cachedMap ??= BuildMap(), center); public override bool Contains(WDir offset) => Poly.Contains(offset); public override float IntersectRay(WDir originOffset, WDir dir) => Intersect.RayPolygon(originOffset, dir, Poly); public override WDir ClampToBounds(WDir offset) diff --git a/BossMod/Pathfinding/Map.cs b/BossMod/Pathfinding/Map.cs index e91e4f152..658828582 100644 --- a/BossMod/Pathfinding/Map.cs +++ b/BossMod/Pathfinding/Map.cs @@ -16,14 +16,14 @@ public struct Pixel public int Priority; // >0 if goal } - public float Resolution { get; private init; } // pixel size, in world units - public int Width { get; private init; } // always even - public int Height { get; private init; } // always even - public Pixel[] Pixels; + public float Resolution { get; private set; } // pixel size, in world units + public int Width { get; private set; } // always even + public int Height { get; private set; } // always even + public Pixel[] Pixels = []; public WPos Center { get; private set; } // position of map center in world units - public Angle Rotation { get; private init; } // rotation relative to world space (=> ToDirection() is equal to direction of local 'height' axis in world space) - private WDir LocalZDivRes { get; init; } + public Angle Rotation { get; private set; } // rotation relative to world space (=> ToDirection() is equal to direction of local 'height' axis in world space) + private WDir LocalZDivRes { get; set; } public float MaxG { get; private set; } // maximal 'maxG' value of all blocked pixels public int MaxPriority { get; private set; } // maximal 'priority' value of all goal pixels @@ -32,25 +32,45 @@ public struct Pixel public Pixel this[int x, int y] => InBounds(x, y) ? Pixels[y * Width + x] : new() { MaxG = float.MaxValue, Priority = 0 }; - public Map(float resolution, WPos center, float worldHalfWidth, float worldHalfHeight, Angle rotation = new()) + public Map() { } + public Map(float resolution, WPos center, float worldHalfWidth, float worldHalfHeight, Angle rotation = new()) => Init(resolution, center, worldHalfWidth, worldHalfHeight, rotation); + + public void Init(float resolution, WPos center, float worldHalfWidth, float worldHalfHeight, Angle rotation = new()) { Resolution = resolution; Width = 2 * (int)MathF.Ceiling(worldHalfWidth / resolution); Height = 2 * (int)MathF.Ceiling(worldHalfHeight / resolution); - Pixels = Utils.MakeArray(Width * Height, new Pixel() { MaxG = float.MaxValue, Priority = 0 }); + + var numPixels = Width * Height; + if (Pixels.Length < numPixels) + Pixels = new Pixel[numPixels]; + Array.Fill(Pixels, new Pixel { MaxG = float.MaxValue, Priority = 0 }, 0, numPixels); Center = center; Rotation = rotation; LocalZDivRes = rotation.ToDirection() / Resolution; + + MaxG = 0; + MaxPriority = 0; } - public Map Clone(WPos center) + public void Init(Map source, WPos center) { - var res = (Map)MemberwiseClone(); - res.Pixels = new Pixel[Pixels.Length]; - Array.Copy(Pixels, res.Pixels, Pixels.Length); - res.Center = center; - return res; + Resolution = source.Resolution; + Width = source.Width; + Height = source.Height; + + var numPixels = Width * Height; + if (Pixels.Length < numPixels) + Pixels = new Pixel[numPixels]; + Array.Copy(source.Pixels, Pixels, numPixels); + + Center = center; + Rotation = source.Rotation; + LocalZDivRes = source.LocalZDivRes; + + MaxG = source.MaxG; + MaxPriority = source.MaxPriority; } public Vector2 WorldToGridFrac(WPos world) diff --git a/BossMod/Pathfinding/MapVisualizer.cs b/BossMod/Pathfinding/MapVisualizer.cs index 6da7d2dd1..3ccf5008d 100644 --- a/BossMod/Pathfinding/MapVisualizer.cs +++ b/BossMod/Pathfinding/MapVisualizer.cs @@ -205,5 +205,10 @@ private void DrawPath(ImDrawListPtr dl, Vector2 tl, int startingIndex) } } - private ThetaStar BuildPathfind() => new(Map, GoalPriority, StartPos, 1.0f / 6); + private ThetaStar BuildPathfind() + { + var res = new ThetaStar(); + res.Start(Map, GoalPriority, StartPos, 1.0f / 6); + return res; + } } diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index c5896e0ed..5b768e0a7 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -8,6 +8,14 @@ // 4. be in range of healers - even less important, but still nice to do public struct NavigationDecision { + // context that allows reusing large memory allocations + public class Context + { + public Map Map = new(); + public Map Map2 = new(); + public ThetaStar ThetaStar = new(); + } + public enum Decision { None, @@ -33,7 +41,7 @@ public enum Decision public const float DefaultForbiddenZoneCushion = 0.7071068f; - public static NavigationDecision Build(WorldState ws, AIHints hints, Actor player, WPos? targetPos, float targetRadius, Angle targetRot, Positional positional, float playerSpeed = 6, float forbiddenZoneCushion = DefaultForbiddenZoneCushion) + public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints, Actor player, WPos? targetPos, float targetRadius, Angle targetRot, Positional positional, float playerSpeed = 6, float forbiddenZoneCushion = DefaultForbiddenZoneCushion) { // TODO: skip pathfinding if there are no forbidden zones, just find closest point in circle/cone... @@ -49,36 +57,36 @@ public static NavigationDecision Build(WorldState ws, AIHints hints, Actor playe // we're in forbidden zone => find path to safety (and ideally to uptime zone) // if such a path can't be found (that's always the case if we're inside imminent forbidden zone, but can also happen in other cases), try instead to find a path to safety that doesn't enter any other zones that we're not inside // first build a map with zones that we're outside of as blockers - var map = hints.Bounds.PathfindMap(hints.Center); + hints.Bounds.PathfindMap(ctx.Map, hints.Center); foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone)) if (!inside) - AddBlockerZone(map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion); + AddBlockerZone(ctx.Map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion); bool inImminentForbiddenZone = inZone.Take(numImminentZones).Any(inside => inside); if (!inImminentForbiddenZone) { - var map2 = map.Clone(map.Center); + ctx.Map2.Init(ctx.Map, ctx.Map.Center); foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone)) if (inside) - AddBlockerZone(map2, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion); - int maxGoal = targetPos != null ? AddTargetGoal(map2, targetPos.Value, targetRadius, targetRot, positional, 0) : 0; - var res = FindPathFromUnsafe(map2, player.Position, 0, maxGoal, targetPos, targetRot, positional, playerSpeed); + AddBlockerZone(ctx.Map2, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion); + int maxGoal = targetPos != null ? AddTargetGoal(ctx.Map2, targetPos.Value, targetRadius, targetRot, positional, 0) : 0; + var res = FindPathFromUnsafe(ctx.ThetaStar, ctx.Map2, player.Position, 0, maxGoal, targetPos, targetRot, positional, playerSpeed); if (res != null) return res.Value; // pathfind to any spot outside aoes we're in that doesn't enter new aoes foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone)) if (inside) - map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1); - return FindPathFromImminent(map, player.Position, playerSpeed); + ctx.Map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1); + return FindPathFromImminent(ctx.ThetaStar, ctx.Map, player.Position, playerSpeed); } else { // try to find a path out of imminent aoes that we're in, while remaining in non-imminent aoes that we're already in - it might be worth it... foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone).Take(numImminentZones)) if (inside) - map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1); - return FindPathFromImminent(map, player.Position, playerSpeed); + ctx.Map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1); + return FindPathFromImminent(ctx.ThetaStar, ctx.Map, player.Position, playerSpeed); } } @@ -88,36 +96,36 @@ public static NavigationDecision Build(WorldState ws, AIHints hints, Actor playe if (!player.Position.InCircle(targetPos.Value, targetRadius)) { // we're not in uptime zone, just run to it, avoiding any aoes - var map = hints.Bounds.PathfindMap(hints.Center); + hints.Bounds.PathfindMap(ctx.Map, hints.Center); foreach (var (shape, activation) in hints.ForbiddenZones) - AddBlockerZone(map, imminent, activation, shape, forbiddenZoneCushion); - int maxGoal = AddTargetGoal(map, targetPos.Value, targetRadius, targetRot, Positional.Any, 0); + AddBlockerZone(ctx.Map, imminent, activation, shape, forbiddenZoneCushion); + int maxGoal = AddTargetGoal(ctx.Map, targetPos.Value, targetRadius, targetRot, Positional.Any, 0); if (maxGoal != 0) { // try to find a path to target - var pathfind = new ThetaStar(map, maxGoal, player.Position, 1.0f / playerSpeed); - int res = pathfind.Execute(); + ctx.ThetaStar.Start(ctx.Map, maxGoal, player.Position, 1.0f / playerSpeed); + int res = ctx.ThetaStar.Execute(); if (res >= 0) - return new() { Destination = GetFirstWaypoint(pathfind, res), LeewaySeconds = float.MaxValue, TimeToGoal = pathfind.NodeByIndex(res).GScore, Map = map, MapGoal = maxGoal, DecisionType = Decision.SafeToUptime }; + return new() { Destination = GetFirstWaypoint(ctx.ThetaStar, res), LeewaySeconds = float.MaxValue, TimeToGoal = ctx.ThetaStar.NodeByIndex(res).GScore, Map = ctx.Map, MapGoal = maxGoal, DecisionType = Decision.SafeToUptime }; } // goal is not reachable, but we can try getting as close to the target as we can until first aoe - var start = map.ClampToGrid(map.WorldToGrid(player.Position)); - var end = map.ClampToGrid(map.WorldToGrid(targetPos.Value)); + var start = ctx.Map.ClampToGrid(ctx.Map.WorldToGrid(player.Position)); + var end = ctx.Map.ClampToGrid(ctx.Map.WorldToGrid(targetPos.Value)); var best = start; - foreach (var (x, y) in map.EnumeratePixelsInLine(start.x, start.y, end.x, end.y)) + foreach (var (x, y) in ctx.Map.EnumeratePixelsInLine(start.x, start.y, end.x, end.y)) { - if (map[x, y].MaxG != float.MaxValue) + if (ctx.Map[x, y].MaxG != float.MaxValue) break; best = (x, y); } if (best != start) { - var dest = map.GridToWorld(best.x, best.y, 0.5f, 0.5f); - return new() { Destination = dest, LeewaySeconds = float.MaxValue, TimeToGoal = (dest - player.Position).Length() / playerSpeed, Map = map, MapGoal = maxGoal, DecisionType = Decision.SafeToCloser }; + var dest = ctx.Map.GridToWorld(best.x, best.y, 0.5f, 0.5f); + return new() { Destination = dest, LeewaySeconds = float.MaxValue, TimeToGoal = (dest - player.Position).Length() / playerSpeed, Map = ctx.Map, MapGoal = maxGoal, DecisionType = Decision.SafeToCloser }; } - return new() { Destination = null, LeewaySeconds = float.MaxValue, TimeToGoal = 0, Map = map, MapGoal = maxGoal, DecisionType = Decision.SafeBlocked }; + return new() { Destination = null, LeewaySeconds = float.MaxValue, TimeToGoal = 0, Map = ctx.Map, MapGoal = maxGoal, DecisionType = Decision.SafeBlocked }; } bool inPositional = positional switch @@ -130,25 +138,25 @@ public static NavigationDecision Build(WorldState ws, AIHints hints, Actor playe if (!inPositional) { // we're in uptime zone, but not in correct quadrant - move there, avoiding all aoes and staying within uptime zone - var map = hints.Bounds.PathfindMap(hints.Center); - map.BlockPixelsInside(ShapeDistance.InvertedCircle(targetPos.Value, targetRadius), 0, 0); + hints.Bounds.PathfindMap(ctx.Map, hints.Center); + ctx.Map.BlockPixelsInside(ShapeDistance.InvertedCircle(targetPos.Value, targetRadius), 0, 0); foreach (var (shape, activation) in hints.ForbiddenZones) - AddBlockerZone(map, imminent, activation, shape, forbiddenZoneCushion); - int maxGoal = AddPositionalGoal(map, targetPos.Value, targetRadius, targetRot, positional, 0); + AddBlockerZone(ctx.Map, imminent, activation, shape, forbiddenZoneCushion); + int maxGoal = AddPositionalGoal(ctx.Map, targetPos.Value, targetRadius, targetRot, positional, 0); if (maxGoal > 0) { // try to find a path to quadrant - var pathfind = new ThetaStar(map, maxGoal, player.Position, 1.0f / playerSpeed); - int res = pathfind.Execute(); + ctx.ThetaStar.Start(ctx.Map, maxGoal, player.Position, 1.0f / playerSpeed); + int res = ctx.ThetaStar.Execute(); if (res >= 0) { - var dest = IncreaseDestinationPrecision(GetFirstWaypoint(pathfind, res), targetPos, targetRot, positional); - return new() { Destination = dest, LeewaySeconds = float.MaxValue, TimeToGoal = pathfind.NodeByIndex(res).GScore, Map = map, MapGoal = maxGoal, DecisionType = Decision.UptimeToPositional }; + var dest = IncreaseDestinationPrecision(GetFirstWaypoint(ctx.ThetaStar, res), targetPos, targetRot, positional); + return new() { Destination = dest, LeewaySeconds = float.MaxValue, TimeToGoal = ctx.ThetaStar.NodeByIndex(res).GScore, Map = ctx.Map, MapGoal = maxGoal, DecisionType = Decision.UptimeToPositional }; } } // fail - return new() { Destination = null, LeewaySeconds = float.MaxValue, TimeToGoal = 0, Map = map, MapGoal = maxGoal, DecisionType = Decision.UptimeBlocked }; + return new() { Destination = null, LeewaySeconds = float.MaxValue, TimeToGoal = 0, Map = ctx.Map, MapGoal = maxGoal, DecisionType = Decision.UptimeBlocked }; } } @@ -241,12 +249,12 @@ public static int AddPositionalGoal(Map map, WPos targetPos, float targetRadius, return adjPrio; } - public static NavigationDecision? FindPathFromUnsafe(Map map, WPos startPos, int safeGoal, int maxGoal, WPos? targetPos, Angle targetRot, Positional positional, float speed = 6) + public static NavigationDecision? FindPathFromUnsafe(ThetaStar pathfind, Map map, WPos startPos, int safeGoal, int maxGoal, WPos? targetPos, Angle targetRot, Positional positional, float speed = 6) { if (maxGoal - safeGoal == 2) { // try finding path to flanking position - var pathfind = new ThetaStar(map, maxGoal, startPos, 1.0f / speed); + pathfind.Start(map, maxGoal, startPos, 1.0f / speed); int res = pathfind.Execute(); if (res >= 0) { @@ -259,7 +267,7 @@ public static int AddPositionalGoal(Map map, WPos targetPos, float targetRadius, if (maxGoal - safeGoal == 1) { // try finding path to uptime position - var pathfind = new ThetaStar(map, maxGoal, startPos, 1.0f / speed); + pathfind.Start(map, maxGoal, startPos, 1.0f / speed); int res = pathfind.Execute(); if (res >= 0) return new() { Destination = GetFirstWaypoint(pathfind, res), LeewaySeconds = pathfind.NodeByIndex(res).PathLeeway, TimeToGoal = pathfind.NodeByIndex(res).GScore, Map = map, MapGoal = maxGoal, DecisionType = Decision.UnsafeToUptime }; @@ -269,7 +277,7 @@ public static int AddPositionalGoal(Map map, WPos targetPos, float targetRadius, if (maxGoal - safeGoal == 0) { // try finding path to any safe spot - var pathfind = new ThetaStar(map, maxGoal, startPos, 1.0f / speed); + pathfind.Start(map, maxGoal, startPos, 1.0f / speed); int res = pathfind.Execute(); if (res >= 0) return new() { Destination = GetFirstWaypoint(pathfind, res), LeewaySeconds = pathfind.NodeByIndex(res).PathLeeway, TimeToGoal = pathfind.NodeByIndex(res).GScore, Map = map, MapGoal = maxGoal, DecisionType = Decision.UnsafeToSafe }; @@ -278,9 +286,9 @@ public static int AddPositionalGoal(Map map, WPos targetPos, float targetRadius, return null; } - public static NavigationDecision FindPathFromImminent(Map map, WPos startPos, float speed = 6) + public static NavigationDecision FindPathFromImminent(ThetaStar pathfind, Map map, WPos startPos, float speed = 6) { - var pathfind = new ThetaStar(map, 0, startPos, 1.0f / speed); + pathfind.Start(map, 0, startPos, 1.0f / speed); int res = pathfind.Execute(); if (res >= 0) { diff --git a/BossMod/Pathfinding/ThetaStar.cs b/BossMod/Pathfinding/ThetaStar.cs index 09b396697..cf22165cb 100644 --- a/BossMod/Pathfinding/ThetaStar.cs +++ b/BossMod/Pathfinding/ThetaStar.cs @@ -12,23 +12,28 @@ public struct Node public float PathLeeway; } - private readonly Map _map; - private readonly (int x, int y)[] _goals; - private readonly Node[] _nodes; + private Map _map = new(); + private readonly List<(int x, int y)> _goals = []; + private Node[] _nodes = []; private readonly List _openList = []; - private readonly float _deltaGSide; - private readonly float _deltaGDiag; + private float _deltaGSide; + private float _deltaGDiag; public ref Node NodeByIndex(int index) => ref _nodes[index]; public int CellIndex(int x, int y) => y * _map.Width + x; public WPos CellCenter(int index) => _map.GridToWorld(index % _map.Width, index / _map.Width, 0.5f, 0.5f); // gMultiplier is typically inverse speed, which turns g-values into time - public ThetaStar(Map map, IEnumerable<(int x, int y)> goals, (int x, int y) start, float gMultiplier) + public void Start(Map map, IEnumerable<(int x, int y)> goals, (int x, int y) start, float gMultiplier) { _map = map; - _goals = goals.ToArray(); - _nodes = new Node[map.Width * map.Height]; + _goals.Clear(); + _goals.AddRange(goals); + var numPixels = map.Width * map.Height; + if (_nodes.Length < numPixels) + _nodes = new Node[numPixels]; + Array.Fill(_nodes, default, 0, numPixels); + _openList.Clear(); _deltaGSide = map.Resolution * gMultiplier; _deltaGDiag = _deltaGSide * 1.414214f; @@ -42,15 +47,12 @@ public ThetaStar(Map map, IEnumerable<(int x, int y)> goals, (int x, int y) star AddToOpen(startIndex); } - public ThetaStar(Map map, int goalPriority, WPos startPos, float gMultiplier) - : this(map, map.Goals().Where(g => g.priority >= goalPriority).Select(g => (g.x, g.y)), map.WorldToGrid(startPos), gMultiplier) - { - } + public void Start(Map map, int goalPriority, WPos startPos, float gMultiplier) => Start(map, map.Goals().Where(g => g.priority >= goalPriority).Select(g => (g.x, g.y)), map.WorldToGrid(startPos), gMultiplier); // returns whether search is to be terminated; on success, first node of the open list would contain found goal public bool ExecuteStep() { - if (_goals.Length == 0 || _openList.Count == 0 || _nodes[_openList[0]].HScore <= 0) + if (_goals.Count == 0 || _openList.Count == 0 || _nodes[_openList[0]].HScore <= 0) return false; int nextNodeIndex = PopMinOpen();