This commit is contained in:
Anuken
2023-11-10 13:48:41 -05:00
parent 69e2f6b93b
commit 24b7d99a69

View File

@@ -14,7 +14,6 @@ import mindustry.game.EventType.*;
import mindustry.game.*; import mindustry.game.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.graphics.*; import mindustry.graphics.*;
import mindustry.ui.*;
import mindustry.world.*; import mindustry.world.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@@ -53,6 +52,7 @@ public class HierarchyPathFinder implements Runnable{
}; };
//maps pathCost -> flattened array of clusters in 2D //maps pathCost -> flattened array of clusters in 2D
//(what about teams? different path costs?)
Cluster[][] clusters; Cluster[][] clusters;
int cwidth, cheight; int cwidth, cheight;
@@ -64,42 +64,51 @@ public class HierarchyPathFinder implements Runnable{
//individual requests based on unit //individual requests based on unit
ObjectMap<Unit, PathRequest> unitRequests = new ObjectMap<>(); ObjectMap<Unit, PathRequest> unitRequests = new ObjectMap<>();
//maps position in world in (x + y * width format) to a cache of flow fields //maps position in world in (x + y * width format) to a cache of flow fields
IntMap<FieldCache> requests = new IntMap<>(); IntMap<FieldCache> fields = new IntMap<>();
//these are for inner edge A* //these are for inner edge A*
IntFloatMap innerCosts = new IntFloatMap(); IntFloatMap innerCosts = new IntFloatMap();
PathfindQueue innerFrontier = new PathfindQueue(); PathfindQueue innerFrontier = new PathfindQueue();
//ONLY modify on pathfinding thread.
IntSet clustersToUpdate = new IntSet();
IntSet clustersToInnerUpdate = new IntSet();
/** Current pathfinding thread */ /** Current pathfinding thread */
@Nullable Thread thread; @Nullable Thread thread;
//path requests are per-unit //path requests are per-unit
//these contain //these contain
static class PathRequest{ static class PathRequest{
int destination; final Unit unit;
final int destination;
//resulting path of nodes //resulting path of nodes
IntSeq resultPath = new IntSeq(); final IntSeq resultPath = new IntSeq();
//node index -> total cost //node index -> total cost
IntFloatMap costs = new IntFloatMap(); final IntFloatMap costs = new IntFloatMap();
//node index (NodeIndex struct) -> node it came from TODO merge them //node index (NodeIndex struct) -> node it came from TODO merge them
IntIntMap cameFrom = new IntIntMap(); final IntIntMap cameFrom = new IntIntMap();
//frontier for A* //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; this.destination = destination;
} }
} }
static class FieldCache{ static class FieldCache{
PathCost cost; final PathCost cost;
int team; final int team;
int goalPos; final int goalPos;
//frontier for flow fields //frontier for flow fields
IntQueue frontier = new IntQueue(); final IntQueue frontier = new IntQueue();
//maps cluster index to field weights; 0 means uninitialized //maps cluster index to field weights; 0 means uninitialized
IntMap<int[]> fields = new IntMap<>(); final IntMap<int[]> fields = new IntMap<>();
//TODO: node map for merging //TODO: node map for merging
//TODO: how to extend flowfields? //TODO: how to extend flowfields?
@@ -129,7 +138,35 @@ public class HierarchyPathFinder implements Runnable{
//TODO very inefficient, this is only for debugging //TODO very inefficient, this is only for debugging
Events.on(TileChangeEvent.class, e -> { 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){ if(debug){
@@ -269,6 +306,7 @@ public class HierarchyPathFinder implements Runnable{
out.set(x, y); 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){ void createCluster(int team, int pathCost, int cx, int cy){
if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight]; if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight];
Cluster cluster = clusters[pathCost][cy * cwidth + cx]; Cluster cluster = clusters[pathCost][cy * cwidth + cx];
@@ -524,6 +562,7 @@ public class HierarchyPathFinder implements Runnable{
//TODO //TODO
PathCost cost = ControlPathfinder.costGround; PathCost cost = ControlPathfinder.costGround;
//TODO: cluster can be null!!
Cluster cluster = clusters[pathCost][cx + cy * cwidth]; 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 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; int costId = 0;
PathCost pcost = ControlPathfinder.costGround; PathCost pcost = ControlPathfinder.costGround;
int team = unit.team.id;
int goalPos = (goalX + goalY * wwidth); 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); int dest = findClosestNode(team, costId, goalX, goalY);
//TODO: not new?
PathRequest request = new PathRequest(dest);
var nodePath = clusterAstar(request, costId, node, dest); var nodePath = clusterAstar(request, costId, node, dest);
//TODO: how to reuse //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()){ if(cache.frontier.isEmpty()){
cache.frontier.addFirst(goalPos); cache.frontier.addFirst(goalPos);
@@ -788,7 +823,7 @@ public class HierarchyPathFinder implements Runnable{
if(nodePath != null){ if(nodePath != null){
int fsize = clusterSize * clusterSize; int fsize = clusterSize * clusterSize;
int cx = unit.tileX() / clusterSize, cy = unit.tileY() / clusterSize; int cx = unitX / clusterSize, cy = unitY / clusterSize;
var fields = cache.fields; 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){ public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){
int costId = 0; 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){ //TODO: collect old requests that have not been accessed in a while. not sure where.
int value = getCost(fields, tileOn.x, tileOn.y); request.lastUpdateId = state.updateId;
Tile current = null; //use existing request if it exists.
int tl = 0; if(request != null && request.destination == destPos){
//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); 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); Tile current = null;
int otherCost = getCost(fields, dx, dy); 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) && Tile other = world.tile(dx, dy);
!(point.x != 0 && point.y != 0 && (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) ||
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 (!passable(ControlPathfinder.costGround, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap
current = other; current = other;
tl = otherCost; 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()))){ }else{
out.set(current); //queue new request.
return true; 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; noResultFound[0] = true;
@@ -919,10 +980,24 @@ public class HierarchyPathFinder implements Runnable{
if(state.isPlaying()){ if(state.isPlaying()){
queue.run(); 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 //TODO: update everything else too
//each update time (not total!) no longer than maxUpdate //each update time (not total!) no longer than maxUpdate
for(FieldCache cache : requests.values()){ for(FieldCache cache : fields.values()){
updateFields(cache, maxUpdate); updateFields(cache, maxUpdate);
} }
} }