added sources for Slick

Former-commit-id: 1647fa32ef6894bd7db44f741f07c2f4dcdf9054
Former-commit-id: 0e5810dcfbe1fd59b13e7cabe9f1e93c5542da2d
This commit is contained in:
Song Minjae
2016-12-30 23:29:12 +09:00
parent d1f01a203d
commit d3080ffb78
329 changed files with 58400 additions and 7 deletions

View File

@@ -0,0 +1,27 @@
package org.newdawn.slick.util.pathfinding;
/**
* The description of a class providing a cost for a given tile based
* on a target location and entity being moved. This heuristic controls
* what priority is placed on different tiles during the search for a path
*
* @author Kevin Glass
*/
public interface AStarHeuristic {
/**
* Get the additional heuristic cost of the given tile. This controls the
* order in which tiles are searched while attempting to find a path to the
* target location. The lower the cost the more likely the tile will
* be searched.
*
* @param map The map on which the path is being found
* @param mover The entity that is moving along the path
* @param x The x coordinate of the tile being evaluated
* @param y The y coordinate of the tile being evaluated
* @param tx The x coordinate of the target location
* @param ty Teh y coordinate of the target location
* @return The cost associated with the given tile
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty);
}

View File

@@ -0,0 +1,590 @@
package org.newdawn.slick.util.pathfinding;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.newdawn.slick.util.pathfinding.heuristics.ClosestHeuristic;
/**
* A path finder implementation that uses the AStar heuristic based algorithm
* to determine a path.
*
* @author Kevin Glass
*/
public class AStarPathFinder implements PathFinder, PathFindingContext {
/** The set of nodes that have been searched through */
private ArrayList closed = new ArrayList();
/** The set of nodes that we do not yet consider fully searched */
private PriorityList open = new PriorityList();
/** The map being searched */
private TileBasedMap map;
/** The maximum depth of search we're willing to accept before giving up */
private int maxSearchDistance;
/** The complete set of nodes across the map */
private Node[][] nodes;
/** True if we allow diaganol movement */
private boolean allowDiagMovement;
/** The heuristic we're applying to determine which nodes to search first */
private AStarHeuristic heuristic;
/** The node we're currently searching from */
private Node current;
/** The mover going through the path */
private Mover mover;
/** The x coordinate of the source tile we're moving from */
private int sourceX;
/** The y coordinate of the source tile we're moving from */
private int sourceY;
/** The distance searched so far */
private int distance;
/**
* Create a path finder with the default heuristic - closest to target.
*
* @param map The map to be searched
* @param maxSearchDistance The maximum depth we'll search before giving up
* @param allowDiagMovement True if the search should try diaganol movement
*/
public AStarPathFinder(TileBasedMap map, int maxSearchDistance, boolean allowDiagMovement) {
this(map, maxSearchDistance, allowDiagMovement, new ClosestHeuristic());
}
/**
* Create a path finder
*
* @param heuristic The heuristic used to determine the search order of the map
* @param map The map to be searched
* @param maxSearchDistance The maximum depth we'll search before giving up
* @param allowDiagMovement True if the search should try diaganol movement
*/
public AStarPathFinder(TileBasedMap map, int maxSearchDistance,
boolean allowDiagMovement, AStarHeuristic heuristic) {
this.heuristic = heuristic;
this.map = map;
this.maxSearchDistance = maxSearchDistance;
this.allowDiagMovement = allowDiagMovement;
nodes = new Node[map.getWidthInTiles()][map.getHeightInTiles()];
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
nodes[x][y] = new Node(x,y);
}
}
}
/**
* @see PathFinder#findPath(Mover, int, int, int, int)
*/
public Path findPath(Mover mover, int sx, int sy, int tx, int ty) {
current = null;
// easy first check, if the destination is blocked, we can't get there
this.mover = mover;
this.sourceX = tx;
this.sourceY = ty;
this.distance = 0;
if (map.blocked(this, tx, ty)) {
return null;
}
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
nodes[x][y].reset();
}
}
// initial state for A*. The closed group is empty. Only the starting
// tile is in the open list and it's cost is zero, i.e. we're already there
nodes[sx][sy].cost = 0;
nodes[sx][sy].depth = 0;
closed.clear();
open.clear();
addToOpen(nodes[sx][sy]);
nodes[tx][ty].parent = null;
// while we haven't found the goal and haven't exceeded our max search depth
int maxDepth = 0;
while ((maxDepth < maxSearchDistance) && (open.size() != 0)) {
// pull out the first node in our open list, this is determined to
// be the most likely to be the next step based on our heuristic
int lx = sx;
int ly = sy;
if (current != null) {
lx = current.x;
ly = current.y;
}
current = getFirstInOpen();
distance = current.depth;
if (current == nodes[tx][ty]) {
if (isValidLocation(mover,lx,ly,tx,ty)) {
break;
}
}
removeFromOpen(current);
addToClosed(current);
// search through all the neighbours of the current node evaluating
// them as next steps
for (int x=-1;x<2;x++) {
for (int y=-1;y<2;y++) {
// not a neighbour, its the current tile
if ((x == 0) && (y == 0)) {
continue;
}
// if we're not allowing diaganol movement then only
// one of x or y can be set
if (!allowDiagMovement) {
if ((x != 0) && (y != 0)) {
continue;
}
}
// determine the location of the neighbour and evaluate it
int xp = x + current.x;
int yp = y + current.y;
if (isValidLocation(mover,current.x,current.y,xp,yp)) {
// the cost to get to this node is cost the current plus the movement
// cost to reach this node. Note that the heursitic value is only used
// in the sorted open list
float nextStepCost = current.cost + getMovementCost(mover, current.x, current.y, xp, yp);
Node neighbour = nodes[xp][yp];
map.pathFinderVisited(xp, yp);
// if the new cost we've determined for this node is lower than
// it has been previously makes sure the node hasn't been discarded. We've
// determined that there might have been a better path to get to
// this node so it needs to be re-evaluated
if (nextStepCost < neighbour.cost) {
if (inOpenList(neighbour)) {
removeFromOpen(neighbour);
}
if (inClosedList(neighbour)) {
removeFromClosed(neighbour);
}
}
// if the node hasn't already been processed and discarded then
// reset it's cost to our current cost and add it as a next possible
// step (i.e. to the open list)
if (!inOpenList(neighbour) && !(inClosedList(neighbour))) {
neighbour.cost = nextStepCost;
neighbour.heuristic = getHeuristicCost(mover, xp, yp, tx, ty);
maxDepth = Math.max(maxDepth, neighbour.setParent(current));
addToOpen(neighbour);
}
}
}
}
}
// since we've got an empty open list or we've run out of search
// there was no path. Just return null
if (nodes[tx][ty].parent == null) {
return null;
}
// At this point we've definitely found a path so we can uses the parent
// references of the nodes to find out way from the target location back
// to the start recording the nodes on the way.
Path path = new Path();
Node target = nodes[tx][ty];
while (target != nodes[sx][sy]) {
path.prependStep(target.x, target.y);
target = target.parent;
}
path.prependStep(sx,sy);
// thats it, we have our path
return path;
}
/**
* Get the X coordinate of the node currently being evaluated
*
* @return The X coordinate of the node currently being evaluated
*/
public int getCurrentX() {
if (current == null) {
return -1;
}
return current.x;
}
/**
* Get the Y coordinate of the node currently being evaluated
*
* @return The Y coordinate of the node currently being evaluated
*/
public int getCurrentY() {
if (current == null) {
return -1;
}
return current.y;
}
/**
* Get the first element from the open list. This is the next
* one to be searched.
*
* @return The first element in the open list
*/
protected Node getFirstInOpen() {
return (Node) open.first();
}
/**
* Add a node to the open list
*
* @param node The node to be added to the open list
*/
protected void addToOpen(Node node) {
node.setOpen(true);
open.add(node);
}
/**
* Check if a node is in the open list
*
* @param node The node to check for
* @return True if the node given is in the open list
*/
protected boolean inOpenList(Node node) {
return node.isOpen();
}
/**
* Remove a node from the open list
*
* @param node The node to remove from the open list
*/
protected void removeFromOpen(Node node) {
node.setOpen(false);
open.remove(node);
}
/**
* Add a node to the closed list
*
* @param node The node to add to the closed list
*/
protected void addToClosed(Node node) {
node.setClosed(true);
closed.add(node);
}
/**
* Check if the node supplied is in the closed list
*
* @param node The node to search for
* @return True if the node specified is in the closed list
*/
protected boolean inClosedList(Node node) {
return node.isClosed();
}
/**
* Remove a node from the closed list
*
* @param node The node to remove from the closed list
*/
protected void removeFromClosed(Node node) {
node.setClosed(false);
closed.remove(node);
}
/**
* Check if a given location is valid for the supplied mover
*
* @param mover The mover that would hold a given location
* @param sx The starting x coordinate
* @param sy The starting y coordinate
* @param x The x coordinate of the location to check
* @param y The y coordinate of the location to check
* @return True if the location is valid for the given mover
*/
protected boolean isValidLocation(Mover mover, int sx, int sy, int x, int y) {
boolean invalid = (x < 0) || (y < 0) || (x >= map.getWidthInTiles()) || (y >= map.getHeightInTiles());
if ((!invalid) && ((sx != x) || (sy != y))) {
this.mover = mover;
this.sourceX = sx;
this.sourceY = sy;
invalid = map.blocked(this, x, y);
}
return !invalid;
}
/**
* Get the cost to move through a given location
*
* @param mover The entity that is being moved
* @param sx The x coordinate of the tile whose cost is being determined
* @param sy The y coordiante of the tile whose cost is being determined
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The cost of movement through the given tile
*/
public float getMovementCost(Mover mover, int sx, int sy, int tx, int ty) {
this.mover = mover;
this.sourceX = sx;
this.sourceY = sy;
return map.getCost(this, tx, ty);
}
/**
* Get the heuristic cost for the given location. This determines in which
* order the locations are processed.
*
* @param mover The entity that is being moved
* @param x The x coordinate of the tile whose cost is being determined
* @param y The y coordiante of the tile whose cost is being determined
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The heuristic cost assigned to the tile
*/
public float getHeuristicCost(Mover mover, int x, int y, int tx, int ty) {
return heuristic.getCost(map, mover, x, y, tx, ty);
}
/**
* A list that sorts any element provided into the list
*
* @author kevin
*/
private class PriorityList {
/** The list of elements */
private List list = new LinkedList();
/**
* Retrieve the first element from the list
*
* @return The first element from the list
*/
public Object first() {
return list.get(0);
}
/**
* Empty the list
*/
public void clear() {
list.clear();
}
/**
* Add an element to the list - causes sorting
*
* @param o The element to add
*/
public void add(Object o) {
// float the new entry
for (int i=0;i<list.size();i++) {
if (((Comparable) list.get(i)).compareTo(o) > 0) {
list.add(i, o);
break;
}
}
if (!list.contains(o)) {
list.add(o);
}
//Collections.sort(list);
}
/**
* Remove an element from the list
*
* @param o The element to remove
*/
public void remove(Object o) {
list.remove(o);
}
/**
* Get the number of elements in the list
*
* @return The number of element in the list
*/
public int size() {
return list.size();
}
/**
* Check if an element is in the list
*
* @param o The element to search for
* @return True if the element is in the list
*/
public boolean contains(Object o) {
return list.contains(o);
}
public String toString() {
String temp = "{";
for (int i=0;i<size();i++) {
temp += list.get(i).toString()+",";
}
temp += "}";
return temp;
}
}
/**
* A single node in the search graph
*/
private class Node implements Comparable {
/** The x coordinate of the node */
private int x;
/** The y coordinate of the node */
private int y;
/** The path cost for this node */
private float cost;
/** The parent of this node, how we reached it in the search */
private Node parent;
/** The heuristic cost of this node */
private float heuristic;
/** The search depth of this node */
private int depth;
/** In the open list */
private boolean open;
/** In the closed list */
private boolean closed;
/**
* Create a new node
*
* @param x The x coordinate of the node
* @param y The y coordinate of the node
*/
public Node(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Set the parent of this node
*
* @param parent The parent node which lead us to this node
* @return The depth we have no reached in searching
*/
public int setParent(Node parent) {
depth = parent.depth + 1;
this.parent = parent;
return depth;
}
/**
* @see Comparable#compareTo(Object)
*/
public int compareTo(Object other) {
Node o = (Node) other;
float f = heuristic + cost;
float of = o.heuristic + o.cost;
if (f < of) {
return -1;
} else if (f > of) {
return 1;
} else {
return 0;
}
}
/**
* Indicate whether the node is in the open list
*
* @param open True if the node is in the open list
*/
public void setOpen(boolean open) {
this.open = open;
}
/**
* Check if the node is in the open list
*
* @return True if the node is in the open list
*/
public boolean isOpen() {
return open;
}
/**
* Indicate whether the node is in the closed list
*
* @param closed True if the node is in the closed list
*/
public void setClosed(boolean closed) {
this.closed = closed;
}
/**
* Check if the node is in the closed list
*
* @return True if the node is in the closed list
*/
public boolean isClosed() {
return closed;
}
/**
* Reset the state of this node
*/
public void reset() {
closed = false;
open = false;
cost = 0;
depth = 0;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return "[Node "+x+","+y+"]";
}
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getMover()
*/
public Mover getMover() {
return mover;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSearchDistance()
*/
public int getSearchDistance() {
return distance;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSourceX()
*/
public int getSourceX() {
return sourceX;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSourceY()
*/
public int getSourceY() {
return sourceY;
}
}

View File

@@ -0,0 +1,17 @@
package org.newdawn.slick.util.pathfinding;
/**
* A tagging interface for an object representing the entity in the game that
* is going to moving along the path. This allows us to pass around entity/state
* information to determine whether a particular tile is blocked, or how much
* cost to apply on a particular tile.
*
* For instance, a Mover might represent a tank or plane on a game map. Passing round
* this entity allows us to determine whether rough ground on a map should effect
* the unit's cost for moving through the tile.
*
* @author Kevin Glass
*/
public interface Mover {
}

View File

@@ -0,0 +1,158 @@
package org.newdawn.slick.util.pathfinding;
import java.io.Serializable;
import java.util.ArrayList;
/**
* A path determined by some path finding algorithm. A series of steps from
* the starting location to the target location. This includes a step for the
* initial location.
*
* @author Kevin Glass
*/
public class Path implements Serializable {
/** The serial identifier for this class */
private static final long serialVersionUID = 1L;
/** The list of steps building up this path */
private ArrayList steps = new ArrayList();
/**
* Create an empty path
*/
public Path() {
}
/**
* Get the length of the path, i.e. the number of steps
*
* @return The number of steps in this path
*/
public int getLength() {
return steps.size();
}
/**
* Get the step at a given index in the path
*
* @param index The index of the step to retrieve. Note this should
* be >= 0 and < getLength();
* @return The step information, the position on the map.
*/
public Step getStep(int index) {
return (Step) steps.get(index);
}
/**
* Get the x coordinate for the step at the given index
*
* @param index The index of the step whose x coordinate should be retrieved
* @return The x coordinate at the step
*/
public int getX(int index) {
return getStep(index).x;
}
/**
* Get the y coordinate for the step at the given index
*
* @param index The index of the step whose y coordinate should be retrieved
* @return The y coordinate at the step
*/
public int getY(int index) {
return getStep(index).y;
}
/**
* Append a step to the path.
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public void appendStep(int x, int y) {
steps.add(new Step(x,y));
}
/**
* Prepend a step to the path.
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public void prependStep(int x, int y) {
steps.add(0, new Step(x, y));
}
/**
* Check if this path contains the given step
*
* @param x The x coordinate of the step to check for
* @param y The y coordinate of the step to check for
* @return True if the path contains the given step
*/
public boolean contains(int x, int y) {
return steps.contains(new Step(x,y));
}
/**
* A single step within the path
*
* @author Kevin Glass
*/
public class Step implements Serializable {
/** The x coordinate at the given step */
private int x;
/** The y coordinate at the given step */
private int y;
/**
* Create a new step
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public Step(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Get the x coordinate of the new step
*
* @return The x coodindate of the new step
*/
public int getX() {
return x;
}
/**
* Get the y coordinate of the new step
*
* @return The y coodindate of the new step
*/
public int getY() {
return y;
}
/**
* @see Object#hashCode()
*/
public int hashCode() {
return x*y;
}
/**
* @see Object#equals(Object)
*/
public boolean equals(Object other) {
if (other instanceof Step) {
Step o = (Step) other;
return (o.x == x) && (o.y == y);
}
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
package org.newdawn.slick.util.pathfinding;
/**
* A description of an implementation that can find a path from one
* location on a tile map to another based on information provided
* by that tile map.
*
* @see TileBasedMap
* @author Kevin Glass
*/
public interface PathFinder {
/**
* Find a path from the starting location provided (sx,sy) to the target
* location (tx,ty) avoiding blockages and attempting to honour costs
* provided by the tile map.
*
* @param mover The entity that will be moving along the path. This provides
* a place to pass context information about the game entity doing the moving, e.g.
* can it fly? can it swim etc.
*
* @param sx The x coordinate of the start location
* @param sy The y coordinate of the start location
* @param tx The x coordinate of the target location
* @param ty Teh y coordinate of the target location
* @return The path found from start to end, or null if no path can be found.
*/
public Path findPath(Mover mover, int sx, int sy, int tx, int ty);
}

View File

@@ -0,0 +1,36 @@
package org.newdawn.slick.util.pathfinding;
/**
* The context describing the current path finding state
*
* @author kevin
*/
public interface PathFindingContext {
/**
* Get the object being moved along the path if any
*
* @return The object being moved along the path
*/
public Mover getMover();
/**
* Get the x coordinate of the source location
*
* @return The x coordinate of the source location
*/
public int getSourceX();
/**
* Get the y coordinate of the source location
*
* @return The y coordinate of the source location
*/
public int getSourceY();
/**
* Get the distance that has been searched to reach this point
*
* @return The distance that has been search to reach this point
*/
public int getSearchDistance();
}

View File

@@ -0,0 +1,58 @@
package org.newdawn.slick.util.pathfinding;
/**
* The description for the data we're pathfinding over. This provides the contract
* between the data being searched (i.e. the in game map) and the path finding
* generic tools
*
* @author Kevin Glass
*/
public interface TileBasedMap {
/**
* Get the width of the tile map. The slightly odd name is used
* to distiguish this method from commonly used names in game maps.
*
* @return The number of tiles across the map
*/
public int getWidthInTiles();
/**
* Get the height of the tile map. The slightly odd name is used
* to distiguish this method from commonly used names in game maps.
*
* @return The number of tiles down the map
*/
public int getHeightInTiles();
/**
* Notification that the path finder visited a given tile. This is
* used for debugging new heuristics.
*
* @param x The x coordinate of the tile that was visited
* @param y The y coordinate of the tile that was visited
*/
public void pathFinderVisited(int x, int y);
/**
* Check if the given location is blocked, i.e. blocks movement of
* the supplied mover.
*
* @param context The context describing the pathfinding at the time of this request
* @param tx The x coordinate of the tile we're moving to
* @param ty The y coordinate of the tile we're moving to
* @return True if the location is blocked
*/
public boolean blocked(PathFindingContext context, int tx, int ty);
/**
* Get the cost of moving through the given tile. This can be used to
* make certain areas more desirable. A simple and valid implementation
* of this method would be to return 1 in all cases.
*
* @param context The context describing the pathfinding at the time of this request
* @param tx The x coordinate of the tile we're moving to
* @param ty The y coordinate of the tile we're moving to
* @return The relative cost of moving across the given tile
*/
public float getCost(PathFindingContext context, int tx, int ty);
}

View File

@@ -0,0 +1,26 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that uses the tile that is closest to the target
* as the next best tile.
*
* @author Kevin Glass
*/
public class ClosestHeuristic implements AStarHeuristic {
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty) {
float dx = tx - x;
float dy = ty - y;
float result = (float) (Math.sqrt((dx*dx)+(dy*dy)));
return result;
}
}

View File

@@ -0,0 +1,26 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that uses the tile that is closest to the target
* as the next best tile. In this case the sqrt is removed
* and the distance squared is used instead
*
* @author Kevin Glass
*/
public class ClosestSquaredHeuristic implements AStarHeuristic {
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty) {
float dx = tx - x;
float dy = ty - y;
return ((dx*dx)+(dy*dy));
}
}

View File

@@ -0,0 +1,34 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that drives the search based on the Manhattan distance
* between the current location and the target
*
* @author Kevin Glass
*/
public class ManhattanHeuristic implements AStarHeuristic {
/** The minimum movement cost from any one square to the next */
private int minimumCost;
/**
* Create a new heuristic
*
* @param minimumCost The minimum movement cost from any one square to the next
*/
public ManhattanHeuristic(int minimumCost) {
this.minimumCost = minimumCost;
}
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx,
int ty) {
return minimumCost * (Math.abs(x-tx) + Math.abs(y-ty));
}
}

View File

@@ -0,0 +1,69 @@
package org.newdawn.slick.util.pathfinding.navmesh;
/**
* A link between this space and another
*
* @author kevin
*/
public class Link {
/** The x coordinate of the joining point */
private float px;
/** The y coordinate of the joining point */
private float py;
/** The target space we'd be linking to */
private Space target;
/**
* Create a new link
*
* @param px The x coordinate of the linking point
* @param py The y coordinate of the linking point
* @param target The target space we're linking to
*/
public Link(float px, float py, Space target) {
this.px = px;
this.py = py;
this.target = target;
}
/**
* Get the distance squared from this link to the given position
*
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The distance squared from this link to the target
*/
public float distance2(float tx, float ty) {
float dx = tx - px;
float dy = ty - py;
return ((dx*dx) + (dy*dy));
}
/**
* Get the x coordinate of the link
*
* @return The x coordinate of the link
*/
public float getX() {
return px;
}
/**
* Get the y coordinate of the link
*
* @return The y coordinate of the link
*/
public float getY() {
return py;
}
/**
* Get the space this object links to
*
* @return The space this object links to
*/
public Space getTarget() {
return target;
}
}

View File

@@ -0,0 +1,176 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
/**
* A nav-mesh is a set of shapes that describe the navigation of a map. These
* shapes are linked together allow path finding but without the high
* resolution that tile maps require. This leads to fast path finding and
* potentially much more accurate map definition.
*
* @author kevin
*
*/
public class NavMesh {
/** The list of spaces that build up this navigation mesh */
private ArrayList spaces = new ArrayList();
/**
* Create a new empty mesh
*/
public NavMesh() {
}
/**
* Create a new mesh with a set of spaces
*
* @param spaces The spaces included in the mesh
*/
public NavMesh(ArrayList spaces) {
this.spaces.addAll(spaces);
}
/**
* Get the number of spaces that are in the mesh
*
* @return The spaces in the mesh
*/
public int getSpaceCount() {
return spaces.size();
}
/**
* Get the space at a given index
*
* @param index The index of the space to retrieve
* @return The space at the given index
*/
public Space getSpace(int index) {
return (Space) spaces.get(index);
}
/**
* Add a single space to the mesh
*
* @param space The space to be added
*/
public void addSpace(Space space) {
spaces.add(space);
}
/**
* Find the space at a given location
*
* @param x The x coordinate at which to find the space
* @param y The y coordinate at which to find the space
* @return The space at the given location
*/
public Space findSpace(float x, float y) {
for (int i=0;i<spaces.size();i++) {
Space space = getSpace(i);
if (space.contains(x,y)) {
return space;
}
}
return null;
}
/**
* Find a path from the source to the target coordinates
*
* @param sx The x coordinate of the source location
* @param sy The y coordinate of the source location
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @param optimize True if paths should be optimized
* @return The path between the two spaces
*/
public NavPath findPath(float sx, float sy, float tx, float ty, boolean optimize) {
Space source = findSpace(sx,sy);
Space target = findSpace(tx,ty);
if ((source == null) || (target == null)) {
return null;
}
for (int i=0;i<spaces.size();i++) {
((Space) spaces.get(i)).clearCost();
}
target.fill(source,tx, ty, 0);
if (target.getCost() == Float.MAX_VALUE) {
return null;
}
if (source.getCost() == Float.MAX_VALUE) {
return null;
}
NavPath path = new NavPath();
path.push(new Link(sx, sy, null));
if (source.pickLowestCost(target, path)) {
path.push(new Link(tx, ty, null));
if (optimize) {
optimize(path);
}
return path;
}
return null;
}
/**
* Check if a particular path is clear
*
* @param x1 The x coordinate of the starting point
* @param y1 The y coordinate of the starting point
* @param x2 The x coordinate of the ending point
* @param y2 The y coordinate of the ending point
* @param step The size of the step between points
* @return True if there are no blockages along the path
*/
private boolean isClear(float x1, float y1, float x2, float y2, float step) {
float dx = (x2 - x1);
float dy = (y2 - y1);
float len = (float) Math.sqrt((dx*dx)+(dy*dy));
dx *= step;
dx /= len;
dy *= step;
dy /= len;
int steps = (int) (len / step);
for (int i=0;i<steps;i++) {
float x = x1 + (dx*i);
float y = y1 + (dy*i);
if (findSpace(x,y) == null) {
return false;
}
}
return true;
}
/**
* Optimize a path by removing segments that arn't required
* to reach the end point
*
* @param path The path to optimize. Redundant segments will be removed
*/
private void optimize(NavPath path) {
int pt = 0;
while (pt < path.length()-2) {
float sx = path.getX(pt);
float sy = path.getY(pt);
float nx = path.getX(pt+2);
float ny = path.getY(pt+2);
if (isClear(sx,sy,nx,ny,0.1f)) {
path.remove(pt+1);
} else {
pt++;
}
}
}
}

View File

@@ -0,0 +1,222 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.PathFindingContext;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* The builder responsible for converting a tile based map into
* a navigation mesh
*
* @author kevin
*/
public class NavMeshBuilder implements PathFindingContext {
/** The current x position we're searching */
private int sx;
/** The current y position we've searching */
private int sy;
/** The smallest space allowed */
private float smallestSpace = 0.2f;
/** True if we're working tile based */
private boolean tileBased;
/**
* Build a navigation mesh based on a tile map
*
* @param map The map to build the navigation mesh from
*
* @return The newly created navigation mesh
*/
public NavMesh build(TileBasedMap map) {
return build(map, true);
}
/**
* Build a navigation mesh based on a tile map
*
* @param map The map to build the navigation mesh from
* @param tileBased True if we'll use the tiles for the mesh initially
* rather than quad spacing
* @return The newly created navigation mesh
*/
public NavMesh build(TileBasedMap map, boolean tileBased) {
this.tileBased = tileBased;
ArrayList spaces = new ArrayList();
if (tileBased) {
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
if (!map.blocked(this, x, y)) {
spaces.add(new Space(x,y,1,1));
}
}
}
} else {
Space space = new Space(0,0,map.getWidthInTiles(),map.getHeightInTiles());
subsection(map, space, spaces);
}
while (mergeSpaces(spaces)) {}
linkSpaces(spaces);
return new NavMesh(spaces);
}
/**
* Merge the spaces that have been created to optimize out anywhere
* we can.
*
* @param spaces The list of spaces to be merged
* @return True if a merge occured and we'll have to start the merge
* process again
*/
private boolean mergeSpaces(ArrayList spaces) {
for (int source=0;source<spaces.size();source++) {
Space a = (Space) spaces.get(source);
for (int target=source+1;target<spaces.size();target++) {
Space b = (Space) spaces.get(target);
if (a.canMerge(b)) {
spaces.remove(a);
spaces.remove(b);
spaces.add(a.merge(b));
return true;
}
}
}
return false;
}
/**
* Determine the links between spaces
*
* @param spaces The spaces to link up
*/
private void linkSpaces(ArrayList spaces) {
for (int source=0;source<spaces.size();source++) {
Space a = (Space) spaces.get(source);
for (int target=source+1;target<spaces.size();target++) {
Space b = (Space) spaces.get(target);
if (a.hasJoinedEdge(b)) {
a.link(b);
b.link(a);
}
}
}
}
/**
* Check if a particular space is clear of blockages
*
* @param map The map the spaces are being built from
* @param space The space to check
* @return True if there are no blockages in the space
*/
public boolean clear(TileBasedMap map, Space space) {
if (tileBased) {
return true;
}
float x = 0;
boolean donex = false;
while (x < space.getWidth()) {
float y = 0;
boolean doney = false;
while (y < space.getHeight()) {
sx = (int) (space.getX()+x);
sy = (int) (space.getY()+y);
if (map.blocked(this, sx, sy)) {
return false;
}
y += 0.1f;
if ((y > space.getHeight()) && (!doney)) {
y = space.getHeight();
doney = true;
}
}
x += 0.1f;
if ((x > space.getWidth()) && (!donex)) {
x = space.getWidth();
donex = true;
}
}
return true;
}
/**
* Subsection a space into smaller spaces if required to find a non-blocked
* area.
*
* @param map The map being processed
* @param space The space being sections
* @param spaces The list of spaces that have been created
*/
private void subsection(TileBasedMap map, Space space, ArrayList spaces) {
if (!clear(map, space)) {
float width2 = space.getWidth()/2;
float height2 = space.getHeight()/2;
if ((width2 < smallestSpace) && (height2 < smallestSpace)) {
return;
}
subsection(map, new Space(space.getX(), space.getY(), width2, height2), spaces);
subsection(map, new Space(space.getX(), space.getY()+height2, width2, height2), spaces);
subsection(map, new Space(space.getX()+width2, space.getY(), width2, height2), spaces);
subsection(map, new Space(space.getX()+width2, space.getY()+height2, width2, height2), spaces);
} else {
spaces.add(space);
}
}
/**
* Path finding context implementation
*
* @return The current mover
*/
public Mover getMover() {
return null;
}
/**
* Path finding context implementation
*
* @return The current search distance
*/
public int getSearchDistance() {
return 0;
}
/**
* Path finding context implementation
*
* @return The current x location
*/
public int getSourceX() {
return sx;
}
/**
* Path finding context implementation
*
* @return The current y location
*/
public int getSourceY() {
return sy;
}
}

View File

@@ -0,0 +1,75 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
/**
* A path across a navigation mesh
*
* @author kevin
*/
public class NavPath {
/** The list of links that form this path */
private ArrayList links = new ArrayList();
/**
* Create a new path
*/
public NavPath() {
}
/**
* Push a link to the end of the path
*
* @param link The link to the end of the path
*/
public void push(Link link) {
links.add(link);
}
/**
* Get the length of the path
*
* @return The number of steps in the path
*/
public int length() {
return links.size();
}
/**
* Get the x coordinate of the given step
*
* @param step The index of the step to retrieve
* @return The x coordinate at the given step index
*/
public float getX(int step) {
return ((Link) links.get(step)).getX();
}
/**
* Get the y coordinate of the given step
*
* @param step The index of the step to retrieve
* @return The y coordinate at the given step index
*/
public float getY(int step) {
return ((Link) links.get(step)).getY();
}
/**
* Get a string representation of this instance
*
* @return The string representation of this instance
*/
public String toString() {
return "[Path length="+length()+"]";
}
/**
* Remove a step in the path
*
* @param i
*/
public void remove(int i) {
links.remove(i);
}
}

View File

@@ -0,0 +1,319 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
import java.util.HashMap;
/**
* A quad space within a navigation mesh
*
* @author kevin
*/
public class Space {
/** The x coordinate of the top corner of the space */
private float x;
/** The y coordinate of the top corner of the space */
private float y;
/** The width of the space */
private float width;
/** The height of the space */
private float height;
/** A map from spaces to the links that connect them to this space */
private HashMap links = new HashMap();
/** A list of the links from this space to others */
private ArrayList linksList = new ArrayList();
/** The cost to get to this node */
private float cost;
/**
* Create a new space
*
* @param x The x coordinate of the top corner of the space
* @param y The y coordinate of the top corner of the space
* @param width The width of the space
* @param height The height of the space
*/
public Space(float x, float y, float width, float height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/**
* Get the width of the space
*
* @return The width of the space
*/
public float getWidth() {
return width;
}
/**
* Get the height of the space
*
* @return The height of the space
*/
public float getHeight() {
return height;
}
/**
* Get the x coordinate of the top corner of the space
*
* @return The x coordinate of the top corner of the space
*/
public float getX() {
return x;
}
/**
* Get the y coordinate of the top corner of the space
*
* @return The y coordinate of the top corner of the space
*/
public float getY() {
return y;
}
/**
* Link this space to another by creating a link and finding the point
* at which the spaces link up
*
* @param other The other space to link to
*/
public void link(Space other) {
// aligned vertical edges
if (inTolerance(x,other.x+other.width) || inTolerance(x+width, other.x)) {
float linkx = x;
if (x+width == other.x) {
linkx = x+width;
}
float top = Math.max(y, other.y);
float bottom = Math.min(y+height, other.y+other.height);
float linky = top + ((bottom-top)/2);
Link link = new Link(linkx, linky, other);
links.put(other,link);
linksList.add(link);
}
// aligned horizontal edges
if (inTolerance(y, other.y+other.height) || inTolerance(y+height, other.y)) {
float linky = y;
if (y+height == other.y) {
linky = y+height;
}
float left = Math.max(x, other.x);
float right = Math.min(x+width, other.x+other.width);
float linkx = left + ((right-left)/2);
Link link = new Link(linkx, linky, other);
links.put(other, link);
linksList.add(link);
}
}
/**
* Check whether two locations are within tolerance distance. This is
* used when finding aligned edges to remove float rounding errors
*
* @param a The first value
* @param b The second value
* @return True if the edges are close enough (tm)
*/
private boolean inTolerance(float a, float b) {
return a == b;
}
/**
* Check if this space has an edge that is joined with another
*
* @param other The other space to check against
* @return True if the spaces have a shared edge
*/
public boolean hasJoinedEdge(Space other) {
// aligned vertical edges
if (inTolerance(x,other.x+other.width) || inTolerance(x+width,other.x)) {
if ((y >= other.y) && (y <= other.y + other.height)) {
return true;
}
if ((y+height >= other.y) && (y+height <= other.y + other.height)) {
return true;
}
if ((other.y >= y) && (other.y <= y + height)) {
return true;
}
if ((other.y+other.height >= y) && (other.y+other.height <= y + height)) {
return true;
}
}
// aligned horizontal edges
if (inTolerance(y, other.y+other.height) || inTolerance(y+height, other.y)) {
if ((x >= other.x) && (x <= other.x + other.width)) {
return true;
}
if ((x+width >= other.x) && (x+width <= other.x + other.width)) {
return true;
}
if ((other.x >= x) && (other.x <= x + width)) {
return true;
}
if ((other.x+other.width >= x) && (other.x+other.width <= x + width)) {
return true;
}
}
return false;
}
/**
* Merge this space with another
*
* @param other The other space to merge with
* @return The result space created by joining the two
*/
public Space merge(Space other) {
float minx = Math.min(x, other.x);
float miny = Math.min(y, other.y);
float newwidth = width+other.width;
float newheight = height+other.height;
if (x == other.x) {
newwidth = width;
} else {
newheight = height;
}
return new Space(minx, miny, newwidth, newheight);
}
/**
* Check if the given space can be merged with this one. It must have
* an adjacent edge and have the same height or width as this space.
*
* @param other The other space to be considered
* @return True if the spaces can be joined together
*/
public boolean canMerge(Space other) {
if (!hasJoinedEdge(other)) {
return false;
}
if ((x == other.x) && (width == other.width)) {
return true;
}
if ((y == other.y) && (height == other.height)) {
return true;
}
return false;
}
/**
* Get the number of links
*
* @return The number of links from the space to others
*/
public int getLinkCount() {
return linksList.size();
}
/**
* Get the link from this space to another at a particular index
*
* @param index The index of the link to retrieve
* @return The link from this space to another
*/
public Link getLink(int index) {
return (Link) linksList.get(index);
}
/**
* Check if this space contains a given point
*
* @param xp The x coordinate to check
* @param yp The y coordinate to check
* @return True if this space container the coordinate given
*/
public boolean contains(float xp, float yp) {
return (xp >= x) && (xp < x+width) && (yp >= y) && (yp < y+height);
}
/**
* Fill the spaces based on the cost from a given starting point
*
* @param target The target space we're heading for
* @param sx The x coordinate of the starting point
* @param sy The y coordinate of the starting point
* @param cost The cost up to this point
*/
public void fill(Space target, float sx, float sy, float cost) {
if (cost >= this.cost) {
return;
}
this.cost = cost;
if (target == this) {
return;
}
for (int i=0;i<getLinkCount();i++) {
Link link = getLink(i);
float extraCost = link.distance2(sx,sy);
float nextCost = cost + extraCost;
link.getTarget().fill(target, link.getX(), link.getY(), nextCost);
}
}
/**
* Clear the costing values across the whole map
*/
public void clearCost() {
cost = Float.MAX_VALUE;
}
/**
* Get the cost to get to this node at the moment
*
* @return The cost to get to this node
*/
public float getCost() {
return cost;
}
/**
* Pick the lowest cost route from this space to another on the path
*
* @param target The target space we're looking for
* @param path The path to add the steps to
* @return True if the path was found
*/
public boolean pickLowestCost(Space target, NavPath path) {
if (target == this) {
return true;
}
if (links.size() == 0) {
return false;
}
Link bestLink = null;
for (int i=0;i<getLinkCount();i++) {
Link link = getLink(i);
if ((bestLink == null) || (link.getTarget().getCost() < bestLink.getTarget().getCost())) {
bestLink = link;
}
}
path.push(bestLink);
return bestLink.getTarget().pickLowestCost(target, path);
}
/**
* Get the string representation of this instance
*
* @return The string representation of this instance
*/
public String toString() {
return "[Space "+x+","+y+" "+width+","+height+"]";
}
}

View File

@@ -0,0 +1,3 @@
<BODY>
A set of classes to provide configurable A* path finding on tilebased maps
</BODY>