// // $Id: AStarPathUtil.java,v 1.19 2002/06/22 00:49:15 ray Exp $ package com.threerings.miso.scene.util; import java.awt.Point; import java.util.*; import com.samskivert.util.HashIntMap; import com.threerings.media.util.MathUtil; import com.threerings.miso.Log; import com.threerings.miso.scene.DisplayMisoScene; import com.threerings.miso.scene.Traverser; import com.threerings.miso.tile.BaseTile; /** * The AStarPathUtil class provides a facility for * finding a reasonable path between two points in a scene using the * A* search algorithm. * *

See the path-finding article on * * Gamasutra for more detailed information. */ public class AStarPathUtil { /** * Return a list of Point objects representing a path * from coordinates (ax, by) to (bx, by), * inclusive, determined by performing an A* search in the given * scene's base tile layer. Assumes the starting and destination nodes * are traversable by the specified traverser. * * @param scene the scene in which a path is to be computed. * @param tilewid the scene width in tiles. * @param tilehei the scene height in tiles. * @param trav the traverser to follow the path. * @param ax the starting x-position in tile coordinates. * @param ay the starting y-position in tile coordinates. * @param bx the ending x-position in tile coordinates. * @param by the ending y-position in tile coordinates. * * @return the list of points in the path. */ public static List getPath ( DisplayMisoScene scene, int tilewid, int tilehei, Traverser trav, int ax, int ay, int bx, int by) { AStarInfo info = new AStarInfo(scene, tilewid, tilehei, trav, bx, by); // set up the starting node AStarNode s = info.getNode(ax, ay); s.g = 0; s.h = getDistanceEstimate(ax, ay, bx, by); s.f = s.g + s.h; // push starting node on the open list info.open.add(s); // while there are more nodes on the open list while (info.open.size() > 0) { // pop the best node so far from open AStarNode n = (AStarNode)info.open.first(); info.open.remove(n); // if node is a goal node if (n.x == bx && n.y == by) { // construct and return the acceptable path return getNodePath(n); } // consider each successor of the node considerStep(info, n, n.x - 1, n.y - 1, DIAGONAL_COST); considerStep(info, n, n.x, n.y - 1, ADJACENT_COST); considerStep(info, n, n.x + 1, n.y - 1, DIAGONAL_COST); considerStep(info, n, n.x - 1, n.y, ADJACENT_COST); considerStep(info, n, n.x + 1, n.y, ADJACENT_COST); considerStep(info, n, n.x - 1, n.y + 1, DIAGONAL_COST); considerStep(info, n, n.x, n.y + 1, ADJACENT_COST); considerStep(info, n, n.x + 1, n.y + 1, DIAGONAL_COST); // push the node on the closed list info.closed.add(n); } // no path found return null; } /** * Consider the step (n.x, n.y) to (x, y) * for possible inclusion in the path. * * @param info the info object. * @param n the originating node for the step. * @param x the x-coordinate for the destination step. * @param y the y-coordinate for the destination step. */ protected static void considerStep ( AStarInfo info, AStarNode n, int x, int y, int cost) { // skip node if it's outside the map bounds or otherwise impassable if (!info.isStepValid(n.x, n.y, x, y)) { return; } // if it's offscreen, bang up the cost considerably if (!info.isCoordinateValid(x, y)) { cost += OFFSCREEN_COST; } // calculate the new cost for this node int newg = n.g + cost; // make sure the cost is reasonable (so we don't go crazy computing // offscreen costs) if (newg > info.maxcost) { // Log.info("Rejected costly step."); return; } // retrieve the node corresponding to this location AStarNode np = info.getNode(x, y); // skip if it's already in the open or closed list or if its // actual cost is less than the just-calculated cost if ((info.open.contains(np) || info.closed.contains(np)) && np.g <= newg) { return; } // remove the node from the open list since we're about to // modify its score which determines its placement in the list info.open.remove(np); // update the node's information np.parent = n; np.g = newg; np.h = getDistanceEstimate(np.x, np.y, info.destx, info.desty); np.f = np.g + np.h; // remove it from the closed list if it's present info.closed.remove(np); // add it to the open list for further consideration info.open.add(np); } /** * Return a list of Point objects detailing the path * from the first node (the given node's ultimate parent) to the * ending node (the given node itself.) * * @param n the ending node in the path. * * @return the list detailing the path. */ protected static List getNodePath (AStarNode n) { AStarNode cur = n; ArrayList path = new ArrayList(); while (cur != null) { // add to the head of the list since we're traversing from // the end to the beginning path.add(0, new Point(cur.x, cur.y)); // advance to the next node in the path cur = cur.parent; } return path; } /** * Return a heuristic estimate of the cost to get from (ax, * ay) to (bx, by). */ protected static int getDistanceEstimate (int ax, int ay, int bx, int by) { // we're doing all of our cost calculations based on geometric // distance times ten int xsq = bx - ax; int ysq = by - ay; return (int) (ADJACENT_COST * Math.sqrt(xsq * xsq + ysq * ysq)); } /** The standard cost to move between nodes. */ public static final int ADJACENT_COST = 10; /** The cost to move diagonally. */ public static final int DIAGONAL_COST = (int) Math.sqrt( (ADJACENT_COST * ADJACENT_COST) * 2); /** A big old additional cost incurred for offscreen movement. */ public static final int OFFSCREEN_COST = 1000; } /** * A holding class to contain the wealth of information referenced * while performing an A* search for a path through a tile array. */ class AStarInfo { /** The scene whose base tile layer is being traversed. */ public DisplayMisoScene scene; /** The tile array dimensions. */ public int tilewid, tilehei; /** The traverser moving along the path. */ public Traverser trav; /** The set of open nodes being searched. */ public SortedSet open; /** The set of closed nodes being searched. */ public ArrayList closed; /** The destination coordinates in the tile array. */ public int destx, desty; /** The maximum cost of any path that we'll consider. */ public int maxcost; public AStarInfo ( DisplayMisoScene scene, int tilewid, int tilehei, Traverser trav, int destx, int desty) { // save off references this.scene = scene; this.tilewid = tilewid; this.tilehei = tilehei; this.trav = trav; this.destx = destx; this.desty = desty; // compute the maximum cost as the maximum onscreen path plus // the maximum offscreen cost this.maxcost = ((tilewid + tilehei) * AStarPathUtil.ADJACENT_COST) + MAX_OFFSCREEN * AStarPathUtil.OFFSCREEN_COST; // construct the open and closed lists open = new TreeSet(); closed = new ArrayList(); } /** * Returns whether the given coordinate is valid based on the * dimensions of the map being traversed. */ protected boolean isCoordinateValid (int x, int y) { return (x >= 0 && y >= 0 && x < tilewid && y < tilehei); } /** * Returns whether moving from the given source to destination * coordinates is a valid move. */ protected boolean isStepValid (int sx, int sy, int dx, int dy) { // not traversable if the destination itself fails test if (!isTraversable(dx, dy)) { return false; } // if the step is diagonal, make sure the corners don't impede // our progress if ((Math.abs(dx - sx) == 1) && (Math.abs(dy - sy) == 1)) { return isTraversable(dx, sy) && isTraversable(sx, dy); } // non-diagonals are always traversable return true; } /** * Returns whether the given coordinate is valid and traversable. */ protected boolean isTraversable (int x, int y) { if (isCoordinateValid(x, y)) { BaseTile tile = scene.getBaseTile(x, y); return (tile == null) || trav.canTraverse(tile); } return true; } /** * Get or create the node for the specified point. */ public AStarNode getNode (int x, int y) { // note: this _could_ break for unusual values of x and y. // perhaps use a IntTuple as a key? Bleah. int key = (x << 16) | (y & 0xffff); AStarNode node = (AStarNode) _nodes.get(key); if (node == null) { node = new AStarNode(x, y); _nodes.put(key, node); } return node; } /** The nodes being considered in the path. */ protected HashIntMap _nodes = new HashIntMap(); /** The maximum number of offscreen points we'll consider. */ protected static final int MAX_OFFSCREEN = 6; } /** * A class that represents a single traversable node in the tile array * along with its current A*-specific search information. */ class AStarNode implements Comparable { /** The node coordinates. */ public int x, y; /** The actual cheapest cost of arriving here from the start. */ public int g; /** The heuristic estimate of the cost to the goal from here. */ public int h; /** The score assigned to this node. */ public int f; /** The node from which we reached this node. */ public AStarNode parent; /** The node's monotonically-increasing unique identifier. */ public int id; public AStarNode (int x, int y) { this.x = x; this.y = y; id = _nextid++; } public int compareTo (Object o) { int bf = ((AStarNode)o).f; // since the set contract is fulfilled using the equality results // returned here, and we'd like to allow multiple nodes with // equivalent scores in our set, we explicitly define object // equivalence as the result of object.equals(), else we use the // unique node id since it will return a consistent ordering for // the objects. if (f == bf) { return (this == o) ? 0 : (id - ((AStarNode)o).id); } return f - bf; } /** The next unique node id. */ protected static int _nextid = 0; }