diff --git a/core/src/io/anuke/mindustry/ai/Pathfinder.java b/core/src/io/anuke/mindustry/ai/Pathfinder.java index 9df0c432c8..66f9113406 100644 --- a/core/src/io/anuke/mindustry/ai/Pathfinder.java +++ b/core/src/io/anuke/mindustry/ai/Pathfinder.java @@ -1,164 +1,249 @@ package io.anuke.mindustry.ai; -import io.anuke.arc.Events; -import io.anuke.arc.collection.IntArray; -import io.anuke.arc.collection.IntQueue; -import io.anuke.arc.math.geom.Geometry; -import io.anuke.arc.math.geom.Point2; +import io.anuke.annotations.Annotations.*; +import io.anuke.arc.*; +import io.anuke.arc.collection.*; +import io.anuke.arc.function.*; +import io.anuke.arc.math.geom.*; import io.anuke.arc.util.*; -import io.anuke.mindustry.game.EventType.TileChangeEvent; -import io.anuke.mindustry.game.EventType.WorldLoadEvent; -import io.anuke.mindustry.game.Team; -import io.anuke.mindustry.game.Teams.TeamData; -import io.anuke.mindustry.net.Net; -import io.anuke.mindustry.world.Pos; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.meta.BlockFlag; +import io.anuke.arc.util.async.*; +import io.anuke.mindustry.core.GameState.*; +import io.anuke.mindustry.game.EventType.*; +import io.anuke.mindustry.game.*; +import io.anuke.mindustry.gen.*; +import io.anuke.mindustry.world.*; +import io.anuke.mindustry.world.meta.*; import static io.anuke.mindustry.Vars.*; -public class Pathfinder{ +public class Pathfinder implements Runnable{ private static final long maxUpdate = Time.millisToNanos(4); - private PathData[] paths; - private IntArray blocked = new IntArray(); + private static final int updateFPS = 60; + private static final int updateInterval = 1000 / updateFPS; + private static final int impassable = -1; + + /** tile data, see PathTileStruct */ + private int[][] tiles; + /** unordered array of path data for iteration only. DO NOT iterate ot access this in the main thread.*/ + private Array list = new Array<>(); + /** Maps teams + flags to a valid path to get to that flag for that team. */ + private PathData[][] pathMap = new PathData[Team.all.length][PathTarget.all.length]; + /** Grid map of created path data that should not be queued again. */ + private GridBits created = new GridBits(Team.all.length, PathTarget.all.length); + /** handles task scheduling on the update thread. */ + private TaskQueue queue = new TaskQueue(); + /** current pathfinding thread */ + private @Nullable Thread thread; public Pathfinder(){ - Events.on(WorldLoadEvent.class, event -> clear()); - Events.on(TileChangeEvent.class, event -> { - if(net.client()) return; + Events.on(WorldLoadEvent.class, event -> { + stop(); - for(Team team : Team.all){ - TeamData data = state.teams.get(team); - if(state.teams.isActive(team) && data.team != event.tile.getTeam()){ - update(event.tile, data.team); + tiles = new int[world.width()][world.height()]; + pathMap = new PathData[Team.all.length][PathTarget.all.length]; + created = new GridBits(Team.all.length, PathTarget.all.length); + list = new Array<>(); + + for(int x = 0; x < world.width(); x++){ + for(int y = 0; y < world.height(); y++){ + tiles[x][y] = packTile(world.rawTile(x, y)); } } - update(event.tile, event.tile.getTeam()); + start(); + }); + + Events.on(TileChangeEvent.class, event -> updateTile(event.tile)); + } + + /** Packs a tile into its internal representation. */ + private int packTile(Tile tile){ + return PathTile.get(tile.cost, tile.getTeamID(), (byte)0, (!tile.solid() || tile.breakable()) && tile.floor().drownTime <= 0f); + } + + /** Starts or restarts the pathfinding thread. */ + private void start(){ + stop(); + thread = Threads.daemon(this); + } + + /** Stops the pathfinding thread. */ + private void stop(){ + if(thread != null){ + thread.interrupt(); + thread = null; + } + queue.clear(); + } + + /** Update a tile in the internal pathfinding grid. Causes a completely pathfinding reclaculation. */ + public void updateTile(Tile tile){ + if(net.client()) return; + + int packed = packTile(tile); + int x = tile.x, y = tile.y; + tiles[x][y] = packed; + + for(PathData[] arr : pathMap){ + for(PathData path : arr){ + if(path != null){ + synchronized(path.targets){ + path.targets.clear(); + path.target.getTargets(tile.getTeam(), path.targets); + } + } + } + } + + queue.post(() -> { + for(PathData data : list){ + updateTargets(data, x, y, packed); + } }); } - public void updateSolid(Tile tile){ - update(tile, tile.getTeam()); - } + /** Thread implementation. */ + @Override + public void run(){ + while(true){ + if(net.client() || state.is(State.menu)) return; - public void update(){ - if(net.client() || paths == null) return; + queue.run(); - for(Team team : Team.all){ - if(state.teams.isActive(team)){ - updateFrontier(team, maxUpdate); + //total update time no longer than maxUpdate + for(PathData data : list){ + updateFrontier(data, maxUpdate / list.size); + } + + try{ + Thread.sleep(updateInterval); + }catch(InterruptedException e){ + //stop looping when interrupted externally + return; } } } - public Tile getTargetTile(Team team, Tile tile){ - float[][] values = paths[team.ordinal()].weights; + /** Gets next tile to travel to. Main thread only. */ + public Tile getTargetTile(Tile tile, Team team, PathTarget target){ + if(tile == null) return tile; - if(values == null || tile == null) return tile; + PathData data = pathMap[team.ordinal()][target.ordinal()]; - float value = values[tile.x][tile.y]; + if(data == null){ + //if this combination is not found, create it on request + if(!created.get(team.ordinal(), target.ordinal())){ + created.set(team.ordinal(), target.ordinal()); + //grab targets since this is run on main thread + IntArray targets = target.getTargets(team, new IntArray()); + queue.post(() -> createFor(team, target, targets)); + } + return tile; + } - Tile target = null; - float tl = 0f; + int[][] values = data.weights; + int value = values[tile.x][tile.y]; + + 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) continue; - if(values[dx][dy] < value && (target == null || values[dx][dy] < tl) && - !other.solid() && other.floor().drownTime <= 0 && + if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && !other.solid() && other.floor().drownTime <= 0 && !(point.x != 0 && point.y != 0 && (world.solid(tile.x + point.x, tile.y) || world.solid(tile.x, tile.y + point.y)))){ //diagonal corner trap - target = other; + current = other; tl = values[dx][dy]; } } - if(target == null || tl == Float.MAX_VALUE) return tile; + if(current == null || tl == impassable) return tile; - return target; + return current; } - public float getValueforTeam(Team team, int x, int y){ - return paths == null || paths[team.ordinal()].weights == null || team.ordinal() >= paths.length ? 0 : Structs.inBounds(x, y, paths[team.ordinal()].weights) ? paths[team.ordinal()].weights[x][y] : 0; - } - - private boolean passable(Tile tile, Team team){ - return ((!tile.solid()) || (tile.breakable() && (tile.getTeam() != team))) && tile.floor().drownTime <= 0f; + /** @return whether a tile can be passed through by this team. Pathfinding thread only.*/ + private boolean passable(int x, int y, Team team){ + int tile = tiles[x][y]; + return PathTile.passable(tile) || (PathTile.team(tile) != team.ordinal() && PathTile.team(tile) != Team.derelict.ordinal()); } /** * Clears the frontier, increments the search and sets up all flow sources. * This only occurs for active teams. */ - private void update(Tile tile, Team team){ - //make sure team exists - if(paths != null && paths[team.ordinal()] != null && paths[team.ordinal()].weights != null && Structs.inBounds(tile.x, tile.y, paths[team.ordinal()].weights)){ - PathData path = paths[team.ordinal()]; + private void updateTargets(PathData path, int x, int y, int tile){ + if(!Structs.inBounds(x, y, path.weights)) return; - if(path.weights[tile.x][tile.y] <= 0.1f){ - //this was a previous target - path.frontier.clear(); - }else if(!path.frontier.isEmpty()){ - return; - } - - //impassable tiles have a weight of float.max - if(!passable(tile, team)){ - path.weights[tile.x][tile.y] = Float.MAX_VALUE; - } - - //increment search, clear frontier - path.search++; + if(path.weights[x][y] == 0){ + //this was a previous target path.frontier.clear(); - path.lastSearchTime = Time.millis(); + }else if(!path.frontier.isEmpty()){ + //skip if this path is processing + return; + } - //add all targets to the frontier - for(Tile other : indexer.getEnemy(team, BlockFlag.target)){ - path.weights[other.x][other.y] = 0; - path.searches[other.x][other.y] = (short)path.search; - path.frontier.addFirst(other.pos()); + //assign impassability to the tile + if(!passable(x, y, path.team)){ + path.weights[x][y] = impassable; + } + + //increment search, clear frontier + path.search++; + path.frontier.clear(); + + synchronized(path.targets){ + //add targets + for(int i = 0; i < path.targets.size; i++){ + int pos = path.targets.get(i); + int tx = Pos.x(pos), ty = Pos.y(pos); + + path.weights[tx][ty] = 0; + path.searches[tx][ty] = (short)path.search; + path.frontier.addFirst(pos); } } } - private void createFor(Team team){ - PathData path = new PathData(); - path.weights = new float[world.width()][world.height()]; - path.searches = new short[world.width()][world.height()]; - path.search++; - path.frontier.ensureCapacity((world.width() + world.height()) * 3); + /** Created a new flowfield that aims to get to a certain target for a certain team. + * Pathfinding thread only. */ + private void createFor(Team team, PathTarget target, IntArray targets){ + PathData path = new PathData(team, target, world.width(), world.height()); - paths[team.ordinal()] = path; + list.add(path); + pathMap[team.ordinal()][target.ordinal()] = path; + //grab targets from passed array + synchronized(path.targets){ + path.targets.clear(); + path.targets.addAll(targets); + } + + //fill with impassables by default for(int x = 0; x < world.width(); x++){ for(int y = 0; y < world.height(); y++){ - Tile tile = world.tile(x, y); - - if(state.teams.areEnemies(tile.getTeam(), team) - && tile.block().flags.contains(BlockFlag.target)){ - path.frontier.addFirst(tile.pos()); - path.weights[x][y] = 0; - path.searches[x][y] = (short)path.search; - }else{ - path.weights[x][y] = Float.MAX_VALUE; - } + path.weights[x][y] = impassable; } } - updateFrontier(team, -1); + //add targets + for(int i = 0; i < path.targets.size; i++){ + int pos = path.targets.get(i); + path.weights[Pos.x(pos)][Pos.y(pos)] = 0; + path.frontier.addFirst(pos); + } } - private void updateFrontier(Team team, long nsToRun){ - PathData path = paths[team.ordinal()]; - + /** Update the frontier for a path. Pathfinding thread only. */ + private void updateFrontier(PathData path, long nsToRun){ long start = Time.nanos(); while(path.frontier.size > 0 && (nsToRun < 0 || Time.timeSinceNanos(start) <= nsToRun)){ Tile tile = world.tile(path.frontier.removeLast()); if(tile == null || path.weights == null) return; //something went horribly wrong, bail - float cost = path.weights[tile.x][tile.y]; + int cost = path.weights[tile.x][tile.y]; //pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully if(path.frontier.size >= world.width() * world.height()){ @@ -166,14 +251,13 @@ public class Pathfinder{ return; } - if(cost < Float.MAX_VALUE){ + if(cost != impassable){ for(Point2 point : Geometry.d4){ int dx = tile.x + point.x, dy = tile.y + point.y; Tile other = world.tile(dx, dy); - if(other != null && (path.weights[dx][dy] > cost + other.cost || path.searches[dx][dy] < path.search) - && passable(other, team)){ + if(other != null && (path.weights[dx][dy] > cost + other.cost || path.searches[dx][dy] < path.search) && passable(dx, dy, path.team)){ if(other.cost < 0) throw new IllegalArgumentException("Tile cost cannot be negative! " + other); path.frontier.addFirst(Pos.get(dx, dy)); path.weights[dx][dy] = cost + other.cost; @@ -184,27 +268,71 @@ public class Pathfinder{ } } - private void clear(){ - Time.mark(); - - paths = new PathData[Team.all.length]; - blocked.clear(); - - for(Team team : Team.all){ - PathData path = new PathData(); - paths[team.ordinal()] = path; - - if(state.teams.isActive(team)){ - createFor(team); + /** A path target defines a set of targets for a path.*/ + public enum PathTarget{ + enemyCores((team, out) -> { + for(Tile other : indexer.getEnemy(team, BlockFlag.core)){ + out.add(other.pos()); } + }), + rallyPoints((team, out) -> { + for(Tile other : indexer.getAllied(team, BlockFlag.rally)){ + out.add(other.pos()); + } + }); + + public static final PathTarget[] all = values(); + + private final BiConsumer targeter; + + PathTarget(BiConsumer targeter){ + this.targeter = targeter; + } + + /** Get targets. This must run on the main thread.*/ + public IntArray getTargets(Team team, IntArray out){ + targeter.accept(team, out); + return out; } } + /** Data for a specific flow field to some set of destinations. */ class PathData{ - float[][] weights; - short[][] searches; - int search = 0; - long lastSearchTime; - IntQueue frontier = new IntQueue(); + /** Team this path is for. */ + final Team team; + /** Flag that is being targeted. */ + final PathTarget target; + /** costs of getting to a specific tile */ + final int[][] weights; + /** search IDs of each position - the highest, most recent search is prioritized and overwritten */ + final short[][] searches; + /** search frontier, these are Pos objects */ + final IntQueue frontier = new IntQueue(); + /** all target positions; these positions have a cost of 0, and must be synchronized on! */ + final IntArray targets = new IntArray(); + /** current search ID */ + int search = 1; + + PathData(Team team, PathTarget target, int width, int height){ + this.team = team; + this.target = target; + + this.weights = new int[width][height]; + this.searches = new short[width][height]; + this.frontier.ensureCapacity((width + height) * 3); + } + } + + /** Holds a copy of tile data for a specific tile position. */ + @Struct + class PathTileStruct{ + //traversal cost + byte cost; + //team of block, if applicable (0 by default) + byte team; + //type of target; TODO remove + byte type; + //whether it's viable to pass this block + boolean passable; } } diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java index eaa60fe80a..9a198f9812 100644 --- a/core/src/io/anuke/mindustry/core/Logic.java +++ b/core/src/io/anuke/mindustry/core/Logic.java @@ -227,8 +227,6 @@ public class Logic implements ApplicationListener{ collisions.collideGroups(bulletGroup, playerGroup); } - - pathfinder.update(); } if(!net.client() && !world.isInvalidMap() && !state.isEditor()){ diff --git a/core/src/io/anuke/mindustry/entities/type/FlyingUnit.java b/core/src/io/anuke/mindustry/entities/type/FlyingUnit.java index 2a0e18160e..05ed326679 100644 --- a/core/src/io/anuke/mindustry/entities/type/FlyingUnit.java +++ b/core/src/io/anuke/mindustry/entities/type/FlyingUnit.java @@ -71,7 +71,7 @@ public abstract class FlyingUnit extends BaseUnit{ public void update(){ if(retarget()){ targetClosest(); - targetClosestEnemyFlag(BlockFlag.target); + targetClosestEnemyFlag(BlockFlag.core); if(target != null && !Units.invalidateTarget(target, team, x, y)){ setState(attack); diff --git a/core/src/io/anuke/mindustry/entities/type/GroundUnit.java b/core/src/io/anuke/mindustry/entities/type/GroundUnit.java index ccc1536c1a..a9dd5d6dc5 100644 --- a/core/src/io/anuke/mindustry/entities/type/GroundUnit.java +++ b/core/src/io/anuke/mindustry/entities/type/GroundUnit.java @@ -6,6 +6,7 @@ import io.anuke.arc.math.*; import io.anuke.arc.math.geom.*; import io.anuke.arc.util.*; import io.anuke.mindustry.*; +import io.anuke.mindustry.ai.Pathfinder.*; import io.anuke.mindustry.entities.*; import io.anuke.mindustry.entities.bullet.*; import io.anuke.mindustry.entities.units.*; @@ -223,7 +224,7 @@ public abstract class GroundUnit extends BaseUnit{ protected void moveToCore(){ Tile tile = world.tileWorld(x, y); if(tile == null) return; - Tile targetTile = pathfinder.getTargetTile(team, tile); + Tile targetTile = pathfinder.getTargetTile(tile, team, PathTarget.enemyCores); if(tile == targetTile) return; @@ -246,7 +247,7 @@ public abstract class GroundUnit extends BaseUnit{ Tile tile = world.tileWorld(x, y); if(tile == null) return; - Tile targetTile = pathfinder.getTargetTile(enemy, tile); + Tile targetTile = pathfinder.getTargetTile(tile, team, PathTarget.enemyCores); TileEntity core = getClosestCore(); if(tile == targetTile || core == null || dst(core) < 120f) return; diff --git a/core/src/io/anuke/mindustry/type/Category.java b/core/src/io/anuke/mindustry/type/Category.java index a90bad9c64..a22b742345 100644 --- a/core/src/io/anuke/mindustry/type/Category.java +++ b/core/src/io/anuke/mindustry/type/Category.java @@ -20,5 +20,7 @@ public enum Category{ /** Things that upgrade the player such as mech pads. */ upgrade, /** Things for storage or passive effects. */ - effect + effect; + + public static final Category[] all = values(); } diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java index 567b49898d..14e1d1e067 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java @@ -26,7 +26,7 @@ public class PlacementFragment extends Fragment{ Array returnArray = new Array<>(); Array returnCatArray = new Array<>(); - boolean[] categoryEmpty = new boolean[Category.values().length]; + boolean[] categoryEmpty = new boolean[Category.all.length]; Category currentCategory = Category.distribution; Block hovered, lastDisplay; Tile lastHover; @@ -91,7 +91,7 @@ public class PlacementFragment extends Fragment{ int i = 0; for(KeyCode key : inputCatGrid){ if(Core.input.keyDown(key)){ - input.block = getByCategory(Category.values()[i]).first(); + input.block = getByCategory(Category.all[i]).first(); currentCategory = input.block.buildCategory; } i++; @@ -258,7 +258,7 @@ public class PlacementFragment extends Fragment{ ButtonGroup group = new ButtonGroup<>(); //update category empty values - for(Category cat : Category.values()){ + for(Category cat : Category.all){ Array blocks = getByCategory(cat); categoryEmpty[cat.ordinal()] = blocks.isEmpty() || !unlocked(blocks.first()); } @@ -289,7 +289,7 @@ public class PlacementFragment extends Fragment{ Array getCategories(){ returnCatArray.clear(); - returnCatArray.addAll(Category.values()); + returnCatArray.addAll(Category.all); returnCatArray.sort((c1, c2) -> Boolean.compare(categoryEmpty[c1.ordinal()], categoryEmpty[c2.ordinal()])); return returnCatArray; } diff --git a/core/src/io/anuke/mindustry/world/blocks/defense/Door.java b/core/src/io/anuke/mindustry/world/blocks/defense/Door.java index db08a2bdf4..d10afac6aa 100644 --- a/core/src/io/anuke/mindustry/world/blocks/defense/Door.java +++ b/core/src/io/anuke/mindustry/world/blocks/defense/Door.java @@ -40,7 +40,7 @@ public class Door extends Wall{ entity.open = open; Door door = (Door)tile.block(); - pathfinder.updateSolid(tile); + pathfinder.updateTile(tile); if(!entity.open){ Effects.effect(door.openfx, tile.drawx(), tile.drawy()); }else{ diff --git a/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java b/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java index ec7cd5f616..cdb2ce0d70 100644 --- a/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java @@ -33,7 +33,7 @@ public class CoreBlock extends StorageBlock{ solid = true; update = true; hasItems = true; - flags = EnumSet.of(BlockFlag.target, BlockFlag.producer); + flags = EnumSet.of(BlockFlag.core, BlockFlag.producer); activeSound = Sounds.respawning; activeSoundVolume = 1f; layer = Layer.overlay; diff --git a/core/src/io/anuke/mindustry/world/blocks/units/RallyPoint.java b/core/src/io/anuke/mindustry/world/blocks/units/RallyPoint.java new file mode 100644 index 0000000000..c4d467a19b --- /dev/null +++ b/core/src/io/anuke/mindustry/world/blocks/units/RallyPoint.java @@ -0,0 +1,14 @@ +package io.anuke.mindustry.world.blocks.units; + +import io.anuke.arc.collection.*; +import io.anuke.mindustry.world.*; +import io.anuke.mindustry.world.meta.*; + +public class RallyPoint extends Block{ + + public RallyPoint(String name){ + super(name); + update = solid = true; + flags = EnumSet.of(BlockFlag.rally); + } +} diff --git a/core/src/io/anuke/mindustry/world/meta/BlockFlag.java b/core/src/io/anuke/mindustry/world/meta/BlockFlag.java index 7b1dd6697e..ea5887abba 100644 --- a/core/src/io/anuke/mindustry/world/meta/BlockFlag.java +++ b/core/src/io/anuke/mindustry/world/meta/BlockFlag.java @@ -1,22 +1,19 @@ package io.anuke.mindustry.world.meta; +/** Stores special flags of blocks for easy querying. */ public enum BlockFlag{ - /** General important target for all types of units. */ - target(0), + /** Enemy core; primary target for all units. */ + core, + /** Rally point for units.*/ + rally, /** Producer of important goods. */ - producer(Float.MAX_VALUE), + producer, /** A turret. */ - turret(Float.MAX_VALUE), + turret, /** Only the command center block.*/ - comandCenter(Float.MAX_VALUE), + comandCenter, /** Repair point. */ - repair(Float.MAX_VALUE); + repair; public final static BlockFlag[] all = values(); - public final float cost; - - BlockFlag(float cost){ - if(cost < 0) throw new RuntimeException("Block flag costs cannot be < 0!"); - this.cost = cost; - } }