diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index cc09494e43..af50b4e377 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -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; diff --git a/core/src/mindustry/ai/Pathfinder.java b/core/src/mindustry/ai/Pathfinder.java index f2b6160623..3ee3ee55d5 100644 --- a/core/src/mindustry/ai/Pathfinder.java +++ b/core/src/mindustry/ai/Pathfinder.java @@ -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(){ diff --git a/core/src/mindustry/ai/types/GroundAI.java b/core/src/mindustry/ai/types/GroundAI.java index 24a7c62b1e..5eac8415bd 100644 --- a/core/src/mindustry/ai/types/GroundAI.java +++ b/core/src/mindustry/ai/types/GroundAI.java @@ -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; } }); } diff --git a/core/src/mindustry/async/AsyncCore.java b/core/src/mindustry/async/AsyncCore.java index 9987bc31bd..5c79ddd397 100644 --- a/core/src/mindustry/async/AsyncCore.java +++ b/core/src/mindustry/async/AsyncCore.java @@ -12,7 +12,8 @@ import static mindustry.Vars.*; public class AsyncCore{ //all processes to be executed each frame public final Seq processes = Seq.with( - new PhysicsProcess() + new PhysicsProcess(), + avoidance = new AvoidanceProcess() ); //futures to be awaited diff --git a/core/src/mindustry/async/AvoidanceProcess.java b/core/src/mindustry/async/AvoidanceProcess.java new file mode 100644 index 0000000000..9038c94591 --- /dev/null +++ b/core/src/mindustry/async/AvoidanceProcess.java @@ -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; + } +} diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index dffe755d49..3b8b03d110 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -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; diff --git a/core/src/mindustry/graphics/DebugCollisionRenderer.java b/core/src/mindustry/graphics/DebugCollisionRenderer.java index ce12848f3f..5ba05e1f82 100644 --- a/core/src/mindustry/graphics/DebugCollisionRenderer.java +++ b/core/src/mindustry/graphics/DebugCollisionRenderer.java @@ -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);