Implemented JPS badly

This commit is contained in:
Anuken
2018-04-20 19:55:41 -04:00
parent 68bfd3017b
commit 4261e6242c
23 changed files with 338 additions and 284 deletions

View File

@@ -1,42 +1,43 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.*;
import com.badlogic.gdx.ai.pfa.GraphPath;
import com.badlogic.gdx.ai.pfa.PathFinderQueue;
import com.badlogic.gdx.ai.pfa.PathFinderRequest;
import com.badlogic.gdx.utils.BinaryHeap;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
/**An IndexedAStarPathfinder that uses an OptimizedGraph, and therefore has less allocations.*/
public class OptimizedPathFinder<N> implements PathFinder<N> {
OptimizedGraph<N> graph;
IntMap<NodeRecord<N>> records = new IntMap<>();
BinaryHeap<NodeRecord<N>> openList;
NodeRecord<N> current;
public class OptimizedPathFinder {
IntMap<NodeRecord> records = new IntMap<>();
BinaryHeap<NodeRecord> openList;
NodeRecord current;
/**
* The unique ID for each search run. Used to mark nodes.
*/
private int searchId;
private Tile cameFrom = null;
private static final byte UNVISITED = 0;
private static final byte OPEN = 1;
private static final byte CLOSED = 2;
@SuppressWarnings("unchecked")
public OptimizedPathFinder(OptimizedGraph<N> graph) {
this.graph = graph;
private static final boolean debug = true;
public static boolean unop = false;
public OptimizedPathFinder() {
this.openList = new BinaryHeap<>();
}
@Override
public boolean searchConnectionPath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<Connection<N>> outPath) {
return false;
}
@Override
public boolean searchNodePath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<N> outPath) {
public boolean searchNodePath(Tile startNode, Tile endNode, GraphPath<Tile> outPath) {
// Perform AStar
boolean found = search(startNode, endNode, heuristic);
boolean found = search(startNode, endNode);
if (found) {
// Create a path made of nodes
@@ -46,9 +47,9 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
return found;
}
protected boolean search(N startNode, N endNode, Heuristic<N> heuristic) {
protected boolean search(Tile startNode, Tile endNode) {
initSearch(startNode, endNode, heuristic);
initSearch(startNode, endNode);
// Iterate through processing each node
do {
@@ -59,7 +60,9 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
// Terminate if we reached the goal node
if (current.node == endNode) return true;
visitChildren(endNode, heuristic);
visitChildren(endNode);
cameFrom = current.node;
} while (openList.size > 0);
@@ -67,14 +70,13 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
return false;
}
@Override
public boolean search(PathFinderRequest<N> request, long timeToRun) {
public boolean search(PathFinderRequest<Tile> request, long timeToRun) {
long lastTime = TimeUtils.nanoTime();
// We have to initialize the search if the status has just changed
if (request.statusChanged) {
initSearch(request.startNode, request.endNode, request.heuristic);
initSearch(request.startNode, request.endNode);
request.statusChanged = false;
}
@@ -100,7 +102,7 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
}
// Visit current node's children
visitChildren(request.endNode, request.heuristic);
visitChildren(request.endNode);
// Store the current time
lastTime = currentTime;
@@ -112,57 +114,52 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
return true;
}
protected void initSearch(N startNode, N endNode, Heuristic<N> heuristic) {
protected void initSearch(Tile startNode, Tile endNode) {
// Increment the search id
if (++searchId < 0) searchId = 1;
// Initialize the open list
openList.clear();
cameFrom = null;
// Initialize the record for the start node and add it to the open list
NodeRecord<N> startRecord = getNodeRecord(startNode);
NodeRecord startRecord = getNodeRecord(startNode);
startRecord.node = startNode;
//startRecord.connection = null;
startRecord.costSoFar = 0;
addToOpenList(startRecord, heuristic.estimate(startNode, endNode));
addToOpenList(startRecord, estimate(startNode, endNode));
current = null;
}
protected void visitChildren(N endNode, Heuristic<N> heuristic) {
// Get current node's outgoing connections
//Array<Connection<N>> connections = graph.getConnections(current.node);
N[] conn = graph.connectionsOf(current.node);
protected void visitChildren(Tile endNode) {
if(debug) Effects.effect(Fx.node3, current.node.worldx(), current.node.worldy());
// Loop through each connection in turn
for (int i = 0; i < conn.length; i++) {
//Connection<N> connection = connections.get(i)
// Get the cost estimate for the node
N node = conn[i];
if(node == null) continue;
float addCost = heuristic.estimate(current.node, node);
jps(current.node, current.from == null ? -1 : relDirection(current.node, current.from), endNode, node -> {
float addCost = estimate(current.node, node);
float nodeCost = current.costSoFar + addCost;
float nodeHeuristic;
NodeRecord<N> nodeRecord = getNodeRecord(node);
NodeRecord nodeRecord = getNodeRecord(node);;
if (nodeRecord.category == CLOSED) { // The node is closed
// If we didn't find a shorter route, skip
if (nodeRecord.costSoFar <= nodeCost) continue;
if (nodeRecord.costSoFar <= nodeCost){
return;
}
// We can use the node's old cost values to calculate its heuristic
// without calling the possibly expensive heuristic function
nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
} else if (nodeRecord.category == OPEN) { // The node is open
// If our route is no better, then skip
if (nodeRecord.costSoFar <= nodeCost) continue;
//If our route is no better, then skip
if (nodeRecord.costSoFar <= nodeCost){
return;
}
// Remove it from the open list (it will be re-added with the new cost)
openList.remove(nodeRecord);
@@ -174,26 +171,162 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
// We'll need to calculate the heuristic value using the function,
// since we don't have a node record with a previously calculated value
nodeHeuristic = heuristic.estimate(node, endNode);
nodeHeuristic = estimate(node, endNode);
}
// Update node record's cost and connection
nodeRecord.costSoFar = nodeCost;
nodeRecord.from = current.node; //TODO ???
nodeRecord.from = current.node;
// Add it to the open list with the estimated total cost
addToOpenList(nodeRecord, nodeCost + nodeHeuristic);
}
});
}
protected void generateNodePath(N startNode, GraphPath<N> outPath) {
protected void jps(Tile current, int direction, Tile end, Consumer<Tile> cons){
if(obstacle(current)) return; //skip solid or off-the-screen stuff
//Log.info("jps {0} {1} // {2}", current.x, current.y, direction);
if(unop){
for(int i = 0; i < 4; i ++){
if(!obstacle(current.getNearby(i))) cons.accept(current.getNearby(i));
}
return;
}
//if there's no start point, scan everything.
if(direction == -1){
for(int i = 0; i < 8; i ++){
jps(current.getNearby(Geometry.d8[i]), i, end, cons);
}
return;
}
if(direction % 2 == 0){
//forced neighbor in the straight pattern
if(obstacle(rel(current, direction + 2)) && !obstacle(rel(current, direction + 1))){
cons.accept(rel(current, direction + 1));
}
if(obstacle(rel(current, direction - 2)) && !obstacle(rel(current, direction - 1))){
cons.accept(rel(current, direction - 1));
}
}else{ //moving diagonal
//forced neighbor in the diagonal pattern
if(obstacle(rel(current, direction + 3)) && !obstacle(rel(current, direction + 2)) && !obstacle(rel(current, direction -2))) {
cons.accept(rel(current, direction + 2));//jps(rel(current, direction + 2), Mathf.mod(direction + 2, 8), end, cons);
}
if(obstacle(rel(current, direction - 3)) && !obstacle(rel(current, direction - 2))&& !obstacle(rel(current, direction + 2))){
cons.accept(rel(current, direction - 2));//jps(rel(current, direction - 2), Mathf.mod(direction - 2, 8), end, cons);
}
}
while(!obstacle(current) && !trap(current, direction)){
if(debug) Effects.effect(Fx.node1, current.worldx(), current.worldy());
//moving straight
if(direction % 2 == 0){
Tile sf = scanDir(rel(current, direction), end, direction); //check if there's anything of interest going straight
if(sf != null){ //if there is, jump to that location immediately and stop
cons.accept(sf);
return;
}
}else{ //moving diagonal
Tile sf = scanDir(rel(current, direction), end, direction);
if(sf != null){
cons.accept(sf);
return;
}
Tile sl = scanDir(rel(current, Mathf.mod(direction - 1, 8)), end, Mathf.mod(direction - 1, 8));
if(sl != null){
cons.accept(sl);
}
Tile sr = scanDir(rel(current, Mathf.mod(direction + 1, 8)), end, Mathf.mod(direction + 1, 8));
if(sr != null){
cons.accept(sr);
}
}
if(current == end){
cons.accept(end);
return;
}
current = rel(current, direction);
}
}
protected boolean trap(Tile tile, int direction){
return direction % 2 == 1 && obstacle(rel(tile, direction - 1)) && obstacle(rel(tile, direction + 1));
}
protected Tile scanDir(Tile tile, Tile end, int direction){
while(!obstacle(tile)){
if(debug) Effects.effect(Fx.node2, tile.worldx(), tile.worldy());
if(tile == end) return tile;
if(direction % 2 == 0){
//forced neighbor in the straight pattern
if((obstacle(rel(tile, direction + 2)) && !obstacle(rel(tile, direction + 1))) ||
(obstacle(rel(tile, direction - 2)) && !obstacle(rel(tile, direction - 1)))){
if(debug) Effects.effect(Fx.node4, tile.worldx(), tile.worldy());
return tile;
}
}else{ //moving diagonal
//forced neighbor in the diagonal pattern, end here
if((obstacle(rel(tile, direction + 3)) && !obstacle(rel(tile, direction + 2)) && !obstacle(rel(tile, direction - 2))) ||
(obstacle(rel(tile, direction - 3)) && !obstacle(rel(tile, direction - 2)) && !obstacle(rel(tile, direction + 2)))) {
if(debug) Effects.effect(Fx.node4, tile.worldx(), tile.worldy());
return tile;
}
}
Tile next = rel(tile, direction);
if(obstacle(next)) break;
tile = next;
}
return null;
}
protected Tile rel(Tile tile, int i){
return tile.getNearby(Geometry.d8[Mathf.mod(i, 8)]);
}
protected boolean obstacle(Tile tile){
return tile == null || tile.solid();
}
;
protected float estimate(Tile tile, Tile other){
return Math.abs(tile.worldx() - other.worldx()) + Math.abs(tile.worldy() - other.worldy());
}
protected int relDirection(Tile from, Tile current){
if(from.y == current.y && from.x > current.x) return 0;
if(from.y == current.y && from.x < current.x) return 4;
if(from.x == current.x && from.y > current.y) return 2;
if(from.x == current.x && from.y < current.y) return 6;
if(from.y > current.y && from.x > current.x) return 1;
if(from.y < current.y && from.x < current.x) return 5;
if(from.x > current.x && from.y < current.y) return 7;
if(from.x < current.x && from.y > current.y) return 3;
return -1;
}
protected void generateNodePath(Tile startNode, GraphPath<Tile> outPath) {
// Work back along the path, accumulating nodes
// outPath.clear();
while (current.from != null) {
outPath.add(current.node);
current = records.get(graph.getIndex(current.from));
current = records.get(indexOf(current.from));
}
outPath.add(startNode);
@@ -201,66 +334,45 @@ public class OptimizedPathFinder<N> implements PathFinder<N> {
outPath.reverse();
}
protected void addToOpenList(NodeRecord<N> nodeRecord, float estimatedTotalCost) {
protected void addToOpenList(NodeRecord nodeRecord, float estimatedTotalCost) {
openList.add(nodeRecord, estimatedTotalCost);
nodeRecord.category = OPEN;
}
protected NodeRecord<N> getNodeRecord(N node) {
if(!records.containsKey(graph.getIndex(node))){
NodeRecord<N> record = new NodeRecord<>();
protected NodeRecord getNodeRecord(Tile node) {
if(!records.containsKey(indexOf(node))){
NodeRecord record = new NodeRecord();
record.node = node;
record.searchId = searchId;
records.put(graph.getIndex(node), record);
records.put(indexOf(node), record);
return record;
}else{
return records.get(graph.getIndex(node));
NodeRecord record = records.get(indexOf(node));
if(record.searchId != searchId){
record.category = UNVISITED;
record.searchId = searchId;
}
return record;
}
}
/**
* This nested class is used to keep track of the information we need for each node during the search.
*
* @param <N> Type of node
* @author davebaol
*/
static class NodeRecord<N> extends BinaryHeap.Node {
/**
* The reference to the node.
*/
N node;
N from;
private int indexOf(Tile node){
return node.packedPosition();
}
/**
* The incoming connection to the node
*/
//Connection<N> connection;
static class NodeRecord extends BinaryHeap.Node {
Tile node;
Tile from;
/**
* The actual cost from the start node.
*/
float costSoFar;
/**
* The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #CLOSED}.
*/
byte category;
/**
* ID of the current search.
*/
int searchId;
/**
* Creates a {@code NodeRecord}.
*/
public NodeRecord() {
super(0);
}
/**
* Returns the estimated total cost.
*/
public float getEstimatedTotalCost() {
return getValue();
}

View File

@@ -1,112 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.PathSmoother;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.game.SpawnPoint;
import io.anuke.mindustry.world.Tile;
public class Pathfind{
/**Maximum time taken per frame on pathfinding for a single path.*/
private static final long maxTime = 1000000 * 5;
/**Tile graph, for determining conenctions between two tiles*/
TileGraph graph = new TileGraph();
/**Smoother that removes extra nodes from a path.*/
PathSmoother<Tile, Vector2> smoother = new PathSmoother<Tile, Vector2>(new Raycaster());
/**temporary vector2 for calculations*/
Vector2 vector = new Vector2();
Vector2 v1 = new Vector2();
Vector2 v2 = new Vector2();
Vector2 v3 = new Vector2();
/**Finds the position on the path an enemy should move to.
* If the path is not yet calculated, this returns the enemy's position (i. e. "don't move")
* @param enemy The enemy to find a path for
* @return The position the enemy should move to.*/
public Vector2 find(BaseUnit enemy){
//TODO!
return v1.set(enemy.x, enemy.y);
}
/**Update the pathfinders and continue calculating the path if it hasn't been calculated yet.
* This method is run each frame.*/
public void update(){
/*
//go through each spawnpoint, and if it's not found a path yet, update it
for(int i = 0; i < world.getSpawns().size; i ++){
SpawnPoint point = world.getSpawns().get(i);
if(point.request == null || point.finder == null){
continue;
}
if(!point.request.pathFound){
try{
if(point.finder.search(point.request, maxTime)){
smoother.smoothPath(point.path);
point.pathTiles = point.path.nodes.toArray(Tile.class);
point.finder = null;
}
}catch (ArrayIndexOutOfBoundsException e){
//no path
point.request.pathFound = true;
}
}
}*/
}
private void resetPathFor(SpawnPoint point){
/*
point.finder = new OptimizedPathFinder<>(graph);
point.path.clear();
point.pathTiles = null;
//TODO
point.request = new PathFinderRequest<>(point.start, world.getCore(), state.difficulty.heuristic, point.path);
point.request.statusChanged = true; //IMPORTANT!*/
}
/**Finds the closest tile to a position, in an array of tiles.*/
private int findClosest(Tile[] tiles, float x, float y){
int cindex = -2;
float dst = Float.MAX_VALUE;
for(int i = 0; i < tiles.length - 1; i ++){
Tile tile = tiles[i];
Tile next = tiles[i + 1];
float d = pointLineDist(tile.worldx(), tile.worldy(), next.worldx(), next.worldy(), x, y);
if(d < dst){
dst = d;
cindex = i;
}
}
return cindex + 1;
}
/**Returns whether a point is on a line.*/
private boolean onLine(Vector2 vector, float x1, float y1, float x2, float y2){
return MathUtils.isEqual(vector.dst(x1, y1) + vector.dst(x2, y2), Vector2.dst(x1, y1, x2, y2), 0.01f);
}
/**Returns distance from a point to a line segment.*/
private float pointLineDist(float x, float y, float x2, float y2, float px, float py){
float l2 = Vector2.dst2(x, y, x2, y2);
float t = Math.max(0, Math.min(1, Vector2.dot(px - x, py - y, x2 - x, y2 - y) / l2));
Vector2 projection = v1.set(x, y).add(v2.set(x2, y2).sub(x, y).scl(t)); // Projection falls on the segment
return projection.dst(px, py);
}
//TODO documentation
private Vector2 projectPoint(float x1, float y1, float x2, float y2, float pointx, float pointy){
float px = x2-x1, py = y2-y1, dAB = px*px + py*py;
float u = ((pointx - x1) * px + (pointy - y1) * py) / dAB;
float x = x1 + u * px, y = y1 + u * py;
return v3.set(x, y); //this is D
}
}

View File

@@ -0,0 +1,42 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.DefaultGraphPath;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.util.Log;
public class Pathfinder {
private OptimizedPathFinder finder = new OptimizedPathFinder();
public Pathfinder(){
Events.on(WorldLoadEvent.class, this::clear);
}
public void test(Tile start, Tile end){
DefaultGraphPath<Tile> p = new DefaultGraphPath<>();
/*
OptimizedPathFinder.unop = true;
Timers.markNs();
finder.searchNodePath(start, end, p);
for(Tile tile : p.nodes){
Effects.effect(Fx.breakBlock, tile.worldx(), tile.worldy());
}
Log.info("Normal elapsed: {0}", Timers.elapsedNs());*/
OptimizedPathFinder.unop = false;
Timers.markNs();
finder.searchNodePath(start, end, p);
for(Tile tile : p.nodes){
Effects.effect(Fx.breakBlock, tile.worldx(), tile.worldy());
}
Log.info("JSFSAF elapsed: {0}", Timers.elapsedNs());
}
private void clear(){
}
}