简体   繁体   中英

Speeding up Dijkstra

Hi I wrote a small Dijkstra implementation for pathfinding in our tiled based game. The problem is, if have 10 Enemies that use this algorithm to find the shortestpath to a target (mainly for patroling at present) the game gets pretty laggy. Especially because the game should run on a Android smartphone. What I tryed sofar to speed the whole thing up:

1. Limit the number of Nodes that are connected via edges by a fixed number, meaning just doing N steps until aboarding of the initializeEgdes methode. That lead to some ugly behavior where not all patrolsrouts where executed because some where to long.

2. Paralise the execution of the Dijkstra in the way that every enemy calculates its shortest path in a own thread. Here was the problem that I'm not that well versed with threading and didn't get the idea into a running state (my enemieobjects didn't move)

I think the first idea regarding to limiting the number of processed nodeConnections could have a fairly big impact, but i cant find a good rule when to aboard the processing.

public class Dijkstra {

PathNode[][] allNodes;
TiledMap tiledMap;

public Dijkstra(TiledMap sourceMap) {
    tiledMap = sourceMap;
    generateAllNodes();
}


/**
 * Node that virtualises an actual unit on gameboard, currently a tile.
 *
 * @author Lucas
 */
public class PathNode {
    boolean walkable = true;
    float x = 0;
    float y = 0;
    public final static float width = 32;
    public final static float height = 32;
    DijkstraNode myDijstraNode;

    public PathNode(int xpos, int ypos) {
        x = width * xpos;
        y = height * ypos;
        myDijstraNode = new DijkstraNode(this);
    }
}

/**
 * Node used for the Dijkstra methodes.
 *
 * @author Lucas
 */
public class DijkstraNode implements Comparable<DijkstraNode> {
    PathNode correspondingNode;
    double minDistance = Double.POSITIVE_INFINITY;
    DijkstraNode previous;
    Edge[] adjacencies;

    public DijkstraNode(PathNode myNode) {
        correspondingNode = myNode;
    }

    @Override
    public String toString() {
        return "TILE[" + correspondingNode.x / PathNode.width + "][" + correspondingNode.y / PathNode.height + "]";
    }


    @Override
    public int compareTo(DijkstraNode arg0) {
        // TODO Auto-generated method stub
        return Double.compare(minDistance, arg0.minDistance);
    }

    public void resetNode()
    {
        minDistance= Double.POSITIVE_INFINITY;
        adjacencies=null;
        previous=null;
    }
}

/**
 * An Edge between two dijkstraNodes
 *
 * @author Lucas
 */
class Edge {
    public final DijkstraNode target;
    public final double weight;

    public Edge(DijkstraNode argTarget, double argWeight) {
        target = argTarget;
        weight = argWeight;
    }
}


private List<DijkstraNode> getNeighbours(DijkstraNode u) {

    List<DijkstraNode> neighbours = new ArrayList<DijkstraNode>();

    float originX, originY;
    originX = u.correspondingNode.x / PathNode.width;
    originY = u.correspondingNode.y / PathNode.height;
    TiledMapTileLayer tl = (TiledMapTileLayer) tiledMap.getLayers().get(
            "main_background");
    //Left
    //Checks if the calculated field is still in allNodes
    if (Math.signum(originX - 1) == 1 && allNodes[(int) originY][(int) (originX - 1)].walkable) {
        neighbours.add(allNodes[(int) originY][(int) (originX - 1)].myDijstraNode);
    }
    //Right
    if ((originX + 1) < tl.getWidth() && allNodes[(int) originY][(int) (originX + 1)].walkable) {
        neighbours.add(allNodes[(int) originY][(int) (originX + 1)].myDijstraNode);
    }
    //Up
    if (originY + 1 < tl.getHeight() && allNodes[(int) originY + 1][(int) (originX)].walkable) {
        neighbours.add(allNodes[(int) originY + 1][(int) (originX)].myDijstraNode);
    }
    //Down
    if (Math.signum(originY - 1) == 1 && allNodes[(int) originY - 1][(int) (originX)].walkable) {
        neighbours.add(allNodes[(int) originY - 1][(int) (originX)].myDijstraNode);
    }
    return neighbours;

}

public DijkstraNode getDijkstraNode(com.hhn.liberation.logic.units.Enemy objectToMove) {
    DijkstraNode startNode = null;
    startNode=getDijkstraNode(new Vector2(objectToMove.getX(),objectToMove.getY()));
    return startNode;
}


//Dijkstra Methoden gefunden auf http://www.algolist.com/code/java/Dijkstra%27s_algorithm
public static List<DijkstraNode> getShortestPathTo(DijkstraNode target) {
    List<DijkstraNode> path = new ArrayList<DijkstraNode>();
    for (DijkstraNode vertex = target; vertex != null; vertex = vertex.previous)
        path.add(vertex);
    Collections.reverse(path);
    return path;
}

public static void computePaths(DijkstraNode source) {
    source.minDistance = 0.;
    PriorityQueue<DijkstraNode> vertexQueue = new PriorityQueue<DijkstraNode>();
    vertexQueue.add(source);

    while (!vertexQueue.isEmpty()) {
        DijkstraNode u = vertexQueue.poll();

        // Visit each edge exiting u
        for (Edge e : u.adjacencies) {
            DijkstraNode v = e.target;
            double weight = e.weight;
            double distanceThroughU = u.minDistance + weight;
            if (distanceThroughU < v.minDistance) {
                vertexQueue.remove(v);
                v.minDistance = distanceThroughU;
                v.previous = u;
                vertexQueue.add(v);
            }
        }
    }
}
//Ende Dijkstra Methoden


public DijkstraNode getDijkstraNode(Vector2 target) {
    // TODO Auto-generated method stub
    for (int i = 0; i < allNodes.length; i++) {
        for (int k = 0; k < allNodes[i].length; k++) {
            PathNode currentNeigbour = allNodes[i][k];
            if (currentNeigbour.x <= target.x && currentNeigbour.x + PathNode.width >= target.x &&
                    currentNeigbour.y <= target.y && currentNeigbour.y + PathNode.height >= target.y) {
                return currentNeigbour.myDijstraNode;
            }
        }
    }
    return null;
}

private void generateAllNodes() {
    TiledMapTileLayer tl = (TiledMapTileLayer) tiledMap.getLayers().get("main_background");

    if(allNodes==null)
    {
        allNodes = new PathNode[tl.getHeight()][tl.getWidth()];
        for (int i = 0; i < tl.getHeight(); i++) {
            for (int k = 0; k < tl.getWidth(); k++) {
                allNodes[i][k] = new PathNode(k, i);
                //TODO use provided method in level?
//                checkForObjectCollision(enemy)
                allNodes[i][k].walkable = !Collider.doesCollideWithWall(new Collider(
                        allNodes[i][k]), tiledMap);
            }
        }
    }
    else
    {
        for (int i = 0; i < tl.getHeight(); i++) {
            for (int k = 0; k < tl.getWidth(); k++) {
                allNodes[i][k].myDijstraNode.resetNode();
            }
        }
    }

}


public void initialiseEdges(DijkstraNode startNode) {
    // TODO Auto-generated method stub
    DijkstraNode currentNode = startNode;

    Queue<DijkstraNode> neigbourQueue=new LinkedList<DijkstraNode>();
    neigbourQueue.offer(currentNode);



    while(!neigbourQueue.isEmpty())
    {
        List<DijkstraNode> newNeigbours=innerFunction(neigbourQueue.poll(),0);
        if(newNeigbours!=null)
        neigbourQueue.addAll(newNeigbours);
    }

}


private List<DijkstraNode> innerFunction(DijkstraNode currentNode, int depth) {
    if (currentNode.adjacencies != null) {
        return null;
    }
//        if(depth>15)
//        {
//            currentNode.adjacencies=new Edge[0];
//            return;
//        }

    List<DijkstraNode> neigbours = getNeighbours(currentNode);

    currentNode.adjacencies = new Edge[neigbours.size()];
    for (int i = 0; i < neigbours.size(); i++) {
        DijkstraNode currentNeigbour = neigbours.get(i);
        currentNode.adjacencies[i] = new Edge(currentNeigbour, 1);
    }
//        for (PathNode pt : neigbours) {
//            innerFunction(pt.myDijstraNode,depth+1);
//        }
    return neigbours;
}


}

Since you say it's a tile-based game, I suppose that valid paths are traversable in both directions, with cost independent of direction (or close enough to independent). In that case, you are doing more work than you need to do if you apply Dijkstra starting at each enemy position.

Instead, start at the target and find the shortest path to all enemies in the same run of Dijkstra (ie terminate only after a path to each enemy has been found). The worst case cost of this approach is independent of the number of enemies.

There is a CodeReview forum. As starters you could forget about floating point and use integer arithmetic, using square of distances for instance.

Math.signum(originX - 1) == 1 ⇔
⇔ originX - 1 > 0 ⇔
⇔ originX > 1

If maximal 8 neighbours, reserve a capacity 8:

List<DijkstraNode> neighbours = new ArrayList<DijkstraNode>(8);

You are right to consider that the algorithm is more decisive, but that is too nice to comment upon.

Assuming you have a normal gameloop, it's normal for pathfinding to take to long for one update iteration. I think your solution to do this in a seperate thread is a good one. Many games face this problem and you don't want your characters to stay still while the pathfinding is calculated. You can get around this problem by calculating a crude path (which can be calculated in one update iteration) with a little bit of luck your characters will start walking in a general good direction, and then pick up the correct path once it is fully calculated. Sometimes they will walk in a wrong direction for a few frames, before turning around to pick up the fully calculated path.

If you can't get the threads solution working, you can execute a part of the Dijkstra calculation within the update of the frame, then pause, draw and calculate the next part in the next update frame.

There are lots of little things you can do that would be more on the side of game design than speeding up the algorithm. One such example would be controlling how often the enemies find a path. If you're doing a simple tower defense game, for example, you can just have the enemies determine a path once from their spawn point to end point, provided that they are static. If the map continues to change, you can spread out the pathfinding so that only a couple enemies are finding a path each frame. Instead of calling computePaths for each enemy every frame, maybe do it in blocks of 3 or 4 at a time, depending on how much faster you need it to run.

If you just want to stick with optimizing the algorithm, I suggest looking into the A* (A Star) algorithm. It's similar to Dijkstra's but in my experience it seems to work much better for most games. I've used it for a couple games in Android and Flash and was able to maintain a consistent 40fps with 100+ enemies dynamically finding new paths using the method mentioned above.

If you're interested, I could go into a bit more depth and post some code samples

Generally speaking it is a bad idea to make complex computations on the UI thread. You can always optimize your code some more, but on slow devices there will be a performance decreasement. The solution should be to offload the computation to an AsyncTask . I think the optimal solution for you would be to make the computation every X milliseconds on the AsyncTask and use this data in the main thread until an update arrives.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM