mirror of
https://github.com/Anuken/Mindustry.git
synced 2025-02-06 08:57:45 +07:00
Invalidate path requests
This commit is contained in:
parent
c8209d568f
commit
565d3313a4
@ -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<Unit, PathRequest> unitRequests = new ObjectMap<>();
|
||||
|
||||
Seq<PathRequest> 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<PathRequest> 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);
|
||||
|
Loading…
Reference in New Issue
Block a user