Path progress

This commit is contained in:
Anuken
2020-08-21 20:45:47 -04:00
parent 43f7bd1cdb
commit 1e7f28f309
2 changed files with 137 additions and 145 deletions

View File

@@ -27,14 +27,8 @@ public class Pathfinder implements Runnable{
private int[][] tiles; private int[][] tiles;
/** unordered array of path data for iteration only. DO NOT iterate or access this in the main thread. */ /** unordered array of path data for iteration only. DO NOT iterate or access this in the main thread. */
private Seq<Flowfield> threadList = new Seq<>(), mainList = new Seq<>(); private Seq<Flowfield> threadList = new Seq<>(), mainList = new Seq<>();
/** Maps team ID and target to to a flowfield.*/
private ObjectMap<PathTarget, Flowfield>[] fieldMap = new ObjectMap[Team.all.length];
/** Used field maps. */
private ObjectSet<PathTarget>[] fieldMapUsed = new ObjectSet[Team.all.length];
/** handles task scheduling on the update thread. */ /** handles task scheduling on the update thread. */
private TaskQueue queue = new TaskQueue(); private TaskQueue queue = new TaskQueue();
/** Stores path target for a position. Main thread only.*/
private ObjectMap<Position, PathTarget> targetCache = new ObjectMap<>();
/** Current pathfinding thread */ /** Current pathfinding thread */
private @Nullable Thread thread; private @Nullable Thread thread;
private IntSeq tmpArray = new IntSeq(); private IntSeq tmpArray = new IntSeq();
@@ -45,9 +39,6 @@ public class Pathfinder implements Runnable{
//reset and update internal tile array //reset and update internal tile array
tiles = new int[world.width()][world.height()]; tiles = new int[world.width()][world.height()];
fieldMap = new ObjectMap[Team.all.length];
fieldMapUsed = new ObjectSet[Team.all.length];
targetCache = new ObjectMap<>();
threadList = new Seq<>(); threadList = new Seq<>();
mainList = new Seq<>(); mainList = new Seq<>();
@@ -56,7 +47,7 @@ public class Pathfinder implements Runnable{
} }
//special preset which may help speed things up; this is optional //special preset which may help speed things up; this is optional
preloadPath(state.rules.waveTeam, FlagTarget.enemyCores); //preloadPath(state.rules.waveTeam, FlagTarget.enemyCores);
start(); start();
}); });
@@ -68,7 +59,29 @@ public class Pathfinder implements Runnable{
/** Packs a tile into its internal representation. */ /** Packs a tile into its internal representation. */
private int packTile(Tile tile){ private int packTile(Tile tile){
return PathTile.get(tile.cost, (byte)tile.getTeamID(), !tile.solid() && tile.floor().drownTime <= 0f, !tile.solid() && tile.floor().isLiquid); //TODO nearGround is just the inverse of nearLiquid?
boolean nearLiquid = false, nearSolid = false, nearGround = false;
for(int i = 0; i < 4; i++){
Tile other = tile.getNearby(i);
if(other != null){
if(other.floor().isLiquid) nearLiquid = true;
if(other.solid()) nearSolid = true;
if(!other.floor().isLiquid) nearGround = true;
}
}
return PathTile.get(
tile.build == null ? 0 : Math.min((int)(tile.build.health / 40), 127),
tile.getTeamID(),
tile.solid(),
tile.floor().isLiquid,
tile.staticDarkness() < 2,
nearLiquid,
nearGround,
nearSolid,
tile.floor().isDeep()
);
} }
/** Starts or restarts the pathfinding thread. */ /** Starts or restarts the pathfinding thread. */
@@ -104,7 +117,7 @@ public class Pathfinder implements Runnable{
if(path != null){ if(path != null){
synchronized(path.targets){ synchronized(path.targets){
path.targets.clear(); path.targets.clear();
path.target.getPositions(path.team, path.targets); path.getPositions(path.targets);
} }
} }
} }
@@ -131,18 +144,19 @@ public class Pathfinder implements Runnable{
updateFrontier(data, maxUpdate / threadList.size); updateFrontier(data, maxUpdate / threadList.size);
//remove flowfields that have 'timed out' so they can be garbage collected and no longer waste space //remove flowfields that have 'timed out' so they can be garbage collected and no longer waste space
if(data.target.refreshRate() > 0 && Time.timeSinceMillis(data.lastUpdateTime) > fieldTimeout){ if(data.refreshRate > 0 && Time.timeSinceMillis(data.lastUpdateTime) > fieldTimeout){
//make sure it doesn't get removed twice //make sure it doesn't get removed twice
data.lastUpdateTime = Time.millis(); data.lastUpdateTime = Time.millis();
Team team = data.team; Team team = data.team;
Core.app.post(() -> { Core.app.post(() -> {
//TODO ?????
//remove its used state //remove its used state
if(fieldMap[team.id] != null){ //if(fieldMap[team.id] != null){
fieldMap[team.id].remove(data.target); // fieldMap[team.id].remove(data.target);
fieldMapUsed[team.id].remove(data.target); // fieldMapUsed[team.id].remove(data.target);
} //}
//remove from main thread list //remove from main thread list
mainList.remove(data); mainList.remove(data);
}); });
@@ -167,51 +181,62 @@ public class Pathfinder implements Runnable{
} }
} }
public @Nullable Tile getTargetTile(Tile tile, Team team, Position target){ //public @Nullable Tile getTargetTile(Tile tile, Team team, Position target){
return getTargetTile(tile, team, getTarget(target)); // return getTargetTile(tile, team, getTarget(target));
// }
public @Nullable Tile getTargetTile(Tile tile, Prov<Flowfield> fieldtype, Team team, PathCost cost){
if(true){ //TODO cache this
Flowfield field = fieldtype.get();
IntSeq out = new IntSeq();
field.team = team;
field.getPositions(out);
createPath(field, cost, team, out);
}
if(false){ //TODO if field exists
//TODO fetch it from the cache
//return getTargetTile(tile, path);
}
return tile;
} }
/** Gets next tile to travel to. Main thread only. */ /** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Team team, PathTarget target){ public @Nullable Tile getTargetTile(Tile tile, Flowfield path){
if(tile == null) return null; if(tile == null) return null;
if(fieldMap[team.id] == null){ if(path == null){
fieldMap[team.id] = new ObjectMap<>();
fieldMapUsed[team.id] = new ObjectSet<>();
}
Flowfield data = fieldMap[team.id].get(target);
if(data == null){
//if this combination is not found, create it on request //if this combination is not found, create it on request
if(fieldMapUsed[team.id].add(target)){ //TODO do above task
//if(fieldMapUsed[team.id].add(target)){
//grab targets since this is run on main thread //grab targets since this is run on main thread
IntSeq targets = target.getPositions(team, new IntSeq()); // IntSeq targets = target.getPositions(team, new IntSeq());
queue.post(() -> createPath(team, target, targets)); // queue.post(() -> createPath(team, target, targets));
} //}
return tile; return tile;
} }
//if refresh rate is positive, queue a refresh //if refresh rate is positive, queue a refresh
if(target.refreshRate() > 0 && Time.timeSinceMillis(data.lastUpdateTime) > target.refreshRate()){ if(path.refreshRate > 0 && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate){
data.lastUpdateTime = Time.millis(); path.lastUpdateTime = Time.millis();
tmpArray.clear(); tmpArray.clear();
data.target.getPositions(data.team, tmpArray); path.getPositions(tmpArray);
synchronized(data.targets){ synchronized(path.targets){
//make sure the position actually changed //make sure the position actually changed
if(!(data.targets.size == 1 && tmpArray.size == 1 && data.targets.first() == tmpArray.first())){ if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){
data.targets.clear(); path.targets.clear();
data.target.getPositions(data.team, data.targets); path.getPositions(path.targets);
//queue an update //queue an update
queue.post(() -> updateTargets(data)); queue.post(() -> updateTargets(path));
} }
} }
} }
int[][] values = data.weights; int[][] values = path.weights;
int value = values[tile.x][tile.y]; int value = values[tile.x][tile.y];
Tile current = null; Tile current = null;
@@ -234,16 +259,6 @@ public class Pathfinder implements Runnable{
return current; return current;
} }
private PathTarget getTarget(Position position){
return targetCache.get(position, () -> new PositionTarget(position));
}
/** @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.id && PathTile.team(tile) != (int)Team.derelict.id);
}
/** /**
* Clears the frontier, increments the search and sets up all flow sources. * Clears the frontier, increments the search and sets up all flow sources.
* This only occurs for active teams. * This only occurs for active teams.
@@ -259,10 +274,8 @@ public class Pathfinder implements Runnable{
return; return;
} }
//assign impassability to the tile //update cost of the tile TODO maybe only update the cost when it's not passable
if(!passable(x, y, path.team)){ path.weights[x][y] = path.cost.getCost(path.team, tiles[x][y]);
path.weights[x][y] = impassable;
}
//clear frontier to prevent contamination //clear frontier to prevent contamination
path.frontier.clear(); path.frontier.clear();
@@ -289,26 +302,30 @@ public class Pathfinder implements Runnable{
} }
} }
private void preloadPath(Team team, PathTarget target){ private void preloadPath(Flowfield path, PathCost cost, Team team){
updateFrontier(createPath(team, target, target.getPositions(team, new IntSeq())), -1); IntSeq out = new IntSeq();
path.team = team;
path.getPositions(out);
updateFrontier(createPath(path, cost, team, out), -1);
} }
/** /**
* Created a new flowfield that aims to get to a certain target for a certain team. * Created a new flowfield that aims to get to a certain target for a certain team.
* Pathfinding thread only. * Pathfinding thread only.
*/ */
private Flowfield createPath(Team team, PathTarget target, IntSeq targets){ private Flowfield createPath(Flowfield path, PathCost cost, Team team, IntSeq targets){
Flowfield path = new Flowfield(team, target, world.width(), world.height());
path.lastUpdateTime = Time.millis(); path.lastUpdateTime = Time.millis();
path.setup(team, cost, tiles.length, tiles[0].length);
threadList.add(path); threadList.add(path);
//add to main thread's list of paths //add to main thread's list of paths
Core.app.post(() -> { Core.app.post(() -> {
mainList.add(path); mainList.add(path);
if(fieldMap[team.id] != null){ //TODO
fieldMap[team.id].put(target, path); //if(fieldMap[team.id] != null){
} // fieldMap[team.id].put(target, path);
//}
}); });
//grab targets from passed array //grab targets from passed array
@@ -350,15 +367,15 @@ public class Pathfinder implements Runnable{
} }
if(cost != impassable){ if(cost != impassable){
//TODO this is probably slow.
for(Point2 point : Geometry.d4){ for(Point2 point : Geometry.d4){
int dx = tile.x + point.x, dy = tile.y + point.y; int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy); int otherCost = path.cost.getCost(path.team, tiles[dx][dy]);
if(other != null && (path.weights[dx][dy] > cost + other.cost || path.searches[dx][dy] < path.search) && passable(dx, dy, path.team)){ if((path.weights[dx][dy] > cost + otherCost || path.searches[dx][dy] < path.search) && otherCost != impassable){
if(other.cost < 0) throw new IllegalArgumentException("Tile cost cannot be negative! " + other);
path.frontier.addFirst(Point2.pack(dx, dy)); path.frontier.addFirst(Point2.pack(dx, dy));
path.weights[dx][dy] = cost + other.cost; path.weights[dx][dy] = cost + otherCost;
path.searches[dx][dy] = (short)path.search; path.searches[dx][dy] = (short)path.search;
} }
} }
@@ -366,9 +383,24 @@ public class Pathfinder implements Runnable{
} }
} }
/** A path target defines a set of targets for a path. */ public static final PathCost
public enum FlagTarget implements PathTarget{
enemyCores((team, out) -> { groundCost = (team, tile) -> (PathTile.team(tile) == team.id || PathTile.team(tile) == 0) && PathTile.solid(tile) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 70 : 0),
legsCost = (team, tile) -> PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.solid(tile) ? 5 : 0),
waterCost = (team, tile) -> PathTile.solid(tile) || !PathTile.liquid(tile) ? impassable : 2 + //TODO cannot go through blocks
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? -1 : 0);
public static class EnemyCoreField extends Flowfield{
@Override
protected void getPositions(IntSeq out){
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){ for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
out.add(other.pos()); out.add(other.pos());
} }
@@ -379,50 +411,31 @@ public class Pathfinder implements Runnable{
out.add(other.pos()); out.add(other.pos());
} }
} }
}),
rallyPoints((team, out) -> {
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
out.add(other.pos());
}
});
public static final FlagTarget[] all = values();
private final Cons2<Team, IntSeq> targeter;
FlagTarget(Cons2<Team, IntSeq> targeter){
this.targeter = targeter;
}
@Override
public IntSeq getPositions(Team team, IntSeq out){
targeter.get(team, out);
return out;
}
@Override
public int refreshRate(){
return 0;
} }
} }
public static class PositionTarget implements PathTarget{ public static class RallyField extends Flowfield{
@Override
protected void getPositions(IntSeq out){
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
out.add(other.pos());
}
}
}
public static class PositionTarget extends Flowfield{
public final Position position; public final Position position;
public PositionTarget(Position position){ public PositionTarget(Position position){
this.position = position; this.position = position;
this.refreshRate = 900;
} }
@Override @Override
public IntSeq getPositions(Team team, IntSeq out){ public void getPositions(IntSeq out){
out.add(Point2.pack(world.toTile(position.getX()), world.toTile(position.getY()))); out.add(Point2.pack(world.toTile(position.getX()), world.toTile(position.getY())));
return out;
} }
@Override
public int refreshRate(){
return 900;
}
} }
/** /**
@@ -434,6 +447,8 @@ public class Pathfinder implements Runnable{
protected int refreshRate; protected int refreshRate;
/** Team this path is for. */ /** Team this path is for. */
protected Team team; protected Team team;
/** Function for calculating path cost. */
protected PathCost cost;
/** costs of getting to a specific tile */ /** costs of getting to a specific tile */
int[][] weights; int[][] weights;
@@ -447,34 +462,47 @@ public class Pathfinder implements Runnable{
int search = 1; int search = 1;
/** last updated time */ /** last updated time */
long lastUpdateTime; long lastUpdateTime;
/** whether this flow field is ready to be used */
boolean initialized;
void setup(Team team, int width, int height){ void setup(Team team, PathCost cost, int width, int height){
this.team = team; this.team = team;
this.cost = cost;
this.weights = new int[width][height]; this.weights = new int[width][height];
this.searches = new int[width][height]; this.searches = new int[width][height];
this.frontier.ensureCapacity((width + height) * 3); this.frontier.ensureCapacity((width + height) * 3);
this.initialized = true;
} }
/** Gets targets to pathfind towards. This must run on the main thread. */ /** Gets targets to pathfind towards. This must run on the main thread. */
protected abstract IntSeq getPositions(IntSeq out); protected abstract void getPositions(IntSeq out);
}
/** Gets the cost of a tile at a position. */ interface PathCost{
protected abstract int getCost(int pathTile); int getCost(Team traversing, int tile);
} }
/** Holds a copy of tile data for a specific tile position. */ /** Holds a copy of tile data for a specific tile position. */
@Struct @Struct
class PathTileStruct{ class PathTileStruct{
//base traversal cost (could be a byte..) //scaled block health
short cost; @StructField(8) int health;
//team of block, if applicable (0 by default) //team of block, if applicable (0 by default)
byte team; @StructField(8) int team;
//general solid state //general solid state
boolean solid; boolean solid;
//whether this block is a liquid that boats can move on //whether this block is a liquid that boats can move on
boolean water; boolean liquid;
//whether this block is solid for leg units that can move over some solid blocks //whether this block is solid for leg units that can move over some solid blocks
boolean legSolid; boolean legSolid;
//whether this block is near liquids
boolean nearLiquid;
//whether this block is near a solid floor tile
boolean nearGround;
//whether this block is near a solid object
boolean nearSolid;
//whether this block is deep / drownable
boolean deep;
} }
} }

