From 88285c79ccfeb92427b21e3d0ed0510f48fc7e09 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 4 Nov 2023 14:00:17 -0400 Subject: [PATCH 01/35] WIP hierarchical pathfinding --- core/src/mindustry/Vars.java | 2 + core/src/mindustry/ai/ControlPathfinder.java | 1 + .../src/mindustry/ai/HierarchyPathFinder.java | 223 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 core/src/mindustry/ai/HierarchyPathFinder.java diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 0dc1cf9e3f..1dc36f8855 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -240,6 +240,7 @@ public class Vars implements Loadable{ public static WaveSpawner spawner; public static BlockIndexer indexer; public static Pathfinder pathfinder; + public static HierarchyPathFinder hpath; public static ControlPathfinder controlPath; public static FogControl fogControl; @@ -312,6 +313,7 @@ public class Vars implements Loadable{ spawner = new WaveSpawner(); indexer = new BlockIndexer(); pathfinder = new Pathfinder(); + hpath = new HierarchyPathFinder(); controlPath = new ControlPathfinder(); fogControl = new FogControl(); bases = new BaseRegistry(); diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index cabb297270..98033a4da2 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -17,6 +17,7 @@ import mindustry.world.*; import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; +//TODO remove/replace public class ControlPathfinder{ //TODO this FPS-based update system could be flawed. private static final long maxUpdate = Time.millisToNanos(30); diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java new file mode 100644 index 0000000000..6a235cfb44 --- /dev/null +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -0,0 +1,223 @@ +package mindustry.ai; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import mindustry.game.*; +import mindustry.game.EventType.*; +import mindustry.graphics.*; + +import static mindustry.Vars.*; +import static mindustry.ai.Pathfinder.*; + +public class HierarchyPathFinder{ + static final boolean debug = true; + + static final int[] offsets = { + 1, 0, //right: bottom to top + 0, 1, //top: left to right + 0, 0, //left: bottom to top + 0, 0 //bottom: left to right + }; + + static final int[] moveDirs = { + 0, 1, + 1, 0, + 0, 1, + 1, 0 + }; + + static final int[] nextOffsets = { + 1, 0, + 0, 1, + -1, 0, + 0, -1 + }; + + //maps pathCost -> flattened array of clusters in 2D + Cluster[][] clusters; + int clusterSize = 12; + + int cwidth, cheight; + + public HierarchyPathFinder(){ + + Events.on(WorldLoadEvent.class, event -> { + //TODO 5 path costs, arbitrary number + clusters = new Cluster[5][]; + clusterSize = 12; //TODO arbitrary + cwidth = Mathf.ceil((float)world.width() / clusterSize); + cheight = Mathf.ceil((float)world.height() / clusterSize); + + for(int cx = 0; cx < cwidth; cx++){ + for(int cy = 0; cy < cheight; cy++){ + createCluster(Team.sharded.id, costGround, cx, cy); + } + } + }); + + //TODO very inefficient, this is only for debugging + Events.on(TileChangeEvent.class, e -> { + createCluster(Team.sharded.id, costGround, e.tile.x / clusterSize, e.tile.y / clusterSize); + }); + + if(debug){ + Events.run(Trigger.draw, () -> { + int team = Team.sharded.id; + int cost = costGround; + + if(clusters == null || clusters[cost] == null) return; + + Draw.draw(Layer.overlayUI, () -> { + Lines.stroke(1f); + for(int cx = 0; cx < cwidth; cx++){ + for(int cy = 0; cy < cheight; cy++){ + var cluster = clusters[cost][cy * cwidth + cx]; + if(cluster != null){ + Draw.color(Color.green); + + Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); + Draw.color(Color.blue); + + for(int d = 0; d < 4; d++){ + IntSeq portals = cluster.portals[d]; + if(portals != null){ + int addX = moveDirs[d * 2], addY = moveDirs[d * 2 + 1]; + + for(int i = 0; i < portals.size; i++){ + int pos = portals.items[i]; + int from = Point2.x(pos), to = Point2.y(pos); + float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; + + float average = (from + to) / 2f; + + float + x = (addX * average + cx * clusterSize + offsets[d * 2] * (clusterSize - 1) + nextOffsets[d * 2] / 2f) * tilesize, + y = (addY * average + cy * clusterSize + offsets[d * 2 + 1] * (clusterSize - 1) + nextOffsets[d * 2 + 1]/2f) * tilesize; + + Lines.ellipse(30, x, y, width / 2f, height / 2f, d * 90f - 90f); + } + } + } + } + } + } + Draw.reset(); + }); + }); + } + } + + void createCluster(int team, int pathCost, int cx, int cy){ + if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight]; + Cluster cluster = clusters[pathCost][cy * cwidth + cx]; + if(cluster == null){ + cluster = clusters[pathCost][cy * cwidth + cx] = new Cluster(); + }else{ + //reset data + for(var p : cluster.portals){ + p.clear(); + } + cluster.innerEdges.clear(); + } + + //TODO look it up based on number. + PathCost cost = ControlPathfinder.costGround; + + for(int direction = 0; direction < 4; direction++){ + int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction); + //out of bounds, no portals in this direction + if(otherX < 0 || otherY < 0 || otherX >= cwidth || otherY >= cheight){ + continue; + } + + Cluster other = clusters[pathCost][otherX + otherY * cwidth]; + IntSeq portals; + + if(other == null){ + //create new portals at direction + portals = cluster.portals[direction] = new IntSeq(); + }else{ + //share portals with the other cluster + portals = cluster.portals[direction] = other.portals[(direction + 2) % 4]; + } + + //Point2 adder = Geometry.d4[(direction + 1) % 4]; + + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + int + baseX = cx * clusterSize + offsets[direction * 2] * (clusterSize - 1), + baseY = cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1), + nextBaseX = baseX + Geometry.d4[direction].x, + nextBaseY = baseY + Geometry.d4[direction].y; + + int lastPortal = -1; + boolean prevSolid = true; + + for(int i = 0; i < clusterSize; i++){ + int x = baseX + addX * i, y = baseY + addY * i; + + //scan for portals + if(solid(team, cost, x, y) || solid(team, cost, nextBaseX + addX * i, nextBaseY + addY * i)){ + int previous = i - 1; + //hit a wall, create portals between the two points + if(!prevSolid && previous >= lastPortal){ + //portals are an inclusive range + portals.add(Point2.pack(previous, lastPortal)); + } + prevSolid = true; + }else{ + //empty area encountered, mark the location of portal start + if(prevSolid){ + lastPortal = i; + } + prevSolid = false; + } + } + + //at the end of the loop, close any un-initialized portals; this is copy pasted code + int previous = clusterSize - 1; + if(!prevSolid && previous >= lastPortal){ + //portals are an inclusive range + portals.add(Point2.pack(previous, lastPortal)); + } + } + } + + Cluster cluster(int pathCost, int cx, int cy){ + return clusters[pathCost][cx + cwidth * cy]; + } + + private static boolean solid(int team, PathCost type, int x, int y){ + return x < 0 || y < 0 || x >= wwidth || y >= wheight || solid(team, type, x + y * wwidth, true); + } + + private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){ + int cost = cost(team, type, tilePos); + return cost == impassable || (checkWall && cost >= 6000); + } + + private static int cost(int team, PathCost cost, int tilePos){ + if(state.rules.limitMapArea && !Team.get(team).isAI()){ + int x = tilePos % wwidth, y = tilePos / wwidth; + if(x < state.rules.limitX || y < state.rules.limitY || x > state.rules.limitX + state.rules.limitWidth || y > state.rules.limitY + state.rules.limitHeight){ + return impassable; + } + } + return cost.getCost(team, pathfinder.tiles[tilePos]); + } + + static class Cluster{ + IntSeq[] portals = new IntSeq[4]; + IntSeq innerEdges = new IntSeq(); + + Cluster(){ + + } + + + } +} From 70538e29853439baa26caeac6eae04f6e789c22f Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 4 Nov 2023 17:18:53 -0400 Subject: [PATCH 02/35] suffering --- core/src/mindustry/Vars.java | 2 +- .../src/mindustry/ai/HierarchyPathFinder.java | 188 +++++++++++++++++- 2 files changed, 181 insertions(+), 9 deletions(-) diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 1dc36f8855..755a9d63bb 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -313,8 +313,8 @@ public class Vars implements Loadable{ spawner = new WaveSpawner(); indexer = new BlockIndexer(); pathfinder = new Pathfinder(); - hpath = new HierarchyPathFinder(); controlPath = new ControlPathfinder(); + hpath = new HierarchyPathFinder(); fogControl = new FogControl(); bases = new BaseRegistry(); logicVars = new GlobalVars(); diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 6a235cfb44..dcc1d2f762 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -6,13 +6,17 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; -import mindustry.game.*; +import arc.util.*; +import mindustry.content.*; import mindustry.game.EventType.*; +import mindustry.game.*; import mindustry.graphics.*; import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; +//https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf +//https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf public class HierarchyPathFinder{ static final boolean debug = true; @@ -80,7 +84,7 @@ public class HierarchyPathFinder{ Draw.color(Color.green); Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); - Draw.color(Color.blue); + Draw.color(Color.red); for(int d = 0; d < 4; d++){ IntSeq portals = cluster.portals[d]; @@ -102,6 +106,16 @@ public class HierarchyPathFinder{ } } } + + Draw.color(Color.magenta); + for(var con : cluster.cons){ + float + x1 = Point2.x(con.posFrom) * tilesize, y1 = Point2.y(con.posFrom) * tilesize, + x2 = Point2.x(con.posTo) * tilesize, y2 = Point2.y(con.posTo) * tilesize, + mx = (cx * clusterSize + clusterSize/2f) * tilesize, my = (cy * clusterSize + clusterSize/2f) * tilesize; + //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); + Lines.line(x1, y1, x2, y2); + } } } } @@ -124,6 +138,8 @@ public class HierarchyPathFinder{ cluster.innerEdges.clear(); } + //TODO: other cluster inner edges should be recomputed if changed. + //TODO look it up based on number. PathCost cost = ControlPathfinder.costGround; @@ -139,14 +155,12 @@ public class HierarchyPathFinder{ if(other == null){ //create new portals at direction - portals = cluster.portals[direction] = new IntSeq(); + portals = cluster.portals[direction] = new IntSeq(4); }else{ //share portals with the other cluster portals = cluster.portals[direction] = other.portals[(direction + 2) % 4]; } - //Point2 adder = Geometry.d4[(direction + 1) % 4]; - int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; int baseX = cx * clusterSize + offsets[direction * 2] * (clusterSize - 1), @@ -185,6 +199,157 @@ public class HierarchyPathFinder{ portals.add(Point2.pack(previous, lastPortal)); } } + + connectInnerEdges(cx, cy, team, cost, cluster); + } + + static PathfindQueue frontier = new PathfindQueue(); + //node index -> total cost + static IntFloatMap costs = new IntFloatMap(); + + static IntSet usedEdges = new IntSet(); + + void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){ + int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); + + usedEdges.clear(); + cluster.cons.clear(); + + //TODO: how the hell to identify a vertex? + //cluster (i16) | direction (i2) | index (i14) + + for(int direction = 0; direction < 4; direction++){ + var portals = cluster.portals[direction]; + if(portals == null) continue; + + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + + for(int i = 0; i < portals.size; i++){ + usedEdges.add(Point2.pack(direction, i)); + + int + portal = portals.items[i], + from = Point2.x(portal), to = Point2.y(portal), + average = (from + to) / 2, + x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1)), + y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1)); + + for(int otherDir = 0; otherDir < 4; otherDir++){ + var otherPortals = cluster.portals[otherDir]; + + for(int j = 0; j < otherPortals.size; j++){ + + //TODO redundant calculations? + if(!usedEdges.contains(Point2.pack(otherDir, j))){ + + int + other = otherPortals.items[j], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + otherAverage = (otherFrom + otherTo) / 2, + ox = cx * clusterSize + offsets[otherDir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[otherDir * 2 + 1] * (clusterSize - 1), + otherX = (moveDirs[otherDir * 2] * otherAverage + ox), + otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy); + + //HOW + if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){ + if(true) continue; + + Log.infoList("self ", direction, " ", i, " | ", otherDir, " ", j); + System.exit(1); + } + + float connectionCost = astar( + team, cost, + minX, minY, maxX, maxY, + x + y * wwidth, + otherX + otherY * wwidth, + + (moveDirs[otherDir * 2] * otherFrom + ox), + (moveDirs[otherDir * 2 + 1] * otherFrom + oy), + (moveDirs[otherDir * 2] * otherTo + ox), + (moveDirs[otherDir * 2 + 1] * otherTo + oy) + + ); + + if(connectionCost != -1f){ + cluster.cons.add(new Con(Point2.pack(x, y), Point2.pack(otherX, otherY), connectionCost)); + + Fx.debugLine.at(x* tilesize, y * tilesize, 0f, Color.purple, + new Vec2[]{new Vec2(x, y).scl(tilesize), new Vec2(otherX, otherY).scl(tilesize)}); + } + } + } + } + } + } + } + + //distance heuristic: manhattan + private static float heuristic(int a, int b){ + int x = a % wwidth, x2 = b % wwidth, y = a / wwidth, y2 = b / wwidth; + return Math.abs(x - x2) + Math.abs(y - y2); + } + + private static int tcost(int team, PathCost cost, int tilePos){ + return cost.getCost(team, pathfinder.tiles[tilePos]); + } + + private static float tileCost(int team, PathCost type, int a, int b){ + //currently flat cost + return cost(team, type, b); + } + + /** @return -1 if no path was found */ + float astar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ + frontier.clear(); + costs.clear(); + + costs.put(startPos, 0); + frontier.add(startPos, 0); + + if(debug && false){ + Fx.debugLine.at(Point2.x(startPos) * tilesize, Point2.y(startPos) * tilesize, 0f, Color.purple, + new Vec2[]{new Vec2(Point2.x(startPos), Point2.y(startPos)).scl(tilesize), new Vec2(Point2.x(goalPos), Point2.y(goalPos)).scl(tilesize)}); + } + + while(frontier.size > 0){ + int current = frontier.poll(); + + int cx = current % wwidth, cy = current / wwidth; + + //found the goal (it's in the portal rectangle) + //TODO portal rectangle approach does not work. + if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){ + return costs.get(current); + } + + for(Point2 point : Geometry.d4){ + int newx = cx + point.x, newy = cy + point.y; + int next = newx + wwidth * newy; + + if(newx > maxX || newy > maxY || newx < minX || newy < minY) continue; + + //TODO fallback mode for enemy walls or whatever + if(tcost(team, cost, next) == impassable) continue; + + float add = tileCost(team, cost, current, next); + float currentCost = costs.get(current); + + if(add < 0) continue; + + float newCost = currentCost + add; + + //a cost of 0 means "not set" + if(!costs.containsKey(next) || newCost < costs.get(next)){ + costs.put(next, newCost); + float priority = newCost + heuristic(next, goalPos); + frontier.add(next, priority); + } + } + } + + return -1f; } Cluster cluster(int pathCost, int cx, int cy){ @@ -213,11 +378,18 @@ public class HierarchyPathFinder{ static class Cluster{ IntSeq[] portals = new IntSeq[4]; IntSeq innerEdges = new IntSeq(); + Seq cons = new Seq<>(); + } - Cluster(){ + //TODO for debugging only + static class Con{ + int posFrom, posTo; + float cost; + public Con(int posFrom, int posTo, float cost){ + this.posFrom = posFrom; + this.posTo = posTo; + this.cost = cost; } - - } } From 2bcf5bf6843773811431f471d2ecf090620891f3 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 4 Nov 2023 17:37:29 -0400 Subject: [PATCH 03/35] Better connection storage --- .../src/mindustry/ai/HierarchyPathFinder.java | 110 ++++++++++++------ gradle.properties | 2 +- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index dcc1d2f762..02eb26df09 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -7,9 +7,11 @@ import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; +import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.EventType.*; import mindustry.game.*; +import mindustry.gen.*; import mindustry.graphics.*; import static mindustry.Vars.*; @@ -56,8 +58,8 @@ public class HierarchyPathFinder{ cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); - for(int cx = 0; cx < cwidth; cx++){ - for(int cy = 0; cy < cheight; cy++){ + for(int cy = 0; cy < cwidth; cy++){ + for(int cx = 0; cx < cheight; cx++){ createCluster(Team.sharded.id, costGround, cx, cy); } } @@ -84,29 +86,47 @@ public class HierarchyPathFinder{ Draw.color(Color.green); Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); - Draw.color(Color.red); + for(int d = 0; d < 4; d++){ IntSeq portals = cluster.portals[d]; if(portals != null){ - int addX = moveDirs[d * 2], addY = moveDirs[d * 2 + 1]; for(int i = 0; i < portals.size; i++){ int pos = portals.items[i]; int from = Point2.x(pos), to = Point2.y(pos); float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; - float average = (from + to) / 2f; + portalToVec(cluster, cx, cy, d, i, Tmp.v1); - float - x = (addX * average + cx * clusterSize + offsets[d * 2] * (clusterSize - 1) + nextOffsets[d * 2] / 2f) * tilesize, - y = (addY * average + cy * clusterSize + offsets[d * 2 + 1] * (clusterSize - 1) + nextOffsets[d * 2 + 1]/2f) * tilesize; + Draw.color(Color.red); + Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); - Lines.ellipse(30, x, y, width / 2f, height / 2f, d * 90f - 90f); + LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; + + if(connections != null){ + Draw.color(Color.magenta); + for(int coni = 0; coni < connections.size; coni ++){ + long con = connections.items[coni]; + + portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); + + float + x1 = Tmp.v1.x, y1 = Tmp.v1.y, + x2 = Tmp.v2.x, y2 = Tmp.v2.y, + mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize; + //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); + Lines.line(x1, y1, x2, y2); + + } + } } } } + //TODO draw connections. + + /* Draw.color(Color.magenta); for(var con : cluster.cons){ float @@ -115,7 +135,7 @@ public class HierarchyPathFinder{ mx = (cx * clusterSize + clusterSize/2f) * tilesize, my = (cy * clusterSize + clusterSize/2f) * tilesize; //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); Lines.line(x1, y1, x2, y2); - } + }*/ } } } @@ -125,6 +145,20 @@ public class HierarchyPathFinder{ } } + void portalToVec(Cluster cluster, int cx, int cy, int d, int i, Vec2 out){ + int pos = cluster.portals[d].items[i]; + int from = Point2.x(pos), to = Point2.y(pos); + int addX = moveDirs[d * 2], addY = moveDirs[d * 2 + 1]; + float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; + float average = (from + to) / 2f; + + float + x = (addX * average + cx * clusterSize + offsets[d * 2] * (clusterSize - 1) + nextOffsets[d * 2] / 2f) * tilesize, + y = (addY * average + cy * clusterSize + offsets[d * 2 + 1] * (clusterSize - 1) + nextOffsets[d * 2 + 1] / 2f) * tilesize; + + out.set(x, y); + } + void createCluster(int team, int pathCost, int cx, int cy){ if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight]; Cluster cluster = clusters[pathCost][cy * cwidth + cx]; @@ -135,9 +169,11 @@ public class HierarchyPathFinder{ for(var p : cluster.portals){ p.clear(); } - cluster.innerEdges.clear(); } + //clear all connections, since portals changed, they need to be recomputed. + cluster.portalConnections = new LongSeq[4][]; + //TODO: other cluster inner edges should be recomputed if changed. //TODO look it up based on number. @@ -159,6 +195,9 @@ public class HierarchyPathFinder{ }else{ //share portals with the other cluster portals = cluster.portals[direction] = other.portals[(direction + 2) % 4]; + + //clear the portals, they're being recalculated now + portals.clear(); } int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; @@ -206,18 +245,19 @@ public class HierarchyPathFinder{ static PathfindQueue frontier = new PathfindQueue(); //node index -> total cost static IntFloatMap costs = new IntFloatMap(); - + // static IntSet usedEdges = new IntSet(); void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){ int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); usedEdges.clear(); - cluster.cons.clear(); //TODO: how the hell to identify a vertex? //cluster (i16) | direction (i2) | index (i14) + //TODO: clear portal connections. also share them? + for(int direction = 0; direction < 4; direction++){ var portals = cluster.portals[direction]; if(portals == null) continue; @@ -236,6 +276,7 @@ public class HierarchyPathFinder{ for(int otherDir = 0; otherDir < 4; otherDir++){ var otherPortals = cluster.portals[otherDir]; + if(otherPortals == null) continue; for(int j = 0; j < otherPortals.size; j++){ @@ -251,15 +292,12 @@ public class HierarchyPathFinder{ otherX = (moveDirs[otherDir * 2] * otherAverage + ox), otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy); - //HOW + //HOW (redundant nodes?) if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){ - if(true) continue; - - Log.infoList("self ", direction, " ", i, " | ", otherDir, " ", j); - System.exit(1); + continue; } - float connectionCost = astar( + float connectionCost = innerAstar( team, cost, minX, minY, maxX, maxY, x + y * wwidth, @@ -273,10 +311,16 @@ public class HierarchyPathFinder{ ); if(connectionCost != -1f){ - cluster.cons.add(new Con(Point2.pack(x, y), Point2.pack(otherX, otherY), connectionCost)); + if(cluster.portalConnections[direction] == null) cluster.portalConnections[direction] = new LongSeq[cluster.portals[direction].size]; + if(cluster.portalConnections[otherDir] == null) cluster.portalConnections[otherDir] = new LongSeq[cluster.portals[otherDir].size]; + if(cluster.portalConnections[direction][i] == null) cluster.portalConnections[direction][i] = new LongSeq(8); + if(cluster.portalConnections[otherDir][j] == null) cluster.portalConnections[otherDir][j] = new LongSeq(8); - Fx.debugLine.at(x* tilesize, y * tilesize, 0f, Color.purple, - new Vec2[]{new Vec2(x, y).scl(tilesize), new Vec2(otherX, otherY).scl(tilesize)}); + //TODO: can there be duplicate edges?? + cluster.portalConnections[direction][i].add(IntraEdge.get(otherDir, j, connectionCost)); + cluster.portalConnections[otherDir][j].add(IntraEdge.get(direction, i, connectionCost)); + + //Fx.debugLine.at(x* tilesize, y * tilesize, 0f, Color.purple, new Vec2[]{new Vec2(x, y).scl(tilesize), new Vec2(otherX, otherY).scl(tilesize)}); } } } @@ -301,7 +345,7 @@ public class HierarchyPathFinder{ } /** @return -1 if no path was found */ - float astar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ + float innerAstar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ frontier.clear(); costs.clear(); @@ -377,19 +421,17 @@ public class HierarchyPathFinder{ static class Cluster{ IntSeq[] portals = new IntSeq[4]; - IntSeq innerEdges = new IntSeq(); - Seq cons = new Seq<>(); + //maps rotation + index of portal to list of IntraEdge objects + LongSeq[][] portalConnections = new LongSeq[4][]; } - //TODO for debugging only - static class Con{ - int posFrom, posTo; - float cost; + @Struct + static class IntraEdgeStruct{ + @StructField(8) + int dir; + @StructField(8) + int portal; - public Con(int posFrom, int posTo, float cost){ - this.posFrom = posFrom; - this.posTo = posTo; - this.cost = cost; - } + float cost; } } diff --git a/gradle.properties b/gradle.properties index a54f280cb0..f9fa0851e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,4 @@ org.gradle.caching=true #used for slow jitpack builds; TODO see if this actually works org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 -archash=1906938dea +archash=96f4f4214a From cfb0ea1c8cd7dc8f5d564a7a498291760a71eaf7 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 6 Nov 2023 19:21:29 -0500 Subject: [PATCH 04/35] Cluster A* progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 171 ++++++++++++++++-- 1 file changed, 151 insertions(+), 20 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 02eb26df09..908bcb5de8 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -49,6 +49,17 @@ public class HierarchyPathFinder{ int cwidth, cheight; + static PathfindQueue frontier = new PathfindQueue(); + //node index -> total cost + static IntFloatMap costs = new IntFloatMap(); + // + static IntSet usedEdges = new IntSet(); + + static LongSeq tmpEdges = new LongSeq(); + + //node index (NodeIndex struct) -> node it came from + static IntIntMap cameFrom = new IntIntMap(); + public HierarchyPathFinder(){ Events.on(WorldLoadEvent.class, event -> { @@ -149,7 +160,6 @@ public class HierarchyPathFinder{ int pos = cluster.portals[d].items[i]; int from = Point2.x(pos), to = Point2.y(pos); int addX = moveDirs[d * 2], addY = moveDirs[d * 2 + 1]; - float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; float average = (from + to) / 2f; float @@ -242,12 +252,6 @@ public class HierarchyPathFinder{ connectInnerEdges(cx, cy, team, cost, cluster); } - static PathfindQueue frontier = new PathfindQueue(); - //node index -> total cost - static IntFloatMap costs = new IntFloatMap(); - // - static IntSet usedEdges = new IntSet(); - void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){ int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); @@ -298,16 +302,14 @@ public class HierarchyPathFinder{ } float connectionCost = innerAstar( - team, cost, - minX, minY, maxX, maxY, - x + y * wwidth, - otherX + otherY * wwidth, - - (moveDirs[otherDir * 2] * otherFrom + ox), - (moveDirs[otherDir * 2 + 1] * otherFrom + oy), - (moveDirs[otherDir * 2] * otherTo + ox), - (moveDirs[otherDir * 2 + 1] * otherTo + oy) - + team, cost, + minX, minY, maxX, maxY, + x + y * wwidth, + otherX + otherY * wwidth, + (moveDirs[otherDir * 2] * otherFrom + ox), + (moveDirs[otherDir * 2 + 1] * otherFrom + oy), + (moveDirs[otherDir * 2] * otherTo + ox), + (moveDirs[otherDir * 2 + 1] * otherTo + oy) ); if(connectionCost != -1f){ @@ -378,17 +380,18 @@ public class HierarchyPathFinder{ if(tcost(team, cost, next) == impassable) continue; float add = tileCost(team, cost, current, next); - float currentCost = costs.get(current); if(add < 0) continue; - float newCost = currentCost + add; + float newCost = costs.get(current) + add; //a cost of 0 means "not set" - if(!costs.containsKey(next) || newCost < costs.get(next)){ + if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ costs.put(next, newCost); float priority = newCost + heuristic(next, goalPos); frontier.add(next, priority); + + //cameFrom.put(next, current); } } } @@ -396,6 +399,124 @@ public class HierarchyPathFinder{ return -1f; } + int makeNodeIndex(int cx, int cy, int dir, int portal){ + //to make sure there's only one way to refer to each node, the direction must be 0 or 1 (referring to portals on the top or right edge) + + //direction can only be 2 if cluster X is 0 (left edge of map) + if(dir == 2 && cx != 0){ + dir = 0; + cx --; + } + + //direction can only be 3 if cluster Y is 0 (bottom edge of map) + if(dir == 3 && cy != 0){ + dir = 1; + cy --; + } + return NodeIndex.get(cx + cy * cwidth, dir, portal); + } + + //distance heuristic: manhattan + private float clusterNodeHeuristic(int pathCost, int nodeA, int nodeB){ + int + clusterA = NodeIndex.cluster(nodeA), + dirA = NodeIndex.dir(nodeA), + portalA = NodeIndex.portal(nodeA), + clusterB = NodeIndex.cluster(nodeB), + dirB = NodeIndex.dir(nodeB), + portalB = NodeIndex.portal(nodeB); + + int rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA]; + int rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB]; + + float + averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f, + x1 = (moveDirs[dirA * 2] * averageA + (clusterA % cwidth) * clusterSize + offsets[dirA * 2] * (clusterSize - 1) + nextOffsets[dirA * 2] / 2f), + y1 = (moveDirs[dirA * 2 + 1] * averageA + (clusterA / cwidth) * clusterSize + offsets[dirA * 2 + 1] * (clusterSize - 1) + nextOffsets[dirA * 2 + 1] / 2f), + + averageB = (Point2.x(rangeB) + Point2.y(rangeB)) / 2f, + x2 = (moveDirs[dirB * 2] * averageB + (clusterB % cwidth) * clusterSize + offsets[dirB * 2] * (clusterSize - 1) + nextOffsets[dirB * 2] / 2f), + y2 = (moveDirs[dirB * 2 + 1] * averageB + (clusterB / cwidth) * clusterSize + offsets[dirB * 2 + 1] * (clusterSize - 1) + nextOffsets[dirB * 2 + 1] / 2f); + + return Math.abs(x1 - x2) + Math.abs(y1 - y2); + } + + @Nullable IntSeq clusterAstar(int pathCost, int startNodeIndex, int endNodeIndex){ + frontier.clear(); + costs.clear(); + + costs.put(startNodeIndex, 0); + frontier.add(endNodeIndex, 0); + cameFrom.clear(); + + boolean foundEnd = false; + + while(frontier.size > 0){ + int current = frontier.poll(); + + if(current == endNodeIndex){ + foundEnd = true; + break; + } + + //tmpEdges holds intra edges + tmpEdges.clear(); + + int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current); + int cx = cluster % wwidth, cy = cluster / wwidth; + Cluster clust = clusters[pathCost][cluster]; + LongSeq innerCons = clust.portalConnections[dir][portal]; + + //edges for the cluster the node is 'in' + if(innerCons != null){ + checkEdges(pathCost, current, cx, cy, innerCons); + } + + int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y; + if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){ + int nextClusteri = nextCx + nextCy * cwidth; + Cluster nextCluster = clusters[pathCost][nextClusteri]; + int relativeDir = (dir + 2) % 4; + LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; + if(outerCons != null){ + checkEdges(pathCost, current, nextCx, nextCy, outerCons); + } + } + } + + if(foundEnd){ + IntSeq result = new IntSeq(); + + int cur = endNodeIndex; + while(cur != startNodeIndex){ + result.add(cur); + cur = cameFrom.get(cur); + } + + result.reverse(); + + return result; + } + return null; + } + + void checkEdges(int pathCost, int current, int cx, int cy, LongSeq connections){ + for(int i = 0; i < connections.size; i++){ + long con = connections.items[i]; + float cost = IntraEdge.cost(con); + int otherDir = IntraEdge.dir(con), otherPortal = IntraEdge.portal(con); + int next = makeNodeIndex(cx, cy, otherDir, otherPortal); + + float newCost = costs.get(current) + cost; + + if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ + costs.put(next, newCost); + frontier.add(next, newCost + clusterNodeHeuristic(pathCost, current, next)); + cameFrom.put(next, current); + } + } + } + Cluster cluster(int pathCost, int cx, int cy){ return clusters[pathCost][cx + cwidth * cy]; } @@ -434,4 +555,14 @@ public class HierarchyPathFinder{ float cost; } + + @Struct + static class NodeIndexStruct{ + @StructField(22) + int cluster; + @StructField(2) + int dir; + @StructField(8) + int portal; + } } From d6d9a52ef9c8715f95479f4f7739c994ba797516 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 6 Nov 2023 22:51:47 -0500 Subject: [PATCH 05/35] It works, but badly --- .../src/mindustry/ai/HierarchyPathFinder.java | 168 +++++++++++++++--- core/src/mindustry/content/Fx.java | 2 +- 2 files changed, 140 insertions(+), 30 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 908bcb5de8..0f5ef0fe48 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -9,10 +9,12 @@ import arc.struct.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; +import mindustry.core.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.ui.*; import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; @@ -49,14 +51,14 @@ public class HierarchyPathFinder{ int cwidth, cheight; + //TODO: make thread-local (they are dereferenced rarely anyway) static PathfindQueue frontier = new PathfindQueue(); //node index -> total cost static IntFloatMap costs = new IntFloatMap(); // static IntSet usedEdges = new IntSet(); - + static IntSeq bfsQueue = new IntSeq(); static LongSeq tmpEdges = new LongSeq(); - //node index (NodeIndex struct) -> node it came from static IntIntMap cameFrom = new IntIntMap(); @@ -94,7 +96,9 @@ public class HierarchyPathFinder{ for(int cy = 0; cy < cheight; cy++){ var cluster = clusters[cost][cy * cwidth + cx]; if(cluster != null){ - Draw.color(Color.green); + Lines.stroke(0.5f); + Draw.color(Color.gray); + Lines.stroke(1f); Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); @@ -110,13 +114,13 @@ public class HierarchyPathFinder{ portalToVec(cluster, cx, cy, d, i, Tmp.v1); - Draw.color(Color.red); + Draw.color(Color.brown); Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; if(connections != null){ - Draw.color(Color.magenta); + Draw.color(Color.forest); for(int coni = 0; coni < connections.size; coni ++){ long con = connections.items[coni]; @@ -150,21 +154,50 @@ public class HierarchyPathFinder{ } } } + + Lines.stroke(3f); + Draw.color(Color.orange); + int node = findClosestNode(Team.sharded.id, 0, player.tileX(), player.tileY()); + int dest = findClosestNode(Team.sharded.id, 0, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); + if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ + var result = clusterAstar(0, node, dest); + if(result != null){ + for(int i = -1; i < result.size - 1; i++){ + int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); + portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); + Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); + } + } + + nodeToVec(dest, Tmp.v1); + Fonts.outline.draw(clusterNodeHeuristic(0, node, dest) + "", Tmp.v1.x, Tmp.v1.y); + } + Draw.reset(); }); }); } } - void portalToVec(Cluster cluster, int cx, int cy, int d, int i, Vec2 out){ - int pos = cluster.portals[d].items[i]; + Vec2 nodeToVec(int current, Vec2 out){ + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); + return out; + } + + void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){ + portalToVec(clusters[pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out); + } + + void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ + int pos = cluster.portals[direction].items[portalIndex]; int from = Point2.x(pos), to = Point2.y(pos); - int addX = moveDirs[d * 2], addY = moveDirs[d * 2 + 1]; + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; float average = (from + to) / 2f; float - x = (addX * average + cx * clusterSize + offsets[d * 2] * (clusterSize - 1) + nextOffsets[d * 2] / 2f) * tilesize, - y = (addY * average + cy * clusterSize + offsets[d * 2 + 1] * (clusterSize - 1) + nextOffsets[d * 2 + 1] / 2f) * tilesize; + x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize, + y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize; out.set(x, y); } @@ -260,7 +293,7 @@ public class HierarchyPathFinder{ //TODO: how the hell to identify a vertex? //cluster (i16) | direction (i2) | index (i14) - //TODO: clear portal connections. also share them? + //TODO: clear portal connections for(int direction = 0; direction < 4; direction++){ var portals = cluster.portals[direction]; @@ -296,7 +329,7 @@ public class HierarchyPathFinder{ otherX = (moveDirs[otherDir * 2] * otherAverage + ox), otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy); - //HOW (redundant nodes?) + //duplicate portal; should never happen. if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){ continue; } @@ -321,8 +354,6 @@ public class HierarchyPathFinder{ //TODO: can there be duplicate edges?? cluster.portalConnections[direction][i].add(IntraEdge.get(otherDir, j, connectionCost)); cluster.portalConnections[otherDir][j].add(IntraEdge.get(direction, i, connectionCost)); - - //Fx.debugLine.at(x* tilesize, y * tilesize, 0f, Color.purple, new Vec2[]{new Vec2(x, y).scl(tilesize), new Vec2(otherX, otherY).scl(tilesize)}); } } } @@ -351,6 +382,7 @@ public class HierarchyPathFinder{ frontier.clear(); costs.clear(); + //TODO: this can be faster and more memory efficient by making costs a NxN array... probably? costs.put(startPos, 0); frontier.add(startPos, 0); @@ -385,13 +417,10 @@ public class HierarchyPathFinder{ float newCost = costs.get(current) + add; - //a cost of 0 means "not set" if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ costs.put(next, newCost); float priority = newCost + heuristic(next, goalPos); frontier.add(next, priority); - - //cameFrom.put(next, current); } } } @@ -413,9 +442,74 @@ public class HierarchyPathFinder{ dir = 1; cy --; } + return NodeIndex.get(cx + cy * cwidth, dir, portal); } + //uses BFS to find the closest node index to specified coordinates + //this node is used in cluster A* + /** @return MAX_VALUE if no node is found */ + private int findClosestNode(int team, int pathCost, int tileX, int tileY){ + int cx = tileX / clusterSize, cy = tileY / clusterSize; + + if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight){ + return Integer.MAX_VALUE; + } + + //TODO + PathCost cost = ControlPathfinder.costGround; + + Cluster cluster = clusters[pathCost][cx + cy * cwidth]; + int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); + + int bestPortalPair = Integer.MAX_VALUE; + float bestCost = Float.MAX_VALUE; + + if(cluster != null){ //TODO create on demand?? + + //A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra) + for(int dir = 0; dir < 4; dir++){ + var portals = cluster.portals[dir]; + if(portals == null) continue; + + for(int j = 0; j < portals.size; j++){ + + int + other = portals.items[j], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + otherAverage = (otherFrom + otherTo) / 2, + ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + otherX = (moveDirs[dir * 2] * otherAverage + ox), + otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy); + + float connectionCost = innerAstar( + team, cost, + minX, minY, maxX, maxY, + tileX + tileY * wwidth, + otherX + otherY * wwidth, + (moveDirs[dir * 2] * otherFrom + ox), + (moveDirs[dir * 2 + 1] * otherFrom + oy), + (moveDirs[dir * 2] * otherTo + ox), + (moveDirs[dir * 2 + 1] * otherTo + oy) + ); + + //better cost found, update and return + if(connectionCost != -1f && connectionCost < bestCost){ + bestPortalPair = Point2.pack(dir, j); + bestCost = connectionCost; + } + } + } + + if(bestPortalPair != Integer.MAX_VALUE){ + return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair)); + } + } + + return Integer.MAX_VALUE; + } + //distance heuristic: manhattan private float clusterNodeHeuristic(int pathCost, int nodeA, int nodeB){ int @@ -424,10 +518,9 @@ public class HierarchyPathFinder{ portalA = NodeIndex.portal(nodeA), clusterB = NodeIndex.cluster(nodeB), dirB = NodeIndex.dir(nodeB), - portalB = NodeIndex.portal(nodeB); - - int rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA]; - int rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB]; + portalB = NodeIndex.portal(nodeB), + rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA], + rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB]; float averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f, @@ -442,13 +535,24 @@ public class HierarchyPathFinder{ } @Nullable IntSeq clusterAstar(int pathCost, int startNodeIndex, int endNodeIndex){ + var v1 = nodeToVec(startNodeIndex, Tmp.v1); + var v2 = nodeToVec(endNodeIndex, Tmp.v2); + Fx.placeBlock.at(v1.x, v1.y, 1); + Fx.placeBlock.at(v2.x, v2.y, 1); + + if(startNodeIndex == endNodeIndex){ + //TODO alloc + return IntSeq.with(startNodeIndex); + } + frontier.clear(); costs.clear(); - - costs.put(startNodeIndex, 0); - frontier.add(endNodeIndex, 0); cameFrom.clear(); + cameFrom.put(startNodeIndex, startNodeIndex); + costs.put(startNodeIndex, 0); + frontier.add(startNodeIndex, 0); + boolean foundEnd = false; while(frontier.size > 0){ @@ -459,19 +563,17 @@ public class HierarchyPathFinder{ break; } - //tmpEdges holds intra edges - tmpEdges.clear(); - int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current); - int cx = cluster % wwidth, cy = cluster / wwidth; + int cx = cluster % cwidth, cy = cluster / cwidth; Cluster clust = clusters[pathCost][cluster]; - LongSeq innerCons = clust.portalConnections[dir][portal]; + LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal]; //edges for the cluster the node is 'in' if(innerCons != null){ checkEdges(pathCost, current, cx, cy, innerCons); } + //edges that this node 'faces' from the other side int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y; if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){ int nextClusteri = nextCx + nextCy * cwidth; @@ -500,6 +602,10 @@ public class HierarchyPathFinder{ return null; } + static void line(Vec2 a, Vec2 b){ + Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); + } + void checkEdges(int pathCost, int current, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; @@ -511,8 +617,12 @@ public class HierarchyPathFinder{ if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ costs.put(next, newCost); + frontier.add(next, newCost + clusterNodeHeuristic(pathCost, current, next)); cameFrom.put(next, current); + + //TODO debug + line(nodeToVec(current, Tmp.v1), nodeToVec(next, Tmp.v2)); } } } diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index 944bd87168..953274f8a1 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -2584,7 +2584,7 @@ public class Fx{ if(!(e.data instanceof Vec2[] vec)) return; Draw.color(e.color); - Lines.stroke(1f); + Lines.stroke(2f); if(vec.length == 2){ Lines.line(vec[0].x, vec[0].y, vec[1].x, vec[1].y); From a17dcbca5aee0147cfb70d331e734984030c4028 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 6 Nov 2023 23:02:26 -0500 Subject: [PATCH 06/35] Heuristic fixed --- core/src/mindustry/ai/HierarchyPathFinder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 0f5ef0fe48..eafe467bc3 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -570,7 +570,7 @@ public class HierarchyPathFinder{ //edges for the cluster the node is 'in' if(innerCons != null){ - checkEdges(pathCost, current, cx, cy, innerCons); + checkEdges(pathCost, current, endNodeIndex, cx, cy, innerCons); } //edges that this node 'faces' from the other side @@ -581,7 +581,7 @@ public class HierarchyPathFinder{ int relativeDir = (dir + 2) % 4; LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; if(outerCons != null){ - checkEdges(pathCost, current, nextCx, nextCy, outerCons); + checkEdges(pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); } } } @@ -606,7 +606,7 @@ public class HierarchyPathFinder{ Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); } - void checkEdges(int pathCost, int current, int cx, int cy, LongSeq connections){ + void checkEdges(int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; float cost = IntraEdge.cost(con); @@ -618,7 +618,7 @@ public class HierarchyPathFinder{ if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ costs.put(next, newCost); - frontier.add(next, newCost + clusterNodeHeuristic(pathCost, current, next)); + frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal)); cameFrom.put(next, current); //TODO debug From 0186c35a1a5d77b3b28a267dc937ef3360bc9e41 Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 7 Nov 2023 00:11:44 -0500 Subject: [PATCH 07/35] Flowfield tests --- .../src/mindustry/ai/HierarchyPathFinder.java | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index eafe467bc3..3989f949b5 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -162,12 +162,36 @@ public class HierarchyPathFinder{ if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ var result = clusterAstar(0, node, dest); if(result != null){ + for(int i = -1; i < result.size - 1; i++){ + int endCluster = NodeIndex.cluster(result.items[i + 1]); + int cx = endCluster % cwidth, cy = endCluster / cwidth; + int[] field = flowField(0, cx, cy, 0, dest); + + for(int y = 0; y < clusterSize; y++){ + for(int x = 0; x < clusterSize; x++){ + int value = field[x + y *clusterSize]; + Tmp.c1.a = 1f; + Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); + Draw.alpha(0.5f); + Lines.rect((x + cx * clusterSize) * tilesize - tilesize/2f, (y + cy * clusterSize) * tilesize - tilesize/2f, tilesize, tilesize); + } + } + } + + Lines.stroke(3f); + Draw.color(Color.orange); + for(int i = -1; i < result.size - 1; i++){ int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); } + + + + + //flowField(0, ) } nodeToVec(dest, Tmp.v1); @@ -446,7 +470,7 @@ public class HierarchyPathFinder{ return NodeIndex.get(cx + cy * cwidth, dir, portal); } - //uses BFS to find the closest node index to specified coordinates + //uses A* to find the closest node index to specified coordinates //this node is used in cluster A* /** @return MAX_VALUE if no node is found */ private int findClosestNode(int team, int pathCost, int tileX, int tileY){ @@ -488,6 +512,7 @@ public class HierarchyPathFinder{ minX, minY, maxX, maxY, tileX + tileY * wwidth, otherX + otherY * wwidth, + //TODO these are wrong and never actually trigger (moveDirs[dir * 2] * otherFrom + ox), (moveDirs[dir * 2 + 1] * otherFrom + oy), (moveDirs[dir * 2] * otherTo + ox), @@ -627,8 +652,70 @@ public class HierarchyPathFinder{ } } - Cluster cluster(int pathCost, int cx, int cy){ - return clusters[pathCost][cx + cwidth * cy]; + //both nodes must be inside the same flow field (cx, cy) + int[] flowField(int pathCost, int cx, int cy, int nodeFrom, int nodeTo){ + + Cluster cluster = clusters[pathCost][cx + cy * cwidth]; + + int[] weights = new int[clusterSize * clusterSize]; + byte[] searches = new byte[clusterSize * clusterSize]; + PathCost pcost = ControlPathfinder.costGround; + int team = Team.sharded.id; + + IntQueue frontier = new IntQueue(); + int search = 1; + + //TODO actually set up the frontier and destinations (where are you going?) + { + int + dir = NodeIndex.dir(nodeTo), + other = cluster.portals[dir].items[NodeIndex.portal(nodeTo)], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + + maxX = (moveDirs[dir * 2] * otherFrom + ox), + maxY = (moveDirs[dir * 2 + 1] * otherFrom + oy), + minX = (moveDirs[dir * 2] * otherTo + ox), + minY = (moveDirs[dir * 2 + 1] * otherTo + oy) + + ; + + //TODO: being zero INSIDE the cluster means that the unit will stop at the edge and not move between clusters - bad! + for(int x = minX; x <= maxX; x++){ + for(int y = minY; y <= maxY; y++){ + frontier.addFirst(x + y * wwidth); + } + } + } + + int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); + //TODO spread this out across many frames + while(frontier.size > 0){ + int tile = frontier.removeLast(); + int baseX = tile % wwidth, baseY = tile / wwidth; + int cost = weights[(baseX - minX) + (baseY - minY) * clusterSize]; + + if(cost != impassable){ + for(Point2 point : Geometry.d4){ + + int dx = baseX + point.x, dy = baseY + point.y; + + if(dx < minX || dy < minY || dx > maxX || dy > maxY) continue; + + int newPos = tile + point.x + point.y * wwidth; + int newPosArray = (dx - minX) + (dy - minY) * clusterSize; + int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); + + if((weights[newPosArray] > cost + otherCost || searches[newPosArray] < search) && otherCost != impassable){ + frontier.addFirst(newPos); + weights[newPosArray] = cost + otherCost; + searches[newPosArray] = (byte)search; + } + } + } + } + return weights; } private static boolean solid(int team, PathCost type, int x, int y){ From 65d0b6adccc0685b8e7dc4ec04b09f4c70e3b535 Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 7 Nov 2023 10:56:17 -0500 Subject: [PATCH 08/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 194 +++++++++++++----- core/src/mindustry/ai/types/CommandAI.java | 2 +- gradle.properties | 2 +- 3 files changed, 144 insertions(+), 54 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 3989f949b5..012c0495e4 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -15,6 +15,7 @@ import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.ui.*; +import mindustry.world.*; import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; @@ -155,50 +156,54 @@ public class HierarchyPathFinder{ } } - Lines.stroke(3f); - Draw.color(Color.orange); - int node = findClosestNode(Team.sharded.id, 0, player.tileX(), player.tileY()); - int dest = findClosestNode(Team.sharded.id, 0, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); - if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ - var result = clusterAstar(0, node, dest); - if(result != null){ - for(int i = -1; i < result.size - 1; i++){ - int endCluster = NodeIndex.cluster(result.items[i + 1]); - int cx = endCluster % cwidth, cy = endCluster / cwidth; - int[] field = flowField(0, cx, cy, 0, dest); + if(false){ + Lines.stroke(3f); + Draw.color(Color.orange); + int node = findClosestNode(Team.sharded.id, 0, player.tileX(), player.tileY()); + int dest = findClosestNode(Team.sharded.id, 0, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); + if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ + var result = clusterAstar(0, node, dest); + if(result != null){ + for(int i = -1; i < result.size - 1; i++){ + int endCluster = NodeIndex.cluster(result.items[i + 1]); + int cx = endCluster % cwidth, cy = endCluster / cwidth; + int[] field = flowField(0, cx, cy, 0, dest, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); - for(int y = 0; y < clusterSize; y++){ - for(int x = 0; x < clusterSize; x++){ - int value = field[x + y *clusterSize]; - Tmp.c1.a = 1f; - Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); - Draw.alpha(0.5f); - Lines.rect((x + cx * clusterSize) * tilesize - tilesize/2f, (y + cy * clusterSize) * tilesize - tilesize/2f, tilesize, tilesize); + for(int y = 0; y < clusterSize; y++){ + for(int x = 0; x < clusterSize; x++){ + int value = field[x + y *clusterSize]; + Tmp.c1.a = 1f; + Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); + Draw.alpha(0.5f); + Lines.rect((x + cx * clusterSize) * tilesize - tilesize/2f, (y + cy * clusterSize) * tilesize - tilesize/2f, tilesize, tilesize); + } } } + + Lines.stroke(3f); + Draw.color(Color.orange); + + for(int i = -1; i < result.size - 1; i++){ + int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); + portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); + Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); + } + + + + + //flowField(0, ) } - Lines.stroke(3f); - Draw.color(Color.orange); - - for(int i = -1; i < result.size - 1; i++){ - int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; - portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); - portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); - Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); - } - - - - - //flowField(0, ) + nodeToVec(dest, Tmp.v1); + Fonts.outline.draw(clusterNodeHeuristic(0, node, dest) + "", Tmp.v1.x, Tmp.v1.y); } - nodeToVec(dest, Tmp.v1); - Fonts.outline.draw(clusterNodeHeuristic(0, node, dest) + "", Tmp.v1.x, Tmp.v1.y); + Draw.reset(); } - Draw.reset(); + }); }); } @@ -631,6 +636,10 @@ public class HierarchyPathFinder{ Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); } + static void line(Vec2 a, Vec2 b, Color color){ + Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); + } + void checkEdges(int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; @@ -653,48 +662,63 @@ public class HierarchyPathFinder{ } //both nodes must be inside the same flow field (cx, cy) - int[] flowField(int pathCost, int cx, int cy, int nodeFrom, int nodeTo){ + int[] flowField(int pathCost, int cx, int cy, int nodeFrom, int nodeTo, int goalX, int goalY){ Cluster cluster = clusters[pathCost][cx + cy * cwidth]; - int[] weights = new int[clusterSize * clusterSize]; - byte[] searches = new byte[clusterSize * clusterSize]; + int realSize = clusterSize + 2; + + int[] weights = new int[realSize * realSize]; + byte[] searches = new byte[realSize * realSize]; PathCost pcost = ControlPathfinder.costGround; int team = Team.sharded.id; IntQueue frontier = new IntQueue(); int search = 1; - //TODO actually set up the frontier and destinations (where are you going?) - { + int + minX = cx * clusterSize - 1, + minY = cy * clusterSize - 1, + maxX = Math.min(minX + clusterSize + 1, wwidth - 1), + maxY = Math.min(minY + clusterSize + 1, wheight - 1), + toCluster = NodeIndex.cluster(nodeTo), + tocx = toCluster % cwidth, + tocy = toCluster / cwidth; + + //you're at the cluster with the goal node + if(goalX / clusterSize == cx && goalY / clusterSize == cy){ + frontier.addFirst(goalX + goalY * wwidth); + }else{ int dir = NodeIndex.dir(nodeTo), other = cluster.portals[dir].items[NodeIndex.portal(nodeTo)], otherFrom = Point2.x(other), otherTo = Point2.y(other), - ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), - oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + ox = tocx * clusterSize + offsets[dir * 2] * (clusterSize - 1), + oy = tocy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), - maxX = (moveDirs[dir * 2] * otherFrom + ox), - maxY = (moveDirs[dir * 2 + 1] * otherFrom + oy), - minX = (moveDirs[dir * 2] * otherTo + ox), - minY = (moveDirs[dir * 2 + 1] * otherTo + oy) + px2 = Mathf.clamp((moveDirs[dir * 2] * otherFrom + ox), minX, maxX), + py2 = Mathf.clamp((moveDirs[dir * 2 + 1] * otherFrom + oy), minY, maxY), + px1 = Mathf.clamp((moveDirs[dir * 2] * otherTo + ox), minX, maxX), + py1 = Mathf.clamp((moveDirs[dir * 2 + 1] * otherTo + oy), minY, maxY); - ; + if(px1 >= cx * clusterSize && px2 < cx * clusterSize + clusterSize && py1 >= cy * clusterSize && py2 < cy * clusterSize){ + Log.info("inside the box"); //TODO broken + } //TODO: being zero INSIDE the cluster means that the unit will stop at the edge and not move between clusters - bad! - for(int x = minX; x <= maxX; x++){ - for(int y = minY; y <= maxY; y++){ + for(int x = px1; x <= px2; x++){ + for(int y = py1; y <= py2; y++){ frontier.addFirst(x + y * wwidth); + Fx.lightBlock.at(x * tilesize, y * tilesize, 1f, Color.orange); } } } - int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); //TODO spread this out across many frames while(frontier.size > 0){ int tile = frontier.removeLast(); int baseX = tile % wwidth, baseY = tile / wwidth; - int cost = weights[(baseX - minX) + (baseY - minY) * clusterSize]; + int cost = weights[(baseX - minX) + (baseY - minY) * realSize]; if(cost != impassable){ for(Point2 point : Geometry.d4){ @@ -704,7 +728,7 @@ public class HierarchyPathFinder{ if(dx < minX || dy < minY || dx > maxX || dy > maxY) continue; int newPos = tile + point.x + point.y * wwidth; - int newPosArray = (dx - minX) + (dy - minY) * clusterSize; + int newPosArray = (dx - minX) + (dy - minY) * realSize; int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); if((weights[newPosArray] > cost + otherCost || searches[newPosArray] < search) && otherCost != impassable){ @@ -718,6 +742,72 @@ public class HierarchyPathFinder{ return weights; } + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ + int cost = 0; + + int node = findClosestNode(unit.team.id, cost, unit.tileX(), unit.tileY()); + int dest = findClosestNode(unit.team.id, cost, World.toTile(destination.x), World.toTile(destination.y)); + + var result = clusterAstar(cost, node, dest); + Tile tile = unit.tileOn(); + if(result != null){ + for(int i = -1; i < result.size - 1; i++){ + int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); + portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); + line(Tmp.v1, Tmp.v2, Color.orange); + } + + int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize, + ox = cx * clusterSize - 1, oy = cy * clusterSize - 1; + + int nextNode = result.items[0]; + + int[] field = flowField(cost, cx, cy, node, nextNode, World.toTile(destination.x), World.toTile(destination.y)); + + if(field != null && tile != null){ + int value = field[(tile.x - ox) + (tile.y - oy) * (clusterSize + 2)]; + + Tile current = null; + int tl = 0; + for(Point2 point : Geometry.d8){ + int dx = tile.x + point.x, dy = tile.y + point.y; + + Tile other = world.tile(dx, dy); + + + if(other == null || dx < ox || dy < oy || dx >= ox + clusterSize + 2 || dy >= oy + clusterSize + 2) continue; + + int local = (dx - ox) + (dy - oy) * (clusterSize + 2); + int packed = world.packArray(dx, dy); + int otherCost = field[local]; + + if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && + !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tile.x + point.x, tile.y)) || + (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tile.x, tile.y + point.y)))))){ //diagonal corner trap + + current = other; + tl = field[local]; + } + } + + if(!(current == null || tl == impassable || (cost == costGround && current.dangerous() && !tile.dangerous()))){ + out.set(current); + return true; + } + } + } + + noResultFound[0] = true; + return false; + } + + private static boolean passable(PathCost cost, int team, int pos){ + int amount = cost.getCost(team, pathfinder.tiles[pos]); + //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable + return amount != impassable && !(cost == costTypes.get(costNaval) && amount >= 6000); + } + private static boolean solid(int team, PathCost type, int x, int y){ return x < 0 || y < 0 || x >= wwidth || y >= wheight || solid(team, type, x + y * wwidth, true); } diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index a44b5b457c..86a5a89447 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -247,7 +247,7 @@ public class CommandAI extends AIController{ } //if you've spent 3 seconds stuck, something is wrong, move regardless - move = Vars.controlPath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); + move = hpath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); //we've reached the final point if the returned coordinate is equal to the supplied input isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f); diff --git a/gradle.properties b/gradle.properties index f9fa0851e3..090150dba9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,4 @@ org.gradle.caching=true #used for slow jitpack builds; TODO see if this actually works org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 -archash=96f4f4214a +archash=e2fdbab477 From be44e283d8abec55bd9a1c5fc702d3065353e2bb Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 8 Nov 2023 08:42:48 -0500 Subject: [PATCH 09/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 259 +++++++++--------- core/src/mindustry/content/Fx.java | 10 + 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 012c0495e4..8f0b500c99 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -53,15 +53,13 @@ public class HierarchyPathFinder{ int cwidth, cheight; //TODO: make thread-local (they are dereferenced rarely anyway) - static PathfindQueue frontier = new PathfindQueue(); + PathfindQueue frontier = new PathfindQueue(); //node index -> total cost - static IntFloatMap costs = new IntFloatMap(); - // - static IntSet usedEdges = new IntSet(); - static IntSeq bfsQueue = new IntSeq(); - static LongSeq tmpEdges = new LongSeq(); + IntFloatMap costs = new IntFloatMap(); + IntSet usedEdges = new IntSet(); //node index (NodeIndex struct) -> node it came from - static IntIntMap cameFrom = new IntIntMap(); + IntIntMap cameFrom = new IntIntMap(); + IntMap fields; public HierarchyPathFinder(){ @@ -164,21 +162,6 @@ public class HierarchyPathFinder{ if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ var result = clusterAstar(0, node, dest); if(result != null){ - for(int i = -1; i < result.size - 1; i++){ - int endCluster = NodeIndex.cluster(result.items[i + 1]); - int cx = endCluster % cwidth, cy = endCluster / cwidth; - int[] field = flowField(0, cx, cy, 0, dest, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); - - for(int y = 0; y < clusterSize; y++){ - for(int x = 0; x < clusterSize; x++){ - int value = field[x + y *clusterSize]; - Tmp.c1.a = 1f; - Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); - Draw.alpha(0.5f); - Lines.rect((x + cx * clusterSize) * tilesize - tilesize/2f, (y + cy * clusterSize) * tilesize - tilesize/2f, tilesize, tilesize); - } - } - } Lines.stroke(3f); Draw.color(Color.orange); @@ -189,11 +172,6 @@ public class HierarchyPathFinder{ portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); } - - - - - //flowField(0, ) } nodeToVec(dest, Tmp.v1); @@ -203,6 +181,21 @@ public class HierarchyPathFinder{ Draw.reset(); } + if(fields != null){ + for(var entry : fields){ + int cx = entry.key % cwidth, cy = entry.key / cwidth; + for(int y = 0; y < clusterSize; y++){ + for(int x = 0; x < clusterSize; x++){ + int value = entry.value[x + y * clusterSize]; + Tmp.c1.a = 1f; + Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); + Draw.alpha(0.5f); + Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize/2f); + } + } + } + } + }); }); @@ -415,11 +408,6 @@ public class HierarchyPathFinder{ costs.put(startPos, 0); frontier.add(startPos, 0); - if(debug && false){ - Fx.debugLine.at(Point2.x(startPos) * tilesize, Point2.y(startPos) * tilesize, 0f, Color.purple, - new Vec2[]{new Vec2(Point2.x(startPos), Point2.y(startPos)).scl(tilesize), new Vec2(Point2.x(goalPos), Point2.y(goalPos)).scl(tilesize)}); - } - while(frontier.size > 0){ int current = frontier.poll(); @@ -567,8 +555,6 @@ public class HierarchyPathFinder{ @Nullable IntSeq clusterAstar(int pathCost, int startNodeIndex, int endNodeIndex){ var v1 = nodeToVec(startNodeIndex, Tmp.v1); var v2 = nodeToVec(endNodeIndex, Tmp.v2); - Fx.placeBlock.at(v1.x, v1.y, 1); - Fx.placeBlock.at(v2.x, v2.y, 1); if(startNodeIndex == endNodeIndex){ //TODO alloc @@ -661,147 +647,166 @@ public class HierarchyPathFinder{ } } - //both nodes must be inside the same flow field (cx, cy) - int[] flowField(int pathCost, int cx, int cy, int nodeFrom, int nodeTo, int goalX, int goalY){ + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ + int costId = 0; - Cluster cluster = clusters[pathCost][cx + cy * cwidth]; + int node = findClosestNode(unit.team.id, costId, unit.tileX(), unit.tileY()); + int dest = findClosestNode(unit.team.id, costId, World.toTile(destination.x), World.toTile(destination.y)); - int realSize = clusterSize + 2; + fields = new IntMap<>(); - int[] weights = new int[realSize * realSize]; - byte[] searches = new byte[realSize * realSize]; PathCost pcost = ControlPathfinder.costGround; int team = Team.sharded.id; + int goalPos = (World.toTile(destination.x) + World.toTile(destination.y) * wwidth); IntQueue frontier = new IntQueue(); - int search = 1; + frontier.addFirst(goalPos); - int - minX = cx * clusterSize - 1, - minY = cy * clusterSize - 1, - maxX = Math.min(minX + clusterSize + 1, wwidth - 1), - maxY = Math.min(minY + clusterSize + 1, wheight - 1), - toCluster = NodeIndex.cluster(nodeTo), - tocx = toCluster % cwidth, - tocy = toCluster / cwidth; + Tile tileOn = unit.tileOn(); - //you're at the cluster with the goal node - if(goalX / clusterSize == cx && goalY / clusterSize == cy){ - frontier.addFirst(goalX + goalY * wwidth); - }else{ - int - dir = NodeIndex.dir(nodeTo), - other = cluster.portals[dir].items[NodeIndex.portal(nodeTo)], - otherFrom = Point2.x(other), otherTo = Point2.y(other), - ox = tocx * clusterSize + offsets[dir * 2] * (clusterSize - 1), - oy = tocy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + var result = clusterAstar(costId, node, dest); + if(result != null && tileOn != null){ - px2 = Mathf.clamp((moveDirs[dir * 2] * otherFrom + ox), minX, maxX), - py2 = Mathf.clamp((moveDirs[dir * 2 + 1] * otherFrom + oy), minY, maxY), - px1 = Mathf.clamp((moveDirs[dir * 2] * otherTo + ox), minX, maxX), - py1 = Mathf.clamp((moveDirs[dir * 2 + 1] * otherTo + oy), minY, maxY); + int fsize = clusterSize * clusterSize; + int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize; - if(px1 >= cx * clusterSize && px2 < cx * clusterSize + clusterSize && py1 >= cy * clusterSize && py2 < cy * clusterSize){ - Log.info("inside the box"); //TODO broken - } + fields.put(cx + cy * cwidth, new int[fsize]); - //TODO: being zero INSIDE the cluster means that the unit will stop at the edge and not move between clusters - bad! - for(int x = px1; x <= px2; x++){ - for(int y = py1; y <= py2; y++){ - frontier.addFirst(x + y * wwidth); - Fx.lightBlock.at(x * tilesize, y * tilesize, 1f, Color.orange); + for(int i = -1; i < result.size; i++){ + int + current = i == -1 ? node : result.items[i], + cluster = NodeIndex.cluster(current), + dir = NodeIndex.dir(current), + dx = Geometry.d4[dir].x, + dy = Geometry.d4[dir].y, + ox = cluster % cwidth + dx, + oy = cluster / cwidth + dy; + + //store current cluster in the path list + if(!fields.containsKey(cluster)){ + fields.put(cluster, new int[fsize]); } - } - } - //TODO spread this out across many frames - while(frontier.size > 0){ - int tile = frontier.removeLast(); - int baseX = tile % wwidth, baseY = tile / wwidth; - int cost = weights[(baseX - minX) + (baseY - minY) * realSize]; + //store directionals TODO out of bounds + for(Point2 p : Geometry.d4){ + int other = cluster + p.x + p.y * cwidth; + if(!fields.containsKey(other)){ + fields.put(other, new int[fsize]); + } + } - if(cost != impassable){ - for(Point2 point : Geometry.d4){ + //store directional/flipped version of cluster + if(ox >= 0 && oy >= 0 && ox < cwidth && oy < cheight){ + int other = ox + oy * cwidth; + if(!fields.containsKey(other)){ + fields.put(other, new int[fsize]); + } - int dx = baseX + point.x, dy = baseY + point.y; - - if(dx < minX || dy < minY || dx > maxX || dy > maxY) continue; - - int newPos = tile + point.x + point.y * wwidth; - int newPosArray = (dx - minX) + (dy - minY) * realSize; - int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); - - if((weights[newPosArray] > cost + otherCost || searches[newPosArray] < search) && otherCost != impassable){ - frontier.addFirst(newPos); - weights[newPosArray] = cost + otherCost; - searches[newPosArray] = (byte)search; + //store directionals again + for(Point2 p : Geometry.d4){ + int other2 = other + p.x + p.y * cwidth; + if(!fields.containsKey(other2)){ + fields.put(other2, new int[fsize]); + } } } } - } - return weights; - } - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ - int cost = 0; - - int node = findClosestNode(unit.team.id, cost, unit.tileX(), unit.tileY()); - int dest = findClosestNode(unit.team.id, cost, World.toTile(destination.x), World.toTile(destination.y)); - - var result = clusterAstar(cost, node, dest); - Tile tile = unit.tileOn(); - if(result != null){ for(int i = -1; i < result.size - 1; i++){ int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); line(Tmp.v1, Tmp.v2, Color.orange); } - int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize, - ox = cx * clusterSize - 1, oy = cy * clusterSize - 1; + //actually do the flow field part + //TODO spread this out across many frames + while(frontier.size > 0){ + int tile = frontier.removeLast(); + int baseX = tile % wwidth, baseY = tile / wwidth; + int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; + int[] curWeights = fields.get(curWeightIndex); - int nextNode = result.items[0]; + int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; - int[] field = flowField(cost, cx, cy, node, nextNode, World.toTile(destination.x), World.toTile(destination.y)); + if(cost != impassable){ + for(Point2 point : Geometry.d4){ - if(field != null && tile != null){ - int value = field[(tile.x - ox) + (tile.y - oy) * (clusterSize + 2)]; + int + dx = baseX + point.x, dy = baseY + point.y, + clx = dx / clusterSize, cly = dy / clusterSize; - Tile current = null; - int tl = 0; - for(Point2 point : Geometry.d8){ - int dx = tile.x + point.x, dy = tile.y + point.y; + if(clx < 0 || cly < 0 || dx >= wwidth || dy >= wheight) continue; - Tile other = world.tile(dx, dy); + int nextWeightIndex = clx + cly * cwidth; + int[] weights = nextWeightIndex == curWeightIndex ? curWeights : fields.get(nextWeightIndex); - if(other == null || dx < ox || dy < oy || dx >= ox + clusterSize + 2 || dy >= oy + clusterSize + 2) continue; + //out of bounds; not allowed to move this way because no weights were registered here + if(weights == null) continue; - int local = (dx - ox) + (dy - oy) * (clusterSize + 2); - int packed = world.packArray(dx, dy); - int otherCost = field[local]; + int newPos = tile + point.x + point.y * wwidth; - if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && - !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tile.x + point.x, tile.y)) || - (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tile.x, tile.y + point.y)))))){ //diagonal corner trap + //can't move back to the goal + if(newPos == goalPos) continue; - current = other; - tl = field[local]; + int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; + int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); + int oldCost = weights[newPosArray]; + + //a cost of 0 means uninitialized, OR it means we're at the goal position, but that's handled above + if((oldCost == 0 || oldCost > cost + otherCost) && otherCost != impassable){ + frontier.addFirst(newPos); + weights[newPosArray] = cost + otherCost; + } } } + } - if(!(current == null || tl == impassable || (cost == costGround && current.dangerous() && !tile.dangerous()))){ - out.set(current); - return true; + + int value = getCost(fields, tileOn.x, tileOn.y); + + Tile current = null; + int tl = 0; + //TODO: use raycasting and iterate on this for N steps + for(Point2 point : Geometry.d8){ + int dx = tileOn.x + point.x, dy = tileOn.y + point.y; + + Tile other = world.tile(dx, dy); + + if(other == null) continue; + + int packed = world.packArray(dx, dy); + int otherCost = getCost(fields, dx, dy); + + if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && + !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || + (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap + + current = other; + tl = otherCost; } } + + if(!(current == null || tl == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ + out.set(current); + return true; + } } noResultFound[0] = true; return false; } + private int getCost(IntMap fields, int x, int y){ + int[] field = fields.get(x / clusterSize + (y / clusterSize) * cwidth); + if(field == null){ + return -1; + } + return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; + } + private static boolean passable(PathCost cost, int team, int pos){ int amount = cost.getCost(team, pathfinder.tiles[pos]); //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index 953274f8a1..246eec104a 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -2596,5 +2596,15 @@ public class Fx{ } Draw.reset(); + }), + debugRect = new Effect(90f, 1000000000000f, e -> { + if(!(e.data instanceof Rect rect)) return; + + Draw.color(e.color); + Lines.stroke(2f); + + Lines.rect(rect); + + Draw.reset(); }); } From 70e112e8849f6246d25a5d6181b7de0f5cfac8ba Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 8 Nov 2023 13:20:07 -0500 Subject: [PATCH 10/35] Moving new pathfinding to a new thread --- .../src/mindustry/ai/HierarchyPathFinder.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 8f0b500c99..97587f7ceb 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -23,6 +23,8 @@ import static mindustry.ai.Pathfinder.*; //https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf //https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf public class HierarchyPathFinder{ + static final int clusterSize = 12; + static final boolean debug = true; static final int[] offsets = { @@ -48,7 +50,6 @@ public class HierarchyPathFinder{ //maps pathCost -> flattened array of clusters in 2D Cluster[][] clusters; - int clusterSize = 12; int cwidth, cheight; @@ -56,17 +57,45 @@ public class HierarchyPathFinder{ PathfindQueue frontier = new PathfindQueue(); //node index -> total cost IntFloatMap costs = new IntFloatMap(); + //temporarily used for resolving connections for intra-edges IntSet usedEdges = new IntSet(); //node index (NodeIndex struct) -> node it came from IntIntMap cameFrom = new IntIntMap(); IntMap fields; + //tasks to run on pathfinding thread + TaskQueue tasks = new TaskQueue(); + //individual requests based on unit + ObjectMap unitRequests = new ObjectMap<>(); + //maps position in world in (x + y * width format) to a cache of flow fields + IntMap requests = new IntMap<>(); + + //path requests are per-unit + //these contain + static class PathRequest{ + int destination; + //node index -> total cost + IntFloatMap costs = new IntFloatMap(); + //node index (NodeIndex struct) -> node it came from TODO merge them + IntIntMap cameFrom = new IntIntMap(); + } + + static class FieldCache{ + int destination; + //frontier for flow fields + PathfindQueue frontier = new PathfindQueue(); + //maps cluster index to field weights; 0 means uninitialized + IntMap fields = new IntMap<>(); + + //TODO: node map for merging + //TODO: how to extend flowfields? + } + public HierarchyPathFinder(){ Events.on(WorldLoadEvent.class, event -> { //TODO 5 path costs, arbitrary number clusters = new Cluster[5][]; - clusterSize = 12; //TODO arbitrary cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); From 358a9ca98bcf292a3845e06128ab1d92dcdfb935 Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 9 Nov 2023 09:49:50 -0500 Subject: [PATCH 11/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 138 +++++++++++------- 1 file changed, 82 insertions(+), 56 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 97587f7ceb..c2bcb34afa 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -22,7 +22,11 @@ import static mindustry.ai.Pathfinder.*; //https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf //https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf -public class HierarchyPathFinder{ +public class HierarchyPathFinder implements Runnable{ + private static final long maxUpdate = Time.millisToNanos(12); + private static final int updateFPS = 30; + private static final int updateInterval = 1000 / updateFPS; + static final int clusterSize = 12; static final boolean debug = true; @@ -53,37 +57,35 @@ public class HierarchyPathFinder{ int cwidth, cheight; - //TODO: make thread-local (they are dereferenced rarely anyway) - PathfindQueue frontier = new PathfindQueue(); - //node index -> total cost - IntFloatMap costs = new IntFloatMap(); //temporarily used for resolving connections for intra-edges IntSet usedEdges = new IntSet(); - //node index (NodeIndex struct) -> node it came from - IntIntMap cameFrom = new IntIntMap(); - IntMap fields; - //tasks to run on pathfinding thread - TaskQueue tasks = new TaskQueue(); + TaskQueue queue = new TaskQueue(); //individual requests based on unit ObjectMap unitRequests = new ObjectMap<>(); //maps position in world in (x + y * width format) to a cache of flow fields IntMap requests = new IntMap<>(); + /** Current pathfinding thread */ + @Nullable Thread thread; //path requests are per-unit //these contain static class PathRequest{ int destination; + //resulting path of nodes + IntSeq resultPath = new IntSeq(); //node index -> total cost IntFloatMap costs = new IntFloatMap(); //node index (NodeIndex struct) -> node it came from TODO merge them IntIntMap cameFrom = new IntIntMap(); + //frontier for A* + PathfindQueue frontier = new PathfindQueue(); } static class FieldCache{ int destination; //frontier for flow fields - PathfindQueue frontier = new PathfindQueue(); + IntQueue frontier = new IntQueue(); //maps cluster index to field weights; 0 means uninitialized IntMap fields = new IntMap<>(); @@ -93,17 +95,17 @@ public class HierarchyPathFinder{ public HierarchyPathFinder(){ + Events.on(ResetEvent.class, event -> stop()); + Events.on(WorldLoadEvent.class, event -> { + stop(); + //TODO 5 path costs, arbitrary number clusters = new Cluster[5][]; cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); - for(int cy = 0; cy < cwidth; cy++){ - for(int cx = 0; cx < cheight; cx++){ - createCluster(Team.sharded.id, costGround, cx, cy); - } - } + start(); }); //TODO very inefficient, this is only for debugging @@ -183,33 +185,7 @@ public class HierarchyPathFinder{ } } - if(false){ - Lines.stroke(3f); - Draw.color(Color.orange); - int node = findClosestNode(Team.sharded.id, 0, player.tileX(), player.tileY()); - int dest = findClosestNode(Team.sharded.id, 0, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY())); - if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){ - var result = clusterAstar(0, node, dest); - if(result != null){ - - Lines.stroke(3f); - Draw.color(Color.orange); - - for(int i = -1; i < result.size - 1; i++){ - int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; - portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); - portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); - Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y); - } - } - - nodeToVec(dest, Tmp.v1); - Fonts.outline.draw(clusterNodeHeuristic(0, node, dest) + "", Tmp.v1.x, Tmp.v1.y); - } - - Draw.reset(); - } - + /* if(fields != null){ for(var entry : fields){ int cx = entry.key % cwidth, cy = entry.key / cwidth; @@ -224,6 +200,7 @@ public class HierarchyPathFinder{ } } } + */ }); @@ -231,6 +208,50 @@ public class HierarchyPathFinder{ } } + /** Starts or restarts the pathfinding thread. */ + private void start(){ + stop(); + if(net.client()) return; + + thread = new Thread(this, "Control Pathfinder"); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setDaemon(true); + thread.start(); + } + + /** Stops the pathfinding thread. */ + private void stop(){ + if(thread != null){ + thread.interrupt(); + thread = null; + } + queue.clear(); + } + + @Override + public void run(){ + while(true){ + if(net.client()) return; + try{ + + if(state.isPlaying()){ + queue.run(); + + //TODO: update everything + } + + try{ + Thread.sleep(updateInterval); + }catch(InterruptedException e){ + //stop looping when interrupted externally + return; + } + }catch(Throwable e){ + e.printStackTrace(); + } + } + } + Vec2 nodeToVec(int current, Vec2 out){ portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); return out; @@ -581,15 +602,20 @@ public class HierarchyPathFinder{ return Math.abs(x1 - x2) + Math.abs(y1 - y2); } - @Nullable IntSeq clusterAstar(int pathCost, int startNodeIndex, int endNodeIndex){ - var v1 = nodeToVec(startNodeIndex, Tmp.v1); - var v2 = nodeToVec(endNodeIndex, Tmp.v2); + @Nullable IntSeq clusterAstar(PathRequest request, int pathCost, int startNodeIndex, int endNodeIndex){ + var result = request.resultPath; if(startNodeIndex == endNodeIndex){ + result.clear(); + result.add(startNodeIndex); //TODO alloc - return IntSeq.with(startNodeIndex); + return result; } + var costs = request.costs; + var cameFrom = request.cameFrom; + var frontier = request.frontier; + frontier.clear(); costs.clear(); cameFrom.clear(); @@ -615,7 +641,7 @@ public class HierarchyPathFinder{ //edges for the cluster the node is 'in' if(innerCons != null){ - checkEdges(pathCost, current, endNodeIndex, cx, cy, innerCons); + checkEdges(request, pathCost, current, endNodeIndex, cx, cy, innerCons); } //edges that this node 'faces' from the other side @@ -626,13 +652,13 @@ public class HierarchyPathFinder{ int relativeDir = (dir + 2) % 4; LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; if(outerCons != null){ - checkEdges(pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); + checkEdges(request, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); } } } if(foundEnd){ - IntSeq result = new IntSeq(); + result.clear(); int cur = endNodeIndex; while(cur != startNodeIndex){ @@ -655,20 +681,20 @@ public class HierarchyPathFinder{ Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); } - void checkEdges(int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ + void checkEdges(PathRequest request, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; float cost = IntraEdge.cost(con); int otherDir = IntraEdge.dir(con), otherPortal = IntraEdge.portal(con); int next = makeNodeIndex(cx, cy, otherDir, otherPortal); - float newCost = costs.get(current) + cost; + float newCost = request.costs.get(current) + cost; - if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ - costs.put(next, newCost); + if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){ + request.costs.put(next, newCost); - frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal)); - cameFrom.put(next, current); + request.frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal)); + request.cameFrom.put(next, current); //TODO debug line(nodeToVec(current, Tmp.v1), nodeToVec(next, Tmp.v2)); From 69e2f6b93b4592bf898af6e6461c98fb15526b59 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 10 Nov 2023 01:57:04 -0500 Subject: [PATCH 12/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 234 +++++++++++------- 1 file changed, 143 insertions(+), 91 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index c2bcb34afa..9c5e27ae1a 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -65,6 +65,12 @@ public class HierarchyPathFinder implements Runnable{ ObjectMap unitRequests = new ObjectMap<>(); //maps position in world in (x + y * width format) to a cache of flow fields IntMap requests = new IntMap<>(); + + + //these are for inner edge A* + IntFloatMap innerCosts = new IntFloatMap(); + PathfindQueue innerFrontier = new PathfindQueue(); + /** Current pathfinding thread */ @Nullable Thread thread; @@ -80,10 +86,16 @@ public class HierarchyPathFinder implements Runnable{ IntIntMap cameFrom = new IntIntMap(); //frontier for A* PathfindQueue frontier = new PathfindQueue(); + + public PathRequest(int destination){ + this.destination = destination; + } } static class FieldCache{ - int destination; + PathCost cost; + int team; + int goalPos; //frontier for flow fields IntQueue frontier = new IntQueue(); //maps cluster index to field weights; 0 means uninitialized @@ -91,6 +103,13 @@ public class HierarchyPathFinder implements Runnable{ //TODO: node map for merging //TODO: how to extend flowfields? + + + public FieldCache(PathCost cost, int team, int goalPos){ + this.cost = cost; + this.team = team; + this.goalPos = goalPos; + } } public HierarchyPathFinder(){ @@ -228,30 +247,6 @@ public class HierarchyPathFinder implements Runnable{ queue.clear(); } - @Override - public void run(){ - while(true){ - if(net.client()) return; - try{ - - if(state.isPlaying()){ - queue.run(); - - //TODO: update everything - } - - try{ - Thread.sleep(updateInterval); - }catch(InterruptedException e){ - //stop looping when interrupted externally - return; - } - }catch(Throwable e){ - e.printStackTrace(); - } - } - } - Vec2 nodeToVec(int current, Vec2 out){ portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); return out; @@ -451,6 +446,9 @@ public class HierarchyPathFinder implements Runnable{ /** @return -1 if no path was found */ float innerAstar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ + var frontier = innerFrontier; + var costs = innerCosts; + frontier.clear(); costs.clear(); @@ -702,34 +700,103 @@ public class HierarchyPathFinder implements Runnable{ } } - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ + private void updateFields(FieldCache cache, long nsToRun){ + var frontier = cache.frontier; + var fields = cache.fields; + var goalPos = cache.goalPos; + var pcost = cache.cost; + var team = cache.team; + + long start = Time.nanos(); + int counter = 0; + + //actually do the flow field part + //TODO spread this out across many frames + while(frontier.size > 0){ + int tile = frontier.removeLast(); + int baseX = tile % wwidth, baseY = tile / wwidth; + int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; + int[] curWeights = fields.get(curWeightIndex); + + int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; + + if(cost != impassable){ + for(Point2 point : Geometry.d4){ + + int + dx = baseX + point.x, dy = baseY + point.y, + clx = dx / clusterSize, cly = dy / clusterSize; + + if(clx < 0 || cly < 0 || dx >= wwidth || dy >= wheight) continue; + + int nextWeightIndex = clx + cly * cwidth; + + int[] weights = nextWeightIndex == curWeightIndex ? curWeights : fields.get(nextWeightIndex); + + //out of bounds; not allowed to move this way because no weights were registered here + if(weights == null) continue; + + int newPos = tile + point.x + point.y * wwidth; + + //can't move back to the goal + if(newPos == goalPos) continue; + + int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; + int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); + int oldCost = weights[newPosArray]; + + //a cost of 0 means uninitialized, OR it means we're at the goal position, but that's handled above + if((oldCost == 0 || oldCost > cost + otherCost) && otherCost != impassable){ + frontier.addFirst(newPos); + weights[newPosArray] = cost + otherCost; + } + } + } + + //every N iterations, check the time spent - this prevents extra calls to nano time, which itself is slow + if(nsToRun >= 0 && (counter++) >= 200){ + counter = 0; + if(Time.timeSinceNanos(start) >= nsToRun){ + return; + } + } + } + } + + public void createPathRequest(Unit unit, int goalX, int goalY){ int costId = 0; - - int node = findClosestNode(unit.team.id, costId, unit.tileX(), unit.tileY()); - int dest = findClosestNode(unit.team.id, costId, World.toTile(destination.x), World.toTile(destination.y)); - - fields = new IntMap<>(); - PathCost pcost = ControlPathfinder.costGround; - int team = Team.sharded.id; - int goalPos = (World.toTile(destination.x) + World.toTile(destination.y) * wwidth); + int team = unit.team.id; - IntQueue frontier = new IntQueue(); - frontier.addFirst(goalPos); + int goalPos = (goalX + goalY * wwidth); - Tile tileOn = unit.tileOn(); + int node = findClosestNode(team, costId, unit.tileX(), unit.tileY()); + int dest = findClosestNode(team, costId, goalX, goalY); - var result = clusterAstar(costId, node, dest); - if(result != null && tileOn != null){ + //TODO: not new? + PathRequest request = new PathRequest(dest); + + var nodePath = clusterAstar(request, costId, node, dest); + + //TODO: how to reuse + FieldCache cache = this.requests.get(goalPos, () -> new FieldCache(pcost, team, goalPos)); + + if(cache.frontier.isEmpty()){ + cache.frontier.addFirst(goalPos); + } + + if(nodePath != null){ int fsize = clusterSize * clusterSize; int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize; + var fields = cache.fields; + fields.put(cx + cy * cwidth, new int[fsize]); - for(int i = -1; i < result.size; i++){ + for(int i = -1; i < nodePath.size; i++){ int - current = i == -1 ? node : result.items[i], + current = i == -1 ? node : nodePath.items[i], cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), dx = Geometry.d4[dir].x, @@ -766,60 +833,16 @@ public class HierarchyPathFinder implements Runnable{ } } } + } - for(int i = -1; i < result.size - 1; i++){ - int current = i == -1 ? node : result.items[i], next = result.items[i + 1]; + } - portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1); - portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2); - line(Tmp.v1, Tmp.v2, Color.orange); - } - - //actually do the flow field part - //TODO spread this out across many frames - while(frontier.size > 0){ - int tile = frontier.removeLast(); - int baseX = tile % wwidth, baseY = tile / wwidth; - int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; - int[] curWeights = fields.get(curWeightIndex); - - int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; - - if(cost != impassable){ - for(Point2 point : Geometry.d4){ - - int - dx = baseX + point.x, dy = baseY + point.y, - clx = dx / clusterSize, cly = dy / clusterSize; - - if(clx < 0 || cly < 0 || dx >= wwidth || dy >= wheight) continue; - - int nextWeightIndex = clx + cly * cwidth; - - int[] weights = nextWeightIndex == curWeightIndex ? curWeights : fields.get(nextWeightIndex); - - //out of bounds; not allowed to move this way because no weights were registered here - if(weights == null) continue; - - int newPos = tile + point.x + point.y * wwidth; - - //can't move back to the goal - if(newPos == goalPos) continue; - - int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; - int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); - int oldCost = weights[newPosArray]; - - //a cost of 0 means uninitialized, OR it means we're at the goal position, but that's handled above - if((oldCost == 0 || oldCost > cost + otherCost) && otherCost != impassable){ - frontier.addFirst(newPos); - weights[newPosArray] = cost + otherCost; - } - } - } - } + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ + int costId = 0; + Tile tileOn = unit.tileOn(); + if(tileOn != null){ int value = getCost(fields, tileOn.x, tileOn.y); Tile current = null; @@ -887,6 +910,35 @@ public class HierarchyPathFinder implements Runnable{ return cost.getCost(team, pathfinder.tiles[tilePos]); } + @Override + public void run(){ + while(true){ + if(net.client()) return; + try{ + + if(state.isPlaying()){ + queue.run(); + + //TODO: update everything else too + + //each update time (not total!) no longer than maxUpdate + for(FieldCache cache : requests.values()){ + updateFields(cache, maxUpdate); + } + } + + try{ + Thread.sleep(updateInterval); + }catch(InterruptedException e){ + //stop looping when interrupted externally + return; + } + }catch(Throwable e){ + e.printStackTrace(); + } + } + } + static class Cluster{ IntSeq[] portals = new IntSeq[4]; //maps rotation + index of portal to list of IntraEdge objects From 24b7d99a691b21162b84d0575665da71fce68f16 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 10 Nov 2023 13:48:41 -0500 Subject: [PATCH 13/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 161 +++++++++++++----- 1 file changed, 118 insertions(+), 43 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 9c5e27ae1a..0a3943be45 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -14,7 +14,6 @@ import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; @@ -53,6 +52,7 @@ public class HierarchyPathFinder implements Runnable{ }; //maps pathCost -> flattened array of clusters in 2D + //(what about teams? different path costs?) Cluster[][] clusters; int cwidth, cheight; @@ -64,42 +64,51 @@ public class HierarchyPathFinder implements Runnable{ //individual requests based on unit ObjectMap unitRequests = new ObjectMap<>(); //maps position in world in (x + y * width format) to a cache of flow fields - IntMap requests = new IntMap<>(); + IntMap fields = new IntMap<>(); //these are for inner edge A* IntFloatMap innerCosts = new IntFloatMap(); PathfindQueue innerFrontier = new PathfindQueue(); + //ONLY modify on pathfinding thread. + IntSet clustersToUpdate = new IntSet(); + IntSet clustersToInnerUpdate = new IntSet(); + /** Current pathfinding thread */ @Nullable Thread thread; //path requests are per-unit //these contain static class PathRequest{ - int destination; + final Unit unit; + final int destination; //resulting path of nodes - IntSeq resultPath = new IntSeq(); + final IntSeq resultPath = new IntSeq(); //node index -> total cost - IntFloatMap costs = new IntFloatMap(); + final IntFloatMap costs = new IntFloatMap(); //node index (NodeIndex struct) -> node it came from TODO merge them - IntIntMap cameFrom = new IntIntMap(); + final IntIntMap cameFrom = new IntIntMap(); //frontier for A* - PathfindQueue frontier = new PathfindQueue(); + final PathfindQueue frontier = new PathfindQueue(); - public PathRequest(int destination){ + //main thread only! + long lastUpdateId; + + public PathRequest(Unit unit, int destination){ + this.unit = unit; this.destination = destination; } } static class FieldCache{ - PathCost cost; - int team; - int goalPos; + final PathCost cost; + final int team; + final int goalPos; //frontier for flow fields - IntQueue frontier = new IntQueue(); + final IntQueue frontier = new IntQueue(); //maps cluster index to field weights; 0 means uninitialized - IntMap fields = new IntMap<>(); + final IntMap fields = new IntMap<>(); //TODO: node map for merging //TODO: how to extend flowfields? @@ -129,7 +138,35 @@ public class HierarchyPathFinder implements Runnable{ //TODO very inefficient, this is only for debugging Events.on(TileChangeEvent.class, e -> { - createCluster(Team.sharded.id, costGround, e.tile.x / clusterSize, e.tile.y / clusterSize); + + e.tile.getLinkedTiles(t -> { + int x = t.x, y = t.y, mx = x % clusterSize, my = y % clusterSize, cx = x / clusterSize, cy = y / clusterSize, cluster = cx + cy * cwidth; + + //is at the edge of a cluster; this means the portals may have changed. + if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){ + queue.post(() -> clustersToUpdate.add(cluster)); + }else{ + //there is no need to recompute portals for block updates that are not on the edge. + queue.post(() -> clustersToInnerUpdate.add(cluster)); + } + }); + + //TODO: if near center of cluster: + //- re-do inner A* only + //- otherwise, re-do everything + + //TODO: recalculate affected flow fields? or just all of them? + }); + + //invalidate paths + Events.run(Trigger.update, () -> { + for(var req : unitRequests.values()){ + //skipped N update -> drop it + if(req.lastUpdateId <= state.updateId - 10){ + //concurrent modification! + Core.app.post(() -> unitRequests.remove(req.unit)); + } + } }); if(debug){ @@ -269,6 +306,7 @@ public class HierarchyPathFinder implements Runnable{ out.set(x, y); } + //TODO: this is never called yet. should be invoked during pathfinding void createCluster(int team, int pathCost, int cx, int cy){ if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight]; Cluster cluster = clusters[pathCost][cy * cwidth + cx]; @@ -524,6 +562,7 @@ public class HierarchyPathFinder implements Runnable{ //TODO PathCost cost = ControlPathfinder.costGround; + //TODO: cluster can be null!! Cluster cluster = clusters[pathCost][cx + cy * cwidth]; int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); @@ -763,23 +802,19 @@ public class HierarchyPathFinder implements Runnable{ } } - public void createPathRequest(Unit unit, int goalX, int goalY){ + public void initializePathRequest(PathRequest request, int team, int unitX, int unitY, int goalX, int goalY){ int costId = 0; PathCost pcost = ControlPathfinder.costGround; - int team = unit.team.id; int goalPos = (goalX + goalY * wwidth); - int node = findClosestNode(team, costId, unit.tileX(), unit.tileY()); + int node = findClosestNode(team, costId, unitX, unitY); int dest = findClosestNode(team, costId, goalX, goalY); - //TODO: not new? - PathRequest request = new PathRequest(dest); - var nodePath = clusterAstar(request, costId, node, dest); //TODO: how to reuse - FieldCache cache = this.requests.get(goalPos, () -> new FieldCache(pcost, team, goalPos)); + FieldCache cache = this.fields.get(goalPos, () -> new FieldCache(pcost, team, goalPos)); if(cache.frontier.isEmpty()){ cache.frontier.addFirst(goalPos); @@ -788,7 +823,7 @@ public class HierarchyPathFinder implements Runnable{ if(nodePath != null){ int fsize = clusterSize * clusterSize; - int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize; + int cx = unitX / clusterSize, cy = unitY / clusterSize; var fields = cache.fields; @@ -840,37 +875,63 @@ public class HierarchyPathFinder implements Runnable{ public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ int costId = 0; - Tile tileOn = unit.tileOn(); + PathRequest request = unitRequests.get(unit); + int + destX = World.toTile(destination.x), + destY = World.toTile(destination.y) * wwidth, + destPos = destX + destY * wwidth; - if(tileOn != null){ - int value = getCost(fields, tileOn.x, tileOn.y); + //TODO: collect old requests that have not been accessed in a while. not sure where. + request.lastUpdateId = state.updateId; - Tile current = null; - int tl = 0; - //TODO: use raycasting and iterate on this for N steps - for(Point2 point : Geometry.d8){ - int dx = tileOn.x + point.x, dy = tileOn.y + point.y; + //use existing request if it exists. + if(request != null && request.destination == destPos){ - Tile other = world.tile(dx, dy); + Tile tileOn = unit.tileOn(); + //TODO: should fields be accessible from this thread? + FieldCache fieldCache = fields.get(destPos); - if(other == null) continue; + if(tileOn != null && fieldCache != null){ + int value = getCost(fieldCache.fields, tileOn.x, tileOn.y); - int packed = world.packArray(dx, dy); - int otherCost = getCost(fields, dx, dy); + Tile current = null; + int tl = 0; + //TODO: use raycasting and iterate on this for N steps + for(Point2 point : Geometry.d8){ + int dx = tileOn.x + point.x, dy = tileOn.y + point.y; - if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && - !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || + Tile other = world.tile(dx, dy); + + if(other == null) continue; + + int packed = world.packArray(dx, dy); + int otherCost = getCost(fieldCache.fields, dx, dy); + + if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && + !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap - current = other; - tl = otherCost; + current = other; + tl = otherCost; + } + } + + if(!(current == null || tl == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ + out.set(current); + return true; } } - if(!(current == null || tl == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ - out.set(current); - return true; - } + }else{ + //queue new request. + unitRequests.put(unit, request = new PathRequest(unit, destPos)); + + PathRequest f = request; + + //on the pathfinding thread: initialize the request, meaning + queue.post(() -> { + initializePathRequest(f, unit.team.id, unit.tileX(), unit.tileY(), destX, destY); + }); } noResultFound[0] = true; @@ -919,10 +980,24 @@ public class HierarchyPathFinder implements Runnable{ if(state.isPlaying()){ queue.run(); + clustersToUpdate.each(cluster -> { + + //just in case: don't redundantly update inner clusters after you've recalculated it entirely + clustersToInnerUpdate.remove(cluster); + }); + + clustersToInnerUpdate.each(cluster -> { + + //only recompute the inner links + }); + + clustersToInnerUpdate.clear(); + clustersToUpdate.clear(); + //TODO: update everything else too //each update time (not total!) no longer than maxUpdate - for(FieldCache cache : requests.values()){ + for(FieldCache cache : fields.values()){ updateFields(cache, maxUpdate); } } From 5a14302d68fe1623e9d0212ccf6f2124240e15ff Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 11 Nov 2023 10:41:53 -0500 Subject: [PATCH 14/35] progress --- core/src/mindustry/ai/ControlPathfinder.java | 13 ++ .../src/mindustry/ai/HierarchyPathFinder.java | 153 +++++++++++++----- 2 files changed, 122 insertions(+), 44 deletions(-) diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index 98033a4da2..f6b166270e 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -62,6 +62,19 @@ public class ControlPathfinder{ ((PathTile.team(tile) != team && PathTile.team(tile) != 0) && PathTile.solid(tile) ? wallImpassableCap : 0) + (PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 6 : 0); + public static final int + costIdGround = 0, + costIdHover = 1, + costIdLegs = 2, + costIdNaval = 3; + + public static final Seq costTypes = Seq.with( + costGround, + costHover, + costLegs, + costNaval + ); + public static boolean showDebug = false; //static access probably faster than object access diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 0a3943be45..d4875637ba 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -51,9 +51,9 @@ public class HierarchyPathFinder implements Runnable{ 0, -1 }; - //maps pathCost -> flattened array of clusters in 2D + //maps team -> pathCost -> flattened array of clusters in 2D //(what about teams? different path costs?) - Cluster[][] clusters; + Cluster[][][] clusters; int cwidth, cheight; @@ -82,7 +82,7 @@ public class HierarchyPathFinder implements Runnable{ //these contain static class PathRequest{ final Unit unit; - final int destination; + final int destination, team; //resulting path of nodes final IntSeq resultPath = new IntSeq(); //node index -> total cost @@ -93,10 +93,11 @@ public class HierarchyPathFinder implements Runnable{ final PathfindQueue frontier = new PathfindQueue(); //main thread only! - long lastUpdateId; + long lastUpdateId = state.updateId; - public PathRequest(Unit unit, int destination){ + public PathRequest(Unit unit, int team, int destination){ this.unit = unit; + this.team = team; this.destination = destination; } } @@ -129,7 +130,7 @@ public class HierarchyPathFinder implements Runnable{ stop(); //TODO 5 path costs, arbitrary number - clusters = new Cluster[5][]; + clusters = new Cluster[256][][]; cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); @@ -144,7 +145,16 @@ public class HierarchyPathFinder implements Runnable{ //is at the edge of a cluster; this means the portals may have changed. if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){ - queue.post(() -> clustersToUpdate.add(cluster)); + + + if(mx == 0) queueClusterUpdate(cx - 1, cy); //left + if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom + if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right + if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top + + + queueClusterUpdate(cx, cy); + //TODO: recompute edge clusters too. }else{ //there is no need to recompute portals for block updates that are not on the edge. queue.post(() -> clustersToInnerUpdate.add(cluster)); @@ -169,7 +179,7 @@ public class HierarchyPathFinder implements Runnable{ } }); - if(debug){ + if(debug && false){ Events.run(Trigger.draw, () -> { int team = Team.sharded.id; int cost = costGround; @@ -180,7 +190,7 @@ public class HierarchyPathFinder implements Runnable{ Lines.stroke(1f); for(int cx = 0; cx < cwidth; cx++){ for(int cy = 0; cy < cheight; cy++){ - var cluster = clusters[cost][cy * cwidth + cx]; + var cluster = clusters[Team.sharded.id][cost][cy * cwidth + cx]; if(cluster != null){ Lines.stroke(0.5f); Draw.color(Color.gray); @@ -264,6 +274,35 @@ public class HierarchyPathFinder implements Runnable{ } } + void queueClusterUpdate(int cx, int cy){ + if(cx >= 0 && cy >= 0 && cx < cwidth && cy < cheight){ + queue.post(() -> clustersToUpdate.add(cx + cy * cwidth)); + } + } + + //DEBUGGING ONLY + Vec2 nodeToVec(int current, Vec2 out){ + portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); + return out; + } + + void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){ + portalToVec(clusters[Team.sharded.id][pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out); + } + + void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ + int pos = cluster.portals[direction].items[portalIndex]; + int from = Point2.x(pos), to = Point2.y(pos); + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + float average = (from + to) / 2f; + + float + x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize, + y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize; + + out.set(x, y); + } + /** Starts or restarts the pathfinding thread. */ private void start(){ stop(); @@ -284,34 +323,56 @@ public class HierarchyPathFinder implements Runnable{ queue.clear(); } - Vec2 nodeToVec(int current, Vec2 out){ - portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); - return out; + /** @return a cluster at coordinates; can be null if not cluster was created yet*/ + @Nullable Cluster getCluster(int team, int pathCost, int cx, int cy){ + return getCluster(team, pathCost, cx + cy * cwidth); } - void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){ - portalToVec(clusters[pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out); + /** @return a cluster at coordinates; can be null if not cluster was created yet*/ + @Nullable Cluster getCluster(int team, int pathCost, int clusterIndex){ + Cluster[][] dim1 = clusters[team]; + + if(dim1 == null) return null; + + Cluster[] dim2 = dim1[pathCost]; + + if(dim2 == null) return null; + + return dim2[clusterIndex]; } - void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ - int pos = cluster.portals[direction].items[portalIndex]; - int from = Point2.x(pos), to = Point2.y(pos); - int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; - float average = (from + to) / 2f; + /** @return the cluster at specified coordinates; never null. */ + Cluster getCreateCluster(int team, int pathCost, int cx, int cy){ + return getCreateCluster(team, pathCost, cx + cy * cwidth); + } - float - x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize, - y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize; - - out.set(x, y); + /** @return the cluster at specified coordinates; never null. */ + Cluster getCreateCluster(int team, int pathCost, int clusterIndex){ + Cluster result = getCluster(team, pathCost, clusterIndex); + if(result == null){ + return createCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + }else{ + return result; + } } //TODO: this is never called yet. should be invoked during pathfinding - void createCluster(int team, int pathCost, int cx, int cy){ - if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight]; - Cluster cluster = clusters[pathCost][cy * cwidth + cx]; + Cluster createCluster(int team, int pathCost, int cx, int cy){ + Cluster[][] dim1 = clusters[team]; + + if(dim1 == null){ + dim1 = clusters[team] = new Cluster[Team.all.length][]; + } + + Cluster[] dim2 = dim1[pathCost]; + + if(dim2 == null){ + dim2 = dim1[pathCost] = new Cluster[cwidth * cheight]; + } + + Cluster cluster = dim2[cy * cwidth + cx]; if(cluster == null){ - cluster = clusters[pathCost][cy * cwidth + cx] = new Cluster(); + cluster = dim2[cy * cwidth + cx] = new Cluster(); }else{ //reset data for(var p : cluster.portals){ @@ -334,7 +395,7 @@ public class HierarchyPathFinder implements Runnable{ continue; } - Cluster other = clusters[pathCost][otherX + otherY * cwidth]; + Cluster other = dim2[otherX + otherY * cwidth]; IntSeq portals; if(other == null){ @@ -388,6 +449,8 @@ public class HierarchyPathFinder implements Runnable{ } connectInnerEdges(cx, cy, team, cost, cluster); + + return cluster; } void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){ @@ -563,7 +626,7 @@ public class HierarchyPathFinder implements Runnable{ PathCost cost = ControlPathfinder.costGround; //TODO: cluster can be null!! - Cluster cluster = clusters[pathCost][cx + cy * cwidth]; + Cluster cluster = getCreateCluster(team, pathCost, cx, cy); int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); int bestPortalPair = Integer.MAX_VALUE; @@ -616,7 +679,7 @@ public class HierarchyPathFinder implements Runnable{ } //distance heuristic: manhattan - private float clusterNodeHeuristic(int pathCost, int nodeA, int nodeB){ + private float clusterNodeHeuristic(int team, int pathCost, int nodeA, int nodeB){ int clusterA = NodeIndex.cluster(nodeA), dirA = NodeIndex.dir(nodeA), @@ -624,8 +687,8 @@ public class HierarchyPathFinder implements Runnable{ clusterB = NodeIndex.cluster(nodeB), dirB = NodeIndex.dir(nodeB), portalB = NodeIndex.portal(nodeB), - rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA], - rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB]; + rangeA = getCreateCluster(team, pathCost, clusterA).portals[dirA].items[portalA], + rangeB = getCreateCluster(team, pathCost, clusterB).portals[dirB].items[portalB]; float averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f, @@ -652,6 +715,7 @@ public class HierarchyPathFinder implements Runnable{ var costs = request.costs; var cameFrom = request.cameFrom; var frontier = request.frontier; + var team = request.team; frontier.clear(); costs.clear(); @@ -673,23 +737,22 @@ public class HierarchyPathFinder implements Runnable{ int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current); int cx = cluster % cwidth, cy = cluster / cwidth; - Cluster clust = clusters[pathCost][cluster]; + Cluster clust = getCreateCluster(team, pathCost, cluster); LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal]; //edges for the cluster the node is 'in' if(innerCons != null){ - checkEdges(request, pathCost, current, endNodeIndex, cx, cy, innerCons); + checkEdges(request, team, pathCost, current, endNodeIndex, cx, cy, innerCons); } //edges that this node 'faces' from the other side int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y; if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){ - int nextClusteri = nextCx + nextCy * cwidth; - Cluster nextCluster = clusters[pathCost][nextClusteri]; + Cluster nextCluster = getCreateCluster(team, pathCost, nextCx, nextCy); int relativeDir = (dir + 2) % 4; LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; if(outerCons != null){ - checkEdges(request, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); + checkEdges(request, team, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); } } } @@ -718,7 +781,7 @@ public class HierarchyPathFinder implements Runnable{ Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); } - void checkEdges(PathRequest request, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ + void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; float cost = IntraEdge.cost(con); @@ -730,7 +793,7 @@ public class HierarchyPathFinder implements Runnable{ if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){ request.costs.put(next, newCost); - request.frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal)); + request.frontier.add(next, newCost + clusterNodeHeuristic(team, pathCost, next, goal)); request.cameFrom.put(next, current); //TODO debug @@ -881,11 +944,10 @@ public class HierarchyPathFinder implements Runnable{ destY = World.toTile(destination.y) * wwidth, destPos = destX + destY * wwidth; - //TODO: collect old requests that have not been accessed in a while. not sure where. - request.lastUpdateId = state.updateId; - //use existing request if it exists. if(request != null && request.destination == destPos){ + //TODO: collect old requests that have not been accessed in a while. not sure where. + request.lastUpdateId = state.updateId; Tile tileOn = unit.tileOn(); //TODO: should fields be accessible from this thread? @@ -923,8 +985,9 @@ public class HierarchyPathFinder implements Runnable{ } }else{ + //queue new request. - unitRequests.put(unit, request = new PathRequest(unit, destPos)); + unitRequests.put(unit, request = new PathRequest(unit, unit.team.id, destPos)); PathRequest f = request; @@ -980,8 +1043,10 @@ public class HierarchyPathFinder implements Runnable{ if(state.isPlaying()){ queue.run(); + //TODO: WHICH clusters need to update here? do I iterate through 256 teams every time? ugh clustersToUpdate.each(cluster -> { + //just in case: don't redundantly update inner clusters after you've recalculated it entirely clustersToInnerUpdate.remove(cluster); }); From 0cf27034cff50205bc9bbdc41c922c2119adc391 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 11 Nov 2023 11:26:42 -0500 Subject: [PATCH 15/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index d4875637ba..38ce549794 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -114,7 +114,6 @@ public class HierarchyPathFinder implements Runnable{ //TODO: node map for merging //TODO: how to extend flowfields? - public FieldCache(PathCost cost, int team, int goalPos){ this.cost = cost; this.team = team; @@ -350,14 +349,13 @@ public class HierarchyPathFinder implements Runnable{ Cluster getCreateCluster(int team, int pathCost, int clusterIndex){ Cluster result = getCluster(team, pathCost, clusterIndex); if(result == null){ - return createCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + return updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); }else{ return result; } } - //TODO: this is never called yet. should be invoked during pathfinding - Cluster createCluster(int team, int pathCost, int cx, int cy){ + Cluster updateCluster(int team, int pathCost, int cx, int cy){ Cluster[][] dim1 = clusters[team]; if(dim1 == null){ @@ -380,13 +378,7 @@ public class HierarchyPathFinder implements Runnable{ } } - //clear all connections, since portals changed, they need to be recomputed. - cluster.portalConnections = new LongSeq[4][]; - - //TODO: other cluster inner edges should be recomputed if changed. - - //TODO look it up based on number. - PathCost cost = ControlPathfinder.costGround; + PathCost cost = ControlPathfinder.costTypes.get(pathCost); for(int direction = 0; direction < 4; direction++){ int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction); @@ -448,20 +440,22 @@ public class HierarchyPathFinder implements Runnable{ } } - connectInnerEdges(cx, cy, team, cost, cluster); + updateInnerEdges(team, cost, cx, cy, cluster); return cluster; } - void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){ + void updateInnerEdges(int team, int cost, int cx, int cy, Cluster cluster){ + updateInnerEdges(team, ControlPathfinder.costTypes.get(cost), cx, cy, cluster); + } + + void updateInnerEdges(int team, PathCost cost, int cx, int cy, Cluster cluster){ int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); usedEdges.clear(); - //TODO: how the hell to identify a vertex? - //cluster (i16) | direction (i2) | index (i14) - - //TODO: clear portal connections + //clear all connections, since portals changed, they need to be recomputed. + cluster.portalConnections = new LongSeq[4][]; for(int direction = 0; direction < 4; direction++){ var portals = cluster.portals[direction]; @@ -485,7 +479,6 @@ public class HierarchyPathFinder implements Runnable{ for(int j = 0; j < otherPortals.size; j++){ - //TODO redundant calculations? if(!usedEdges.contains(Point2.pack(otherDir, j))){ int @@ -1034,6 +1027,40 @@ public class HierarchyPathFinder implements Runnable{ return cost.getCost(team, pathfinder.tiles[tilePos]); } + private void updateClustersComplete(int clusterIndex){ + for(int team = 0; team < clusters.length; team++){ + var dim1 = clusters[team]; + if(dim1 != null){ + for(int pathCost = 0; pathCost < dim1.length; pathCost++){ + var dim2 = dim1[pathCost]; + if(dim2 != null){ + var cluster = dim2[clusterIndex]; + if(cluster != null){ + updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + } + } + } + } + } + } + + private void updateClustersInner(int clusterIndex){ + for(int team = 0; team < clusters.length; team++){ + var dim1 = clusters[team]; + if(dim1 != null){ + for(int pathCost = 0; pathCost < dim1.length; pathCost++){ + var dim2 = dim1[pathCost]; + if(dim2 != null){ + var cluster = dim2[clusterIndex]; + if(cluster != null){ + updateInnerEdges(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth, cluster); + } + } + } + } + } + } + @Override public void run(){ while(true){ @@ -1043,17 +1070,16 @@ public class HierarchyPathFinder implements Runnable{ if(state.isPlaying()){ queue.run(); - //TODO: WHICH clusters need to update here? do I iterate through 256 teams every time? ugh clustersToUpdate.each(cluster -> { - + updateClustersComplete(cluster); //just in case: don't redundantly update inner clusters after you've recalculated it entirely clustersToInnerUpdate.remove(cluster); }); clustersToInnerUpdate.each(cluster -> { - //only recompute the inner links + updateClustersInner(cluster); }); clustersToInnerUpdate.clear(); From cacfe063625acff8b8a1be40dc54b940ad229c94 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 11 Nov 2023 12:28:10 -0500 Subject: [PATCH 16/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 250 +++++++++++------- 1 file changed, 148 insertions(+), 102 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 38ce549794..3732e4709b 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -61,8 +61,13 @@ public class HierarchyPathFinder implements Runnable{ IntSet usedEdges = new IntSet(); //tasks to run on pathfinding thread TaskQueue queue = new TaskQueue(); - //individual requests based on unit + + //individual requests based on unit - MAIN THREAD ONLY ObjectMap unitRequests = new ObjectMap<>(); + + //TODO: very dangerous usage; + //TODO - it is accessed from the main thread + //TODO - it is written to on the pathfinding thread //maps position in world in (x + y * width format) to a cache of flow fields IntMap fields = new IntMap<>(); @@ -79,7 +84,6 @@ public class HierarchyPathFinder implements Runnable{ @Nullable Thread thread; //path requests are per-unit - //these contain static class PathRequest{ final Unit unit; final int destination, team; @@ -111,8 +115,10 @@ public class HierarchyPathFinder implements Runnable{ //maps cluster index to field weights; 0 means uninitialized final IntMap fields = new IntMap<>(); - //TODO: node map for merging - //TODO: how to extend flowfields? + //main thread only! + long lastUpdateId = state.updateId; + + //TODO: how are the nodes merged? CAN they be merged? public FieldCache(PathCost cost, int team, int goalPos){ this.cost = cost; @@ -128,7 +134,7 @@ public class HierarchyPathFinder implements Runnable{ Events.on(WorldLoadEvent.class, event -> { stop(); - //TODO 5 path costs, arbitrary number + //TODO: can the pathfinding thread even see these? clusters = new Cluster[256][][]; cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); @@ -136,7 +142,6 @@ public class HierarchyPathFinder implements Runnable{ start(); }); - //TODO very inefficient, this is only for debugging Events.on(TileChangeEvent.class, e -> { e.tile.getLinkedTiles(t -> { @@ -160,10 +165,6 @@ public class HierarchyPathFinder implements Runnable{ } }); - //TODO: if near center of cluster: - //- re-do inner A* only - //- otherwise, re-do everything - //TODO: recalculate affected flow fields? or just all of them? }); @@ -176,9 +177,17 @@ public class HierarchyPathFinder implements Runnable{ Core.app.post(() -> unitRequests.remove(req.unit)); } } + + for(var field : fields.values()){ + //skipped N update -> drop it + if(field.lastUpdateId <= state.updateId - 20){ + //make sure it's only modified on the main thread...? but what about calling get() on this thread?? + queue.post(() -> fields.remove(field.goalPos)); + } + } }); - if(debug && false){ + if(debug){ Events.run(Trigger.draw, () -> { int team = Team.sharded.id; int cost = costGround; @@ -189,7 +198,9 @@ public class HierarchyPathFinder implements Runnable{ Lines.stroke(1f); for(int cx = 0; cx < cwidth; cx++){ for(int cy = 0; cy < cheight; cy++){ - var cluster = clusters[Team.sharded.id][cost][cy * cwidth + cx]; + if(clusters[Team.sharded.id] == null || clusters[team][cost] == null) continue; + + var cluster = clusters[team][cost][cy * cwidth + cx]; if(cluster != null){ Lines.stroke(0.5f); Draw.color(Color.gray); @@ -279,6 +290,14 @@ public class HierarchyPathFinder implements Runnable{ } } + static void line(Vec2 a, Vec2 b){ + Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); + } + + static void line(Vec2 a, Vec2 b, Color color){ + Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); + } + //DEBUGGING ONLY Vec2 nodeToVec(int current, Vec2 out){ portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); @@ -329,6 +348,8 @@ public class HierarchyPathFinder implements Runnable{ /** @return a cluster at coordinates; can be null if not cluster was created yet*/ @Nullable Cluster getCluster(int team, int pathCost, int clusterIndex){ + if(clusters == null) return null; + Cluster[][] dim1 = clusters[team]; if(dim1 == null) return null; @@ -356,6 +377,8 @@ public class HierarchyPathFinder implements Runnable{ } Cluster updateCluster(int team, int pathCost, int cx, int cy){ + //TODO: what if clusters are null for thread visibility reasons? + Cluster[][] dim1 = clusters[team]; if(dim1 == null){ @@ -378,7 +401,7 @@ public class HierarchyPathFinder implements Runnable{ } } - PathCost cost = ControlPathfinder.costTypes.get(pathCost); + PathCost cost = idToCost(pathCost); for(int direction = 0; direction < 4; direction++){ int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction); @@ -446,7 +469,7 @@ public class HierarchyPathFinder implements Runnable{ } void updateInnerEdges(int team, int cost, int cx, int cy, Cluster cluster){ - updateInnerEdges(team, ControlPathfinder.costTypes.get(cost), cx, cy, cluster); + updateInnerEdges(team, idToCost(cost), cx, cy, cluster); } void updateInnerEdges(int team, PathCost cost, int cx, int cy, Cluster cluster){ @@ -556,7 +579,7 @@ public class HierarchyPathFinder implements Runnable{ int cx = current % wwidth, cy = current / wwidth; //found the goal (it's in the portal rectangle) - //TODO portal rectangle approach does not work. + //TODO portal rectangle approach does not work, making this slower than it should be if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){ return costs.get(current); } @@ -615,59 +638,54 @@ public class HierarchyPathFinder implements Runnable{ return Integer.MAX_VALUE; } - //TODO - PathCost cost = ControlPathfinder.costGround; - - //TODO: cluster can be null!! + PathCost cost = idToCost(pathCost); Cluster cluster = getCreateCluster(team, pathCost, cx, cy); int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); int bestPortalPair = Integer.MAX_VALUE; float bestCost = Float.MAX_VALUE; - if(cluster != null){ //TODO create on demand?? + //A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra) + for(int dir = 0; dir < 4; dir++){ + var portals = cluster.portals[dir]; + if(portals == null) continue; - //A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra) - for(int dir = 0; dir < 4; dir++){ - var portals = cluster.portals[dir]; - if(portals == null) continue; + for(int j = 0; j < portals.size; j++){ - for(int j = 0; j < portals.size; j++){ + int + other = portals.items[j], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + otherAverage = (otherFrom + otherTo) / 2, + ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + otherX = (moveDirs[dir * 2] * otherAverage + ox), + otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy); - int - other = portals.items[j], - otherFrom = Point2.x(other), otherTo = Point2.y(other), - otherAverage = (otherFrom + otherTo) / 2, - ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), - oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), - otherX = (moveDirs[dir * 2] * otherAverage + ox), - otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy); + float connectionCost = innerAstar( + team, cost, + minX, minY, maxX, maxY, + tileX + tileY * wwidth, + otherX + otherY * wwidth, + //TODO these are wrong and never actually trigger + (moveDirs[dir * 2] * otherFrom + ox), + (moveDirs[dir * 2 + 1] * otherFrom + oy), + (moveDirs[dir * 2] * otherTo + ox), + (moveDirs[dir * 2 + 1] * otherTo + oy) + ); - float connectionCost = innerAstar( - team, cost, - minX, minY, maxX, maxY, - tileX + tileY * wwidth, - otherX + otherY * wwidth, - //TODO these are wrong and never actually trigger - (moveDirs[dir * 2] * otherFrom + ox), - (moveDirs[dir * 2 + 1] * otherFrom + oy), - (moveDirs[dir * 2] * otherTo + ox), - (moveDirs[dir * 2 + 1] * otherTo + oy) - ); - - //better cost found, update and return - if(connectionCost != -1f && connectionCost < bestCost){ - bestPortalPair = Point2.pack(dir, j); - bestCost = connectionCost; - } + //better cost found, update and return + if(connectionCost != -1f && connectionCost < bestCost){ + bestPortalPair = Point2.pack(dir, j); + bestCost = connectionCost; } } - - if(bestPortalPair != Integer.MAX_VALUE){ - return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair)); - } } + if(bestPortalPair != Integer.MAX_VALUE){ + return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair)); + } + + return Integer.MAX_VALUE; } @@ -701,7 +719,6 @@ public class HierarchyPathFinder implements Runnable{ if(startNodeIndex == endNodeIndex){ result.clear(); result.add(startNodeIndex); - //TODO alloc return result; } @@ -766,15 +783,7 @@ public class HierarchyPathFinder implements Runnable{ return null; } - static void line(Vec2 a, Vec2 b){ - Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); - } - - static void line(Vec2 a, Vec2 b, Color color){ - Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); - } - - void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ + private void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ for(int i = 0; i < connections.size; i++){ long con = connections.items[i]; float cost = IntraEdge.cost(con); @@ -788,9 +797,6 @@ public class HierarchyPathFinder implements Runnable{ request.frontier.add(next, newCost + clusterNodeHeuristic(team, pathCost, next, goal)); request.cameFrom.put(next, current); - - //TODO debug - line(nodeToVec(current, Tmp.v1), nodeToVec(next, Tmp.v2)); } } } @@ -806,7 +812,6 @@ public class HierarchyPathFinder implements Runnable{ int counter = 0; //actually do the flow field part - //TODO spread this out across many frames while(frontier.size > 0){ int tile = frontier.removeLast(); int baseX = tile % wwidth, baseY = tile / wwidth; @@ -858,9 +863,58 @@ public class HierarchyPathFinder implements Runnable{ } } - public void initializePathRequest(PathRequest request, int team, int unitX, int unitY, int goalX, int goalY){ - int costId = 0; - PathCost pcost = ControlPathfinder.costGround; + private void addFlowCluster(FieldCache cache, int cluster, boolean addingFrontier){ + addFlowCluster(cache, cluster % cwidth, cluster / cwidth, addingFrontier); + } + + private void addFlowCluster(FieldCache cache, int cx, int cy, boolean addingFrontier){ + //out of bounds + if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight) return; + + var fields = cache.fields; + int key = cx + cy * cwidth; + + if(!fields.containsKey(key)){ + fields.put(key, new int[clusterSize * clusterSize]); + + //TODO: now, scan d4 for nearby clusters. + if(addingFrontier){ + + for(int dir = 0; dir < 4; dir++){ + int ox = cx + moveDirs[dir * 2], oy = cy + moveDirs[dir * 2 + 1]; + + if(ox < 0 || oy < 0 || ox >= cwidth || ox >= cheight) continue; + + var otherField = cache.fields.get(ox + oy * cwidth); + + if(otherField == null) continue; + + int + relOffset = (dir + 2) % 4, + movex = moveDirs[relOffset * 2], + movey = moveDirs[relOffset * 2 + 1], + otherx1 = offsets[relOffset * 2] * (clusterSize - 1), + othery1 = offsets[relOffset * 2 + 1] * (clusterSize - 1); + + //scan the edge of the cluster + for(int i = 0; i < clusterSize; i++){ + int x = otherx1 + movex * i, y = othery1 + movey * i; + + //check to make sure it's not 0 (uninitialized flowfield data) + if(otherField[x + y * clusterSize] != 0){ + int worldX = x + ox * clusterSize, worldY = y + oy * clusterSize; + + //add the world-relative position to the frontier, so it recalculates + cache.frontier.addFirst(worldX + worldY * wwidth); + } + } + } + } + } + } + + private void initializePathRequest(PathRequest request, int team, int costId, int unitX, int unitY, int goalX, int goalY){ + PathCost pcost = idToCost(costId); int goalPos = (goalX + goalY * wwidth); @@ -869,21 +923,22 @@ public class HierarchyPathFinder implements Runnable{ var nodePath = clusterAstar(request, costId, node, dest); - //TODO: how to reuse - FieldCache cache = this.fields.get(goalPos, () -> new FieldCache(pcost, team, goalPos)); + //TODO: how to reuse properly. what if the flowfields don't go through this position (the fields are finished?) how to incrementally extend the flowfield? + FieldCache cache = fields.get(goalPos); + //if true, extra values are added on the sides of existing field cells that face new cells. + boolean addingFrontier = true; - if(cache.frontier.isEmpty()){ + //create the cache if it doesn't exist, and initialize it + if(cache == null){ + fields.put(goalPos, cache = new FieldCache(pcost, team, goalPos)); cache.frontier.addFirst(goalPos); + addingFrontier = false; //when it's a new field, there is no need to add to the frontier to merge the flowfield } if(nodePath != null){ - - int fsize = clusterSize * clusterSize; int cx = unitX / clusterSize, cy = unitY / clusterSize; - var fields = cache.fields; - - fields.put(cx + cy * cwidth, new int[fsize]); + addFlowCluster(cache, cx, cy, addingFrontier); for(int i = -1; i < nodePath.size; i++){ int @@ -895,41 +950,35 @@ public class HierarchyPathFinder implements Runnable{ ox = cluster % cwidth + dx, oy = cluster / cwidth + dy; - //store current cluster in the path list - if(!fields.containsKey(cluster)){ - fields.put(cluster, new int[fsize]); - } + addFlowCluster(cache, cluster, addingFrontier); - //store directionals TODO out of bounds + //store directionals TODO can be out of bounds for(Point2 p : Geometry.d4){ - int other = cluster + p.x + p.y * cwidth; - if(!fields.containsKey(other)){ - fields.put(other, new int[fsize]); - } + addFlowCluster(cache, cluster + p.x + p.y * cwidth, addingFrontier); } //store directional/flipped version of cluster if(ox >= 0 && oy >= 0 && ox < cwidth && oy < cheight){ int other = ox + oy * cwidth; - if(!fields.containsKey(other)){ - fields.put(other, new int[fsize]); - } + + addFlowCluster(cache, other, addingFrontier); //store directionals again for(Point2 p : Geometry.d4){ - int other2 = other + p.x + p.y * cwidth; - if(!fields.containsKey(other2)){ - fields.put(other2, new int[fsize]); - } + addFlowCluster(cache, other + p.x + p.y * cwidth, addingFrontier); } } } } + } + private PathCost idToCost(int costId){ + return ControlPathfinder.costTypes.get(costId); } public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ int costId = 0; + PathCost cost = idToCost(costId); PathRequest request = unitRequests.get(unit); int @@ -939,7 +988,6 @@ public class HierarchyPathFinder implements Runnable{ //use existing request if it exists. if(request != null && request.destination == destPos){ - //TODO: collect old requests that have not been accessed in a while. not sure where. request.lastUpdateId = state.updateId; Tile tileOn = unit.tileOn(); @@ -962,9 +1010,9 @@ public class HierarchyPathFinder implements Runnable{ int packed = world.packArray(dx, dy); int otherCost = getCost(fieldCache.fields, dx, dy); - if(otherCost < value && (current == null || otherCost < tl) && passable(ControlPathfinder.costGround, unit.team.id, packed) && - !(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || - (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap + if(otherCost < value && (current == null || otherCost < tl) && passable(cost, unit.team.id, packed) && + !(point.x != 0 && point.y != 0 && (!passable(cost, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || + (!passable(cost, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap current = other; tl = otherCost; @@ -986,7 +1034,7 @@ public class HierarchyPathFinder implements Runnable{ //on the pathfinding thread: initialize the request, meaning queue.post(() -> { - initializePathRequest(f, unit.team.id, unit.tileX(), unit.tileY(), destX, destY); + initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY); }); } @@ -1085,8 +1133,6 @@ public class HierarchyPathFinder implements Runnable{ clustersToInnerUpdate.clear(); clustersToUpdate.clear(); - //TODO: update everything else too - //each update time (not total!) no longer than maxUpdate for(FieldCache cache : fields.values()){ updateFields(cache, maxUpdate); From af7598dcc6c5800208bbe5e26cacf29b80c3939d Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 11 Nov 2023 14:20:48 -0500 Subject: [PATCH 17/35] Actually functional, but terrible --- .../src/mindustry/ai/HierarchyPathFinder.java | 287 ++++++++++++------ core/src/mindustry/ai/RtsAI.java | 2 + core/src/mindustry/ai/types/CommandAI.java | 5 +- 3 files changed, 206 insertions(+), 88 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 3732e4709b..67064abdb7 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -22,7 +22,7 @@ import static mindustry.ai.Pathfinder.*; //https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf //https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf public class HierarchyPathFinder implements Runnable{ - private static final long maxUpdate = Time.millisToNanos(12); + private static final long maxUpdate = 100;//Time.millisToNanos(12); private static final int updateFPS = 30; private static final int updateInterval = 1000 / updateFPS; @@ -70,9 +70,10 @@ public class HierarchyPathFinder implements Runnable{ //TODO - it is written to on the pathfinding thread //maps position in world in (x + y * width format) to a cache of flow fields IntMap fields = new IntMap<>(); + //MAIN THREAD ONLY + Seq fieldList = new Seq<>(false); - - //these are for inner edge A* + //these are for inner edge A* (temporary!) IntFloatMap innerCosts = new IntFloatMap(); PathfindQueue innerFrontier = new PathfindQueue(); @@ -98,6 +99,10 @@ public class HierarchyPathFinder implements Runnable{ //main thread only! long lastUpdateId = state.updateId; + volatile boolean notFound = false; + + int lastTile; //TODO only re-raycast when unit moves a tile. + @Nullable Tile lastTargetTile; public PathRequest(Unit unit, int team, int destination){ this.unit = unit; @@ -135,10 +140,15 @@ public class HierarchyPathFinder implements Runnable{ stop(); //TODO: can the pathfinding thread even see these? + unitRequests = new ObjectMap<>(); + fields = new IntMap<>(); + fieldList = new Seq<>(false); + clusters = new Cluster[256][][]; cwidth = Mathf.ceil((float)world.width() / clusterSize); cheight = Mathf.ceil((float)world.height() / clusterSize); + start(); }); @@ -165,7 +175,7 @@ public class HierarchyPathFinder implements Runnable{ } }); - //TODO: recalculate affected flow fields? or just all of them? + //TODO: recalculate affected flow fields? or just all of them? how to reflow? }); //invalidate paths @@ -178,74 +188,74 @@ public class HierarchyPathFinder implements Runnable{ } } - for(var field : fields.values()){ + for(var field : fieldList){ //skipped N update -> drop it if(field.lastUpdateId <= state.updateId - 20){ //make sure it's only modified on the main thread...? but what about calling get() on this thread?? queue.post(() -> fields.remove(field.goalPos)); + Core.app.post(() -> fieldList.remove(field)); } } }); if(debug){ Events.run(Trigger.draw, () -> { - int team = Team.sharded.id; + int team = player.team().id; int cost = costGround; - if(clusters == null || clusters[cost] == null) return; - Draw.draw(Layer.overlayUI, () -> { Lines.stroke(1f); - for(int cx = 0; cx < cwidth; cx++){ - for(int cy = 0; cy < cheight; cy++){ - if(clusters[Team.sharded.id] == null || clusters[team][cost] == null) continue; - var cluster = clusters[team][cost][cy * cwidth + cx]; - if(cluster != null){ - Lines.stroke(0.5f); - Draw.color(Color.gray); - Lines.stroke(1f); + if(clusters[team] != null && clusters[team][cost] != null){ + for(int cx = 0; cx < cwidth; cx++){ + for(int cy = 0; cy < cheight; cy++){ - Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); + var cluster = clusters[team][cost][cy * cwidth + cx]; + if(cluster != null){ + Lines.stroke(0.5f); + Draw.color(Color.gray); + Lines.stroke(1f); + + Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); - for(int d = 0; d < 4; d++){ - IntSeq portals = cluster.portals[d]; - if(portals != null){ + for(int d = 0; d < 4; d++){ + IntSeq portals = cluster.portals[d]; + if(portals != null){ - for(int i = 0; i < portals.size; i++){ - int pos = portals.items[i]; - int from = Point2.x(pos), to = Point2.y(pos); - float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; + for(int i = 0; i < portals.size; i++){ + int pos = portals.items[i]; + int from = Point2.x(pos), to = Point2.y(pos); + float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; - portalToVec(cluster, cx, cy, d, i, Tmp.v1); + portalToVec(cluster, cx, cy, d, i, Tmp.v1); - Draw.color(Color.brown); - Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); + Draw.color(Color.brown); + Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); - LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; + LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; - if(connections != null){ - Draw.color(Color.forest); - for(int coni = 0; coni < connections.size; coni ++){ - long con = connections.items[coni]; + if(connections != null){ + Draw.color(Color.forest); + for(int coni = 0; coni < connections.size; coni ++){ + long con = connections.items[coni]; - portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); + portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); - float - x1 = Tmp.v1.x, y1 = Tmp.v1.y, - x2 = Tmp.v2.x, y2 = Tmp.v2.y, - mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize; - //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); - Lines.line(x1, y1, x2, y2); + float + x1 = Tmp.v1.x, y1 = Tmp.v1.y, + x2 = Tmp.v2.x, y2 = Tmp.v2.y, + mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize; + //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); + Lines.line(x1, y1, x2, y2); + } } } } } - } - //TODO draw connections. + //TODO draw connections. /* Draw.color(Color.magenta); @@ -257,28 +267,27 @@ public class HierarchyPathFinder implements Runnable{ //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); Lines.line(x1, y1, x2, y2); }*/ - } - } - } - - /* - if(fields != null){ - for(var entry : fields){ - int cx = entry.key % cwidth, cy = entry.key / cwidth; - for(int y = 0; y < clusterSize; y++){ - for(int x = 0; x < clusterSize; x++){ - int value = entry.value[x + y * clusterSize]; - Tmp.c1.a = 1f; - Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); - Draw.alpha(0.5f); - Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize/2f); } } } } - */ - + for(var fields : fieldList){ + try{ + for(var entry : fields.fields){ + int cx = entry.key % cwidth, cy = entry.key / cwidth; + for(int y = 0; y < clusterSize; y++){ + for(int x = 0; x < clusterSize; x++){ + int value = entry.value[x + y * clusterSize]; + Tmp.c1.a = 1f; + Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); + Draw.alpha(0.5f); + Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize / 2f); + } + } + } + }catch(Exception ignored){} //probably has some concurrency issues when iterating but I don't care, this is for debugging + } }); }); } @@ -816,7 +825,10 @@ public class HierarchyPathFinder implements Runnable{ int tile = frontier.removeLast(); int baseX = tile % wwidth, baseY = tile / wwidth; int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; + + //TODO: how can this be null??? serious problem! int[] curWeights = fields.get(curWeightIndex); + if(curWeights == null) continue; int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; @@ -921,6 +933,12 @@ public class HierarchyPathFinder implements Runnable{ int node = findClosestNode(team, costId, unitX, unitY); int dest = findClosestNode(team, costId, goalX, goalY); + if(dest == Integer.MAX_VALUE){ + request.notFound = true; + //no node found (TODO: invalid state??) + return; + } + var nodePath = clusterAstar(request, costId, node, dest); //TODO: how to reuse properly. what if the flowfields don't go through this position (the fields are finished?) how to incrementally extend the flowfield? @@ -931,6 +949,9 @@ public class HierarchyPathFinder implements Runnable{ //create the cache if it doesn't exist, and initialize it if(cache == null){ fields.put(goalPos, cache = new FieldCache(pcost, team, goalPos)); + FieldCache fcache = cache; + //register field in main thread for iteration + Core.app.post(() -> fieldList.add(fcache)); cache.frontier.addFirst(goalPos); addingFrontier = false; //when it's a new field, there is no need to add to the frontier to merge the flowfield } @@ -938,6 +959,7 @@ public class HierarchyPathFinder implements Runnable{ if(nodePath != null){ int cx = unitX / clusterSize, cy = unitY / clusterSize; + //TODO: instead of adding a bunch of clusters nobody cares about, dynamically add them later when needed addFlowCluster(cache, cx, cy, addingFrontier); for(int i = -1; i < nodePath.size; i++){ @@ -976,16 +998,35 @@ public class HierarchyPathFinder implements Runnable{ return ControlPathfinder.costTypes.get(costId); } - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){ + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 mainDestination, Vec2 out, boolean[] noResultFound){ int costId = 0; PathCost cost = idToCost(costId); - PathRequest request = unitRequests.get(unit); int - destX = World.toTile(destination.x), - destY = World.toTile(destination.y) * wwidth, + team = unit.team.id, + tileX = unit.tileX(), + tileY = unit.tileY(), + destX = World.toTile(mainDestination.x), + destY = World.toTile(mainDestination.y), + actualDestX = World.toTile(destination.x), + actualDestY = World.toTile(destination.y), destPos = destX + destY * wwidth; + PathRequest request = unitRequests.get(unit); + + //if the destination can be trivially reached in a straight line, do that. + if(!raycast(team, cost, tileX, tileY, actualDestX, actualDestY)){ + out.set(destination); + return true; + } + + //TODO: the destination should not be the exact key. units have slightly different destinations based on offset from formation! + + //TODO raycast both diagonal edges to make sure it's reachable near corners + //var test = Geometry.raycastRect(unit.x, unit.y, current.worldx(), current.worldy(), Tmp.r1.setCentered(1f, 1f, tilesize).grow(7.8f)) != null; + + boolean any = false; + //use existing request if it exists. if(request != null && request.destination == destPos){ request.lastUpdateId = state.updateId; @@ -994,54 +1035,128 @@ public class HierarchyPathFinder implements Runnable{ //TODO: should fields be accessible from this thread? FieldCache fieldCache = fields.get(destPos); - if(tileOn != null && fieldCache != null){ - int value = getCost(fieldCache.fields, tileOn.x, tileOn.y); + if(fieldCache != null && tileOn != null){ + fieldCache.lastUpdateId = state.updateId; + int maxIterations = 30; //TODO higher/lower number? + int i = 0; - Tile current = null; - int tl = 0; - //TODO: use raycasting and iterate on this for N steps - for(Point2 point : Geometry.d8){ - int dx = tileOn.x + point.x, dy = tileOn.y + point.y; + if(tileOn.pos() != request.lastTile || request.lastTargetTile == null){ + //TODO tanks have weird behavior near edges of walls, as they try to avoid them - Tile other = world.tile(dx, dy); + while(i ++ < maxIterations && (!any || !raycast(team, cost, tileX, tileY, tileOn.x, tileOn.y))){ + //TODO: if there's no flowfield at this position, add it. + int value = getCost(fieldCache.fields, tileOn.x, tileOn.y); - if(other == null) continue; + Tile current = null; + int minCost = 0; + //TODO: use raycasting and iterate on this for N steps + for(Point2 point : Geometry.d8){ + int dx = tileOn.x + point.x, dy = tileOn.y + point.y; - int packed = world.packArray(dx, dy); - int otherCost = getCost(fieldCache.fields, dx, dy); + Tile other = world.tile(dx, dy); - if(otherCost < value && (current == null || otherCost < tl) && passable(cost, unit.team.id, packed) && - !(point.x != 0 && point.y != 0 && (!passable(cost, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) || - (!passable(cost, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap + if(other == null) continue; - current = other; - tl = otherCost; + int packed = world.packArray(dx, dy); + int otherCost = getCost(fieldCache.fields, dx, dy); + + //TODO: issue with hugging corners (you should not be able to move diagonally when there is a wall in the way) + + if(otherCost < value && (current == null || otherCost < minCost) && passable(cost, unit.team.id, packed) && + //diagonal corner trap + !( + (!passable(cost, team, world.packArray(tileOn.x + point.x, tileOn.y)) || + (!passable(cost, team, world.packArray(tileOn.x, tileOn.y + point.y)))) + ) + ){ + + current = other; + minCost = otherCost; + } + } + + if(!(current == null || minCost == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ + tileOn = current; + any = true; + }else{ + break; + } } + + request.lastTargetTile = any ? tileOn : null; } - if(!(current == null || tl == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ - out.set(current); + if(request.lastTargetTile != null){ + out.set(request.lastTargetTile); return true; } } - - }else{ + }else if(request == null){ //queue new request. - unitRequests.put(unit, request = new PathRequest(unit, unit.team.id, destPos)); + unitRequests.put(unit, request = new PathRequest(unit, team, destPos)); PathRequest f = request; - //on the pathfinding thread: initialize the request, meaning + //on the pathfinding thread: initialize the request queue.post(() -> { - initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY); + initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY); }); + + out.set(destination); + + return true; } - noResultFound[0] = true; + if(request != null){ + noResultFound[0] = request.notFound; + } return false; } + private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){ + int ww = wwidth, wh = wheight; + int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; + int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; + int e2, err = dx - dy; + + while(x >= 0 && y >= 0 && x < ww && y < wh){ + if(avoid(team, type, x + y * wwidth)) return true; + if(x == x2 && y == y2) return false; + + //TODO no diagonals???? is this a good idea? + /* + //no diagonal ver + if(2 * err + dy > dx - 2 * err){ + err -= dy; + x += sx; + }else{ + err += dx; + y += sy; + }*/ + + //diagonal ver + e2 = 2 * err; + if(e2 > -dy){ + err -= dy; + x += sx; + } + + if(e2 < dx){ + err += dx; + y += sy; + } + + } + + return true; + } + + private static boolean avoid(int team, PathCost type, int tilePos){ + int cost = cost(team, type, tilePos); + return cost == impassable || cost >= 2; + } + private int getCost(IntMap fields, int x, int y){ int[] field = fields.get(x / clusterSize + (y / clusterSize) * cwidth); if(field == null){ diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index 6b37777c29..4d8698f6ba 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -77,6 +77,8 @@ public class RtsAI{ } public void update(){ + if(true) return; + if(timer.get(timeUpdate, 60f * 2f)){ assignSquads(); checkBuilding(); diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 86a5a89447..de1f2f5cfc 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -219,7 +219,8 @@ public class CommandAI extends AIController{ } if(unit.isGrounded() && stance != UnitStance.ram){ - if(timer.get(timerTarget3, avoidInterval)){ + //TODO no blocking. + if(timer.get(timerTarget3, avoidInterval) && false){ Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f); float max = unit.hitSize/2f; float radius = Math.max(7f, max); @@ -247,7 +248,7 @@ public class CommandAI extends AIController{ } //if you've spent 3 seconds stuck, something is wrong, move regardless - move = hpath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); + move = hpath.getPathPosition(unit, pathId, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); //we've reached the final point if the returned coordinate is equal to the supplied input isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f); From 381fd12aad9ed3db919694662b46a6617d2e2de8 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 11 Nov 2023 19:59:14 -0500 Subject: [PATCH 18/35] Still terrible --- .../src/mindustry/ai/HierarchyPathFinder.java | 101 ++++++++++++------ core/src/mindustry/ai/RtsAI.java | 1 - .../src/mindustry/entities/comp/TankComp.java | 2 +- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 67064abdb7..3d20e75dee 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -28,7 +28,7 @@ public class HierarchyPathFinder implements Runnable{ static final int clusterSize = 12; - static final boolean debug = true; + static final boolean debug = false; static final int[] offsets = { 1, 0, //right: bottom to top @@ -160,13 +160,11 @@ public class HierarchyPathFinder implements Runnable{ //is at the edge of a cluster; this means the portals may have changed. if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){ - if(mx == 0) queueClusterUpdate(cx - 1, cy); //left if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top - queueClusterUpdate(cx, cy); //TODO: recompute edge clusters too. }else{ @@ -190,7 +188,7 @@ public class HierarchyPathFinder implements Runnable{ for(var field : fieldList){ //skipped N update -> drop it - if(field.lastUpdateId <= state.updateId - 20){ + if(field.lastUpdateId <= state.updateId - 30){ //make sure it's only modified on the main thread...? but what about calling get() on this thread?? queue.post(() -> fields.remove(field.goalPos)); Core.app.post(() -> fieldList.remove(field)); @@ -893,7 +891,7 @@ public class HierarchyPathFinder implements Runnable{ if(addingFrontier){ for(int dir = 0; dir < 4; dir++){ - int ox = cx + moveDirs[dir * 2], oy = cy + moveDirs[dir * 2 + 1]; + int ox = cx + nextOffsets[dir * 2], oy = cy + nextOffsets[dir * 2 + 1]; if(ox < 0 || oy < 0 || ox >= cwidth || ox >= cheight) continue; @@ -913,11 +911,15 @@ public class HierarchyPathFinder implements Runnable{ int x = otherx1 + movex * i, y = othery1 + movey * i; //check to make sure it's not 0 (uninitialized flowfield data) - if(otherField[x + y * clusterSize] != 0){ + if(otherField[x + y * clusterSize] > 0){ int worldX = x + ox * clusterSize, worldY = y + oy * clusterSize; //add the world-relative position to the frontier, so it recalculates cache.frontier.addFirst(worldX + worldY * wwidth); + + if(debug){ + Core.app.post(() -> Fx.placeBlock.at(worldX *tilesize, worldY * tilesize, 1f)); + } } } } @@ -976,7 +978,7 @@ public class HierarchyPathFinder implements Runnable{ //store directionals TODO can be out of bounds for(Point2 p : Geometry.d4){ - addFlowCluster(cache, cluster + p.x + p.y * cwidth, addingFrontier); + //addFlowCluster(cache, cluster + p.x + p.y * cwidth, addingFrontier); } //store directional/flipped version of cluster @@ -987,7 +989,7 @@ public class HierarchyPathFinder implements Runnable{ //store directionals again for(Point2 p : Geometry.d4){ - addFlowCluster(cache, other + p.x + p.y * cwidth, addingFrontier); + //addFlowCluster(cache, other + p.x + p.y * cwidth, addingFrontier); } } } @@ -1023,7 +1025,7 @@ public class HierarchyPathFinder implements Runnable{ //TODO: the destination should not be the exact key. units have slightly different destinations based on offset from formation! //TODO raycast both diagonal edges to make sure it's reachable near corners - //var test = Geometry.raycastRect(unit.x, unit.y, current.worldx(), current.worldy(), Tmp.r1.setCentered(1f, 1f, tilesize).grow(7.8f)) != null; + // boolean any = false; @@ -1039,18 +1041,20 @@ public class HierarchyPathFinder implements Runnable{ fieldCache.lastUpdateId = state.updateId; int maxIterations = 30; //TODO higher/lower number? int i = 0; + //TODO: tanks do not reach max speed when near a tile they are flowing to. if(tileOn.pos() != request.lastTile || request.lastTargetTile == null){ //TODO tanks have weird behavior near edges of walls, as they try to avoid them + boolean anyNearSolid = false; - while(i ++ < maxIterations && (!any || !raycast(team, cost, tileX, tileY, tileOn.x, tileOn.y))){ - //TODO: if there's no flowfield at this position, add it. - int value = getCost(fieldCache.fields, tileOn.x, tileOn.y); + //find the next tile until one near a solid block is discovered + while(i ++ < maxIterations && !anyNearSolid){ + int value = getCost(fieldCache, tileOn.x, tileOn.y); Tile current = null; int minCost = 0; - //TODO: use raycasting and iterate on this for N steps - for(Point2 point : Geometry.d8){ + for(int dir = 0; dir < 8; dir ++){ + Point2 point = Geometry.d8[dir]; int dx = tileOn.x + point.x, dy = tileOn.y + point.y; Tile other = world.tile(dx, dy); @@ -1058,32 +1062,51 @@ public class HierarchyPathFinder implements Runnable{ if(other == null) continue; int packed = world.packArray(dx, dy); - int otherCost = getCost(fieldCache.fields, dx, dy); + int otherCost = getCost(fieldCache, dx, dy), relCost = otherCost - value; - //TODO: issue with hugging corners (you should not be able to move diagonally when there is a wall in the way) + if(relCost > 2 || otherCost <= 0){ + anyNearSolid = true; + } - if(otherCost < value && (current == null || otherCost < minCost) && passable(cost, unit.team.id, packed) && + if(relCost == 7 || relCost == 8) otherCost = value + 1; + + //check for corner preventing movement + if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && + (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever + + //keep moving even if it's blocked + any = true; + continue; + } + + if(otherCost < value && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(cost, unit.team.id, packed) && //diagonal corner trap !( (!passable(cost, team, world.packArray(tileOn.x + point.x, tileOn.y)) || (!passable(cost, team, world.packArray(tileOn.x, tileOn.y + point.y)))) ) ){ - current = other; minCost = otherCost; } } - if(!(current == null || minCost == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ + if(!(current == null || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ tileOn = current; any = true; + + if(current.array() == destPos){ + break; + } }else{ break; } } request.lastTargetTile = any ? tileOn : null; + if(debug && tileOn != null){ + Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); + } } if(request.lastTargetTile != null){ @@ -1108,12 +1131,38 @@ public class HierarchyPathFinder implements Runnable{ return true; } - if(request != null){ - noResultFound[0] = request.notFound; - } + noResultFound[0] = request.notFound; return false; } + private boolean checkSolid(Unit unit, Tile tile, int dir){ + var p = Geometry.d8[Mathf.mod(dir, 8)]; + return !unit.canPass(tile.x + p.x, tile.y + p.y); + } + + private boolean checkCorner(Unit unit, Tile tile, Tile next, int dir){ + Tile other = tile.nearby(Geometry.d8[Mathf.mod(dir, 8)]); + if(other == null){ + return true; + } + + if(!unit.canPass(other.x, other.y)){ + return Geometry.raycastRect(unit.x, unit.y, next.worldx(), next.worldy(), Tmp.r1.setCentered(other.worldx(), other.worldy(), tilesize).grow(Math.min(unit.hitSize * 0.66f, 7.6f))) != null; + } + + return false; + } + + private int getCost(FieldCache cache, int x, int y){ + int[] field = cache.fields.get(x / clusterSize + (y / clusterSize) * cwidth); + if(field == null){ + //request a new flow cluster if one wasn't found; this may be a spammed a bit, but the function will return early once it's created the first time + queue.post(() -> addFlowCluster(cache, x / clusterSize, y / clusterSize, true)); + return -1; + } + return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; + } + private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){ int ww = wwidth, wh = wheight; int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; @@ -1157,14 +1206,6 @@ public class HierarchyPathFinder implements Runnable{ return cost == impassable || cost >= 2; } - private int getCost(IntMap fields, int x, int y){ - int[] field = fields.get(x / clusterSize + (y / clusterSize) * cwidth); - if(field == null){ - return -1; - } - return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; - } - private static boolean passable(PathCost cost, int team, int pos){ int amount = cost.getCost(team, pathfinder.tiles[pos]); //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index 4d8698f6ba..92025da099 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -77,7 +77,6 @@ public class RtsAI{ } public void update(){ - if(true) return; if(timer.get(timeUpdate, 60f * 2f)){ assignSquads(); diff --git a/core/src/mindustry/entities/comp/TankComp.java b/core/src/mindustry/entities/comp/TankComp.java index 38946709ac..986a17cb56 100644 --- a/core/src/mindustry/entities/comp/TankComp.java +++ b/core/src/mindustry/entities/comp/TankComp.java @@ -51,7 +51,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec } //calculate overlapping tiles so it slows down when going "over" walls - int r = Math.max(Math.round(hitSize * 0.6f / tilesize), 1); + int r = Math.max((int)(hitSize * 0.6f / tilesize), 0); int solids = 0, total = (r*2+1)*(r*2+1); for(int dx = -r; dx <= r; dx++){ From 997702b9de207bd3e33269135e42c9e768baa283 Mon Sep 17 00:00:00 2001 From: Anuken Date: Sun, 12 Nov 2023 12:34:03 -0500 Subject: [PATCH 19/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 44 +++++++++---------- core/src/mindustry/ai/types/LogicAI.java | 3 +- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 3d20e75dee..741285a5f1 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -28,7 +28,7 @@ public class HierarchyPathFinder implements Runnable{ static final int clusterSize = 12; - static final boolean debug = false; + static final boolean debug = true; static final int[] offsets = { 1, 0, //right: bottom to top @@ -104,7 +104,7 @@ public class HierarchyPathFinder implements Runnable{ int lastTile; //TODO only re-raycast when unit moves a tile. @Nullable Tile lastTargetTile; - public PathRequest(Unit unit, int team, int destination){ + PathRequest(Unit unit, int team, int destination){ this.unit = unit; this.team = team; this.destination = destination; @@ -125,7 +125,7 @@ public class HierarchyPathFinder implements Runnable{ //TODO: how are the nodes merged? CAN they be merged? - public FieldCache(PathCost cost, int team, int goalPos){ + FieldCache(PathCost cost, int team, int goalPos){ this.cost = cost; this.team = team; this.goalPos = goalPos; @@ -851,7 +851,10 @@ public class HierarchyPathFinder implements Runnable{ //can't move back to the goal if(newPos == goalPos) continue; + if(dx - clx * clusterSize < 0 || dy - cly * clusterSize < 0) continue; + int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; + int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); int oldCost = weights[newPosArray]; @@ -961,7 +964,6 @@ public class HierarchyPathFinder implements Runnable{ if(nodePath != null){ int cx = unitX / clusterSize, cy = unitY / clusterSize; - //TODO: instead of adding a bunch of clusters nobody cares about, dynamically add them later when needed addFlowCluster(cache, cx, cy, addingFrontier); for(int i = -1; i < nodePath.size; i++){ @@ -976,21 +978,11 @@ public class HierarchyPathFinder implements Runnable{ addFlowCluster(cache, cluster, addingFrontier); - //store directionals TODO can be out of bounds - for(Point2 p : Geometry.d4){ - //addFlowCluster(cache, cluster + p.x + p.y * cwidth, addingFrontier); - } - //store directional/flipped version of cluster if(ox >= 0 && oy >= 0 && ox < cwidth && oy < cheight){ int other = ox + oy * cwidth; addFlowCluster(cache, other, addingFrontier); - - //store directionals again - for(Point2 p : Geometry.d4){ - //addFlowCluster(cache, other + p.x + p.y * cwidth, addingFrontier); - } } } } @@ -1000,7 +992,7 @@ public class HierarchyPathFinder implements Runnable{ return ControlPathfinder.costTypes.get(costId); } - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 mainDestination, Vec2 out, boolean[] noResultFound){ + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){ int costId = 0; PathCost cost = idToCost(costId); @@ -1022,11 +1014,6 @@ public class HierarchyPathFinder implements Runnable{ return true; } - //TODO: the destination should not be the exact key. units have slightly different destinations based on offset from formation! - - //TODO raycast both diagonal edges to make sure it's reachable near corners - // - boolean any = false; //use existing request if it exists. @@ -1103,8 +1090,10 @@ public class HierarchyPathFinder implements Runnable{ } } + //TODO: there are some serious issues with tileOn and the raycast position... + //TODO intense vibration request.lastTargetTile = any ? tileOn : null; - if(debug && tileOn != null){ + if(debug && tileOn != null && false){ Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); } } @@ -1131,7 +1120,9 @@ public class HierarchyPathFinder implements Runnable{ return true; } - noResultFound[0] = request.notFound; + if(noResultFound != null){ + noResultFound[0] = request.notFound; + } return false; } @@ -1231,6 +1222,13 @@ public class HierarchyPathFinder implements Runnable{ return cost.getCost(team, pathfinder.tiles[tilePos]); } + private void clusterChanged(int team, int pathCost, int cx, int cy){ + //TODO very important: invalidate paths! + //reset all flowfields that contain this cluster + //remove all paths that contain this cluster + //VERY important: don't replace all the data. + } + private void updateClustersComplete(int clusterIndex){ for(int team = 0; team < clusters.length; team++){ var dim1 = clusters[team]; @@ -1241,6 +1239,7 @@ public class HierarchyPathFinder implements Runnable{ var cluster = dim2[clusterIndex]; if(cluster != null){ updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); } } } @@ -1258,6 +1257,7 @@ public class HierarchyPathFinder implements Runnable{ var cluster = dim2[clusterIndex]; if(cluster != null){ updateInnerEdges(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth, cluster); + clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); } } } diff --git a/core/src/mindustry/ai/types/LogicAI.java b/core/src/mindustry/ai/types/LogicAI.java index d334f9dec1..cb7250cd7b 100644 --- a/core/src/mindustry/ai/types/LogicAI.java +++ b/core/src/mindustry/ai/types/LogicAI.java @@ -3,7 +3,6 @@ package mindustry.ai.types; import arc.math.*; import arc.struct.*; import arc.util.*; -import mindustry.*; import mindustry.ai.*; import mindustry.entities.units.*; import mindustry.gen.*; @@ -86,7 +85,7 @@ public class LogicAI extends AIController{ if(unit.isFlying()){ moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f); }else{ - if(Vars.controlPath.getPathPosition(unit, lastPathId, Tmp.v2.set(moveX, moveY), Tmp.v1, null)){ + if(hpath.getPathPosition(unit, lastPathId, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){ moveTo(Tmp.v1, 1f, Tmp.v2.epsilonEquals(Tmp.v1, 4.1f) ? 30f : 0f); } } From 587ff8bb467b2bd0f9f870669f1582f8d3833dab Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 15 Nov 2023 20:03:55 -0500 Subject: [PATCH 20/35] Proper path cost support --- core/src/mindustry/ai/HierarchyPathFinder.java | 5 +++-- core/src/mindustry/ai/types/CommandAI.java | 2 +- core/src/mindustry/ai/types/LogicAI.java | 2 +- core/src/mindustry/entities/comp/UnitComp.java | 2 +- core/src/mindustry/type/UnitType.java | 5 +++++ gradle.properties | 2 +- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 741285a5f1..31b8f1cdba 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -992,8 +992,8 @@ public class HierarchyPathFinder implements Runnable{ return ControlPathfinder.costTypes.get(costId); } - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){ - int costId = 0; + public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){ + int costId = unit.type.pathCostId; PathCost cost = idToCost(costId); int @@ -1006,6 +1006,7 @@ public class HierarchyPathFinder implements Runnable{ actualDestY = World.toTile(destination.y), destPos = destX + destY * wwidth; + //TODO: what if the destination is different...? PathRequest request = unitRequests.get(unit); //if the destination can be trivially reached in a straight line, do that. diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index de1f2f5cfc..eb05008d27 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -248,7 +248,7 @@ public class CommandAI extends AIController{ } //if you've spent 3 seconds stuck, something is wrong, move regardless - move = hpath.getPathPosition(unit, pathId, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); + move = hpath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); //we've reached the final point if the returned coordinate is equal to the supplied input isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f); diff --git a/core/src/mindustry/ai/types/LogicAI.java b/core/src/mindustry/ai/types/LogicAI.java index cb7250cd7b..2ac8840d3f 100644 --- a/core/src/mindustry/ai/types/LogicAI.java +++ b/core/src/mindustry/ai/types/LogicAI.java @@ -85,7 +85,7 @@ public class LogicAI extends AIController{ if(unit.isFlying()){ moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f); }else{ - if(hpath.getPathPosition(unit, lastPathId, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){ + if(hpath.getPathPosition(unit, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){ moveTo(Tmp.v1, 1f, Tmp.v2.epsilonEquals(Tmp.v1, 4.1f) ? 30f : 0f); } } diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 4ff6b3168f..d6483f8f25 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -403,7 +403,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying; } - /** @return pathfinder path type for calculating costs */ + /** @return pathfinder path type for calculating costs. This is used for wave AI only. (TODO: remove) */ public int pathType(){ return Pathfinder.costGround; } diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index a8623a6d65..3a2cf891b8 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -286,6 +286,8 @@ public class UnitType extends UnlockableContent implements Senseable{ /** Function used for calculating cost of moving with ControlPathfinder. Does not affect "normal" flow field pathfinding. */ public @Nullable PathCost pathCost; + /** ID for path cost, to be used in the control path finder. This is the value that actually matters; do not assign manually. Set in init(). */ + public int pathCostId; /** A sample of the unit that this type creates. Do not modify! */ public @Nullable Unit sample; @@ -689,6 +691,9 @@ public class UnitType extends UnlockableContent implements Senseable{ ControlPathfinder.costGround; } + pathCostId = ControlPathfinder.costTypes.indexOf(pathCost); + if(pathCostId == -1) pathCostId = 0; + if(flying){ envEnabled |= Env.space; } diff --git a/gradle.properties b/gradle.properties index 090150dba9..da998b39a0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,4 @@ org.gradle.caching=true #used for slow jitpack builds; TODO see if this actually works org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 -archash=e2fdbab477 +archash=96dd703d5d From c8209d568ffcda72898b57d8dbe8a824cc3c14d5 Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 15 Nov 2023 20:50:24 -0500 Subject: [PATCH 21/35] progress --- .../src/mindustry/ai/HierarchyPathFinder.java | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 31b8f1cdba..4c7b4c738f 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -81,6 +81,12 @@ public class HierarchyPathFinder implements Runnable{ IntSet clustersToUpdate = new IntSet(); IntSet clustersToInnerUpdate = new IntSet(); + //invalid request implies invalid field as well. + //there should be a list of temporary evicted fields... + //TODO path requests should not be actually invalidated until the paths they refer to have completed processing. + // - also, only do this every couple of seconds at least. + ObjectSet invalidRequests = new ObjectSet<>(); + /** Current pathfinding thread */ @Nullable Thread thread; @@ -90,18 +96,22 @@ public class HierarchyPathFinder implements Runnable{ final int destination, team; //resulting path of nodes final IntSeq resultPath = new IntSeq(); + //node index -> total cost - final IntFloatMap costs = new IntFloatMap(); - //node index (NodeIndex struct) -> node it came from TODO merge them - final IntIntMap cameFrom = new IntIntMap(); + IntFloatMap costs = new IntFloatMap(); + //node index (NodeIndex struct) -> node it came from TODO merge them, make properties of FieldCache? + IntIntMap cameFrom = new IntIntMap(); //frontier for A* - final PathfindQueue frontier = new PathfindQueue(); + PathfindQueue frontier = new PathfindQueue(); //main thread only! long lastUpdateId = state.updateId; - volatile boolean notFound = false; - int lastTile; //TODO only re-raycast when unit moves a tile. + //both threads + volatile boolean notFound = false; + volatile boolean invalidated = false; + + int lastTile; @Nullable Tile lastTargetTile; PathRequest(Unit unit, int team, int destination){ @@ -181,6 +191,7 @@ public class HierarchyPathFinder implements Runnable{ for(var req : unitRequests.values()){ //skipped N update -> drop it if(req.lastUpdateId <= state.updateId - 10){ + req.invalidated = true; //concurrent modification! Core.app.post(() -> unitRequests.remove(req.unit)); } @@ -729,14 +740,17 @@ public class HierarchyPathFinder implements Runnable{ return result; } + var team = request.team; + + if(request.costs == null) request.costs = new IntFloatMap(); + if(request.cameFrom == null) request.cameFrom = new IntIntMap(); + if(request.frontier == null) request.frontier = new PathfindQueue(); + + //note: these are NOT cleared, it is assumed that this function cleans up after itself at the end + //is this a good idea? don't know, might hammer the GC with unnecessary objects too var costs = request.costs; var cameFrom = request.cameFrom; var frontier = request.frontier; - var team = request.team; - - frontier.clear(); - costs.clear(); - cameFrom.clear(); cameFrom.put(startNodeIndex, startNodeIndex); costs.put(startNodeIndex, 0); @@ -774,6 +788,12 @@ public class HierarchyPathFinder implements Runnable{ } } + //null them out, so they get GC'ed later + //there's no reason to keep them around and waste memory, since this path may never be recalculated + request.costs = null; + request.cameFrom = null; + request.frontier = null; + if(foundEnd){ result.clear(); @@ -1006,7 +1026,6 @@ public class HierarchyPathFinder implements Runnable{ actualDestY = World.toTile(destination.y), destPos = destX + destY * wwidth; - //TODO: what if the destination is different...? PathRequest request = unitRequests.get(unit); //if the destination can be trivially reached in a straight line, do that. @@ -1029,7 +1048,6 @@ public class HierarchyPathFinder implements Runnable{ fieldCache.lastUpdateId = state.updateId; int maxIterations = 30; //TODO higher/lower number? int i = 0; - //TODO: tanks do not reach max speed when near a tile they are flowing to. if(tileOn.pos() != request.lastTile || request.lastTargetTile == null){ //TODO tanks have weird behavior near edges of walls, as they try to avoid them @@ -1091,8 +1109,7 @@ public class HierarchyPathFinder implements Runnable{ } } - //TODO: there are some serious issues with tileOn and the raycast position... - //TODO intense vibration + //TODO: there are some serious issues with tileOn and the raycast position, intense vibration request.lastTargetTile = any ? tileOn : null; if(debug && tileOn != null && false){ Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); @@ -1228,6 +1245,15 @@ public class HierarchyPathFinder implements Runnable{ //reset all flowfields that contain this cluster //remove all paths that contain this cluster //VERY important: don't replace all the data. + + int index = cx + cy * cwidth; + + //TODO go through each path request: + // - if it contains this cluster in its field: + // - mark for it to be recomputed next frame in a Set (so it doesn't happen twice!) + // - recomputing should invalidate the flowfield + // - invalidations should be batched every few seconds (let's say, 2) + } private void updateClustersComplete(int clusterIndex){ From 565d3313a4e0df5d06e4f74f4a6b21bf046d494c Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 16 Nov 2023 09:56:58 -0500 Subject: [PATCH 22/35] Invalidate path requests --- .../src/mindustry/ai/HierarchyPathFinder.java | 117 +++++++++++++++--- 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 4c7b4c738f..c68f031086 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -23,8 +23,9 @@ import static mindustry.ai.Pathfinder.*; //https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf public class HierarchyPathFinder implements Runnable{ private static final long maxUpdate = 100;//Time.millisToNanos(12); + private static final int updateStepInterval = 20;//200; private static final int updateFPS = 30; - private static final int updateInterval = 1000 / updateFPS; + private static final int updateInterval = 1000 / updateFPS, invalidateCheckInterval = 1000; static final int clusterSize = 12; @@ -65,6 +66,8 @@ public class HierarchyPathFinder implements Runnable{ //individual requests based on unit - MAIN THREAD ONLY ObjectMap unitRequests = new ObjectMap<>(); + Seq threadPathRequests = new Seq<>(false); + //TODO: very dangerous usage; //TODO - it is accessed from the main thread //TODO - it is written to on the pathfinding thread @@ -81,10 +84,7 @@ public class HierarchyPathFinder implements Runnable{ IntSet clustersToUpdate = new IntSet(); IntSet clustersToInnerUpdate = new IntSet(); - //invalid request implies invalid field as well. - //there should be a list of temporary evicted fields... - //TODO path requests should not be actually invalidated until the paths they refer to have completed processing. - // - also, only do this every couple of seconds at least. + //PATHFINDING THREAD - requests that should be recomputed ObjectSet invalidRequests = new ObjectSet<>(); /** Current pathfinding thread */ @@ -93,16 +93,16 @@ public class HierarchyPathFinder implements Runnable{ //path requests are per-unit static class PathRequest{ final Unit unit; - final int destination, team; + final int destination, team, costId; //resulting path of nodes final IntSeq resultPath = new IntSeq(); //node index -> total cost - IntFloatMap costs = new IntFloatMap(); + @Nullable IntFloatMap costs = new IntFloatMap(); //node index (NodeIndex struct) -> node it came from TODO merge them, make properties of FieldCache? - IntIntMap cameFrom = new IntIntMap(); + @Nullable IntIntMap cameFrom = new IntIntMap(); //frontier for A* - PathfindQueue frontier = new PathfindQueue(); + @Nullable PathfindQueue frontier = new PathfindQueue(); //main thread only! long lastUpdateId = state.updateId; @@ -110,12 +110,15 @@ public class HierarchyPathFinder implements Runnable{ //both threads volatile boolean notFound = false; volatile boolean invalidated = false; + //old field assigned before everything was recomputed + @Nullable volatile FieldCache oldCache; int lastTile; @Nullable Tile lastTargetTile; - PathRequest(Unit unit, int team, int destination){ + PathRequest(Unit unit, int team, int costId, int destination){ this.unit = unit; + this.costId = costId; this.team = team; this.destination = destination; } @@ -193,6 +196,7 @@ public class HierarchyPathFinder implements Runnable{ if(req.lastUpdateId <= state.updateId - 10){ req.invalidated = true; //concurrent modification! + queue.post(() -> threadPathRequests.remove(req)); Core.app.post(() -> unitRequests.remove(req.unit)); } } @@ -887,7 +891,7 @@ public class HierarchyPathFinder implements Runnable{ } //every N iterations, check the time spent - this prevents extra calls to nano time, which itself is slow - if(nsToRun >= 0 && (counter++) >= 200){ + if(nsToRun >= 0 && (counter++) >= updateStepInterval){ counter = 0; if(Time.timeSinceNanos(start) >= nsToRun){ return; @@ -1045,6 +1049,12 @@ public class HierarchyPathFinder implements Runnable{ FieldCache fieldCache = fields.get(destPos); if(fieldCache != null && tileOn != null){ + FieldCache old = request.oldCache; + //nullify the old field to be GCed, as it cannot be relevant anymore (this path is complete) + if(fieldCache.frontier.isEmpty() && old != null){ + request.oldCache = null; + } + fieldCache.lastUpdateId = state.updateId; int maxIterations = 30; //TODO higher/lower number? int i = 0; @@ -1055,7 +1065,7 @@ public class HierarchyPathFinder implements Runnable{ //find the next tile until one near a solid block is discovered while(i ++ < maxIterations && !anyNearSolid){ - int value = getCost(fieldCache, tileOn.x, tileOn.y); + int value = getCost(fieldCache, old, tileOn.x, tileOn.y); Tile current = null; int minCost = 0; @@ -1068,7 +1078,7 @@ public class HierarchyPathFinder implements Runnable{ if(other == null) continue; int packed = world.packArray(dx, dy); - int otherCost = getCost(fieldCache, dx, dy), relCost = otherCost - value; + int otherCost = getCost(fieldCache, old, dx, dy), relCost = otherCost - value; if(relCost > 2 || otherCost <= 0){ anyNearSolid = true; @@ -1124,13 +1134,14 @@ public class HierarchyPathFinder implements Runnable{ }else if(request == null){ //queue new request. - unitRequests.put(unit, request = new PathRequest(unit, team, destPos)); + unitRequests.put(unit, request = new PathRequest(unit, team, costId, destPos)); PathRequest f = request; //on the pathfinding thread: initialize the request queue.post(() -> { - initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY); + threadPathRequests.add(f); + recalculatePath(f); }); out.set(destination); @@ -1144,6 +1155,10 @@ public class HierarchyPathFinder implements Runnable{ return false; } + private void recalculatePath(PathRequest request){ + initializePathRequest(request, request.team, request.costId, request.unit.tileX(), request.unit.tileY(), request.destination % wwidth, request.destination / wwidth); + } + private boolean checkSolid(Unit unit, Tile tile, int dir){ var p = Geometry.d8[Mathf.mod(dir, 8)]; return !unit.canPass(tile.x + p.x, tile.y + p.y); @@ -1162,12 +1177,21 @@ public class HierarchyPathFinder implements Runnable{ return false; } - private int getCost(FieldCache cache, int x, int y){ + private int getCost(FieldCache cache, FieldCache old, int x, int y){ + //fall back to the old flowfield when possible - it's best not to use partial results from the base cache + if(old != null){ + return getCost(old, x, y, false); + } + return getCost(cache, x, y, true); + } + + private int getCost(FieldCache cache, int x, int y, boolean requeue){ int[] field = cache.fields.get(x / clusterSize + (y / clusterSize) * cwidth); if(field == null){ + if(!requeue) return 0; //request a new flow cluster if one wasn't found; this may be a spammed a bit, but the function will return early once it's created the first time queue.post(() -> addFlowCluster(cache, x / clusterSize, y / clusterSize, true)); - return -1; + return 0; } return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; } @@ -1250,9 +1274,16 @@ public class HierarchyPathFinder implements Runnable{ //TODO go through each path request: // - if it contains this cluster in its field: - // - mark for it to be recomputed next frame in a Set (so it doesn't happen twice!) - // - recomputing should invalidate the flowfield - // - invalidations should be batched every few seconds (let's say, 2) + // - DONE mark for it to be recomputed next frame in a Set (so it doesn't happen twice!) + // - DONE recomputing should invalidate the flowfield + // - recomputing should save the old flowfield Somewhere + // - DONE invalidations should be batched every few seconds (let's say, 2) + for(var req : threadPathRequests){ + var field = fields.get(req.destination); + if(field != null && field.fields.containsKey(index)){ + invalidRequests.add(req); + } + } } @@ -1294,10 +1325,13 @@ public class HierarchyPathFinder implements Runnable{ @Override public void run(){ + long lastInvalidCheck = Time.millis() + invalidateCheckInterval; + while(true){ if(net.client()) return; try{ + if(state.isPlaying()){ queue.run(); @@ -1316,6 +1350,49 @@ public class HierarchyPathFinder implements Runnable{ clustersToInnerUpdate.clear(); clustersToUpdate.clear(); + //periodically check for invalidated paths + if(Time.timeSinceMillis(lastInvalidCheck) > invalidateCheckInterval){ + lastInvalidCheck = Time.millis(); + + var it = invalidRequests.iterator(); + while(it.hasNext()){ + var request = it.next(); + + //invalid request, ignore it + if(request.invalidated){ + it.remove(); + continue; + } + + var field = fields.get(request.destination); + + if(field != null){ + //it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete. + if(field.frontier.isEmpty()){ + + //remove the field, to be recalculated next update one recalculatePath is processed + fields.remove(field.goalPos); + Core.app.post(() -> fieldList.remove(field)); + + //once the field is invalidated, make sure that all the requests that have it stored in their 'old' field, so units don't stutter during recalculations + for(var otherRequest : threadPathRequests){ + if(otherRequest.destination == request.destination){ + otherRequest.oldCache = field; + } + } + + //the recalculation is done next update, so multiple path requests in the same batch don't end up removing and recalculating the field multiple times. + queue.post(() -> recalculatePath(request)); + //it has been processed. + it.remove(); + } + }else{ //there's no field, presumably because a previous request already invalidated it. + queue.post(() -> recalculatePath(request)); + it.remove(); + } + } + } + //each update time (not total!) no longer than maxUpdate for(FieldCache cache : fields.values()){ updateFields(cache, maxUpdate); From 7a3476b9f7ed456aca9416c7a261688cbafda437 Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 16 Nov 2023 09:58:01 -0500 Subject: [PATCH 23/35] Debug property --- core/src/mindustry/ai/HierarchyPathFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index c68f031086..b77badf5e0 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -29,7 +29,7 @@ public class HierarchyPathFinder implements Runnable{ static final int clusterSize = 12; - static final boolean debug = true; + static final boolean debug = OS.hasProp("mindustry.debug"); static final int[] offsets = { 1, 0, //right: bottom to top From af71006f3f2b5654ccb01dddedff56205316b389 Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 16 Nov 2023 09:58:25 -0500 Subject: [PATCH 24/35] Removed flowfield time restrictions --- core/src/mindustry/ai/HierarchyPathFinder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index b77badf5e0..0c4ec1f695 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -22,8 +22,8 @@ import static mindustry.ai.Pathfinder.*; //https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf //https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf public class HierarchyPathFinder implements Runnable{ - private static final long maxUpdate = 100;//Time.millisToNanos(12); - private static final int updateStepInterval = 20;//200; + private static final long maxUpdate = Time.millisToNanos(12); + private static final int updateStepInterval = 200; private static final int updateFPS = 30; private static final int updateInterval = 1000 / updateFPS, invalidateCheckInterval = 1000; From c72eb5bd9c4f27496ae5bcb5085ae9ee353f0fee Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 16 Nov 2023 11:00:31 -0500 Subject: [PATCH 25/35] Cleanup --- .../src/mindustry/ai/HierarchyPathFinder.java | 127 ++++++++---------- core/src/mindustry/ai/types/CommandAI.java | 2 +- 2 files changed, 58 insertions(+), 71 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 0c4ec1f695..16904bdfd9 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -29,7 +29,7 @@ public class HierarchyPathFinder implements Runnable{ static final int clusterSize = 12; - static final boolean debug = OS.hasProp("mindustry.debug"); + static final boolean debug = false;//OS.hasProp("mindustry.debug"); static final int[] offsets = { 1, 0, //right: bottom to top @@ -145,6 +145,12 @@ public class HierarchyPathFinder implements Runnable{ } } + static class Cluster{ + IntSeq[] portals = new IntSeq[4]; + //maps rotation + index of portal to list of IntraEdge objects + LongSeq[][] portalConnections = new LongSeq[4][]; + } + public HierarchyPathFinder(){ Events.on(ResetEvent.class, event -> stop()); @@ -257,9 +263,7 @@ public class HierarchyPathFinder implements Runnable{ float x1 = Tmp.v1.x, y1 = Tmp.v1.y, - x2 = Tmp.v2.x, y2 = Tmp.v2.y, - mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize; - //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); + x2 = Tmp.v2.x, y2 = Tmp.v2.y; Lines.line(x1, y1, x2, y2); } @@ -267,19 +271,6 @@ public class HierarchyPathFinder implements Runnable{ } } } - - //TODO draw connections. - - /* - Draw.color(Color.magenta); - for(var con : cluster.cons){ - float - x1 = Point2.x(con.posFrom) * tilesize, y1 = Point2.y(con.posFrom) * tilesize, - x2 = Point2.x(con.posTo) * tilesize, y2 = Point2.y(con.posTo) * tilesize, - mx = (cx * clusterSize + clusterSize/2f) * tilesize, my = (cy * clusterSize + clusterSize/2f) * tilesize; - //Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20); - Lines.line(x1, y1, x2, y2); - }*/ } } } @@ -312,24 +303,7 @@ public class HierarchyPathFinder implements Runnable{ } } - static void line(Vec2 a, Vec2 b){ - Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()}); - } - - static void line(Vec2 a, Vec2 b, Color color){ - Fx.debugLine.at(a.x, a.y, 0f, color, new Vec2[]{a.cpy(), b.cpy()}); - } - - //DEBUGGING ONLY - Vec2 nodeToVec(int current, Vec2 out){ - portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out); - return out; - } - - void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){ - portalToVec(clusters[Team.sharded.id][pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out); - } - + //debugging only! void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ int pos = cluster.portals[direction].items[portalIndex]; int from = Point2.x(pos), to = Point2.y(pos); @@ -595,13 +569,24 @@ public class HierarchyPathFinder implements Runnable{ costs.put(startPos, 0); frontier.add(startPos, 0); + if(goalX2 < goalX1){ + int tmp = goalX1; + goalX1 = goalX2; + goalX2 = tmp; + } + + if(goalY2 < goalY1){ + int tmp = goalY1; + goalY1 = goalY2; + goalY2 = tmp; + } + while(frontier.size > 0){ int current = frontier.poll(); int cx = current % wwidth, cy = current / wwidth; //found the goal (it's in the portal rectangle) - //TODO portal rectangle approach does not work, making this slower than it should be if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){ return costs.get(current); } @@ -610,10 +595,7 @@ public class HierarchyPathFinder implements Runnable{ int newx = cx + point.x, newy = cy + point.y; int next = newx + wwidth * newy; - if(newx > maxX || newy > maxY || newx < minX || newy < minY) continue; - - //TODO fallback mode for enemy walls or whatever - if(tcost(team, cost, next) == impassable) continue; + if(newx > maxX || newy > maxY || newx < minX || newy < minY || tcost(team, cost, next) == impassable) continue; float add = tileCost(team, cost, current, next); @@ -688,7 +670,6 @@ public class HierarchyPathFinder implements Runnable{ minX, minY, maxX, maxY, tileX + tileY * wwidth, otherX + otherY * wwidth, - //TODO these are wrong and never actually trigger (moveDirs[dir * 2] * otherFrom + ox), (moveDirs[dir * 2 + 1] * otherFrom + oy), (moveDirs[dir * 2] * otherTo + ox), @@ -914,9 +895,7 @@ public class HierarchyPathFinder implements Runnable{ if(!fields.containsKey(key)){ fields.put(key, new int[clusterSize * clusterSize]); - //TODO: now, scan d4 for nearby clusters. if(addingFrontier){ - for(int dir = 0; dir < 4; dir++){ int ox = cx + nextOffsets[dir * 2], oy = cy + nextOffsets[dir * 2 + 1]; @@ -970,7 +949,6 @@ public class HierarchyPathFinder implements Runnable{ var nodePath = clusterAstar(request, costId, node, dest); - //TODO: how to reuse properly. what if the flowfields don't go through this position (the fields are finished?) how to incrementally extend the flowfield? FieldCache cache = fields.get(goalPos); //if true, extra values are added on the sides of existing field cells that face new cells. boolean addingFrontier = true; @@ -1033,7 +1011,8 @@ public class HierarchyPathFinder implements Runnable{ PathRequest request = unitRequests.get(unit); //if the destination can be trivially reached in a straight line, do that. - if(!raycast(team, cost, tileX, tileY, actualDestX, actualDestY)){ + //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls + if(unit.within(destination, tilesize * 2.5f) ? !raycastNear(team, cost, tileX, tileY, actualDestX, actualDestY) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY)){ out.set(destination); return true; } @@ -1056,6 +1035,7 @@ public class HierarchyPathFinder implements Runnable{ } fieldCache.lastUpdateId = state.updateId; + //TODO: 30 iterations every frame is incredibly slow and terrible and drops the FPS on mobile devices significantly. int maxIterations = 30; //TODO higher/lower number? int i = 0; @@ -1084,9 +1064,9 @@ public class HierarchyPathFinder implements Runnable{ anyNearSolid = true; } - if(relCost == 7 || relCost == 8) otherCost = value + 1; + if(relCost == 7) otherCost = value; - //check for corner preventing movement + //check for corner preventing movement TODO will break with the new last tile pos stuff if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever @@ -1095,11 +1075,11 @@ public class HierarchyPathFinder implements Runnable{ continue; } - if(otherCost < value && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(cost, unit.team.id, packed) && + if(otherCost < value && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed) && //diagonal corner trap !( - (!passable(cost, team, world.packArray(tileOn.x + point.x, tileOn.y)) || - (!passable(cost, team, world.packArray(tileOn.x, tileOn.y + point.y)))) + (!passable(team, cost, world.packArray(tileOn.x + point.x, tileOn.y)) || + (!passable(team, cost, world.packArray(tileOn.x, tileOn.y + point.y)))) ) ){ current = other; @@ -1119,7 +1099,6 @@ public class HierarchyPathFinder implements Runnable{ } } - //TODO: there are some serious issues with tileOn and the raycast position, intense vibration request.lastTargetTile = any ? tileOn : null; if(debug && tileOn != null && false){ Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); @@ -1128,6 +1107,8 @@ public class HierarchyPathFinder implements Runnable{ if(request.lastTargetTile != null){ out.set(request.lastTargetTile); + //TODO: broken + //request.lastTile = tileOn.pos(); return true; } } @@ -1206,17 +1187,6 @@ public class HierarchyPathFinder implements Runnable{ if(avoid(team, type, x + y * wwidth)) return true; if(x == x2 && y == y2) return false; - //TODO no diagonals???? is this a good idea? - /* - //no diagonal ver - if(2 * err + dy > dx - 2 * err){ - err -= dy; - x += sx; - }else{ - err += dx; - y += sy; - }*/ - //diagonal ver e2 = 2 * err; if(e2 > -dy){ @@ -1228,6 +1198,29 @@ public class HierarchyPathFinder implements Runnable{ err += dx; y += sy; } + } + + return true; + } + + private static boolean raycastNear(int team, PathCost type, int x1, int y1, int x2, int y2){ + int ww = wwidth, wh = wheight; + int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; + int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; + int e2, err = dx - dy; + + while(x >= 0 && y >= 0 && x < ww && y < wh){ + if(!passable(team, type, x + y * wwidth)) return true; + if(x == x2 && y == y2) return false; + + //no diagonal ver + if(2 * err + dy > dx - 2 * err){ + err -= dy; + x += sx; + }else{ + err += dx; + y += sy; + } } @@ -1239,7 +1232,7 @@ public class HierarchyPathFinder implements Runnable{ return cost == impassable || cost >= 2; } - private static boolean passable(PathCost cost, int team, int pos){ + private static boolean passable(int team, PathCost cost, int pos){ int amount = cost.getCost(team, pathfinder.tiles[pos]); //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable return amount != impassable && !(cost == costTypes.get(costNaval) && amount >= 6000); @@ -1280,7 +1273,7 @@ public class HierarchyPathFinder implements Runnable{ // - DONE invalidations should be batched every few seconds (let's say, 2) for(var req : threadPathRequests){ var field = fields.get(req.destination); - if(field != null && field.fields.containsKey(index)){ + if((field != null && field.fields.containsKey(index)) || req.notFound){ invalidRequests.add(req); } } @@ -1411,12 +1404,6 @@ public class HierarchyPathFinder implements Runnable{ } } - static class Cluster{ - IntSeq[] portals = new IntSeq[4]; - //maps rotation + index of portal to list of IntraEdge objects - LongSeq[][] portalConnections = new LongSeq[4][]; - } - @Struct static class IntraEdgeStruct{ @StructField(8) diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index eb05008d27..2e2a24851d 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -219,7 +219,7 @@ public class CommandAI extends AIController{ } if(unit.isGrounded() && stance != UnitStance.ram){ - //TODO no blocking. + //TODO: blocking is disabled, doesn't work well if(timer.get(timerTarget3, avoidInterval) && false){ Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f); float max = unit.hitSize/2f; From 171da8cf9bbbd130c317cbb6dd615d90a7b2e21f Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 17 Nov 2023 16:48:03 -0500 Subject: [PATCH 26/35] Minor optimizations --- .../src/mindustry/ai/HierarchyPathFinder.java | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 16904bdfd9..8fb7dd5334 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -113,6 +113,8 @@ public class HierarchyPathFinder implements Runnable{ //old field assigned before everything was recomputed @Nullable volatile FieldCache oldCache; + boolean lastRaycastResult = false; + int lastRaycastTile, lastWorldUpdate; int lastTile; @Nullable Tile lastTargetTile; @@ -1002,6 +1004,7 @@ public class HierarchyPathFinder implements Runnable{ team = unit.team.id, tileX = unit.tileX(), tileY = unit.tileY(), + packedPos = world.packArray(tileX, tileY), destX = World.toTile(mainDestination.x), destY = World.toTile(mainDestination.y), actualDestX = World.toTile(destination.x), @@ -1010,9 +1013,23 @@ public class HierarchyPathFinder implements Runnable{ PathRequest request = unitRequests.get(unit); + int lastRaycastTile = request == null || world.tileChanges != request.lastWorldUpdate ? -1 : request.lastRaycastTile; + boolean raycastResult = request != null && request.lastRaycastResult; + + //cache raycast results to run every time the world updates, and every tile the unit crosses + if(lastRaycastTile != packedPos){ + //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls + raycastResult = unit.within(destination, tilesize * 2.5f) ? !raycastNear(team, cost, tileX, tileY, actualDestX, actualDestY) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY); + + if(request != null){ + request.lastRaycastTile = packedPos; + request.lastRaycastResult = raycastResult; + request.lastWorldUpdate = world.tileChanges; + } + } + //if the destination can be trivially reached in a straight line, do that. - //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls - if(unit.within(destination, tilesize * 2.5f) ? !raycastNear(team, cost, tileX, tileY, actualDestX, actualDestY) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY)){ + if(raycastResult){ out.set(destination); return true; } @@ -1023,7 +1040,7 @@ public class HierarchyPathFinder implements Runnable{ if(request != null && request.destination == destPos){ request.lastUpdateId = state.updateId; - Tile tileOn = unit.tileOn(); + Tile tileOn = unit.tileOn(), initialTileOn = tileOn; //TODO: should fields be accessible from this thread? FieldCache fieldCache = fields.get(destPos); @@ -1038,8 +1055,10 @@ public class HierarchyPathFinder implements Runnable{ //TODO: 30 iterations every frame is incredibly slow and terrible and drops the FPS on mobile devices significantly. int maxIterations = 30; //TODO higher/lower number? int i = 0; + boolean recalc = false; - if(tileOn.pos() != request.lastTile || request.lastTargetTile == null){ + //TODO last pos can change if the flowfield changes. + if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ //TODO tanks have weird behavior near edges of walls, as they try to avoid them boolean anyNearSolid = false; @@ -1064,12 +1083,11 @@ public class HierarchyPathFinder implements Runnable{ anyNearSolid = true; } - if(relCost == 7) otherCost = value; - - //check for corner preventing movement TODO will break with the new last tile pos stuff + //check for corner preventing movement if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever + recalc = true; //keep moving even if it's blocked any = true; continue; @@ -1100,15 +1118,14 @@ public class HierarchyPathFinder implements Runnable{ } request.lastTargetTile = any ? tileOn : null; - if(debug && tileOn != null && false){ + if(true && tileOn != null){ Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); } } if(request.lastTargetTile != null){ out.set(request.lastTargetTile); - //TODO: broken - //request.lastTile = tileOn.pos(); + request.lastTile = recalc ? -1 : initialTileOn.pos(); return true; } } @@ -1207,7 +1224,8 @@ public class HierarchyPathFinder implements Runnable{ int ww = wwidth, wh = wheight; int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; - int e2, err = dx - dy; + int err = dx - dy; + while(x >= 0 && y >= 0 && x < ww && y < wh){ if(!passable(team, type, x + y * wwidth)) return true; From 9fab556e81e2aa3354601b9aeec073b8550e0af0 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 17 Nov 2023 17:04:47 -0500 Subject: [PATCH 27/35] Debugging disabled --- core/src/mindustry/ai/HierarchyPathFinder.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 8fb7dd5334..40526d0cad 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -1052,8 +1052,7 @@ public class HierarchyPathFinder implements Runnable{ } fieldCache.lastUpdateId = state.updateId; - //TODO: 30 iterations every frame is incredibly slow and terrible and drops the FPS on mobile devices significantly. - int maxIterations = 30; //TODO higher/lower number? + int maxIterations = 30; //TODO higher/lower number? is this still too slow? int i = 0; boolean recalc = false; @@ -1118,7 +1117,7 @@ public class HierarchyPathFinder implements Runnable{ } request.lastTargetTile = any ? tileOn : null; - if(true && tileOn != null){ + if(debug && tileOn != null){ Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); } } From 320b2ae54d76a1fdfe027ab0c4533f444790c7e5 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 27 Nov 2023 06:53:43 -0500 Subject: [PATCH 28/35] Cleanup --- core/src/mindustry/ai/HierarchyPathFinder.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 40526d0cad..972261fef3 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -1275,19 +1275,8 @@ public class HierarchyPathFinder implements Runnable{ } private void clusterChanged(int team, int pathCost, int cx, int cy){ - //TODO very important: invalidate paths! - //reset all flowfields that contain this cluster - //remove all paths that contain this cluster - //VERY important: don't replace all the data. - int index = cx + cy * cwidth; - //TODO go through each path request: - // - if it contains this cluster in its field: - // - DONE mark for it to be recomputed next frame in a Set (so it doesn't happen twice!) - // - DONE recomputing should invalidate the flowfield - // - recomputing should save the old flowfield Somewhere - // - DONE invalidations should be batched every few seconds (let's say, 2) for(var req : threadPathRequests){ var field = fields.get(req.destination); if((field != null && field.fields.containsKey(index)) || req.notFound){ From d9f981cbdeb57109a8a7b0e32535c74f51216ed3 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 15 Apr 2024 13:17:11 -0400 Subject: [PATCH 29/35] Version update --- core/src/mindustry/ai/HierarchyPathFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 972261fef3..4bc2efbf0a 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -29,7 +29,7 @@ public class HierarchyPathFinder implements Runnable{ static final int clusterSize = 12; - static final boolean debug = false;//OS.hasProp("mindustry.debug"); + static final boolean debug = OS.hasProp("mindustry.debug"); static final int[] offsets = { 1, 0, //right: bottom to top From 183c1a576378864c286b50e17ea731fc9e5b5f46 Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 17 Apr 2024 22:10:43 -0400 Subject: [PATCH 30/35] Flow field raycasting --- .../src/mindustry/ai/HierarchyPathFinder.java | 102 ++++++++++++------ core/src/mindustry/ai/types/CommandAI.java | 6 +- .../entities/units/AIController.java | 2 +- gradle.properties | 2 +- 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 4bc2efbf0a..e120bfefd2 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -295,6 +295,8 @@ public class HierarchyPathFinder implements Runnable{ }catch(Exception ignored){} //probably has some concurrency issues when iterating but I don't care, this is for debugging } }); + + Draw.reset(); }); } } @@ -1055,14 +1057,16 @@ public class HierarchyPathFinder implements Runnable{ int maxIterations = 30; //TODO higher/lower number? is this still too slow? int i = 0; boolean recalc = false; + unit.hitboxTile(Tmp.r3); + //tile rect size has tile size factored in, since the ray cannot have thickness + float tileRectSize = tilesize + Tmp.r3.height; //TODO last pos can change if the flowfield changes. if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ - //TODO tanks have weird behavior near edges of walls, as they try to avoid them boolean anyNearSolid = false; //find the next tile until one near a solid block is discovered - while(i ++ < maxIterations && !anyNearSolid){ + while(i ++ < maxIterations){ int value = getCost(fieldCache, old, tileOn.x, tileOn.y); Tile current = null; @@ -1083,16 +1087,16 @@ public class HierarchyPathFinder implements Runnable{ } //check for corner preventing movement - if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && - (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever + //if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && + // (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever - recalc = true; + //recalc = true; //keep moving even if it's blocked - any = true; - continue; - } + //any = true; + // continue; + //} - if(otherCost < value && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed) && + if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed) && //diagonal corner trap !( (!passable(team, cost, world.packArray(tileOn.x + point.x, tileOn.y)) || @@ -1104,13 +1108,28 @@ public class HierarchyPathFinder implements Runnable{ } } + //TODO raycast spam = extremely slow if(!(current == null || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ - tileOn = current; - any = true; - if(current.array() == destPos){ + //when anyNearSolid is false, no solid tiles have been encountered anywhere so far, so raycasting is a waste of time + if(anyNearSolid && !tileOn.dangerous() && raycastRect(unit.x, unit.y, current.x * tilesize, current.y * tilesize, team, cost, initialTileOn.x, initialTileOn.y, current.x, current.y, tileRectSize)){ + + //TODO this may be a mistake + if(tileOn == initialTileOn){ + recalc = true; + any = true; + } + break; + }else{ + tileOn = current; + any = true; + + if(current.array() == destPos){ + break; + } } + }else{ break; } @@ -1156,24 +1175,6 @@ public class HierarchyPathFinder implements Runnable{ initializePathRequest(request, request.team, request.costId, request.unit.tileX(), request.unit.tileY(), request.destination % wwidth, request.destination / wwidth); } - private boolean checkSolid(Unit unit, Tile tile, int dir){ - var p = Geometry.d8[Mathf.mod(dir, 8)]; - return !unit.canPass(tile.x + p.x, tile.y + p.y); - } - - private boolean checkCorner(Unit unit, Tile tile, Tile next, int dir){ - Tile other = tile.nearby(Geometry.d8[Mathf.mod(dir, 8)]); - if(other == null){ - return true; - } - - if(!unit.canPass(other.x, other.y)){ - return Geometry.raycastRect(unit.x, unit.y, next.worldx(), next.worldy(), Tmp.r1.setCentered(other.worldx(), other.worldy(), tilesize).grow(Math.min(unit.hitSize * 0.66f, 7.6f))) != null; - } - - return false; - } - private int getCost(FieldCache cache, FieldCache old, int x, int y){ //fall back to the old flowfield when possible - it's best not to use partial results from the base cache if(old != null){ @@ -1244,6 +1245,47 @@ public class HierarchyPathFinder implements Runnable{ return true; } + private static boolean overlap(int team, PathCost type, int x, int y, float startX, float startY, float endX, float endY, float rectSize){ + if(x < 0 || y < 0 || x >= wwidth || y >= wheight) return false; + if(!passable(team, type, x + y * wwidth)){ + return Intersector.intersectSegmentRectangleFast(startX, startY, endX, endY, x * tilesize - rectSize/2f, y * tilesize - rectSize/2f, rectSize, rectSize); + } + return false; + } + + private static boolean raycastRect(float startX, float startY, float endX, float endY, int team, PathCost type, int x1, int y1, int x2, int y2, float rectSize){ + int ww = wwidth, wh = wheight; + int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; + int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; + int e2, err = dx - dy; + + while(x >= 0 && y >= 0 && x < ww && y < wh){ + if( + !passable(team, type, x + y * wwidth) || + overlap(team, type, x + 1, y, startX, startY, endX, endY, rectSize) || + overlap(team, type, x - 1, y, startX, startY, endX, endY, rectSize) || + overlap(team, type, x, y + 1, startX, startY, endX, endY, rectSize) || + overlap(team, type, x, y - 1, startX, startY, endX, endY, rectSize) + ) return true; + + if(x == x2 && y == y2) return false; + + //diagonal ver + e2 = 2 * err; + if(e2 > -dy){ + err -= dy; + x += sx; + } + + if(e2 < dx){ + err += dx; + y += sy; + } + } + + return true; + } + private static boolean avoid(int team, PathCost type, int tilePos){ int cost = cost(team, type, tilePos); return cost == impassable || cost >= 2; diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 3be38e515a..042852e05f 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -205,6 +205,8 @@ public class CommandAI extends AIController{ } } + boolean alwaysArrive = false; + if(targetPos != null){ boolean move = true, isFinalPoint = commandQueue.size == 0; vecOut.set(targetPos); @@ -251,6 +253,8 @@ public class CommandAI extends AIController{ //if you've spent 3 seconds stuck, something is wrong, move regardless move = hpath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); + //rare case where unit must be perfectly aligned (happens with 1-tile gaps) + alwaysArrive = vecOut.epsilonEquals(unit.tileX() * tilesize, unit.tileY() * tilesize); //we've reached the final point if the returned coordinate is equal to the supplied input isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f); @@ -278,7 +282,7 @@ public class CommandAI extends AIController{ attackTarget != null && unit.within(attackTarget, engageRange) && stance != UnitStance.ram ? engageRange : unit.isGrounded() ? 0f : attackTarget != null && stance != UnitStance.ram ? engageRange : - 0f, unit.isFlying() ? 40f : 100f, false, null, isFinalPoint); + 0f, unit.isFlying() ? 40f : 100f, false, null, isFinalPoint || alwaysArrive); } } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 009e56d6ad..6025ea611c 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -341,7 +341,7 @@ public class AIController implements UnitController{ vec.setLength(speed * length); } - //do not move when infinite vectors are used or if its zero. + //ignore invalid movement values if(vec.isNaN() || vec.isInfinite() || vec.isZero()) return; if(!unit.type.omniMovement && unit.type.rotateMoveFirst){ diff --git a/gradle.properties b/gradle.properties index 25be0e37a2..025014ab41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,4 @@ org.gradle.caching=true #used for slow jitpack builds; TODO see if this actually works org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 -archash=8a2decd656 +archash=e812c7a008 From dbb17b7f21dfe029a3835ca72150b6b35e3aa91b Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 17 Apr 2024 22:31:50 -0400 Subject: [PATCH 31/35] Reduced turning margin --- core/src/mindustry/ai/HierarchyPathFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index e120bfefd2..dce60a10a4 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -1059,7 +1059,7 @@ public class HierarchyPathFinder implements Runnable{ boolean recalc = false; unit.hitboxTile(Tmp.r3); //tile rect size has tile size factored in, since the ray cannot have thickness - float tileRectSize = tilesize + Tmp.r3.height; + float tileRectSize = tilesize + Tmp.r3.height - 0.1f; //TODO last pos can change if the flowfield changes. if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ From 95e4be7186e7b65f20839d6699554840b3447438 Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 17 Apr 2024 22:46:07 -0400 Subject: [PATCH 32/35] comments --- core/src/mindustry/ai/HierarchyPathFinder.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index dce60a10a4..de066116b8 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -1086,16 +1086,6 @@ public class HierarchyPathFinder implements Runnable{ anyNearSolid = true; } - //check for corner preventing movement - //if((checkCorner(unit, tileOn, other, dir - 1) || checkCorner(unit, tileOn, other, dir + 1)) && - // (checkSolid(unit, tileOn, dir - 2) || checkSolid(unit, tileOn, dir + 2))){ //there must be a tile to the left or right to keep the unit from going back and forth forever - - //recalc = true; - //keep moving even if it's blocked - //any = true; - // continue; - //} - if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed) && //diagonal corner trap !( @@ -1109,6 +1099,7 @@ public class HierarchyPathFinder implements Runnable{ } //TODO raycast spam = extremely slow + //...flowfield integration spam is also really slow. if(!(current == null || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ //when anyNearSolid is false, no solid tiles have been encountered anywhere so far, so raycasting is a waste of time From 5e22b093e65095c1a791e95f63eabd5e6e757ccb Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 17 Apr 2024 23:13:10 -0400 Subject: [PATCH 33/35] more bugfixes --- .../src/mindustry/ai/HierarchyPathFinder.java | 37 ++++++++++++------- .../mindustry/entities/comp/HitboxComp.java | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index de066116b8..3479f94c57 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -71,8 +71,9 @@ public class HierarchyPathFinder implements Runnable{ //TODO: very dangerous usage; //TODO - it is accessed from the main thread //TODO - it is written to on the pathfinding thread - //maps position in world in (x + y * width format) to a cache of flow fields - IntMap fields = new IntMap<>(); + //TODO - it does not include + //maps position in world in (x + y * width format) | type (bitpacked to long) to a cache of flow fields + LongMap fields = new LongMap<>(); //MAIN THREAD ONLY Seq fieldList = new Seq<>(false); @@ -128,22 +129,26 @@ public class HierarchyPathFinder implements Runnable{ static class FieldCache{ final PathCost cost; + final int costId; final int team; final int goalPos; //frontier for flow fields final IntQueue frontier = new IntQueue(); //maps cluster index to field weights; 0 means uninitialized final IntMap fields = new IntMap<>(); + final long mapKey; //main thread only! long lastUpdateId = state.updateId; //TODO: how are the nodes merged? CAN they be merged? - FieldCache(PathCost cost, int team, int goalPos){ + FieldCache(PathCost cost, int costId, int team, int goalPos){ this.cost = cost; this.team = team; this.goalPos = goalPos; + this.costId = costId; + this.mapKey = Pack.longInt(goalPos, costId); } } @@ -162,7 +167,7 @@ public class HierarchyPathFinder implements Runnable{ //TODO: can the pathfinding thread even see these? unitRequests = new ObjectMap<>(); - fields = new IntMap<>(); + fields = new LongMap<>(); fieldList = new Seq<>(false); clusters = new Cluster[256][][]; @@ -213,7 +218,7 @@ public class HierarchyPathFinder implements Runnable{ //skipped N update -> drop it if(field.lastUpdateId <= state.updateId - 30){ //make sure it's only modified on the main thread...? but what about calling get() on this thread?? - queue.post(() -> fields.remove(field.goalPos)); + queue.post(() -> fields.remove(field.mapKey)); Core.app.post(() -> fieldList.remove(field)); } } @@ -222,7 +227,7 @@ public class HierarchyPathFinder implements Runnable{ if(debug){ Events.run(Trigger.draw, () -> { int team = player.team().id; - int cost = costGround; + int cost = 0; Draw.draw(Layer.overlayUI, () -> { Lines.stroke(1f); @@ -953,13 +958,14 @@ public class HierarchyPathFinder implements Runnable{ var nodePath = clusterAstar(request, costId, node, dest); - FieldCache cache = fields.get(goalPos); + FieldCache cache = fields.get(Pack.longInt(goalPos, costId)); //if true, extra values are added on the sides of existing field cells that face new cells. boolean addingFrontier = true; //create the cache if it doesn't exist, and initialize it if(cache == null){ - fields.put(goalPos, cache = new FieldCache(pcost, team, goalPos)); + cache = new FieldCache(pcost, costId, team, goalPos); + fields.put(cache.mapKey, cache); FieldCache fcache = cache; //register field in main thread for iteration Core.app.post(() -> fieldList.add(fcache)); @@ -1038,13 +1044,15 @@ public class HierarchyPathFinder implements Runnable{ boolean any = false; + long fieldKey = Pack.longInt(destPos, costId); + //use existing request if it exists. if(request != null && request.destination == destPos){ request.lastUpdateId = state.updateId; Tile tileOn = unit.tileOn(), initialTileOn = tileOn; //TODO: should fields be accessible from this thread? - FieldCache fieldCache = fields.get(destPos); + FieldCache fieldCache = fields.get(fieldKey); if(fieldCache != null && tileOn != null){ FieldCache old = request.oldCache; @@ -1059,7 +1067,7 @@ public class HierarchyPathFinder implements Runnable{ boolean recalc = false; unit.hitboxTile(Tmp.r3); //tile rect size has tile size factored in, since the ray cannot have thickness - float tileRectSize = tilesize + Tmp.r3.height - 0.1f; + float tileRectSize = tilesize + Tmp.r3.height; //TODO last pos can change if the flowfield changes. if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ @@ -1311,7 +1319,8 @@ public class HierarchyPathFinder implements Runnable{ int index = cx + cy * cwidth; for(var req : threadPathRequests){ - var field = fields.get(req.destination); + long mapKey = Pack.longInt(req.destination, pathCost); + var field = fields.get(mapKey); if((field != null && field.fields.containsKey(index)) || req.notFound){ invalidRequests.add(req); } @@ -1396,14 +1405,16 @@ public class HierarchyPathFinder implements Runnable{ continue; } - var field = fields.get(request.destination); + long mapKey = Pack.longInt(request.destination, request.costId); + + var field = fields.get(mapKey); if(field != null){ //it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete. if(field.frontier.isEmpty()){ //remove the field, to be recalculated next update one recalculatePath is processed - fields.remove(field.goalPos); + fields.remove(field.mapKey); Core.app.post(() -> fieldList.remove(field)); //once the field is invalidated, make sure that all the requests that have it stored in their 'old' field, so units don't stutter during recalculations diff --git a/core/src/mindustry/entities/comp/HitboxComp.java b/core/src/mindustry/entities/comp/HitboxComp.java index a444f350ab..178ab1c228 100644 --- a/core/src/mindustry/entities/comp/HitboxComp.java +++ b/core/src/mindustry/entities/comp/HitboxComp.java @@ -68,7 +68,7 @@ abstract class HitboxComp implements Posc, Sized, QuadTreeObject{ public void hitboxTile(Rect rect){ //tile hitboxes are never bigger than a tile, otherwise units get stuck - float size = Math.min(hitSize * 0.66f, 7.9f); + float size = Math.min(hitSize * 0.66f, 7.8f); //TODO: better / more accurate version is //float size = hitSize * 0.85f; //- for tanks? From 1ac1263aa4b3bba6a0a2b3aa677cb6a2a1c58704 Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 18 Apr 2024 12:31:53 -0400 Subject: [PATCH 34/35] Unit avoiding re-enabled --- .../src/mindustry/ai/HierarchyPathFinder.java | 21 +++++++------------ core/src/mindustry/ai/types/CommandAI.java | 5 ++--- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java index 3479f94c57..7d43a2bdcd 100644 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ b/core/src/mindustry/ai/HierarchyPathFinder.java @@ -1021,13 +1021,17 @@ public class HierarchyPathFinder implements Runnable{ PathRequest request = unitRequests.get(unit); + unit.hitboxTile(Tmp.r3); + //tile rect size has tile size factored in, since the ray cannot have thickness + float tileRectSize = tilesize + Tmp.r3.height; + int lastRaycastTile = request == null || world.tileChanges != request.lastWorldUpdate ? -1 : request.lastRaycastTile; boolean raycastResult = request != null && request.lastRaycastResult; //cache raycast results to run every time the world updates, and every tile the unit crosses if(lastRaycastTile != packedPos){ //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls - raycastResult = unit.within(destination, tilesize * 2.5f) ? !raycastNear(team, cost, tileX, tileY, actualDestX, actualDestY) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY); + raycastResult = unit.within(destination, tilesize * 2.5f) ? !raycastRect(unit.x, unit.y, destination.x, destination.y, team, cost, tileX, tileY, actualDestX, actualDestY, tileRectSize) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY); if(request != null){ request.lastRaycastTile = packedPos; @@ -1065,9 +1069,6 @@ public class HierarchyPathFinder implements Runnable{ int maxIterations = 30; //TODO higher/lower number? is this still too slow? int i = 0; boolean recalc = false; - unit.hitboxTile(Tmp.r3); - //tile rect size has tile size factored in, since the ray cannot have thickness - float tileRectSize = tilesize + Tmp.r3.height; //TODO last pos can change if the flowfield changes. if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ @@ -1079,8 +1080,8 @@ public class HierarchyPathFinder implements Runnable{ Tile current = null; int minCost = 0; - for(int dir = 0; dir < 8; dir ++){ - Point2 point = Geometry.d8[dir]; + for(int dir = 0; dir < 4; dir ++){ + Point2 point = Geometry.d4[dir]; int dx = tileOn.x + point.x, dy = tileOn.y + point.y; Tile other = world.tile(dx, dy); @@ -1094,13 +1095,7 @@ public class HierarchyPathFinder implements Runnable{ anyNearSolid = true; } - if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed) && - //diagonal corner trap - !( - (!passable(team, cost, world.packArray(tileOn.x + point.x, tileOn.y)) || - (!passable(team, cost, world.packArray(tileOn.x, tileOn.y + point.y)))) - ) - ){ + if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed)){ current = other; minCost = otherCost; } diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 042852e05f..07a30878b1 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -223,8 +223,8 @@ public class CommandAI extends AIController{ } if(unit.isGrounded() && stance != UnitStance.ram){ - //TODO: blocking is disabled, doesn't work well - if(timer.get(timerTarget3, avoidInterval) && false){ + //TODO: blocking enable or disable? + if(timer.get(timerTarget3, avoidInterval)){ Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f); float max = unit.hitSize/2f; float radius = Math.max(7f, max); @@ -251,7 +251,6 @@ public class CommandAI extends AIController{ timeSpentBlocked = 0f; } - //if you've spent 3 seconds stuck, something is wrong, move regardless move = hpath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); //rare case where unit must be perfectly aligned (happens with 1-tile gaps) alwaysArrive = vecOut.epsilonEquals(unit.tileX() * tilesize, unit.tileY() * tilesize); From 3a7d7647d98ea5e53eba2f64440b8e713d4789ac Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 18 Apr 2024 12:48:04 -0400 Subject: [PATCH 35/35] HPA* merged in --- core/src/mindustry/Vars.java | 2 - core/src/mindustry/ai/ControlPathfinder.java | 1840 ++++++++++++----- .../src/mindustry/ai/HierarchyPathFinder.java | 1471 ------------- core/src/mindustry/ai/types/CommandAI.java | 6 +- core/src/mindustry/ai/types/LogicAI.java | 2 +- 5 files changed, 1331 insertions(+), 1990 deletions(-) delete mode 100644 core/src/mindustry/ai/HierarchyPathFinder.java diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 6d4006b074..552f0ac54f 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -241,7 +241,6 @@ public class Vars implements Loadable{ public static WaveSpawner spawner; public static BlockIndexer indexer; public static Pathfinder pathfinder; - public static HierarchyPathFinder hpath; public static ControlPathfinder controlPath; public static FogControl fogControl; @@ -315,7 +314,6 @@ public class Vars implements Loadable{ indexer = new BlockIndexer(); pathfinder = new Pathfinder(); controlPath = new ControlPathfinder(); - hpath = new HierarchyPathFinder(); fogControl = new FogControl(); bases = new BaseRegistry(); logicVars = new GlobalVars(); diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index f6b166270e..f50345a554 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -7,6 +7,8 @@ import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.core.*; import mindustry.game.EventType.*; import mindustry.game.*; @@ -17,14 +19,13 @@ import mindustry.world.*; import static mindustry.Vars.*; import static mindustry.ai.Pathfinder.*; -//TODO remove/replace -public class ControlPathfinder{ - //TODO this FPS-based update system could be flawed. - private static final long maxUpdate = Time.millisToNanos(30); - private static final int updateFPS = 60; - private static final int updateInterval = 1000 / updateFPS; +//https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf +//https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf +public class ControlPathfinder implements Runnable{ private static final int wallImpassableCap = 1_000_000; + public static boolean showDebug; + public static final PathCost costGround = (team, tile) -> @@ -75,295 +76,1188 @@ public class ControlPathfinder{ costNaval ); - public static boolean showDebug = false; + private static final long maxUpdate = Time.millisToNanos(12); + private static final int updateStepInterval = 200; + private static final int updateFPS = 30; + private static final int updateInterval = 1000 / updateFPS, invalidateCheckInterval = 1000; - //static access probably faster than object access - static int wwidth, wheight; - //increments each tile change - static volatile int worldUpdateId; + static final int clusterSize = 12; - /** Current pathfinding threads, contents may be null */ - @Nullable PathfindThread[] threads; - /** for unique target IDs */ - int lastTargetId = 1; - /** requests per-unit */ - ObjectMap requests = new ObjectMap<>(); + static final int[] offsets = { + 1, 0, //right: bottom to top + 0, 1, //top: left to right + 0, 0, //left: bottom to top + 0, 0 //bottom: left to right + }; + + static final int[] moveDirs = { + 0, 1, + 1, 0, + 0, 1, + 1, 0 + }; + + static final int[] nextOffsets = { + 1, 0, + 0, 1, + -1, 0, + 0, -1 + }; + + //maps team -> pathCost -> flattened array of clusters in 2D + //(what about teams? different path costs?) + Cluster[][][] clusters; + + int cwidth, cheight; + + //temporarily used for resolving connections for intra-edges + IntSet usedEdges = new IntSet(); + //tasks to run on pathfinding thread + TaskQueue queue = new TaskQueue(); + + //individual requests based on unit - MAIN THREAD ONLY + ObjectMap unitRequests = new ObjectMap<>(); + + Seq threadPathRequests = new Seq<>(false); + + //TODO: very dangerous usage; + //TODO - it is accessed from the main thread + //TODO - it is written to on the pathfinding thread + //maps position in world in (x + y * width format) | type (bitpacked to long) to a cache of flow fields + LongMap fields = new LongMap<>(); + //MAIN THREAD ONLY + Seq fieldList = new Seq<>(false); + + //these are for inner edge A* (temporary!) + IntFloatMap innerCosts = new IntFloatMap(); + PathfindQueue innerFrontier = new PathfindQueue(); + + //ONLY modify on pathfinding thread. + IntSet clustersToUpdate = new IntSet(); + IntSet clustersToInnerUpdate = new IntSet(); + + //PATHFINDING THREAD - requests that should be recomputed + ObjectSet invalidRequests = new ObjectSet<>(); + + /** Current pathfinding thread */ + @Nullable Thread thread; + + //path requests are per-unit + static class PathRequest{ + final Unit unit; + final int destination, team, costId; + //resulting path of nodes + final IntSeq resultPath = new IntSeq(); + + //node index -> total cost + @Nullable IntFloatMap costs = new IntFloatMap(); + //node index (NodeIndex struct) -> node it came from TODO merge them, make properties of FieldCache? + @Nullable IntIntMap cameFrom = new IntIntMap(); + //frontier for A* + @Nullable PathfindQueue frontier = new PathfindQueue(); + + //main thread only! + long lastUpdateId = state.updateId; + + //both threads + volatile boolean notFound = false; + volatile boolean invalidated = false; + //old field assigned before everything was recomputed + @Nullable volatile FieldCache oldCache; + + boolean lastRaycastResult = false; + int lastRaycastTile, lastWorldUpdate; + int lastTile; + @Nullable Tile lastTargetTile; + + PathRequest(Unit unit, int team, int costId, int destination){ + this.unit = unit; + this.costId = costId; + this.team = team; + this.destination = destination; + } + } + + static class FieldCache{ + final PathCost cost; + final int costId; + final int team; + final int goalPos; + //frontier for flow fields + final IntQueue frontier = new IntQueue(); + //maps cluster index to field weights; 0 means uninitialized + final IntMap fields = new IntMap<>(); + final long mapKey; + + //main thread only! + long lastUpdateId = state.updateId; + + //TODO: how are the nodes merged? CAN they be merged? + + FieldCache(PathCost cost, int costId, int team, int goalPos){ + this.cost = cost; + this.team = team; + this.goalPos = goalPos; + this.costId = costId; + this.mapKey = Pack.longInt(goalPos, costId); + } + } + + static class Cluster{ + IntSeq[] portals = new IntSeq[4]; + //maps rotation + index of portal to list of IntraEdge objects + LongSeq[][] portalConnections = new LongSeq[4][]; + } public ControlPathfinder(){ + Events.on(ResetEvent.class, event -> stop()); + Events.on(WorldLoadEvent.class, event -> { stop(); - wwidth = world.width(); - wheight = world.height(); + + //TODO: can the pathfinding thread even see these? + unitRequests = new ObjectMap<>(); + fields = new LongMap<>(); + fieldList = new Seq<>(false); + + clusters = new Cluster[256][][]; + cwidth = Mathf.ceil((float)world.width() / clusterSize); + cheight = Mathf.ceil((float)world.height() / clusterSize); + start(); }); - //only update the world when a solid block is removed or placed, everything else doesn't matter - Events.on(TilePreChangeEvent.class, e -> { - if(e.tile.solid()){ - worldUpdateId ++; - } - }); - Events.on(TileChangeEvent.class, e -> { - if(e.tile.solid()){ - worldUpdateId ++; - } - }); - Events.on(ResetEvent.class, event -> stop()); + e.tile.getLinkedTiles(t -> { + int x = t.x, y = t.y, mx = x % clusterSize, my = y % clusterSize, cx = x / clusterSize, cy = y / clusterSize, cluster = cx + cy * cwidth; + + //is at the edge of a cluster; this means the portals may have changed. + if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){ + + if(mx == 0) queueClusterUpdate(cx - 1, cy); //left + if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom + if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right + if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top + + queueClusterUpdate(cx, cy); + //TODO: recompute edge clusters too. + }else{ + //there is no need to recompute portals for block updates that are not on the edge. + queue.post(() -> clustersToInnerUpdate.add(cluster)); + } + }); + + //TODO: recalculate affected flow fields? or just all of them? how to reflow? + }); //invalidate paths Events.run(Trigger.update, () -> { - for(var req : requests.values()){ + for(var req : unitRequests.values()){ //skipped N update -> drop it if(req.lastUpdateId <= state.updateId - 10){ + req.invalidated = true; //concurrent modification! - Core.app.post(() -> requests.remove(req.unit)); - req.thread.queue.post(() -> req.thread.requests.remove(req)); + queue.post(() -> threadPathRequests.remove(req)); + Core.app.post(() -> unitRequests.remove(req.unit)); + } + } + + for(var field : fieldList){ + //skipped N update -> drop it + if(field.lastUpdateId <= state.updateId - 30){ + //make sure it's only modified on the main thread...? but what about calling get() on this thread?? + queue.post(() -> fields.remove(field.mapKey)); + Core.app.post(() -> fieldList.remove(field)); } } }); - Events.run(Trigger.draw, () -> { - if(!showDebug) return; + if(showDebug){ + Events.run(Trigger.draw, () -> { + int team = player.team().id; + int cost = 0; - for(var req : requests.values()){ - if(req.frontier == null) continue; Draw.draw(Layer.overlayUI, () -> { - if(req.done){ - int len = req.result.size; - int rp = req.rayPathIndex; - if(rp < len && rp >= 0){ - Draw.color(Color.royal); - Tile tile = tile(req.result.items[rp]); - Lines.line(req.unit.x, req.unit.y, tile.worldx(), tile.worldy()); - } + Lines.stroke(1f); - for(int i = 0; i < len; i++){ - Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f)); - int pos = req.result.items[i]; - Fill.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 3f); + if(clusters[team] != null && clusters[team][cost] != null){ + for(int cx = 0; cx < cwidth; cx++){ + for(int cy = 0; cy < cheight; cy++){ - if(i == req.pathIndex){ - Draw.color(Color.green); - Lines.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 5f); - } - } - }else{ - var view = Core.camera.bounds(Tmp.r1); - int len = req.frontier.size; - float[] weights = req.frontier.weights; - int[] poses = req.frontier.queue; - for(int i = 0; i < Math.min(len, 1000); i++){ - int pos = poses[i]; - if(view.contains(pos % wwidth * tilesize, pos / wwidth * tilesize)){ - Draw.color(Tmp.c1.set(Color.white).fromHsv((weights[i] * 4f) % 360f, 1f, 0.9f)); + var cluster = clusters[team][cost][cy * cwidth + cx]; + if(cluster != null){ + Lines.stroke(0.5f); + Draw.color(Color.gray); + Lines.stroke(1f); - Lines.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 4f); + Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); + + + for(int d = 0; d < 4; d++){ + IntSeq portals = cluster.portals[d]; + if(portals != null){ + + for(int i = 0; i < portals.size; i++){ + int pos = portals.items[i]; + int from = Point2.x(pos), to = Point2.y(pos); + float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; + + portalToVec(cluster, cx, cy, d, i, Tmp.v1); + + Draw.color(Color.brown); + Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); + + LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; + + if(connections != null){ + Draw.color(Color.forest); + for(int coni = 0; coni < connections.size; coni ++){ + long con = connections.items[coni]; + + portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); + + float + x1 = Tmp.v1.x, y1 = Tmp.v1.y, + x2 = Tmp.v2.x, y2 = Tmp.v2.y; + Lines.line(x1, y1, x2, y2); + + } + } + } + } + } + } } } } - Draw.reset(); + + for(var fields : fieldList){ + try{ + for(var entry : fields.fields){ + int cx = entry.key % cwidth, cy = entry.key / cwidth; + for(int y = 0; y < clusterSize; y++){ + for(int x = 0; x < clusterSize; x++){ + int value = entry.value[x + y * clusterSize]; + Tmp.c1.a = 1f; + Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); + Draw.alpha(0.5f); + Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize / 2f); + } + } + } + }catch(Exception ignored){} //probably has some concurrency issues when iterating but I don't care, this is for debugging + } }); - } - }); + + Draw.reset(); + }); + } } - - /** @return the next target ID to use as a unique path identifier. */ - public int nextTargetId(){ - return lastTargetId ++; + void queueClusterUpdate(int cx, int cy){ + if(cx >= 0 && cy >= 0 && cx < cwidth && cy < cheight){ + queue.post(() -> clustersToUpdate.add(cx + cy * cwidth)); + } } - /** - * @return whether a path is ready. - * @param pathId a unique ID for this location query, which should change every time the 'destination' vector is modified. - * */ - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){ - return getPathPosition(unit, pathId, destination, out, null); + //debugging only! + void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ + int pos = cluster.portals[direction].items[portalIndex]; + int from = Point2.x(pos), to = Point2.y(pos); + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + float average = (from + to) / 2f; + + float + x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize, + y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize; + + out.set(x, y); } - /** - * @return whether a path is ready. - * @param pathId a unique ID for this location query, which should change every time the 'destination' vector is modified. - * @param noResultFound extra return value for storing whether no valid path to the destination exists (thanks java!) - * */ - public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){ - if(noResultFound != null){ - noResultFound[0] = false; - } - - //uninitialized - if(threads == null || !world.tiles.in(World.toTile(destination.x), World.toTile(destination.y))) return false; - - PathCost costType = unit.type.pathCost; - int team = unit.team.id; - - //if the destination can be trivially reached in a straight line, do that. - if((!requests.containsKey(unit) || requests.get(unit).curId != pathId) && !raycast(team, costType, unit.tileX(), unit.tileY(), World.toTile(destination.x), World.toTile(destination.y))){ - out.set(destination); - return true; - } - - //destination is impassable, can't go there. - if(solid(team, costType, world.packArray(World.toTile(destination.x), World.toTile(destination.y)), false)){ - return false; - } - - //check for request existence - if(!requests.containsKey(unit)){ - PathfindThread thread = Structs.findMin(threads, t -> t.requestSize); - - var req = new PathRequest(thread); - req.unit = unit; - req.cost = costType; - req.destination.set(destination); - req.curId = pathId; - req.team = team; - req.lastUpdateId = state.updateId; - req.lastPos.set(unit); - req.lastWorldUpdate = worldUpdateId; - //raycast immediately when done - req.raycastTimer = 9999f; - - requests.put(unit, req); - - //add to thread so it gets processed next update - thread.queue.post(() -> thread.requests.add(req)); - }else{ - var req = requests.get(unit); - req.lastUpdateId = state.updateId; - req.team = unit.team.id; - if(req.curId != req.lastId || req.curId != pathId){ - req.pathIndex = 0; - req.rayPathIndex = -1; - req.done = false; - req.foundEnd = false; - } - - req.destination.set(destination); - req.curId = pathId; - - //check for the unit getting stuck every N seconds - if(req.done && (req.stuckTimer += Time.delta) >= 60f * 1.5f){ - req.stuckTimer = 0f; - //force recalculate - if(req.lastPos.within(unit, 1.5f)){ - req.forceRecalculate(); - } - req.lastPos.set(unit); - } - - if(req.done){ - int[] items = req.result.items; - int len = req.result.size; - int tileX = unit.tileX(), tileY = unit.tileY(); - float range = 4f; - - float minDst = req.pathIndex < len ? unit.dst2(world.tiles.geti(items[req.pathIndex])) : 0f; - int idx = req.pathIndex; - - //find closest node that is in front of the path index and hittable with raycast - for(int i = len - 1; i >= idx; i--){ - Tile tile = tile(items[i]); - float dst = unit.dst2(tile); - //TODO maybe put this on a timer since raycasts can be expensive? - if(dst < minDst && !permissiveRaycast(team, costType, tileX, tileY, tile.x, tile.y)){ - if(avoid(req.team, req.cost, items[i + 1])){ - range = 0.5f; - } - - req.pathIndex = Math.max(dst <= range * range ? i + 1 : i, req.pathIndex); - minDst = Math.min(dst, minDst); - }else if(dst <= 1f){ - req.pathIndex = Math.min(Math.max(i + 1, req.pathIndex), len - 1); - } - } - - if(req.rayPathIndex < 0){ - req.rayPathIndex = req.pathIndex; - } - - if((req.raycastTimer += Time.delta) >= 50f){ - for(int i = len - 1; i > req.pathIndex; i--){ - int val = items[i]; - if(!raycast(team, costType, tileX, tileY, val % wwidth, val / wwidth)){ - req.rayPathIndex = i; - break; - } - } - req.raycastTimer = 0; - } - - if(req.rayPathIndex < len && req.rayPathIndex >= 0){ - Tile tile = tile(items[req.rayPathIndex]); - out.set(tile); - - if(req.rayPathIndex > 0){ - float angleToNext = tile(items[req.rayPathIndex - 1]).angleTo(tile); - float angleToDest = unit.angleTo(tile); - //force recalculate when the unit moves backwards - if(Angles.angleDist(angleToNext, angleToDest) > 80f && !unit.within(tile, 1f)){ - req.forceRecalculate(); - } - } - - if(avoid(req.team, req.cost, items[req.rayPathIndex])){ - range = 0.5f; - } - - if(unit.within(tile, range)){ - req.pathIndex = req.rayPathIndex = Math.max(req.pathIndex, req.rayPathIndex + 1); - } - }else{ - //implicit done - out.set(unit); - //end of path, we're done here? reset path? what??? - } - - if(noResultFound != null){ - noResultFound[0] = !req.foundEnd; - } - } - - return req.done; - } - - return false; - } /** Starts or restarts the pathfinding thread. */ private void start(){ stop(); - if(net.client()) return; - //TODO currently capped at 6 threads, might be a good idea to make it more? - threads = new PathfindThread[Mathf.clamp(Runtime.getRuntime().availableProcessors() - 1, 1, 6)]; - for(int i = 0; i < threads.length; i ++){ - threads[i] = new PathfindThread("ControlPathfindThread-" + i); - threads[i].setPriority(Thread.MIN_PRIORITY); - threads[i].setDaemon(true); - threads[i].start(); - } + thread = new Thread(this, "Control Pathfinder"); + thread.setPriority(Thread.MIN_PRIORITY); + thread.setDaemon(true); + thread.start(); } /** Stops the pathfinding thread. */ private void stop(){ - if(threads != null){ - for(var thread : threads){ - thread.interrupt(); + if(thread != null){ + thread.interrupt(); + thread = null; + } + queue.clear(); + } + + /** @return a cluster at coordinates; can be null if not cluster was created yet*/ + @Nullable Cluster getCluster(int team, int pathCost, int cx, int cy){ + return getCluster(team, pathCost, cx + cy * cwidth); + } + + /** @return a cluster at coordinates; can be null if not cluster was created yet*/ + @Nullable Cluster getCluster(int team, int pathCost, int clusterIndex){ + if(clusters == null) return null; + + Cluster[][] dim1 = clusters[team]; + + if(dim1 == null) return null; + + Cluster[] dim2 = dim1[pathCost]; + + if(dim2 == null) return null; + + return dim2[clusterIndex]; + } + + /** @return the cluster at specified coordinates; never null. */ + Cluster getCreateCluster(int team, int pathCost, int cx, int cy){ + return getCreateCluster(team, pathCost, cx + cy * cwidth); + } + + /** @return the cluster at specified coordinates; never null. */ + Cluster getCreateCluster(int team, int pathCost, int clusterIndex){ + Cluster result = getCluster(team, pathCost, clusterIndex); + if(result == null){ + return updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + }else{ + return result; + } + } + + Cluster updateCluster(int team, int pathCost, int cx, int cy){ + //TODO: what if clusters are null for thread visibility reasons? + + Cluster[][] dim1 = clusters[team]; + + if(dim1 == null){ + dim1 = clusters[team] = new Cluster[Team.all.length][]; + } + + Cluster[] dim2 = dim1[pathCost]; + + if(dim2 == null){ + dim2 = dim1[pathCost] = new Cluster[cwidth * cheight]; + } + + Cluster cluster = dim2[cy * cwidth + cx]; + if(cluster == null){ + cluster = dim2[cy * cwidth + cx] = new Cluster(); + }else{ + //reset data + for(var p : cluster.portals){ + p.clear(); } } - threads = null; - requests.clear(); + + PathCost cost = idToCost(pathCost); + + for(int direction = 0; direction < 4; direction++){ + int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction); + //out of bounds, no portals in this direction + if(otherX < 0 || otherY < 0 || otherX >= cwidth || otherY >= cheight){ + continue; + } + + Cluster other = dim2[otherX + otherY * cwidth]; + IntSeq portals; + + if(other == null){ + //create new portals at direction + portals = cluster.portals[direction] = new IntSeq(4); + }else{ + //share portals with the other cluster + portals = cluster.portals[direction] = other.portals[(direction + 2) % 4]; + + //clear the portals, they're being recalculated now + portals.clear(); + } + + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + int + baseX = cx * clusterSize + offsets[direction * 2] * (clusterSize - 1), + baseY = cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1), + nextBaseX = baseX + Geometry.d4[direction].x, + nextBaseY = baseY + Geometry.d4[direction].y; + + int lastPortal = -1; + boolean prevSolid = true; + + for(int i = 0; i < clusterSize; i++){ + int x = baseX + addX * i, y = baseY + addY * i; + + //scan for portals + if(solid(team, cost, x, y) || solid(team, cost, nextBaseX + addX * i, nextBaseY + addY * i)){ + int previous = i - 1; + //hit a wall, create portals between the two points + if(!prevSolid && previous >= lastPortal){ + //portals are an inclusive range + portals.add(Point2.pack(previous, lastPortal)); + } + prevSolid = true; + }else{ + //empty area encountered, mark the location of portal start + if(prevSolid){ + lastPortal = i; + } + prevSolid = false; + } + } + + //at the end of the loop, close any un-initialized portals; this is copy pasted code + int previous = clusterSize - 1; + if(!prevSolid && previous >= lastPortal){ + //portals are an inclusive range + portals.add(Point2.pack(previous, lastPortal)); + } + } + + updateInnerEdges(team, cost, cx, cy, cluster); + + return cluster; + } + + void updateInnerEdges(int team, int cost, int cx, int cy, Cluster cluster){ + updateInnerEdges(team, idToCost(cost), cx, cy, cluster); + } + + void updateInnerEdges(int team, PathCost cost, int cx, int cy, Cluster cluster){ + int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); + + usedEdges.clear(); + + //clear all connections, since portals changed, they need to be recomputed. + cluster.portalConnections = new LongSeq[4][]; + + for(int direction = 0; direction < 4; direction++){ + var portals = cluster.portals[direction]; + if(portals == null) continue; + + int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; + + for(int i = 0; i < portals.size; i++){ + usedEdges.add(Point2.pack(direction, i)); + + int + portal = portals.items[i], + from = Point2.x(portal), to = Point2.y(portal), + average = (from + to) / 2, + x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1)), + y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1)); + + for(int otherDir = 0; otherDir < 4; otherDir++){ + var otherPortals = cluster.portals[otherDir]; + if(otherPortals == null) continue; + + for(int j = 0; j < otherPortals.size; j++){ + + if(!usedEdges.contains(Point2.pack(otherDir, j))){ + + int + other = otherPortals.items[j], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + otherAverage = (otherFrom + otherTo) / 2, + ox = cx * clusterSize + offsets[otherDir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[otherDir * 2 + 1] * (clusterSize - 1), + otherX = (moveDirs[otherDir * 2] * otherAverage + ox), + otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy); + + //duplicate portal; should never happen. + if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){ + continue; + } + + float connectionCost = innerAstar( + team, cost, + minX, minY, maxX, maxY, + x + y * wwidth, + otherX + otherY * wwidth, + (moveDirs[otherDir * 2] * otherFrom + ox), + (moveDirs[otherDir * 2 + 1] * otherFrom + oy), + (moveDirs[otherDir * 2] * otherTo + ox), + (moveDirs[otherDir * 2 + 1] * otherTo + oy) + ); + + if(connectionCost != -1f){ + if(cluster.portalConnections[direction] == null) cluster.portalConnections[direction] = new LongSeq[cluster.portals[direction].size]; + if(cluster.portalConnections[otherDir] == null) cluster.portalConnections[otherDir] = new LongSeq[cluster.portals[otherDir].size]; + if(cluster.portalConnections[direction][i] == null) cluster.portalConnections[direction][i] = new LongSeq(8); + if(cluster.portalConnections[otherDir][j] == null) cluster.portalConnections[otherDir][j] = new LongSeq(8); + + //TODO: can there be duplicate edges?? + cluster.portalConnections[direction][i].add(IntraEdge.get(otherDir, j, connectionCost)); + cluster.portalConnections[otherDir][j].add(IntraEdge.get(direction, i, connectionCost)); + } + } + } + } + } + } + } + + //distance heuristic: manhattan + private static float heuristic(int a, int b){ + int x = a % wwidth, x2 = b % wwidth, y = a / wwidth, y2 = b / wwidth; + return Math.abs(x - x2) + Math.abs(y - y2); + } + + private static int tcost(int team, PathCost cost, int tilePos){ + return cost.getCost(team, pathfinder.tiles[tilePos]); + } + + private static float tileCost(int team, PathCost type, int a, int b){ + //currently flat cost + return cost(team, type, b); + } + + /** @return -1 if no path was found */ + float innerAstar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ + var frontier = innerFrontier; + var costs = innerCosts; + + frontier.clear(); + costs.clear(); + + //TODO: this can be faster and more memory efficient by making costs a NxN array... probably? + costs.put(startPos, 0); + frontier.add(startPos, 0); + + if(goalX2 < goalX1){ + int tmp = goalX1; + goalX1 = goalX2; + goalX2 = tmp; + } + + if(goalY2 < goalY1){ + int tmp = goalY1; + goalY1 = goalY2; + goalY2 = tmp; + } + + while(frontier.size > 0){ + int current = frontier.poll(); + + int cx = current % wwidth, cy = current / wwidth; + + //found the goal (it's in the portal rectangle) + if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){ + return costs.get(current); + } + + for(Point2 point : Geometry.d4){ + int newx = cx + point.x, newy = cy + point.y; + int next = newx + wwidth * newy; + + if(newx > maxX || newy > maxY || newx < minX || newy < minY || tcost(team, cost, next) == impassable) continue; + + float add = tileCost(team, cost, current, next); + + if(add < 0) continue; + + float newCost = costs.get(current) + add; + + if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ + costs.put(next, newCost); + float priority = newCost + heuristic(next, goalPos); + frontier.add(next, priority); + } + } + } + + return -1f; + } + + int makeNodeIndex(int cx, int cy, int dir, int portal){ + //to make sure there's only one way to refer to each node, the direction must be 0 or 1 (referring to portals on the top or right edge) + + //direction can only be 2 if cluster X is 0 (left edge of map) + if(dir == 2 && cx != 0){ + dir = 0; + cx --; + } + + //direction can only be 3 if cluster Y is 0 (bottom edge of map) + if(dir == 3 && cy != 0){ + dir = 1; + cy --; + } + + return NodeIndex.get(cx + cy * cwidth, dir, portal); + } + + //uses A* to find the closest node index to specified coordinates + //this node is used in cluster A* + /** @return MAX_VALUE if no node is found */ + private int findClosestNode(int team, int pathCost, int tileX, int tileY){ + int cx = tileX / clusterSize, cy = tileY / clusterSize; + + if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight){ + return Integer.MAX_VALUE; + } + + PathCost cost = idToCost(pathCost); + Cluster cluster = getCreateCluster(team, pathCost, cx, cy); + int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); + + int bestPortalPair = Integer.MAX_VALUE; + float bestCost = Float.MAX_VALUE; + + //A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra) + for(int dir = 0; dir < 4; dir++){ + var portals = cluster.portals[dir]; + if(portals == null) continue; + + for(int j = 0; j < portals.size; j++){ + + int + other = portals.items[j], + otherFrom = Point2.x(other), otherTo = Point2.y(other), + otherAverage = (otherFrom + otherTo) / 2, + ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), + oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), + otherX = (moveDirs[dir * 2] * otherAverage + ox), + otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy); + + float connectionCost = innerAstar( + team, cost, + minX, minY, maxX, maxY, + tileX + tileY * wwidth, + otherX + otherY * wwidth, + (moveDirs[dir * 2] * otherFrom + ox), + (moveDirs[dir * 2 + 1] * otherFrom + oy), + (moveDirs[dir * 2] * otherTo + ox), + (moveDirs[dir * 2 + 1] * otherTo + oy) + ); + + //better cost found, update and return + if(connectionCost != -1f && connectionCost < bestCost){ + bestPortalPair = Point2.pack(dir, j); + bestCost = connectionCost; + } + } + } + + if(bestPortalPair != Integer.MAX_VALUE){ + return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair)); + } + + + return Integer.MAX_VALUE; + } + + //distance heuristic: manhattan + private float clusterNodeHeuristic(int team, int pathCost, int nodeA, int nodeB){ + int + clusterA = NodeIndex.cluster(nodeA), + dirA = NodeIndex.dir(nodeA), + portalA = NodeIndex.portal(nodeA), + clusterB = NodeIndex.cluster(nodeB), + dirB = NodeIndex.dir(nodeB), + portalB = NodeIndex.portal(nodeB), + rangeA = getCreateCluster(team, pathCost, clusterA).portals[dirA].items[portalA], + rangeB = getCreateCluster(team, pathCost, clusterB).portals[dirB].items[portalB]; + + float + averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f, + x1 = (moveDirs[dirA * 2] * averageA + (clusterA % cwidth) * clusterSize + offsets[dirA * 2] * (clusterSize - 1) + nextOffsets[dirA * 2] / 2f), + y1 = (moveDirs[dirA * 2 + 1] * averageA + (clusterA / cwidth) * clusterSize + offsets[dirA * 2 + 1] * (clusterSize - 1) + nextOffsets[dirA * 2 + 1] / 2f), + + averageB = (Point2.x(rangeB) + Point2.y(rangeB)) / 2f, + x2 = (moveDirs[dirB * 2] * averageB + (clusterB % cwidth) * clusterSize + offsets[dirB * 2] * (clusterSize - 1) + nextOffsets[dirB * 2] / 2f), + y2 = (moveDirs[dirB * 2 + 1] * averageB + (clusterB / cwidth) * clusterSize + offsets[dirB * 2 + 1] * (clusterSize - 1) + nextOffsets[dirB * 2 + 1] / 2f); + + return Math.abs(x1 - x2) + Math.abs(y1 - y2); + } + + @Nullable IntSeq clusterAstar(PathRequest request, int pathCost, int startNodeIndex, int endNodeIndex){ + var result = request.resultPath; + + if(startNodeIndex == endNodeIndex){ + result.clear(); + result.add(startNodeIndex); + return result; + } + + var team = request.team; + + if(request.costs == null) request.costs = new IntFloatMap(); + if(request.cameFrom == null) request.cameFrom = new IntIntMap(); + if(request.frontier == null) request.frontier = new PathfindQueue(); + + //note: these are NOT cleared, it is assumed that this function cleans up after itself at the end + //is this a good idea? don't know, might hammer the GC with unnecessary objects too + var costs = request.costs; + var cameFrom = request.cameFrom; + var frontier = request.frontier; + + cameFrom.put(startNodeIndex, startNodeIndex); + costs.put(startNodeIndex, 0); + frontier.add(startNodeIndex, 0); + + boolean foundEnd = false; + + while(frontier.size > 0){ + int current = frontier.poll(); + + if(current == endNodeIndex){ + foundEnd = true; + break; + } + + int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current); + int cx = cluster % cwidth, cy = cluster / cwidth; + Cluster clust = getCreateCluster(team, pathCost, cluster); + LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal]; + + //edges for the cluster the node is 'in' + if(innerCons != null){ + checkEdges(request, team, pathCost, current, endNodeIndex, cx, cy, innerCons); + } + + //edges that this node 'faces' from the other side + int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y; + if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){ + Cluster nextCluster = getCreateCluster(team, pathCost, nextCx, nextCy); + int relativeDir = (dir + 2) % 4; + LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; + if(outerCons != null){ + checkEdges(request, team, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); + } + } + } + + //null them out, so they get GC'ed later + //there's no reason to keep them around and waste memory, since this path may never be recalculated + request.costs = null; + request.cameFrom = null; + request.frontier = null; + + if(foundEnd){ + result.clear(); + + int cur = endNodeIndex; + while(cur != startNodeIndex){ + result.add(cur); + cur = cameFrom.get(cur); + } + + result.reverse(); + + return result; + } + return null; + } + + private void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ + for(int i = 0; i < connections.size; i++){ + long con = connections.items[i]; + float cost = IntraEdge.cost(con); + int otherDir = IntraEdge.dir(con), otherPortal = IntraEdge.portal(con); + int next = makeNodeIndex(cx, cy, otherDir, otherPortal); + + float newCost = request.costs.get(current) + cost; + + if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){ + request.costs.put(next, newCost); + + request.frontier.add(next, newCost + clusterNodeHeuristic(team, pathCost, next, goal)); + request.cameFrom.put(next, current); + } + } + } + + private void updateFields(FieldCache cache, long nsToRun){ + var frontier = cache.frontier; + var fields = cache.fields; + var goalPos = cache.goalPos; + var pcost = cache.cost; + var team = cache.team; + + long start = Time.nanos(); + int counter = 0; + + //actually do the flow field part + while(frontier.size > 0){ + int tile = frontier.removeLast(); + int baseX = tile % wwidth, baseY = tile / wwidth; + int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; + + //TODO: how can this be null??? serious problem! + int[] curWeights = fields.get(curWeightIndex); + if(curWeights == null) continue; + + int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; + + if(cost != impassable){ + for(Point2 point : Geometry.d4){ + + int + dx = baseX + point.x, dy = baseY + point.y, + clx = dx / clusterSize, cly = dy / clusterSize; + + if(clx < 0 || cly < 0 || dx >= wwidth || dy >= wheight) continue; + + int nextWeightIndex = clx + cly * cwidth; + + int[] weights = nextWeightIndex == curWeightIndex ? curWeights : fields.get(nextWeightIndex); + + //out of bounds; not allowed to move this way because no weights were registered here + if(weights == null) continue; + + int newPos = tile + point.x + point.y * wwidth; + + //can't move back to the goal + if(newPos == goalPos) continue; + + if(dx - clx * clusterSize < 0 || dy - cly * clusterSize < 0) continue; + + int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; + + int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); + int oldCost = weights[newPosArray]; + + //a cost of 0 means uninitialized, OR it means we're at the goal position, but that's handled above + if((oldCost == 0 || oldCost > cost + otherCost) && otherCost != impassable){ + frontier.addFirst(newPos); + weights[newPosArray] = cost + otherCost; + } + } + } + + //every N iterations, check the time spent - this prevents extra calls to nano time, which itself is slow + if(nsToRun >= 0 && (counter++) >= updateStepInterval){ + counter = 0; + if(Time.timeSinceNanos(start) >= nsToRun){ + return; + } + } + } + } + + private void addFlowCluster(FieldCache cache, int cluster, boolean addingFrontier){ + addFlowCluster(cache, cluster % cwidth, cluster / cwidth, addingFrontier); + } + + private void addFlowCluster(FieldCache cache, int cx, int cy, boolean addingFrontier){ + //out of bounds + if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight) return; + + var fields = cache.fields; + int key = cx + cy * cwidth; + + if(!fields.containsKey(key)){ + fields.put(key, new int[clusterSize * clusterSize]); + + if(addingFrontier){ + for(int dir = 0; dir < 4; dir++){ + int ox = cx + nextOffsets[dir * 2], oy = cy + nextOffsets[dir * 2 + 1]; + + if(ox < 0 || oy < 0 || ox >= cwidth || ox >= cheight) continue; + + var otherField = cache.fields.get(ox + oy * cwidth); + + if(otherField == null) continue; + + int + relOffset = (dir + 2) % 4, + movex = moveDirs[relOffset * 2], + movey = moveDirs[relOffset * 2 + 1], + otherx1 = offsets[relOffset * 2] * (clusterSize - 1), + othery1 = offsets[relOffset * 2 + 1] * (clusterSize - 1); + + //scan the edge of the cluster + for(int i = 0; i < clusterSize; i++){ + int x = otherx1 + movex * i, y = othery1 + movey * i; + + //check to make sure it's not 0 (uninitialized flowfield data) + if(otherField[x + y * clusterSize] > 0){ + int worldX = x + ox * clusterSize, worldY = y + oy * clusterSize; + + //add the world-relative position to the frontier, so it recalculates + cache.frontier.addFirst(worldX + worldY * wwidth); + + if(showDebug){ + Core.app.post(() -> Fx.placeBlock.at(worldX *tilesize, worldY * tilesize, 1f)); + } + } + } + } + } + } + } + + private void initializePathRequest(PathRequest request, int team, int costId, int unitX, int unitY, int goalX, int goalY){ + PathCost pcost = idToCost(costId); + + int goalPos = (goalX + goalY * wwidth); + + int node = findClosestNode(team, costId, unitX, unitY); + int dest = findClosestNode(team, costId, goalX, goalY); + + if(dest == Integer.MAX_VALUE){ + request.notFound = true; + //no node found (TODO: invalid state??) + return; + } + + var nodePath = clusterAstar(request, costId, node, dest); + + FieldCache cache = fields.get(Pack.longInt(goalPos, costId)); + //if true, extra values are added on the sides of existing field cells that face new cells. + boolean addingFrontier = true; + + //create the cache if it doesn't exist, and initialize it + if(cache == null){ + cache = new FieldCache(pcost, costId, team, goalPos); + fields.put(cache.mapKey, cache); + FieldCache fcache = cache; + //register field in main thread for iteration + Core.app.post(() -> fieldList.add(fcache)); + cache.frontier.addFirst(goalPos); + addingFrontier = false; //when it's a new field, there is no need to add to the frontier to merge the flowfield + } + + if(nodePath != null){ + int cx = unitX / clusterSize, cy = unitY / clusterSize; + + addFlowCluster(cache, cx, cy, addingFrontier); + + for(int i = -1; i < nodePath.size; i++){ + int + current = i == -1 ? node : nodePath.items[i], + cluster = NodeIndex.cluster(current), + dir = NodeIndex.dir(current), + dx = Geometry.d4[dir].x, + dy = Geometry.d4[dir].y, + ox = cluster % cwidth + dx, + oy = cluster / cwidth + dy; + + addFlowCluster(cache, cluster, addingFrontier); + + //store directional/flipped version of cluster + if(ox >= 0 && oy >= 0 && ox < cwidth && oy < cheight){ + int other = ox + oy * cwidth; + + addFlowCluster(cache, other, addingFrontier); + } + } + } + } + + private PathCost idToCost(int costId){ + return ControlPathfinder.costTypes.get(costId); } public static boolean isNearObstacle(Unit unit, int x1, int y1, int x2, int y2){ return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2); } + @Deprecated + public int nextTargetId(){ + return 0; + } + + @Deprecated + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){ + return getPathPosition(unit, pathId, destination, out, null); + } + + @Deprecated + public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){ + return getPathPosition(unit, destination, destination, out, noResultFound); + } + + public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){ + int costId = unit.type.pathCostId; + PathCost cost = idToCost(costId); + + int + team = unit.team.id, + tileX = unit.tileX(), + tileY = unit.tileY(), + packedPos = world.packArray(tileX, tileY), + destX = World.toTile(mainDestination.x), + destY = World.toTile(mainDestination.y), + actualDestX = World.toTile(destination.x), + actualDestY = World.toTile(destination.y), + destPos = destX + destY * wwidth; + + PathRequest request = unitRequests.get(unit); + + unit.hitboxTile(Tmp.r3); + //tile rect size has tile size factored in, since the ray cannot have thickness + float tileRectSize = tilesize + Tmp.r3.height; + + int lastRaycastTile = request == null || world.tileChanges != request.lastWorldUpdate ? -1 : request.lastRaycastTile; + boolean raycastResult = request != null && request.lastRaycastResult; + + //cache raycast results to run every time the world updates, and every tile the unit crosses + if(lastRaycastTile != packedPos){ + //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls + raycastResult = unit.within(destination, tilesize * 2.5f) ? !raycastRect(unit.x, unit.y, destination.x, destination.y, team, cost, tileX, tileY, actualDestX, actualDestY, tileRectSize) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY); + + if(request != null){ + request.lastRaycastTile = packedPos; + request.lastRaycastResult = raycastResult; + request.lastWorldUpdate = world.tileChanges; + } + } + + //if the destination can be trivially reached in a straight line, do that. + if(raycastResult){ + out.set(destination); + return true; + } + + boolean any = false; + + long fieldKey = Pack.longInt(destPos, costId); + + //use existing request if it exists. + if(request != null && request.destination == destPos){ + request.lastUpdateId = state.updateId; + + Tile tileOn = unit.tileOn(), initialTileOn = tileOn; + //TODO: should fields be accessible from this thread? + FieldCache fieldCache = fields.get(fieldKey); + + if(fieldCache != null && tileOn != null){ + FieldCache old = request.oldCache; + //nullify the old field to be GCed, as it cannot be relevant anymore (this path is complete) + if(fieldCache.frontier.isEmpty() && old != null){ + request.oldCache = null; + } + + fieldCache.lastUpdateId = state.updateId; + int maxIterations = 30; //TODO higher/lower number? is this still too slow? + int i = 0; + boolean recalc = false; + + //TODO last pos can change if the flowfield changes. + if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ + boolean anyNearSolid = false; + + //find the next tile until one near a solid block is discovered + while(i ++ < maxIterations){ + int value = getCost(fieldCache, old, tileOn.x, tileOn.y); + + Tile current = null; + int minCost = 0; + for(int dir = 0; dir < 4; dir ++){ + Point2 point = Geometry.d4[dir]; + int dx = tileOn.x + point.x, dy = tileOn.y + point.y; + + Tile other = world.tile(dx, dy); + + if(other == null) continue; + + int packed = world.packArray(dx, dy); + int otherCost = getCost(fieldCache, old, dx, dy), relCost = otherCost - value; + + if(relCost > 2 || otherCost <= 0){ + anyNearSolid = true; + } + + if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed)){ + current = other; + minCost = otherCost; + } + } + + //TODO raycast spam = extremely slow + //...flowfield integration spam is also really slow. + if(!(current == null || (costId == costIdGround && current.dangerous() && !tileOn.dangerous()))){ + + //when anyNearSolid is false, no solid tiles have been encountered anywhere so far, so raycasting is a waste of time + if(anyNearSolid && !tileOn.dangerous() && raycastRect(unit.x, unit.y, current.x * tilesize, current.y * tilesize, team, cost, initialTileOn.x, initialTileOn.y, current.x, current.y, tileRectSize)){ + + //TODO this may be a mistake + if(tileOn == initialTileOn){ + recalc = true; + any = true; + } + + break; + }else{ + tileOn = current; + any = true; + + if(current.array() == destPos){ + break; + } + } + + }else{ + break; + } + } + + request.lastTargetTile = any ? tileOn : null; + if(showDebug && tileOn != null){ + Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); + } + } + + if(request.lastTargetTile != null){ + out.set(request.lastTargetTile); + request.lastTile = recalc ? -1 : initialTileOn.pos(); + return true; + } + } + }else if(request == null){ + + //queue new request. + unitRequests.put(unit, request = new PathRequest(unit, team, costId, destPos)); + + PathRequest f = request; + + //on the pathfinding thread: initialize the request + queue.post(() -> { + threadPathRequests.add(f); + recalculatePath(f); + }); + + out.set(destination); + + return true; + } + + if(noResultFound != null){ + noResultFound[0] = request.notFound; + } + return false; + } + + private void recalculatePath(PathRequest request){ + initializePathRequest(request, request.team, request.costId, request.unit.tileX(), request.unit.tileY(), request.destination % wwidth, request.destination / wwidth); + } + + private int getCost(FieldCache cache, FieldCache old, int x, int y){ + //fall back to the old flowfield when possible - it's best not to use partial results from the base cache + if(old != null){ + return getCost(old, x, y, false); + } + return getCost(cache, x, y, true); + } + + private int getCost(FieldCache cache, int x, int y, boolean requeue){ + int[] field = cache.fields.get(x / clusterSize + (y / clusterSize) * cwidth); + if(field == null){ + if(!requeue) return 0; + //request a new flow cluster if one wasn't found; this may be a spammed a bit, but the function will return early once it's created the first time + queue.post(() -> addFlowCluster(cache, x / clusterSize, y / clusterSize, true)); + return 0; + } + return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; + } + private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){ int ww = wwidth, wh = wheight; int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; @@ -374,17 +1268,6 @@ public class ControlPathfinder{ if(avoid(team, type, x + y * wwidth)) return true; if(x == x2 && y == y2) return false; - //TODO no diagonals???? is this a good idea? - /* - //no diagonal ver - if(2 * err + dy > dx - 2 * err){ - err -= dy; - x += sx; - }else{ - err += dx; - y += sy; - }*/ - //diagonal ver e2 = 2 * err; if(e2 > -dy){ @@ -396,30 +1279,6 @@ public class ControlPathfinder{ err += dx; y += sy; } - - } - - return true; - } - - private static boolean permissiveRaycast(int team, PathCost type, int x1, int y1, int x2, int y2){ - int ww = wwidth, wh = wheight; - int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; - int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; - int err = dx - dy; - - while(x >= 0 && y >= 0 && x < ww && y < wh){ - if(solid(team, type, x + y * wwidth, true)) return true; - if(x == x2 && y == y2) return false; - - //no diagonals - if(2 * err + dy > dx - 2 * err){ - err -= dy; - x += sx; - }else{ - err += dx; - y += sy; - } } return true; @@ -449,22 +1308,65 @@ public class ControlPathfinder{ return 0; } - static boolean cast(int team, PathCost cost, int from, int to){ - return raycast(team, cost, from % wwidth, from / wwidth, to % wwidth, to / wwidth); + private static boolean overlap(int team, PathCost type, int x, int y, float startX, float startY, float endX, float endY, float rectSize){ + if(x < 0 || y < 0 || x >= wwidth || y >= wheight) return false; + if(!passable(team, type, x + y * wwidth)){ + return Intersector.intersectSegmentRectangleFast(startX, startY, endX, endY, x * tilesize - rectSize/2f, y * tilesize - rectSize/2f, rectSize, rectSize); + } + return false; } - private Tile tile(int pos){ - return world.tiles.geti(pos); + private static boolean raycastRect(float startX, float startY, float endX, float endY, int team, PathCost type, int x1, int y1, int x2, int y2, float rectSize){ + int ww = wwidth, wh = wheight; + int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; + int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; + int e2, err = dx - dy; + + while(x >= 0 && y >= 0 && x < ww && y < wh){ + if( + !passable(team, type, x + y * wwidth) || + overlap(team, type, x + 1, y, startX, startY, endX, endY, rectSize) || + overlap(team, type, x - 1, y, startX, startY, endX, endY, rectSize) || + overlap(team, type, x, y + 1, startX, startY, endX, endY, rectSize) || + overlap(team, type, x, y - 1, startX, startY, endX, endY, rectSize) + ) return true; + + if(x == x2 && y == y2) return false; + + //diagonal ver + e2 = 2 * err; + if(e2 > -dy){ + err -= dy; + x += sx; + } + + if(e2 < dx){ + err += dx; + y += sy; + } + } + + return true; } - //distance heuristic: manhattan - private static float heuristic(int a, int b){ - int x = a % wwidth, x2 = b % wwidth, y = a / wwidth, y2 = b / wwidth; - return Math.abs(x - x2) + Math.abs(y - y2); + private static boolean avoid(int team, PathCost type, int tilePos){ + int cost = cost(team, type, tilePos); + return cost == impassable || cost >= 2; } - private static int tcost(int team, PathCost cost, int tilePos){ - return cost.getCost(team, pathfinder.tiles[tilePos]); + private static boolean passable(int team, PathCost cost, int pos){ + int amount = cost.getCost(team, pathfinder.tiles[pos]); + //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable + return amount != impassable && !(cost == costNaval && amount >= 6000); + } + + private static boolean solid(int team, PathCost type, int x, int y){ + return x < 0 || y < 0 || x >= wwidth || y >= wheight || solid(team, type, x + y * wwidth, true); + } + + private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){ + int cost = cost(team, type, tilePos); + return cost == impassable || (checkWall && cost >= 6000); } private static int cost(int team, PathCost cost, int tilePos){ @@ -477,246 +1379,162 @@ public class ControlPathfinder{ return cost.getCost(team, pathfinder.tiles[tilePos]); } - private static boolean avoid(int team, PathCost type, int tilePos){ - int cost = cost(team, type, tilePos); - return cost == impassable || cost >= 2; - } + private void clusterChanged(int team, int pathCost, int cx, int cy){ + int index = cx + cy * cwidth; - private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){ - int cost = cost(team, type, tilePos); - return cost == impassable || (checkWall && cost >= 6000); - } - - private static float tileCost(int team, PathCost type, int a, int b){ - //currently flat cost - return cost(team, type, b); - } - - static class PathfindThread extends Thread{ - /** handles task scheduling on the update thread. */ - TaskQueue queue = new TaskQueue(); - /** pathfinding thread access only! */ - Seq requests = new Seq<>(); - /** volatile for access across threads */ - volatile int requestSize; - - public PathfindThread(String name){ - super(name); + for(var req : threadPathRequests){ + long mapKey = Pack.longInt(req.destination, pathCost); + var field = fields.get(mapKey); + if((field != null && field.fields.containsKey(index)) || req.notFound){ + invalidRequests.add(req); + } } - @Override - public void run(){ - while(true){ - //stop on client, no updating - if(net.client()) return; - try{ - if(state.isPlaying()){ - queue.run(); - requestSize = requests.size; + } - //total update time no longer than maxUpdate - for(var req : requests){ - //TODO this is flawed with many paths - req.update(maxUpdate / requests.size); + private void updateClustersComplete(int clusterIndex){ + for(int team = 0; team < clusters.length; team++){ + var dim1 = clusters[team]; + if(dim1 != null){ + for(int pathCost = 0; pathCost < dim1.length; pathCost++){ + var dim2 = dim1[pathCost]; + if(dim2 != null){ + var cluster = dim2[clusterIndex]; + if(cluster != null){ + updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + } + } + } + } + } + } + + private void updateClustersInner(int clusterIndex){ + for(int team = 0; team < clusters.length; team++){ + var dim1 = clusters[team]; + if(dim1 != null){ + for(int pathCost = 0; pathCost < dim1.length; pathCost++){ + var dim2 = dim1[pathCost]; + if(dim2 != null){ + var cluster = dim2[clusterIndex]; + if(cluster != null){ + updateInnerEdges(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth, cluster); + clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); + } + } + } + } + } + } + + @Override + public void run(){ + long lastInvalidCheck = Time.millis() + invalidateCheckInterval; + + while(true){ + if(net.client()) return; + try{ + + + if(state.isPlaying()){ + queue.run(); + + clustersToUpdate.each(cluster -> { + updateClustersComplete(cluster); + + //just in case: don't redundantly update inner clusters after you've recalculated it entirely + clustersToInnerUpdate.remove(cluster); + }); + + clustersToInnerUpdate.each(cluster -> { + //only recompute the inner links + updateClustersInner(cluster); + }); + + clustersToInnerUpdate.clear(); + clustersToUpdate.clear(); + + //periodically check for invalidated paths + if(Time.timeSinceMillis(lastInvalidCheck) > invalidateCheckInterval){ + lastInvalidCheck = Time.millis(); + + var it = invalidRequests.iterator(); + while(it.hasNext()){ + var request = it.next(); + + //invalid request, ignore it + if(request.invalidated){ + it.remove(); + continue; + } + + long mapKey = Pack.longInt(request.destination, request.costId); + + var field = fields.get(mapKey); + + if(field != null){ + //it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete. + if(field.frontier.isEmpty()){ + + //remove the field, to be recalculated next update one recalculatePath is processed + fields.remove(field.mapKey); + Core.app.post(() -> fieldList.remove(field)); + + //once the field is invalidated, make sure that all the requests that have it stored in their 'old' field, so units don't stutter during recalculations + for(var otherRequest : threadPathRequests){ + if(otherRequest.destination == request.destination){ + otherRequest.oldCache = field; + } + } + + //the recalculation is done next update, so multiple path requests in the same batch don't end up removing and recalculating the field multiple times. + queue.post(() -> recalculatePath(request)); + //it has been processed. + it.remove(); + } + }else{ //there's no field, presumably because a previous request already invalidated it. + queue.post(() -> recalculatePath(request)); + it.remove(); + } } } - try{ - Thread.sleep(updateInterval); - }catch(InterruptedException e){ - //stop looping when interrupted externally - return; + //each update time (not total!) no longer than maxUpdate + for(FieldCache cache : fields.values()){ + updateFields(cache, maxUpdate); } - }catch(Throwable e){ - //do not crash the pathfinding thread - Log.err(e); } + + try{ + Thread.sleep(updateInterval); + }catch(InterruptedException e){ + //stop looping when interrupted externally + return; + } + }catch(Throwable e){ + e.printStackTrace(); } } } - static class PathRequest{ - final PathfindThread thread; + @Struct + static class IntraEdgeStruct{ + @StructField(8) + int dir; + @StructField(8) + int portal; - volatile boolean done = false; - volatile boolean foundEnd = false; - volatile Unit unit; - volatile PathCost cost; - volatile int team; - volatile int lastWorldUpdate; - volatile boolean forcedRecalc; + float cost; + } - final Vec2 lastPos = new Vec2(); - float stuckTimer = 0f; - - final Vec2 destination = new Vec2(); - final Vec2 lastDestination = new Vec2(); - - //TODO only access on main thread?? - volatile int pathIndex; - - int rayPathIndex = -1; - IntSeq result = new IntSeq(); - volatile float raycastTimer; - - PathfindQueue frontier = new PathfindQueue(); - //node index -> node it came from - IntIntMap cameFrom = new IntIntMap(); - //node index -> total cost - IntFloatMap costs = new IntFloatMap(); - - int start, goal; - - long lastUpdateId; - long lastTime; - long forceRecalcTime; - - volatile int lastId, curId; - - public PathRequest(PathfindThread thread){ - this.thread = thread; - } - - public void forceRecalculate(){ - //keep it at 3 times/sec - if(Time.timeSinceMillis(forceRecalcTime) < 1000 / 3) return; - forcedRecalc = true; - forceRecalcTime = Time.millis(); - } - - void update(long maxUpdateNs){ - if(curId != lastId){ - clear(true); - } - lastId = curId; - - //re-do everything when world updates, but keep the old path around - if(forcedRecalc || (Time.timeSinceMillis(lastTime) > 1000 * 3 && (worldUpdateId != lastWorldUpdate || !destination.epsilonEquals(lastDestination, 2f)))){ - lastTime = Time.millis(); - lastWorldUpdate = worldUpdateId; - forcedRecalc = false; - clear(false); - } - - if(done) return; - - long ns = Time.nanos(); - int counter = 0; - - while(frontier.size > 0){ - int current = frontier.poll(); - - if(current == goal){ - foundEnd = true; - break; - } - - int cx = current % wwidth, cy = current / wwidth; - - for(Point2 point : Geometry.d4){ - int newx = cx + point.x, newy = cy + point.y; - int next = newx + wwidth * newy; - - if(newx >= wwidth || newy >= wheight || newx < 0 || newy < 0) continue; - - //in fallback mode, enemy walls are passable - if(tcost(team, cost, next) == impassable) continue; - - float add = tileCost(team, cost, current, next); - float currentCost = costs.get(current); - - if(add < 0) continue; - - //the cost can include an impassable enemy wall, so cap the cost if so and add the base cost instead - //essentially this means that any path with enemy walls will only count the walls once, preventing strange behavior like avoiding based on wall count - float newCost = currentCost >= wallImpassableCap && add >= wallImpassableCap ? currentCost + add - wallImpassableCap : currentCost + add; - - //a cost of 0 means "not set" - if(!costs.containsKey(next) || newCost < costs.get(next)){ - costs.put(next, newCost); - float priority = newCost + heuristic(next, goal); - frontier.add(next, priority); - cameFrom.put(next, current); - } - } - - //only check every N iterations to prevent nanoTime spam (slow) - if((counter ++) >= 100){ - counter = 0; - - //exit when out of time. - if(Time.timeSinceNanos(ns) > maxUpdateNs){ - return; - } - } - } - - lastTime = Time.millis(); - raycastTimer = 9999f; - result.clear(); - - pathIndex = 0; - rayPathIndex = -1; - - if(foundEnd){ - int cur = goal; - while(cur != start){ - result.add(cur); - cur = cameFrom.get(cur); - } - - result.reverse(); - - smoothPath(); - } - - //don't keep this around in memory, better to dump entirely - using clear() keeps around massive arrays for paths - frontier = new PathfindQueue(); - cameFrom = new IntIntMap(); - costs = new IntFloatMap(); - - done = true; - } - - void smoothPath(){ - int len = result.size; - if(len <= 2) return; - - int output = 1, input = 2; - - while(input < len){ - if(cast(team, cost, result.get(output - 1), result.get(input))){ - result.swap(output, input - 1); - output++; - } - input++; - } - - result.swap(output, input - 1); - result.size = output + 1; - } - - void clear(boolean resetCurrent){ - done = false; - - frontier = new PathfindQueue(20); - cameFrom.clear(); - costs.clear(); - - start = world.packArray(unit.tileX(), unit.tileY()); - goal = world.packArray(World.toTile(destination.x), World.toTile(destination.y)); - - cameFrom.put(start, start); - costs.put(start, 0); - - frontier.add(start, 0); - - foundEnd = false; - lastDestination.set(destination); - - if(resetCurrent){ - result.clear(); - } - } + @Struct + static class NodeIndexStruct{ + @StructField(22) + int cluster; + @StructField(2) + int dir; + @StructField(8) + int portal; } } diff --git a/core/src/mindustry/ai/HierarchyPathFinder.java b/core/src/mindustry/ai/HierarchyPathFinder.java deleted file mode 100644 index 7d43a2bdcd..0000000000 --- a/core/src/mindustry/ai/HierarchyPathFinder.java +++ /dev/null @@ -1,1471 +0,0 @@ -package mindustry.ai; - -import arc.*; -import arc.graphics.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.math.geom.*; -import arc.struct.*; -import arc.util.*; -import mindustry.annotations.Annotations.*; -import mindustry.content.*; -import mindustry.core.*; -import mindustry.game.EventType.*; -import mindustry.game.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.world.*; - -import static mindustry.Vars.*; -import static mindustry.ai.Pathfinder.*; - -//https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf -//https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf -public class HierarchyPathFinder implements Runnable{ - private static final long maxUpdate = Time.millisToNanos(12); - private static final int updateStepInterval = 200; - private static final int updateFPS = 30; - private static final int updateInterval = 1000 / updateFPS, invalidateCheckInterval = 1000; - - static final int clusterSize = 12; - - static final boolean debug = OS.hasProp("mindustry.debug"); - - static final int[] offsets = { - 1, 0, //right: bottom to top - 0, 1, //top: left to right - 0, 0, //left: bottom to top - 0, 0 //bottom: left to right - }; - - static final int[] moveDirs = { - 0, 1, - 1, 0, - 0, 1, - 1, 0 - }; - - static final int[] nextOffsets = { - 1, 0, - 0, 1, - -1, 0, - 0, -1 - }; - - //maps team -> pathCost -> flattened array of clusters in 2D - //(what about teams? different path costs?) - Cluster[][][] clusters; - - int cwidth, cheight; - - //temporarily used for resolving connections for intra-edges - IntSet usedEdges = new IntSet(); - //tasks to run on pathfinding thread - TaskQueue queue = new TaskQueue(); - - //individual requests based on unit - MAIN THREAD ONLY - ObjectMap unitRequests = new ObjectMap<>(); - - Seq threadPathRequests = new Seq<>(false); - - //TODO: very dangerous usage; - //TODO - it is accessed from the main thread - //TODO - it is written to on the pathfinding thread - //TODO - it does not include - //maps position in world in (x + y * width format) | type (bitpacked to long) to a cache of flow fields - LongMap fields = new LongMap<>(); - //MAIN THREAD ONLY - Seq fieldList = new Seq<>(false); - - //these are for inner edge A* (temporary!) - IntFloatMap innerCosts = new IntFloatMap(); - PathfindQueue innerFrontier = new PathfindQueue(); - - //ONLY modify on pathfinding thread. - IntSet clustersToUpdate = new IntSet(); - IntSet clustersToInnerUpdate = new IntSet(); - - //PATHFINDING THREAD - requests that should be recomputed - ObjectSet invalidRequests = new ObjectSet<>(); - - /** Current pathfinding thread */ - @Nullable Thread thread; - - //path requests are per-unit - static class PathRequest{ - final Unit unit; - final int destination, team, costId; - //resulting path of nodes - final IntSeq resultPath = new IntSeq(); - - //node index -> total cost - @Nullable IntFloatMap costs = new IntFloatMap(); - //node index (NodeIndex struct) -> node it came from TODO merge them, make properties of FieldCache? - @Nullable IntIntMap cameFrom = new IntIntMap(); - //frontier for A* - @Nullable PathfindQueue frontier = new PathfindQueue(); - - //main thread only! - long lastUpdateId = state.updateId; - - //both threads - volatile boolean notFound = false; - volatile boolean invalidated = false; - //old field assigned before everything was recomputed - @Nullable volatile FieldCache oldCache; - - boolean lastRaycastResult = false; - int lastRaycastTile, lastWorldUpdate; - int lastTile; - @Nullable Tile lastTargetTile; - - PathRequest(Unit unit, int team, int costId, int destination){ - this.unit = unit; - this.costId = costId; - this.team = team; - this.destination = destination; - } - } - - static class FieldCache{ - final PathCost cost; - final int costId; - final int team; - final int goalPos; - //frontier for flow fields - final IntQueue frontier = new IntQueue(); - //maps cluster index to field weights; 0 means uninitialized - final IntMap fields = new IntMap<>(); - final long mapKey; - - //main thread only! - long lastUpdateId = state.updateId; - - //TODO: how are the nodes merged? CAN they be merged? - - FieldCache(PathCost cost, int costId, int team, int goalPos){ - this.cost = cost; - this.team = team; - this.goalPos = goalPos; - this.costId = costId; - this.mapKey = Pack.longInt(goalPos, costId); - } - } - - static class Cluster{ - IntSeq[] portals = new IntSeq[4]; - //maps rotation + index of portal to list of IntraEdge objects - LongSeq[][] portalConnections = new LongSeq[4][]; - } - - public HierarchyPathFinder(){ - - Events.on(ResetEvent.class, event -> stop()); - - Events.on(WorldLoadEvent.class, event -> { - stop(); - - //TODO: can the pathfinding thread even see these? - unitRequests = new ObjectMap<>(); - fields = new LongMap<>(); - fieldList = new Seq<>(false); - - clusters = new Cluster[256][][]; - cwidth = Mathf.ceil((float)world.width() / clusterSize); - cheight = Mathf.ceil((float)world.height() / clusterSize); - - - start(); - }); - - Events.on(TileChangeEvent.class, e -> { - - e.tile.getLinkedTiles(t -> { - int x = t.x, y = t.y, mx = x % clusterSize, my = y % clusterSize, cx = x / clusterSize, cy = y / clusterSize, cluster = cx + cy * cwidth; - - //is at the edge of a cluster; this means the portals may have changed. - if(mx == 0 || my == 0 || mx == clusterSize - 1 || my == clusterSize - 1){ - - if(mx == 0) queueClusterUpdate(cx - 1, cy); //left - if(my == 0) queueClusterUpdate(cx, cy - 1); //bottom - if(mx == clusterSize - 1) queueClusterUpdate(cx + 1, cy); //right - if(my == clusterSize - 1) queueClusterUpdate(cx, cy + 1); //top - - queueClusterUpdate(cx, cy); - //TODO: recompute edge clusters too. - }else{ - //there is no need to recompute portals for block updates that are not on the edge. - queue.post(() -> clustersToInnerUpdate.add(cluster)); - } - }); - - //TODO: recalculate affected flow fields? or just all of them? how to reflow? - }); - - //invalidate paths - Events.run(Trigger.update, () -> { - for(var req : unitRequests.values()){ - //skipped N update -> drop it - if(req.lastUpdateId <= state.updateId - 10){ - req.invalidated = true; - //concurrent modification! - queue.post(() -> threadPathRequests.remove(req)); - Core.app.post(() -> unitRequests.remove(req.unit)); - } - } - - for(var field : fieldList){ - //skipped N update -> drop it - if(field.lastUpdateId <= state.updateId - 30){ - //make sure it's only modified on the main thread...? but what about calling get() on this thread?? - queue.post(() -> fields.remove(field.mapKey)); - Core.app.post(() -> fieldList.remove(field)); - } - } - }); - - if(debug){ - Events.run(Trigger.draw, () -> { - int team = player.team().id; - int cost = 0; - - Draw.draw(Layer.overlayUI, () -> { - Lines.stroke(1f); - - if(clusters[team] != null && clusters[team][cost] != null){ - for(int cx = 0; cx < cwidth; cx++){ - for(int cy = 0; cy < cheight; cy++){ - - var cluster = clusters[team][cost][cy * cwidth + cx]; - if(cluster != null){ - Lines.stroke(0.5f); - Draw.color(Color.gray); - Lines.stroke(1f); - - Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize); - - - for(int d = 0; d < 4; d++){ - IntSeq portals = cluster.portals[d]; - if(portals != null){ - - for(int i = 0; i < portals.size; i++){ - int pos = portals.items[i]; - int from = Point2.x(pos), to = Point2.y(pos); - float width = tilesize * (Math.abs(from - to) + 1), height = tilesize; - - portalToVec(cluster, cx, cy, d, i, Tmp.v1); - - Draw.color(Color.brown); - Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f); - - LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i]; - - if(connections != null){ - Draw.color(Color.forest); - for(int coni = 0; coni < connections.size; coni ++){ - long con = connections.items[coni]; - - portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2); - - float - x1 = Tmp.v1.x, y1 = Tmp.v1.y, - x2 = Tmp.v2.x, y2 = Tmp.v2.y; - Lines.line(x1, y1, x2, y2); - - } - } - } - } - } - } - } - } - } - - for(var fields : fieldList){ - try{ - for(var entry : fields.fields){ - int cx = entry.key % cwidth, cy = entry.key / cwidth; - for(int y = 0; y < clusterSize; y++){ - for(int x = 0; x < clusterSize; x++){ - int value = entry.value[x + y * clusterSize]; - Tmp.c1.a = 1f; - Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f)); - Draw.alpha(0.5f); - Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize / 2f); - } - } - } - }catch(Exception ignored){} //probably has some concurrency issues when iterating but I don't care, this is for debugging - } - }); - - Draw.reset(); - }); - } - } - - void queueClusterUpdate(int cx, int cy){ - if(cx >= 0 && cy >= 0 && cx < cwidth && cy < cheight){ - queue.post(() -> clustersToUpdate.add(cx + cy * cwidth)); - } - } - - //debugging only! - void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){ - int pos = cluster.portals[direction].items[portalIndex]; - int from = Point2.x(pos), to = Point2.y(pos); - int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; - float average = (from + to) / 2f; - - float - x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize, - y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize; - - out.set(x, y); - } - - /** Starts or restarts the pathfinding thread. */ - private void start(){ - stop(); - if(net.client()) return; - - thread = new Thread(this, "Control Pathfinder"); - thread.setPriority(Thread.MIN_PRIORITY); - thread.setDaemon(true); - thread.start(); - } - - /** Stops the pathfinding thread. */ - private void stop(){ - if(thread != null){ - thread.interrupt(); - thread = null; - } - queue.clear(); - } - - /** @return a cluster at coordinates; can be null if not cluster was created yet*/ - @Nullable Cluster getCluster(int team, int pathCost, int cx, int cy){ - return getCluster(team, pathCost, cx + cy * cwidth); - } - - /** @return a cluster at coordinates; can be null if not cluster was created yet*/ - @Nullable Cluster getCluster(int team, int pathCost, int clusterIndex){ - if(clusters == null) return null; - - Cluster[][] dim1 = clusters[team]; - - if(dim1 == null) return null; - - Cluster[] dim2 = dim1[pathCost]; - - if(dim2 == null) return null; - - return dim2[clusterIndex]; - } - - /** @return the cluster at specified coordinates; never null. */ - Cluster getCreateCluster(int team, int pathCost, int cx, int cy){ - return getCreateCluster(team, pathCost, cx + cy * cwidth); - } - - /** @return the cluster at specified coordinates; never null. */ - Cluster getCreateCluster(int team, int pathCost, int clusterIndex){ - Cluster result = getCluster(team, pathCost, clusterIndex); - if(result == null){ - return updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); - }else{ - return result; - } - } - - Cluster updateCluster(int team, int pathCost, int cx, int cy){ - //TODO: what if clusters are null for thread visibility reasons? - - Cluster[][] dim1 = clusters[team]; - - if(dim1 == null){ - dim1 = clusters[team] = new Cluster[Team.all.length][]; - } - - Cluster[] dim2 = dim1[pathCost]; - - if(dim2 == null){ - dim2 = dim1[pathCost] = new Cluster[cwidth * cheight]; - } - - Cluster cluster = dim2[cy * cwidth + cx]; - if(cluster == null){ - cluster = dim2[cy * cwidth + cx] = new Cluster(); - }else{ - //reset data - for(var p : cluster.portals){ - p.clear(); - } - } - - PathCost cost = idToCost(pathCost); - - for(int direction = 0; direction < 4; direction++){ - int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction); - //out of bounds, no portals in this direction - if(otherX < 0 || otherY < 0 || otherX >= cwidth || otherY >= cheight){ - continue; - } - - Cluster other = dim2[otherX + otherY * cwidth]; - IntSeq portals; - - if(other == null){ - //create new portals at direction - portals = cluster.portals[direction] = new IntSeq(4); - }else{ - //share portals with the other cluster - portals = cluster.portals[direction] = other.portals[(direction + 2) % 4]; - - //clear the portals, they're being recalculated now - portals.clear(); - } - - int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; - int - baseX = cx * clusterSize + offsets[direction * 2] * (clusterSize - 1), - baseY = cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1), - nextBaseX = baseX + Geometry.d4[direction].x, - nextBaseY = baseY + Geometry.d4[direction].y; - - int lastPortal = -1; - boolean prevSolid = true; - - for(int i = 0; i < clusterSize; i++){ - int x = baseX + addX * i, y = baseY + addY * i; - - //scan for portals - if(solid(team, cost, x, y) || solid(team, cost, nextBaseX + addX * i, nextBaseY + addY * i)){ - int previous = i - 1; - //hit a wall, create portals between the two points - if(!prevSolid && previous >= lastPortal){ - //portals are an inclusive range - portals.add(Point2.pack(previous, lastPortal)); - } - prevSolid = true; - }else{ - //empty area encountered, mark the location of portal start - if(prevSolid){ - lastPortal = i; - } - prevSolid = false; - } - } - - //at the end of the loop, close any un-initialized portals; this is copy pasted code - int previous = clusterSize - 1; - if(!prevSolid && previous >= lastPortal){ - //portals are an inclusive range - portals.add(Point2.pack(previous, lastPortal)); - } - } - - updateInnerEdges(team, cost, cx, cy, cluster); - - return cluster; - } - - void updateInnerEdges(int team, int cost, int cx, int cy, Cluster cluster){ - updateInnerEdges(team, idToCost(cost), cx, cy, cluster); - } - - void updateInnerEdges(int team, PathCost cost, int cx, int cy, Cluster cluster){ - int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); - - usedEdges.clear(); - - //clear all connections, since portals changed, they need to be recomputed. - cluster.portalConnections = new LongSeq[4][]; - - for(int direction = 0; direction < 4; direction++){ - var portals = cluster.portals[direction]; - if(portals == null) continue; - - int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1]; - - for(int i = 0; i < portals.size; i++){ - usedEdges.add(Point2.pack(direction, i)); - - int - portal = portals.items[i], - from = Point2.x(portal), to = Point2.y(portal), - average = (from + to) / 2, - x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1)), - y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1)); - - for(int otherDir = 0; otherDir < 4; otherDir++){ - var otherPortals = cluster.portals[otherDir]; - if(otherPortals == null) continue; - - for(int j = 0; j < otherPortals.size; j++){ - - if(!usedEdges.contains(Point2.pack(otherDir, j))){ - - int - other = otherPortals.items[j], - otherFrom = Point2.x(other), otherTo = Point2.y(other), - otherAverage = (otherFrom + otherTo) / 2, - ox = cx * clusterSize + offsets[otherDir * 2] * (clusterSize - 1), - oy = cy * clusterSize + offsets[otherDir * 2 + 1] * (clusterSize - 1), - otherX = (moveDirs[otherDir * 2] * otherAverage + ox), - otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy); - - //duplicate portal; should never happen. - if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){ - continue; - } - - float connectionCost = innerAstar( - team, cost, - minX, minY, maxX, maxY, - x + y * wwidth, - otherX + otherY * wwidth, - (moveDirs[otherDir * 2] * otherFrom + ox), - (moveDirs[otherDir * 2 + 1] * otherFrom + oy), - (moveDirs[otherDir * 2] * otherTo + ox), - (moveDirs[otherDir * 2 + 1] * otherTo + oy) - ); - - if(connectionCost != -1f){ - if(cluster.portalConnections[direction] == null) cluster.portalConnections[direction] = new LongSeq[cluster.portals[direction].size]; - if(cluster.portalConnections[otherDir] == null) cluster.portalConnections[otherDir] = new LongSeq[cluster.portals[otherDir].size]; - if(cluster.portalConnections[direction][i] == null) cluster.portalConnections[direction][i] = new LongSeq(8); - if(cluster.portalConnections[otherDir][j] == null) cluster.portalConnections[otherDir][j] = new LongSeq(8); - - //TODO: can there be duplicate edges?? - cluster.portalConnections[direction][i].add(IntraEdge.get(otherDir, j, connectionCost)); - cluster.portalConnections[otherDir][j].add(IntraEdge.get(direction, i, connectionCost)); - } - } - } - } - } - } - } - - //distance heuristic: manhattan - private static float heuristic(int a, int b){ - int x = a % wwidth, x2 = b % wwidth, y = a / wwidth, y2 = b / wwidth; - return Math.abs(x - x2) + Math.abs(y - y2); - } - - private static int tcost(int team, PathCost cost, int tilePos){ - return cost.getCost(team, pathfinder.tiles[tilePos]); - } - - private static float tileCost(int team, PathCost type, int a, int b){ - //currently flat cost - return cost(team, type, b); - } - - /** @return -1 if no path was found */ - float innerAstar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){ - var frontier = innerFrontier; - var costs = innerCosts; - - frontier.clear(); - costs.clear(); - - //TODO: this can be faster and more memory efficient by making costs a NxN array... probably? - costs.put(startPos, 0); - frontier.add(startPos, 0); - - if(goalX2 < goalX1){ - int tmp = goalX1; - goalX1 = goalX2; - goalX2 = tmp; - } - - if(goalY2 < goalY1){ - int tmp = goalY1; - goalY1 = goalY2; - goalY2 = tmp; - } - - while(frontier.size > 0){ - int current = frontier.poll(); - - int cx = current % wwidth, cy = current / wwidth; - - //found the goal (it's in the portal rectangle) - if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){ - return costs.get(current); - } - - for(Point2 point : Geometry.d4){ - int newx = cx + point.x, newy = cy + point.y; - int next = newx + wwidth * newy; - - if(newx > maxX || newy > maxY || newx < minX || newy < minY || tcost(team, cost, next) == impassable) continue; - - float add = tileCost(team, cost, current, next); - - if(add < 0) continue; - - float newCost = costs.get(current) + add; - - if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){ - costs.put(next, newCost); - float priority = newCost + heuristic(next, goalPos); - frontier.add(next, priority); - } - } - } - - return -1f; - } - - int makeNodeIndex(int cx, int cy, int dir, int portal){ - //to make sure there's only one way to refer to each node, the direction must be 0 or 1 (referring to portals on the top or right edge) - - //direction can only be 2 if cluster X is 0 (left edge of map) - if(dir == 2 && cx != 0){ - dir = 0; - cx --; - } - - //direction can only be 3 if cluster Y is 0 (bottom edge of map) - if(dir == 3 && cy != 0){ - dir = 1; - cy --; - } - - return NodeIndex.get(cx + cy * cwidth, dir, portal); - } - - //uses A* to find the closest node index to specified coordinates - //this node is used in cluster A* - /** @return MAX_VALUE if no node is found */ - private int findClosestNode(int team, int pathCost, int tileX, int tileY){ - int cx = tileX / clusterSize, cy = tileY / clusterSize; - - if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight){ - return Integer.MAX_VALUE; - } - - PathCost cost = idToCost(pathCost); - Cluster cluster = getCreateCluster(team, pathCost, cx, cy); - int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1); - - int bestPortalPair = Integer.MAX_VALUE; - float bestCost = Float.MAX_VALUE; - - //A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra) - for(int dir = 0; dir < 4; dir++){ - var portals = cluster.portals[dir]; - if(portals == null) continue; - - for(int j = 0; j < portals.size; j++){ - - int - other = portals.items[j], - otherFrom = Point2.x(other), otherTo = Point2.y(other), - otherAverage = (otherFrom + otherTo) / 2, - ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1), - oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1), - otherX = (moveDirs[dir * 2] * otherAverage + ox), - otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy); - - float connectionCost = innerAstar( - team, cost, - minX, minY, maxX, maxY, - tileX + tileY * wwidth, - otherX + otherY * wwidth, - (moveDirs[dir * 2] * otherFrom + ox), - (moveDirs[dir * 2 + 1] * otherFrom + oy), - (moveDirs[dir * 2] * otherTo + ox), - (moveDirs[dir * 2 + 1] * otherTo + oy) - ); - - //better cost found, update and return - if(connectionCost != -1f && connectionCost < bestCost){ - bestPortalPair = Point2.pack(dir, j); - bestCost = connectionCost; - } - } - } - - if(bestPortalPair != Integer.MAX_VALUE){ - return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair)); - } - - - return Integer.MAX_VALUE; - } - - //distance heuristic: manhattan - private float clusterNodeHeuristic(int team, int pathCost, int nodeA, int nodeB){ - int - clusterA = NodeIndex.cluster(nodeA), - dirA = NodeIndex.dir(nodeA), - portalA = NodeIndex.portal(nodeA), - clusterB = NodeIndex.cluster(nodeB), - dirB = NodeIndex.dir(nodeB), - portalB = NodeIndex.portal(nodeB), - rangeA = getCreateCluster(team, pathCost, clusterA).portals[dirA].items[portalA], - rangeB = getCreateCluster(team, pathCost, clusterB).portals[dirB].items[portalB]; - - float - averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f, - x1 = (moveDirs[dirA * 2] * averageA + (clusterA % cwidth) * clusterSize + offsets[dirA * 2] * (clusterSize - 1) + nextOffsets[dirA * 2] / 2f), - y1 = (moveDirs[dirA * 2 + 1] * averageA + (clusterA / cwidth) * clusterSize + offsets[dirA * 2 + 1] * (clusterSize - 1) + nextOffsets[dirA * 2 + 1] / 2f), - - averageB = (Point2.x(rangeB) + Point2.y(rangeB)) / 2f, - x2 = (moveDirs[dirB * 2] * averageB + (clusterB % cwidth) * clusterSize + offsets[dirB * 2] * (clusterSize - 1) + nextOffsets[dirB * 2] / 2f), - y2 = (moveDirs[dirB * 2 + 1] * averageB + (clusterB / cwidth) * clusterSize + offsets[dirB * 2 + 1] * (clusterSize - 1) + nextOffsets[dirB * 2 + 1] / 2f); - - return Math.abs(x1 - x2) + Math.abs(y1 - y2); - } - - @Nullable IntSeq clusterAstar(PathRequest request, int pathCost, int startNodeIndex, int endNodeIndex){ - var result = request.resultPath; - - if(startNodeIndex == endNodeIndex){ - result.clear(); - result.add(startNodeIndex); - return result; - } - - var team = request.team; - - if(request.costs == null) request.costs = new IntFloatMap(); - if(request.cameFrom == null) request.cameFrom = new IntIntMap(); - if(request.frontier == null) request.frontier = new PathfindQueue(); - - //note: these are NOT cleared, it is assumed that this function cleans up after itself at the end - //is this a good idea? don't know, might hammer the GC with unnecessary objects too - var costs = request.costs; - var cameFrom = request.cameFrom; - var frontier = request.frontier; - - cameFrom.put(startNodeIndex, startNodeIndex); - costs.put(startNodeIndex, 0); - frontier.add(startNodeIndex, 0); - - boolean foundEnd = false; - - while(frontier.size > 0){ - int current = frontier.poll(); - - if(current == endNodeIndex){ - foundEnd = true; - break; - } - - int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current); - int cx = cluster % cwidth, cy = cluster / cwidth; - Cluster clust = getCreateCluster(team, pathCost, cluster); - LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal]; - - //edges for the cluster the node is 'in' - if(innerCons != null){ - checkEdges(request, team, pathCost, current, endNodeIndex, cx, cy, innerCons); - } - - //edges that this node 'faces' from the other side - int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y; - if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){ - Cluster nextCluster = getCreateCluster(team, pathCost, nextCx, nextCy); - int relativeDir = (dir + 2) % 4; - LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal]; - if(outerCons != null){ - checkEdges(request, team, pathCost, current, endNodeIndex, nextCx, nextCy, outerCons); - } - } - } - - //null them out, so they get GC'ed later - //there's no reason to keep them around and waste memory, since this path may never be recalculated - request.costs = null; - request.cameFrom = null; - request.frontier = null; - - if(foundEnd){ - result.clear(); - - int cur = endNodeIndex; - while(cur != startNodeIndex){ - result.add(cur); - cur = cameFrom.get(cur); - } - - result.reverse(); - - return result; - } - return null; - } - - private void checkEdges(PathRequest request, int team, int pathCost, int current, int goal, int cx, int cy, LongSeq connections){ - for(int i = 0; i < connections.size; i++){ - long con = connections.items[i]; - float cost = IntraEdge.cost(con); - int otherDir = IntraEdge.dir(con), otherPortal = IntraEdge.portal(con); - int next = makeNodeIndex(cx, cy, otherDir, otherPortal); - - float newCost = request.costs.get(current) + cost; - - if(newCost < request.costs.get(next, Float.POSITIVE_INFINITY)){ - request.costs.put(next, newCost); - - request.frontier.add(next, newCost + clusterNodeHeuristic(team, pathCost, next, goal)); - request.cameFrom.put(next, current); - } - } - } - - private void updateFields(FieldCache cache, long nsToRun){ - var frontier = cache.frontier; - var fields = cache.fields; - var goalPos = cache.goalPos; - var pcost = cache.cost; - var team = cache.team; - - long start = Time.nanos(); - int counter = 0; - - //actually do the flow field part - while(frontier.size > 0){ - int tile = frontier.removeLast(); - int baseX = tile % wwidth, baseY = tile / wwidth; - int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth; - - //TODO: how can this be null??? serious problem! - int[] curWeights = fields.get(curWeightIndex); - if(curWeights == null) continue; - - int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)]; - - if(cost != impassable){ - for(Point2 point : Geometry.d4){ - - int - dx = baseX + point.x, dy = baseY + point.y, - clx = dx / clusterSize, cly = dy / clusterSize; - - if(clx < 0 || cly < 0 || dx >= wwidth || dy >= wheight) continue; - - int nextWeightIndex = clx + cly * cwidth; - - int[] weights = nextWeightIndex == curWeightIndex ? curWeights : fields.get(nextWeightIndex); - - //out of bounds; not allowed to move this way because no weights were registered here - if(weights == null) continue; - - int newPos = tile + point.x + point.y * wwidth; - - //can't move back to the goal - if(newPos == goalPos) continue; - - if(dx - clx * clusterSize < 0 || dy - cly * clusterSize < 0) continue; - - int newPosArray = (dx - clx * clusterSize) + (dy - cly * clusterSize) * clusterSize; - - int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]); - int oldCost = weights[newPosArray]; - - //a cost of 0 means uninitialized, OR it means we're at the goal position, but that's handled above - if((oldCost == 0 || oldCost > cost + otherCost) && otherCost != impassable){ - frontier.addFirst(newPos); - weights[newPosArray] = cost + otherCost; - } - } - } - - //every N iterations, check the time spent - this prevents extra calls to nano time, which itself is slow - if(nsToRun >= 0 && (counter++) >= updateStepInterval){ - counter = 0; - if(Time.timeSinceNanos(start) >= nsToRun){ - return; - } - } - } - } - - private void addFlowCluster(FieldCache cache, int cluster, boolean addingFrontier){ - addFlowCluster(cache, cluster % cwidth, cluster / cwidth, addingFrontier); - } - - private void addFlowCluster(FieldCache cache, int cx, int cy, boolean addingFrontier){ - //out of bounds - if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight) return; - - var fields = cache.fields; - int key = cx + cy * cwidth; - - if(!fields.containsKey(key)){ - fields.put(key, new int[clusterSize * clusterSize]); - - if(addingFrontier){ - for(int dir = 0; dir < 4; dir++){ - int ox = cx + nextOffsets[dir * 2], oy = cy + nextOffsets[dir * 2 + 1]; - - if(ox < 0 || oy < 0 || ox >= cwidth || ox >= cheight) continue; - - var otherField = cache.fields.get(ox + oy * cwidth); - - if(otherField == null) continue; - - int - relOffset = (dir + 2) % 4, - movex = moveDirs[relOffset * 2], - movey = moveDirs[relOffset * 2 + 1], - otherx1 = offsets[relOffset * 2] * (clusterSize - 1), - othery1 = offsets[relOffset * 2 + 1] * (clusterSize - 1); - - //scan the edge of the cluster - for(int i = 0; i < clusterSize; i++){ - int x = otherx1 + movex * i, y = othery1 + movey * i; - - //check to make sure it's not 0 (uninitialized flowfield data) - if(otherField[x + y * clusterSize] > 0){ - int worldX = x + ox * clusterSize, worldY = y + oy * clusterSize; - - //add the world-relative position to the frontier, so it recalculates - cache.frontier.addFirst(worldX + worldY * wwidth); - - if(debug){ - Core.app.post(() -> Fx.placeBlock.at(worldX *tilesize, worldY * tilesize, 1f)); - } - } - } - } - } - } - } - - private void initializePathRequest(PathRequest request, int team, int costId, int unitX, int unitY, int goalX, int goalY){ - PathCost pcost = idToCost(costId); - - int goalPos = (goalX + goalY * wwidth); - - int node = findClosestNode(team, costId, unitX, unitY); - int dest = findClosestNode(team, costId, goalX, goalY); - - if(dest == Integer.MAX_VALUE){ - request.notFound = true; - //no node found (TODO: invalid state??) - return; - } - - var nodePath = clusterAstar(request, costId, node, dest); - - FieldCache cache = fields.get(Pack.longInt(goalPos, costId)); - //if true, extra values are added on the sides of existing field cells that face new cells. - boolean addingFrontier = true; - - //create the cache if it doesn't exist, and initialize it - if(cache == null){ - cache = new FieldCache(pcost, costId, team, goalPos); - fields.put(cache.mapKey, cache); - FieldCache fcache = cache; - //register field in main thread for iteration - Core.app.post(() -> fieldList.add(fcache)); - cache.frontier.addFirst(goalPos); - addingFrontier = false; //when it's a new field, there is no need to add to the frontier to merge the flowfield - } - - if(nodePath != null){ - int cx = unitX / clusterSize, cy = unitY / clusterSize; - - addFlowCluster(cache, cx, cy, addingFrontier); - - for(int i = -1; i < nodePath.size; i++){ - int - current = i == -1 ? node : nodePath.items[i], - cluster = NodeIndex.cluster(current), - dir = NodeIndex.dir(current), - dx = Geometry.d4[dir].x, - dy = Geometry.d4[dir].y, - ox = cluster % cwidth + dx, - oy = cluster / cwidth + dy; - - addFlowCluster(cache, cluster, addingFrontier); - - //store directional/flipped version of cluster - if(ox >= 0 && oy >= 0 && ox < cwidth && oy < cheight){ - int other = ox + oy * cwidth; - - addFlowCluster(cache, other, addingFrontier); - } - } - } - } - - private PathCost idToCost(int costId){ - return ControlPathfinder.costTypes.get(costId); - } - - public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 mainDestination, Vec2 out, @Nullable boolean[] noResultFound){ - int costId = unit.type.pathCostId; - PathCost cost = idToCost(costId); - - int - team = unit.team.id, - tileX = unit.tileX(), - tileY = unit.tileY(), - packedPos = world.packArray(tileX, tileY), - destX = World.toTile(mainDestination.x), - destY = World.toTile(mainDestination.y), - actualDestX = World.toTile(destination.x), - actualDestY = World.toTile(destination.y), - destPos = destX + destY * wwidth; - - PathRequest request = unitRequests.get(unit); - - unit.hitboxTile(Tmp.r3); - //tile rect size has tile size factored in, since the ray cannot have thickness - float tileRectSize = tilesize + Tmp.r3.height; - - int lastRaycastTile = request == null || world.tileChanges != request.lastWorldUpdate ? -1 : request.lastRaycastTile; - boolean raycastResult = request != null && request.lastRaycastResult; - - //cache raycast results to run every time the world updates, and every tile the unit crosses - if(lastRaycastTile != packedPos){ - //near the destination, standard raycasting tends to break down, so use the more permissive 'near' variant that doesn't take into account edges of walls - raycastResult = unit.within(destination, tilesize * 2.5f) ? !raycastRect(unit.x, unit.y, destination.x, destination.y, team, cost, tileX, tileY, actualDestX, actualDestY, tileRectSize) : !raycast(team, cost, tileX, tileY, actualDestX, actualDestY); - - if(request != null){ - request.lastRaycastTile = packedPos; - request.lastRaycastResult = raycastResult; - request.lastWorldUpdate = world.tileChanges; - } - } - - //if the destination can be trivially reached in a straight line, do that. - if(raycastResult){ - out.set(destination); - return true; - } - - boolean any = false; - - long fieldKey = Pack.longInt(destPos, costId); - - //use existing request if it exists. - if(request != null && request.destination == destPos){ - request.lastUpdateId = state.updateId; - - Tile tileOn = unit.tileOn(), initialTileOn = tileOn; - //TODO: should fields be accessible from this thread? - FieldCache fieldCache = fields.get(fieldKey); - - if(fieldCache != null && tileOn != null){ - FieldCache old = request.oldCache; - //nullify the old field to be GCed, as it cannot be relevant anymore (this path is complete) - if(fieldCache.frontier.isEmpty() && old != null){ - request.oldCache = null; - } - - fieldCache.lastUpdateId = state.updateId; - int maxIterations = 30; //TODO higher/lower number? is this still too slow? - int i = 0; - boolean recalc = false; - - //TODO last pos can change if the flowfield changes. - if(initialTileOn.pos() != request.lastTile || request.lastTargetTile == null){ - boolean anyNearSolid = false; - - //find the next tile until one near a solid block is discovered - while(i ++ < maxIterations){ - int value = getCost(fieldCache, old, tileOn.x, tileOn.y); - - Tile current = null; - int minCost = 0; - for(int dir = 0; dir < 4; dir ++){ - Point2 point = Geometry.d4[dir]; - int dx = tileOn.x + point.x, dy = tileOn.y + point.y; - - Tile other = world.tile(dx, dy); - - if(other == null) continue; - - int packed = world.packArray(dx, dy); - int otherCost = getCost(fieldCache, old, dx, dy), relCost = otherCost - value; - - if(relCost > 2 || otherCost <= 0){ - anyNearSolid = true; - } - - if((value == 0 || otherCost < value) && otherCost != impassable && (otherCost != 0 || packed == destPos) && (current == null || otherCost < minCost) && passable(unit.team.id, cost, packed)){ - current = other; - minCost = otherCost; - } - } - - //TODO raycast spam = extremely slow - //...flowfield integration spam is also really slow. - if(!(current == null || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){ - - //when anyNearSolid is false, no solid tiles have been encountered anywhere so far, so raycasting is a waste of time - if(anyNearSolid && !tileOn.dangerous() && raycastRect(unit.x, unit.y, current.x * tilesize, current.y * tilesize, team, cost, initialTileOn.x, initialTileOn.y, current.x, current.y, tileRectSize)){ - - //TODO this may be a mistake - if(tileOn == initialTileOn){ - recalc = true; - any = true; - } - - break; - }else{ - tileOn = current; - any = true; - - if(current.array() == destPos){ - break; - } - } - - }else{ - break; - } - } - - request.lastTargetTile = any ? tileOn : null; - if(debug && tileOn != null){ - Fx.placeBlock.at(tileOn.worldx(), tileOn.worldy(), 1); - } - } - - if(request.lastTargetTile != null){ - out.set(request.lastTargetTile); - request.lastTile = recalc ? -1 : initialTileOn.pos(); - return true; - } - } - }else if(request == null){ - - //queue new request. - unitRequests.put(unit, request = new PathRequest(unit, team, costId, destPos)); - - PathRequest f = request; - - //on the pathfinding thread: initialize the request - queue.post(() -> { - threadPathRequests.add(f); - recalculatePath(f); - }); - - out.set(destination); - - return true; - } - - if(noResultFound != null){ - noResultFound[0] = request.notFound; - } - return false; - } - - private void recalculatePath(PathRequest request){ - initializePathRequest(request, request.team, request.costId, request.unit.tileX(), request.unit.tileY(), request.destination % wwidth, request.destination / wwidth); - } - - private int getCost(FieldCache cache, FieldCache old, int x, int y){ - //fall back to the old flowfield when possible - it's best not to use partial results from the base cache - if(old != null){ - return getCost(old, x, y, false); - } - return getCost(cache, x, y, true); - } - - private int getCost(FieldCache cache, int x, int y, boolean requeue){ - int[] field = cache.fields.get(x / clusterSize + (y / clusterSize) * cwidth); - if(field == null){ - if(!requeue) return 0; - //request a new flow cluster if one wasn't found; this may be a spammed a bit, but the function will return early once it's created the first time - queue.post(() -> addFlowCluster(cache, x / clusterSize, y / clusterSize, true)); - return 0; - } - return field[(x % clusterSize) + (y % clusterSize) * clusterSize]; - } - - private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){ - int ww = wwidth, wh = wheight; - int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; - int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; - int e2, err = dx - dy; - - while(x >= 0 && y >= 0 && x < ww && y < wh){ - if(avoid(team, type, x + y * wwidth)) return true; - if(x == x2 && y == y2) return false; - - //diagonal ver - e2 = 2 * err; - if(e2 > -dy){ - err -= dy; - x += sx; - } - - if(e2 < dx){ - err += dx; - y += sy; - } - } - - return true; - } - - private static boolean raycastNear(int team, PathCost type, int x1, int y1, int x2, int y2){ - int ww = wwidth, wh = wheight; - int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; - int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; - int err = dx - dy; - - - while(x >= 0 && y >= 0 && x < ww && y < wh){ - if(!passable(team, type, x + y * wwidth)) return true; - if(x == x2 && y == y2) return false; - - //no diagonal ver - if(2 * err + dy > dx - 2 * err){ - err -= dy; - x += sx; - }else{ - err += dx; - y += sy; - } - - } - - return true; - } - - private static boolean overlap(int team, PathCost type, int x, int y, float startX, float startY, float endX, float endY, float rectSize){ - if(x < 0 || y < 0 || x >= wwidth || y >= wheight) return false; - if(!passable(team, type, x + y * wwidth)){ - return Intersector.intersectSegmentRectangleFast(startX, startY, endX, endY, x * tilesize - rectSize/2f, y * tilesize - rectSize/2f, rectSize, rectSize); - } - return false; - } - - private static boolean raycastRect(float startX, float startY, float endX, float endY, int team, PathCost type, int x1, int y1, int x2, int y2, float rectSize){ - int ww = wwidth, wh = wheight; - int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1; - int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1; - int e2, err = dx - dy; - - while(x >= 0 && y >= 0 && x < ww && y < wh){ - if( - !passable(team, type, x + y * wwidth) || - overlap(team, type, x + 1, y, startX, startY, endX, endY, rectSize) || - overlap(team, type, x - 1, y, startX, startY, endX, endY, rectSize) || - overlap(team, type, x, y + 1, startX, startY, endX, endY, rectSize) || - overlap(team, type, x, y - 1, startX, startY, endX, endY, rectSize) - ) return true; - - if(x == x2 && y == y2) return false; - - //diagonal ver - e2 = 2 * err; - if(e2 > -dy){ - err -= dy; - x += sx; - } - - if(e2 < dx){ - err += dx; - y += sy; - } - } - - return true; - } - - private static boolean avoid(int team, PathCost type, int tilePos){ - int cost = cost(team, type, tilePos); - return cost == impassable || cost >= 2; - } - - private static boolean passable(int team, PathCost cost, int pos){ - int amount = cost.getCost(team, pathfinder.tiles[pos]); - //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable - return amount != impassable && !(cost == costTypes.get(costNaval) && amount >= 6000); - } - - private static boolean solid(int team, PathCost type, int x, int y){ - return x < 0 || y < 0 || x >= wwidth || y >= wheight || solid(team, type, x + y * wwidth, true); - } - - private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){ - int cost = cost(team, type, tilePos); - return cost == impassable || (checkWall && cost >= 6000); - } - - private static int cost(int team, PathCost cost, int tilePos){ - if(state.rules.limitMapArea && !Team.get(team).isAI()){ - int x = tilePos % wwidth, y = tilePos / wwidth; - if(x < state.rules.limitX || y < state.rules.limitY || x > state.rules.limitX + state.rules.limitWidth || y > state.rules.limitY + state.rules.limitHeight){ - return impassable; - } - } - return cost.getCost(team, pathfinder.tiles[tilePos]); - } - - private void clusterChanged(int team, int pathCost, int cx, int cy){ - int index = cx + cy * cwidth; - - for(var req : threadPathRequests){ - long mapKey = Pack.longInt(req.destination, pathCost); - var field = fields.get(mapKey); - if((field != null && field.fields.containsKey(index)) || req.notFound){ - invalidRequests.add(req); - } - } - - } - - private void updateClustersComplete(int clusterIndex){ - for(int team = 0; team < clusters.length; team++){ - var dim1 = clusters[team]; - if(dim1 != null){ - for(int pathCost = 0; pathCost < dim1.length; pathCost++){ - var dim2 = dim1[pathCost]; - if(dim2 != null){ - var cluster = dim2[clusterIndex]; - if(cluster != null){ - updateCluster(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); - clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); - } - } - } - } - } - } - - private void updateClustersInner(int clusterIndex){ - for(int team = 0; team < clusters.length; team++){ - var dim1 = clusters[team]; - if(dim1 != null){ - for(int pathCost = 0; pathCost < dim1.length; pathCost++){ - var dim2 = dim1[pathCost]; - if(dim2 != null){ - var cluster = dim2[clusterIndex]; - if(cluster != null){ - updateInnerEdges(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth, cluster); - clusterChanged(team, pathCost, clusterIndex % cwidth, clusterIndex / cwidth); - } - } - } - } - } - } - - @Override - public void run(){ - long lastInvalidCheck = Time.millis() + invalidateCheckInterval; - - while(true){ - if(net.client()) return; - try{ - - - if(state.isPlaying()){ - queue.run(); - - clustersToUpdate.each(cluster -> { - updateClustersComplete(cluster); - - //just in case: don't redundantly update inner clusters after you've recalculated it entirely - clustersToInnerUpdate.remove(cluster); - }); - - clustersToInnerUpdate.each(cluster -> { - //only recompute the inner links - updateClustersInner(cluster); - }); - - clustersToInnerUpdate.clear(); - clustersToUpdate.clear(); - - //periodically check for invalidated paths - if(Time.timeSinceMillis(lastInvalidCheck) > invalidateCheckInterval){ - lastInvalidCheck = Time.millis(); - - var it = invalidRequests.iterator(); - while(it.hasNext()){ - var request = it.next(); - - //invalid request, ignore it - if(request.invalidated){ - it.remove(); - continue; - } - - long mapKey = Pack.longInt(request.destination, request.costId); - - var field = fields.get(mapKey); - - if(field != null){ - //it's only worth recalculating a path when the current frontier has finished; otherwise the unit will be following something incomplete. - if(field.frontier.isEmpty()){ - - //remove the field, to be recalculated next update one recalculatePath is processed - fields.remove(field.mapKey); - Core.app.post(() -> fieldList.remove(field)); - - //once the field is invalidated, make sure that all the requests that have it stored in their 'old' field, so units don't stutter during recalculations - for(var otherRequest : threadPathRequests){ - if(otherRequest.destination == request.destination){ - otherRequest.oldCache = field; - } - } - - //the recalculation is done next update, so multiple path requests in the same batch don't end up removing and recalculating the field multiple times. - queue.post(() -> recalculatePath(request)); - //it has been processed. - it.remove(); - } - }else{ //there's no field, presumably because a previous request already invalidated it. - queue.post(() -> recalculatePath(request)); - it.remove(); - } - } - } - - //each update time (not total!) no longer than maxUpdate - for(FieldCache cache : fields.values()){ - updateFields(cache, maxUpdate); - } - } - - try{ - Thread.sleep(updateInterval); - }catch(InterruptedException e){ - //stop looping when interrupted externally - return; - } - }catch(Throwable e){ - e.printStackTrace(); - } - } - } - - @Struct - static class IntraEdgeStruct{ - @StructField(8) - int dir; - @StructField(8) - int portal; - - float cost; - } - - @Struct - static class NodeIndexStruct{ - @StructField(22) - int cluster; - @StructField(2) - int dir; - @StructField(8) - int portal; - } -} diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 07a30878b1..dec38c14ed 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -4,7 +4,6 @@ import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; -import mindustry.*; import mindustry.ai.*; import mindustry.core.*; import mindustry.entities.*; @@ -35,7 +34,6 @@ public class CommandAI extends AIController{ protected boolean stopAtTarget, stopWhenInRange; protected Vec2 lastTargetPos; - protected int pathId = -1; protected boolean blockingUnit; protected float timeSpentBlocked; @@ -251,7 +249,7 @@ public class CommandAI extends AIController{ timeSpentBlocked = 0f; } - move = hpath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); + move = controlPath.getPathPosition(unit, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime); //rare case where unit must be perfectly aligned (happens with 1-tile gaps) alwaysArrive = vecOut.epsilonEquals(unit.tileX() * tilesize, unit.tileY() * tilesize); //we've reached the final point if the returned coordinate is equal to the supplied input @@ -421,7 +419,6 @@ public class CommandAI extends AIController{ //this is an allocation, but it's relatively rarely called anyway, and outside mutations must be prevented targetPos = lastTargetPos = pos.cpy(); attackTarget = null; - pathId = Vars.controlPath.nextTargetId(); this.stopWhenInRange = stopWhenInRange; } @@ -436,7 +433,6 @@ public class CommandAI extends AIController{ public void commandTarget(Teamc moveTo, boolean stopAtTarget){ attackTarget = moveTo; this.stopAtTarget = stopAtTarget; - pathId = Vars.controlPath.nextTargetId(); } /* diff --git a/core/src/mindustry/ai/types/LogicAI.java b/core/src/mindustry/ai/types/LogicAI.java index 2ac8840d3f..9486ebea42 100644 --- a/core/src/mindustry/ai/types/LogicAI.java +++ b/core/src/mindustry/ai/types/LogicAI.java @@ -85,7 +85,7 @@ public class LogicAI extends AIController{ if(unit.isFlying()){ moveTo(Tmp.v1.set(moveX, moveY), 1f, 30f); }else{ - if(hpath.getPathPosition(unit, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){ + if(controlPath.getPathPosition(unit, Tmp.v2.set(moveX, moveY), Tmp.v2, Tmp.v1, null)){ moveTo(Tmp.v1, 1f, Tmp.v2.epsilonEquals(Tmp.v1, 4.1f) ? 30f : 0f); } }