Faster avoidance calculation

This commit is contained in:
Anuken
2025-07-06 14:23:50 -04:00
parent b5eba35ed9
commit f0d8c06380
7 changed files with 142 additions and 48 deletions

View File

@@ -204,6 +204,8 @@ public class Vars implements Loadable{
public static boolean enableDarkness = true;
/** Whether to draw debug lines for collisions. */
public static boolean drawDebugHitboxes = false;
/** Whether to draw avoidance fields. */
public static boolean debugDrawAvoidance = false;
/** application data directory, equivalent to {@link Settings#getDataDirectory()} */
public static Fi dataDirectory;
/** data subdirectory used for screenshots */
@@ -257,6 +259,7 @@ public class Vars implements Loadable{
public static BaseRegistry bases;
public static GlobalVars logicVars;
public static MapEditor editor;
public static AvoidanceProcess avoidance;
public static GameService service = new GameService();
public static Universe universe;

View File

@@ -107,8 +107,6 @@ public class Pathfinder implements Runnable{
boolean needsRefresh;
int[][] avoidance;
public Pathfinder(){
clearCache();
@@ -123,8 +121,6 @@ public class Pathfinder implements Runnable{
mainList = new Seq<>();
clearCache();
avoidance = new int[4][tiles.length];
for(int i = 0; i < tiles.length; i++){
Tile tile = world.tiles.geti(i);
tiles[i] = packTile(tile);
@@ -146,38 +142,6 @@ public class Pathfinder implements Runnable{
start();
});
//TODO: the before/after game update avoidance code is very slow, but it works as a concept. future versions can do the clearing/area definitions in another thread
Events.run(Trigger.beforeGameUpdate, () -> {
for(var unit : Groups.unit){
if(!unit.isFlying()){
int layer = unit.collisionLayer();
if(layer < avoidance.length){
float scaling = 2f;
int r = Math.max(1, Mathf.ceil(unit.hitSize * 0.6f / tilesize * scaling));
var arr = avoidance[layer];
float rad2 = (unit.hitSize * unitCollisionRadiusScale / tilesize * scaling) * (unit.hitSize * unitCollisionRadiusScale / tilesize * scaling);
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
int x = dx + unit.tileX(), y = dy + unit.tileY();
if(x >= 0 && y >= 0 && x < wwidth && y < wheight && (dx*dx + dy*dy) <= rad2){
arr[x + y * wwidth] = Math.max(arr[x + y * wwidth], Integer.MAX_VALUE - unit.id);
}
}
}
}
}
}
});
Events.run(Trigger.afterGameUpdate, () -> {
for(var arr : avoidance){
Arrays.fill(arr, 0);
}
});
Events.on(ResetEvent.class, event -> stop());
Events.on(TileChangeEvent.class, event -> {
@@ -394,11 +358,11 @@ public class Pathfinder implements Runnable{
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals){
return getTargetTile(tile, path, diagonals, -1, 0);
return getTargetTile(tile, path, diagonals, 0);
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals, int collisionLayer, int unitId){
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals, int avoidanceId){
if(tile == null) return null;
//uninitialized flowfields are not applicable
@@ -430,7 +394,7 @@ public class Pathfinder implements Runnable{
int value = values[apos];
var points = diagonals ? Geometry.d8 : Geometry.d4;
int[] avoid = collisionLayer == -1 ? null : avoidance[collisionLayer];
int[] avoid = avoidanceId <= 0 ? null : avoidance.getAvoidance();
Tile current = null;
int tl = 0;
@@ -441,7 +405,7 @@ public class Pathfinder implements Runnable{
if(other == null) continue;
int packed = dx/res + dy/res * ww;
int avoidance = avoid == null || unitId == 0 ? 0 : avoid[packed] > Integer.MAX_VALUE - unitId ? 1 : 0;
int avoidance = avoid == null ? 0 : avoid[packed] > Integer.MAX_VALUE - avoidanceId ? 1 : 0;
int cost = values[packed] + avoidance;
if(cost < value && avoidance == 0 && (current == null || cost < tl) && path.passable(packed) &&
@@ -699,8 +663,8 @@ public class Pathfinder implements Runnable{
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from, int collisionlayer, int unitId){
return pathfinder.getTargetTile(from, this, true, collisionlayer, unitId);
public @Nullable Tile getNextTile(Tile from, int unitAvoidanceId){
return pathfinder.getTargetTile(from, this, true, unitAvoidanceId);
}
public boolean hasCompleteWeights(){

View File

@@ -15,11 +15,14 @@ public class GroundAI extends AIController{
float stuckTime = 0f;
float stuckX = -999f, stuckY = -999f;
static final float stuckThreshold = 1.5f * 60f;
static final float stuckRange = tilesize * 1.5f;
@Override
public void updateMovement(){
//if it hasn't moved the stuck range in twice the time it should have taken, it's stuck
float stuckThreshold = Math.max(1f, stuckRange * 2f / unit.type.speed);
Building core = unit.closestEnemyCore();
boolean moved = false;
@@ -59,7 +62,7 @@ public class GroundAI extends AIController{
if(moved){
if(unit.within(stuckX, stuckY, tilesize * 1.5f)){
if(unit.within(stuckX, stuckY, stuckRange)){
stuckTime += Time.delta;
if(stuckTime - Time.delta < stuckThreshold && stuckTime >= stuckThreshold){
float radius = unit.hitSize * Vars.unitCollisionRadiusScale * 2f;
@@ -67,7 +70,7 @@ public class GroundAI extends AIController{
if(other != unit && other.controller() instanceof GroundAI ai && other.within(unit.x, unit.y, radius + other.hitSize * unitCollisionRadiusScale)){
ai.stuckX = other.x;
ai.stuckY = other.y;
ai.stuckTime = stuckThreshold + 1f;
ai.stuckTime = Math.max(1f, stuckRange * 2f / other.type.speed) + 1f;
}
});
}

View File

@@ -12,7 +12,8 @@ import static mindustry.Vars.*;
public class AsyncCore{
//all processes to be executed each frame
public final Seq<AsyncProcess> processes = Seq.with(
new PhysicsProcess()
new PhysicsProcess(),
avoidance = new AvoidanceProcess()
);
//futures to be awaited

View File

@@ -0,0 +1,115 @@
package mindustry.async;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import java.util.*;
import static mindustry.Vars.*;
public class AvoidanceProcess implements AsyncProcess{
/** cached world size */
static int wwidth, wheight;
@Nullable int[] buffer1, buffer2;
volatile boolean swap;
IntSeq requests = new IntSeq();
@Nullable int[] avoidance;
boolean modified;
boolean active;
public @Nullable int[] getAvoidance(){
if(!active){
//lazily initialize and begin processing after this first request
buffer1 = new int[wwidth * wheight];
buffer2 = new int[wwidth * wheight];
active = true;
}
return avoidance;
}
@Override
public void init(){
wwidth = Vars.world.width();
wheight = Vars.world.height();
}
@Override
public void reset(){
buffer1 = buffer2 = avoidance = null;
swap = false;
modified = false;
active = false;
requests.clear();
}
@Override
public void begin(){
if(!active) return;
requests.clear();
avoidance = !swap ? buffer1 : buffer2;
for(var team : state.teams.present){
//only do avoidance if it's relevant to the team
if(team.team.isAI() && !team.team.rules().rtsAi){
for(var unit : team.units){
if(!unit.isFlying()){
int layer = unit.collisionLayer();
if(layer < PhysicsProcess.layers){
//scaling is oversized 2x because units need to avoid things that are at their origin tile
float scaling = 2f;
requests.add(Point2.pack(unit.tileX(), unit.tileY()), Float.floatToRawIntBits(unit.hitSize * unitCollisionRadiusScale / tilesize * scaling), unit.id);
}
}
}
}
}
}
@Override
public void process(){
//double buffering; one buffer is always valid (not being updated)
var buffer = swap ? buffer1 : buffer2;
swap = !swap;
if(buffer == null) return;
//technically, this is wrong, and will lead to flickering avoidance when all units are gone, but this doesn't matter because it's not being queried either way
if(modified){
Arrays.fill(buffer, 0);
}
modified = requests.size > 0;
int total = requests.size;
int[] items = requests.items;
for(int i = 0; i < total; i += 3){
int point = items[i], id = items[i + 2];
int rx = Point2.x(point), ry = Point2.y(point);
float rad = Float.intBitsToFloat(items[i + 1]);
float rad2 = rad * rad;
int r = Math.max(1, Mathf.ceil(rad));
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
int x = dx + rx, y = dy + ry;
if(x >= 0 && y >= 0 && x < wwidth && y < wheight && (dx*dx + dy*dy) <= rad2){
buffer[x + y * wwidth] = Math.max(buffer[x + y * wwidth], Integer.MAX_VALUE - id);
}
}
}
}
}
@Override
public boolean shouldProcess(){
return active;
}
}

View File

@@ -4,6 +4,7 @@ import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.*;
import mindustry.async.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -140,7 +141,7 @@ public class AIController implements UnitController{
Tile tile = unit.tileOn();
if(tile == null) return;
Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile, avoidance ? unit.collisionLayer() : -1, unit.id);
Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile, avoidance && unit.collisionLayer() == PhysicsProcess.layerGround ? unit.id : 0);
if((tile == targetTile && stopAtTargetTile) || !unit.canPass(targetTile.x, targetTile.y)) return;

View File

@@ -56,10 +56,17 @@ public class DebugCollisionRenderer{
}
}
}
if(debugDrawAvoidance && tile != null){
int[] avoid = avoidance.getAvoidance();
if(avoid != null && avoid[tile.array()] != 0){
Draw.color(0f, 1f, 1f, 0.25f);
Fill.square(tile.worldx(), tile.worldy(), 4f);
}
}
}
}
Groups.draw.each(d -> {
if(d instanceof Unit u && rect.overlaps(Tmp.r1.setCentered(u.x, u.y, d.clipSize())) && !u.isFlying()){
u.hitboxTile(Tmp.r1);