View File

@@ -23,8 +23,6 @@ public class Tile implements Position, QuadTreeObject, Displayable{
/** Extra data for very specific blocks. */ /** Extra data for very specific blocks. */
public byte data; public byte data;
/** Tile traversal cost. */
public short cost = 1;
/** Tile entity, usually null. */ /** Tile entity, usually null. */
public @Nullable Building build; public @Nullable Building build;
public short x, y; public short x, y;
@@ -442,42 +440,8 @@ public class Tile implements Position, QuadTreeObject, Displayable{
return block.solid && block.fillsTile && !block.synthetic() ? data : 0; return block.solid && block.fillsTile && !block.synthetic() ? data : 0;
} }
//TODO remove this method?
public void updateOcclusion(){ public void updateOcclusion(){
cost = 1;
boolean occluded = false;
//check for occlusion
for(int i = 0; i < 8; i++){
Point2 point = Geometry.d8[i];
Tile tile = world.tile(x + point.x, y + point.y);
if(tile != null && tile.floor.isLiquid){
cost += 4;
}
if(tile != null && tile.solid()){
occluded = true;
break;
}
}
if(occluded){
cost += 2;
}
if(block.synthetic() && solid()){
cost += Mathf.clamp(block.health / 6f, 0, 1000);
}
if(floor.isLiquid){
cost += 10;
}
if(floor.drownTime > 0){
cost += 70;
}
if(cost < 0){
cost = Byte.MAX_VALUE;
}
} }
protected void preChanged(){ protected void preChanged(){