Actually functional, but terrible

This commit is contained in:
Anuken
2023-11-11 14:20:48 -05:00
parent cacfe06362
commit af7598dcc6
3 changed files with 206 additions and 88 deletions

View File

@@ -22,7 +22,7 @@ import static mindustry.ai.Pathfinder.*;
//https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf
//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 = Time.millisToNanos(12);
private static final long maxUpdate = 100;//Time.millisToNanos(12);
private static final int updateFPS = 30;
private static final int updateInterval = 1000 / updateFPS;
@@ -70,9 +70,10 @@ public class HierarchyPathFinder implements Runnable{
//TODO - it is written to on the pathfinding thread
//maps position in world in (x + y * width format) to a cache of flow fields
IntMap<FieldCache> fields = new IntMap<>();
//MAIN THREAD ONLY
Seq<FieldCache> fieldList = new Seq<>(false);
//these are for inner edge A*
//these are for inner edge A* (temporary!)
IntFloatMap innerCosts = new IntFloatMap();
PathfindQueue innerFrontier = new PathfindQueue();
@@ -98,6 +99,10 @@ public class HierarchyPathFinder implements Runnable{
//main thread only!
long lastUpdateId = state.updateId;
volatile boolean notFound = false;
int lastTile; //TODO only re-raycast when unit moves a tile.
@Nullable Tile lastTargetTile;
public PathRequest(Unit unit, int team, int destination){
this.unit = unit;
@@ -135,10 +140,15 @@ public class HierarchyPathFinder implements Runnable{
stop();
//TODO: can the pathfinding thread even see these?
unitRequests = new ObjectMap<>();
fields = new IntMap<>();
fieldList = new Seq<>(false);
clusters = new Cluster[256][][];
cwidth = Mathf.ceil((float)world.width() / clusterSize);
cheight = Mathf.ceil((float)world.height() / clusterSize);
start();
});
@@ -165,7 +175,7 @@ public class HierarchyPathFinder implements Runnable{
}
});
//TODO: recalculate affected flow fields? or just all of them?
//TODO: recalculate affected flow fields? or just all of them? how to reflow?
});
//invalidate paths
@@ -178,74 +188,74 @@ public class HierarchyPathFinder implements Runnable{
}
}
for(var field : fields.values()){
for(var field : fieldList){
//skipped N update -> drop it
if(field.lastUpdateId <= state.updateId - 20){
//make sure it's only modified on the main thread...? but what about calling get() on this thread??
queue.post(() -> fields.remove(field.goalPos));
Core.app.post(() -> fieldList.remove(field));
}
}
});
if(debug){
Events.run(Trigger.draw, () -> {
int team = Team.sharded.id;
int team = player.team().id;
int cost = costGround;
if(clusters == null || clusters[cost] == null) return;
Draw.draw(Layer.overlayUI, () -> {
Lines.stroke(1f);
for(int cx = 0; cx < cwidth; cx++){
for(int cy = 0; cy < cheight; cy++){
if(clusters[Team.sharded.id] == null || clusters[team][cost] == null) continue;
var cluster = clusters[team][cost][cy * cwidth + cx];
if(cluster != null){
Lines.stroke(0.5f);
Draw.color(Color.gray);
Lines.stroke(1f);
if(clusters[team] != null && clusters[team][cost] != null){
for(int cx = 0; cx < cwidth; cx++){
for(int cy = 0; cy < cheight; cy++){
Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize);
var cluster = clusters[team][cost][cy * cwidth + cx];
if(cluster != null){
Lines.stroke(0.5f);
Draw.color(Color.gray);
Lines.stroke(1f);
Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize);
for(int d = 0; d < 4; d++){
IntSeq portals = cluster.portals[d];
if(portals != null){
for(int d = 0; d < 4; d++){
IntSeq portals = cluster.portals[d];
if(portals != null){
for(int i = 0; i < portals.size; i++){
int pos = portals.items[i];
int from = Point2.x(pos), to = Point2.y(pos);
float width = tilesize * (Math.abs(from - to) + 1), height = tilesize;
for(int i = 0; i < portals.size; i++){
int pos = portals.items[i];
int from = Point2.x(pos), to = Point2.y(pos);
float width = tilesize * (Math.abs(from - to) + 1), height = tilesize;
portalToVec(cluster, cx, cy, d, i, Tmp.v1);
portalToVec(cluster, cx, cy, d, i, Tmp.v1);
Draw.color(Color.brown);
Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f);
Draw.color(Color.brown);
Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f);
LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i];
LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i];
if(connections != null){
Draw.color(Color.forest);
for(int coni = 0; coni < connections.size; coni ++){
long con = connections.items[coni];
if(connections != null){
Draw.color(Color.forest);
for(int coni = 0; coni < connections.size; coni ++){
long con = connections.items[coni];
portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2);
portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2);
float
x1 = Tmp.v1.x, y1 = Tmp.v1.y,
x2 = Tmp.v2.x, y2 = Tmp.v2.y,
mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize;
//Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20);
Lines.line(x1, y1, x2, y2);
float
x1 = Tmp.v1.x, y1 = Tmp.v1.y,
x2 = Tmp.v2.x, y2 = Tmp.v2.y,
mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize;
//Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20);
Lines.line(x1, y1, x2, y2);
}
}
}
}
}
}
//TODO draw connections.
//TODO draw connections.
/*
Draw.color(Color.magenta);
@@ -257,28 +267,27 @@ public class HierarchyPathFinder implements Runnable{
//Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20);
Lines.line(x1, y1, x2, y2);
}*/
}
}
}
/*
if(fields != null){
for(var entry : fields){
int cx = entry.key % cwidth, cy = entry.key / cwidth;
for(int y = 0; y < clusterSize; y++){
for(int x = 0; x < clusterSize; x++){
int value = entry.value[x + y * clusterSize];
Tmp.c1.a = 1f;
Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f));
Draw.alpha(0.5f);
Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize/2f);
}
}
}
}
*/
for(var fields : fieldList){
try{
for(var entry : fields.fields){
int cx = entry.key % cwidth, cy = entry.key / cwidth;
for(int y = 0; y < clusterSize; y++){
for(int x = 0; x < clusterSize; x++){
int value = entry.value[x + y * clusterSize];
Tmp.c1.a = 1f;
Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f));
Draw.alpha(0.5f);
Fill.square((x + cx * clusterSize) * tilesize, (y + cy * clusterSize) * tilesize, tilesize / 2f);
}
}
}
}catch(Exception ignored){} //probably has some concurrency issues when iterating but I don't care, this is for debugging
}
});
});
}
@@ -816,7 +825,10 @@ public class HierarchyPathFinder implements Runnable{
int tile = frontier.removeLast();
int baseX = tile % wwidth, baseY = tile / wwidth;
int curWeightIndex = (baseX / clusterSize) + (baseY / clusterSize) * cwidth;
//TODO: how can this be null??? serious problem!
int[] curWeights = fields.get(curWeightIndex);
if(curWeights == null) continue;
int cost = curWeights[baseX % clusterSize + ((baseY % clusterSize) * clusterSize)];
@@ -921,6 +933,12 @@ public class HierarchyPathFinder implements Runnable{
int node = findClosestNode(team, costId, unitX, unitY);
int dest = findClosestNode(team, costId, goalX, goalY);
if(dest == Integer.MAX_VALUE){
request.notFound = true;
//no node found (TODO: invalid state??)
return;
}
var nodePath = clusterAstar(request, costId, node, dest);
//TODO: how to reuse properly. what if the flowfields don't go through this position (the fields are finished?) how to incrementally extend the flowfield?
@@ -931,6 +949,9 @@ public class HierarchyPathFinder implements Runnable{
//create the cache if it doesn't exist, and initialize it
if(cache == null){
fields.put(goalPos, cache = new FieldCache(pcost, team, goalPos));
FieldCache fcache = cache;
//register field in main thread for iteration
Core.app.post(() -> fieldList.add(fcache));
cache.frontier.addFirst(goalPos);
addingFrontier = false; //when it's a new field, there is no need to add to the frontier to merge the flowfield
}
@@ -938,6 +959,7 @@ public class HierarchyPathFinder implements Runnable{
if(nodePath != null){
int cx = unitX / clusterSize, cy = unitY / clusterSize;
//TODO: instead of adding a bunch of clusters nobody cares about, dynamically add them later when needed
addFlowCluster(cache, cx, cy, addingFrontier);
for(int i = -1; i < nodePath.size; i++){
@@ -976,16 +998,35 @@ public class HierarchyPathFinder implements Runnable{
return ControlPathfinder.costTypes.get(costId);
}
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, boolean[] noResultFound){
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 mainDestination, Vec2 out, boolean[] noResultFound){
int costId = 0;
PathCost cost = idToCost(costId);
PathRequest request = unitRequests.get(unit);
int
destX = World.toTile(destination.x),
destY = World.toTile(destination.y) * wwidth,
team = unit.team.id,
tileX = unit.tileX(),
tileY = unit.tileY(),
destX = World.toTile(mainDestination.x),
destY = World.toTile(mainDestination.y),
actualDestX = World.toTile(destination.x),
actualDestY = World.toTile(destination.y),
destPos = destX + destY * wwidth;
PathRequest request = unitRequests.get(unit);
//if the destination can be trivially reached in a straight line, do that.
if(!raycast(team, cost, tileX, tileY, actualDestX, actualDestY)){
out.set(destination);
return true;
}
//TODO: the destination should not be the exact key. units have slightly different destinations based on offset from formation!
//TODO raycast both diagonal edges to make sure it's reachable near corners
//var test = Geometry.raycastRect(unit.x, unit.y, current.worldx(), current.worldy(), Tmp.r1.setCentered(1f, 1f, tilesize).grow(7.8f)) != null;
boolean any = false;
//use existing request if it exists.
if(request != null && request.destination == destPos){
request.lastUpdateId = state.updateId;
@@ -994,54 +1035,128 @@ public class HierarchyPathFinder implements Runnable{
//TODO: should fields be accessible from this thread?
FieldCache fieldCache = fields.get(destPos);
if(tileOn != null && fieldCache != null){
int value = getCost(fieldCache.fields, tileOn.x, tileOn.y);
if(fieldCache != null && tileOn != null){
fieldCache.lastUpdateId = state.updateId;
int maxIterations = 30; //TODO higher/lower number?
int i = 0;
Tile current = null;
int tl = 0;
//TODO: use raycasting and iterate on this for N steps
for(Point2 point : Geometry.d8){
int dx = tileOn.x + point.x, dy = tileOn.y + point.y;
if(tileOn.pos() != request.lastTile || request.lastTargetTile == null){
//TODO tanks have weird behavior near edges of walls, as they try to avoid them
Tile other = world.tile(dx, dy);
while(i ++ < maxIterations && (!any || !raycast(team, cost, tileX, tileY, tileOn.x, tileOn.y))){
//TODO: if there's no flowfield at this position, add it.
int value = getCost(fieldCache.fields, tileOn.x, tileOn.y);
if(other == null) continue;
Tile current = null;
int minCost = 0;
//TODO: use raycasting and iterate on this for N steps
for(Point2 point : Geometry.d8){
int dx = tileOn.x + point.x, dy = tileOn.y + point.y;
int packed = world.packArray(dx, dy);
int otherCost = getCost(fieldCache.fields, dx, dy);
Tile other = world.tile(dx, dy);
if(otherCost < value && (current == null || otherCost < tl) && passable(cost, unit.team.id, packed) &&
!(point.x != 0 && point.y != 0 && (!passable(cost, unit.team.id, world.packArray(tileOn.x + point.x, tileOn.y)) ||
(!passable(cost, unit.team.id, world.packArray(tileOn.x, tileOn.y + point.y)))))){ //diagonal corner trap
if(other == null) continue;
current = other;
tl = otherCost;
int packed = world.packArray(dx, dy);
int otherCost = getCost(fieldCache.fields, dx, dy);
//TODO: issue with hugging corners (you should not be able to move diagonally when there is a wall in the way)
if(otherCost < value && (current == null || otherCost < minCost) && passable(cost, unit.team.id, packed) &&
//diagonal corner trap
!(
(!passable(cost, team, world.packArray(tileOn.x + point.x, tileOn.y)) ||
(!passable(cost, team, world.packArray(tileOn.x, tileOn.y + point.y))))
)
){
current = other;
minCost = otherCost;
}
}
if(!(current == null || minCost == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){
tileOn = current;
any = true;
}else{
break;
}
}
request.lastTargetTile = any ? tileOn : null;
}
if(!(current == null || tl == impassable || (costId == costGround && current.dangerous() && !tileOn.dangerous()))){
out.set(current);
if(request.lastTargetTile != null){
out.set(request.lastTargetTile);
return true;
}
}
}else{
}else if(request == null){
//queue new request.
unitRequests.put(unit, request = new PathRequest(unit, unit.team.id, destPos));
unitRequests.put(unit, request = new PathRequest(unit, team, destPos));
PathRequest f = request;
//on the pathfinding thread: initialize the request, meaning
//on the pathfinding thread: initialize the request
queue.post(() -> {
initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY);
initializePathRequest(f, unit.team.id, costId, unit.tileX(), unit.tileY(), destX, destY);
});
out.set(destination);
return true;
}
noResultFound[0] = true;
if(request != null){
noResultFound[0] = request.notFound;
}
return false;
}
private static boolean raycast(int team, PathCost type, int x1, int y1, int x2, int y2){
int ww = wwidth, wh = wheight;
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
int e2, err = dx - dy;
while(x >= 0 && y >= 0 && x < ww && y < wh){
if(avoid(team, type, x + y * wwidth)) return true;
if(x == x2 && y == y2) return false;
//TODO no diagonals???? is this a good idea?
/*
//no diagonal ver
if(2 * err + dy > dx - 2 * err){
err -= dy;
x += sx;
}else{
err += dx;
y += sy;
}*/
//diagonal ver
e2 = 2 * err;
if(e2 > -dy){
err -= dy;
x += sx;
}
if(e2 < dx){
err += dx;
y += sy;
}
}
return true;
}
private static boolean avoid(int team, PathCost type, int tilePos){
int cost = cost(team, type, tilePos);
return cost == impassable || cost >= 2;
}
private int getCost(IntMap<int[]> fields, int x, int y){
int[] field = fields.get(x / clusterSize + (y / clusterSize) * cwidth);
if(field == null){

View File

@@ -77,6 +77,8 @@ public class RtsAI{
}
public void update(){
if(true) return;
if(timer.get(timeUpdate, 60f * 2f)){
assignSquads();
checkBuilding();

View File

@@ -219,7 +219,8 @@ public class CommandAI extends AIController{
}
if(unit.isGrounded() && stance != UnitStance.ram){
if(timer.get(timerTarget3, avoidInterval)){
//TODO no blocking.
if(timer.get(timerTarget3, avoidInterval) && false){
Vec2 dstPos = Tmp.v1.trns(unit.rotation, unit.hitSize/2f);
float max = unit.hitSize/2f;
float radius = Math.max(7f, max);
@@ -247,7 +248,7 @@ public class CommandAI extends AIController{
}
//if you've spent 3 seconds stuck, something is wrong, move regardless
move = hpath.getPathPosition(unit, pathId, vecMovePos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
move = hpath.getPathPosition(unit, pathId, vecMovePos, targetPos, vecOut, noFound) && (!blockingUnit || timeSpentBlocked > maxBlockTime);
//we've reached the final point if the returned coordinate is equal to the supplied input
isFinalPoint &= vecMovePos.epsilonEquals(vecOut, 4.1f);