Dauntless
%Europe/Berlin %633 %2005, 16:12
Haai!
Sommigen zullen dit topic nog wel kennen van het vorige forum. Wel, nu met GreenVille had ik weer een A* nodig, maar een leuke snelle dus heb ik hem terug vanaf 0 herschreven :D .Voor degene die het niet kennen: A* is een pathfinding algoritme: het zoekt de korste weg van A naar B en liefst zo snel en kort mogelijk. Omdat A* flexibel is kan je ook verschillende terrein costen per tegel zetten zodat hij bv eerder via een weg rond een moeras gaat, dan erdoor.
De Astar op zich bestaat uit 3 classes: Tile, Astar en BinaryHeap.
Ik heb hem gebrouwd zodat hij echt dynamisch is en zodat anderen (jullie) er ook gebruik van kunnen maken. Ik heb alles ook zo goed mogelijk gedocumenteerd en de documentatie staat dus ook online.
Classes
Tile class:
import be.dauntless.Astar.*;
class be.dauntless.Astar.Tile
{
private var inOpen:Boolean = false;
private var closed:Boolean = false;
private var parent:Tile;
private static var ids:Number = 0;
private var gUp:Boolean = false;
private var h:Number;
private var g:Number;
private var cost:Number = Astar.standardCost;
public var x:Number;
public var y:Number;
public var id:Number;
public var walkable:Boolean = true;
/*
* Function: Tile
* Constructor. Set the x, y and cost of the tile;
*
* Parameters:
*
* x - x position of the tile;
* y - y position of the tile;
* cost - cost of the tile;
*
* Returns:
*
* Nothing
*/
public function Tile(xp:Number, yp:Number, cost:Number){
x = xp;
y = yp;
id = ids++;
this.cost = cost;
}
/*
* Function: setWalkable
* Set the ability to walk on this the tile
*
* Parameters:
*
* walkable - Boolean value whether or not the tile is walkable
*
* Returns:
*
* Nothing
*/
public function setWalkable(b:Boolean):Void
{
walkable = b;
}
/*
* Function: setOpen
* Set this tile to be in the open-array. This is used by the findPath method of the Astar class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*
* See also:
*
* <setClosed>
*
* <setWalkable>
*/
public function setOpen():Void
{
inOpen = true;
closed = false;
}
/*
* Function: setClosed
* Set this tile to be in the closed-array. This is used by the findPath method of the Astar class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*
* See also:
*
* <setOpen>
*
* <setWalkable>
*/
public function setClosed():Void
{
inOpen = false;
closed = true;
}
/*
* Function: isClosed
* returns whether or not the tile is in the closed list.
*
* Parameters:
*
* None
*
* Returns:
*
* closed - Boolean representing whether or not its closed
*/
public function isClosed():Boolean
{
return closed;
}
/*
* Function: getOpen
* returns whether or not the tile is in the open list.
*
* Parameters:
*
* None
*
* Returns:
*
* open - Boolean representing whether or not its open
*/
public function getOpen():Boolean
{
return inOpen;
}
/*
* Function: setParent
* sets the parent of the node
*
* Parameters:
*
* Tile - Parent of the node
*
* Returns:
*
* Nothing
*/
public function setParent(tile:Tile):Void
{
parent = tile;
}
/*
* Function: getParent
* Returns the parent of the node
*
* Parameters:
*
* None
*
* Returns:
*
* The parent of the node
*/
public function getParent():Tile
{
return parent;
}
/*
* Function: setH
* sets the H for the node. H is calculated with the manhatten method.
*
* Parameters:
*
* bx - number representing the x position of the tile
* by - number representing the y position of the tile
* ex - number representing the x position of the endPoint
* ey - number representing the y position of the endPoint
*
* Returns:
*
* Nothing
*/
public function setH(bx:Number, by:Number, ex:Number, ey:Number):Void
{
h = Math.abs(ex - bx) * cost + Math.abs(ey - by) * cost;
}
/*
* Function: setG
* Sets the G (cost) for the tile
*
* Parameters:
*
* g - the cost of the parent tile
*
* Returns:
*
* Nothing
*/
public function setG(_g:Number):Void
{
g = _g + getCost();
}
/*
* Function: getF
* Returns the F of the tile (F = G + H);
*
* Parameters:
*
* None
*
* Returns:
*
* F - The F of the tile
*/
public function getF():Number
{
return this.getG() + h;
}
/*
* Function: getH
* Returns the H of the tile
*
* Parameters:
*
* None
*
* Returns:
*
* H - The H of the tile
*/
public function getH():Number
{
return h;
}
/*
* Function: setCost
* Set the cost for the tile
*
* Parameters:
*
* cost - cost of the tile
*
* Returns:
*
* Nothing
*/
public function setCost(_cost:Number):Void
{
cost = _cost;
}
/*
* Function: getG
* get the G (cost) of the tile
*
* Parameters:
*
* None
*
* Returns:
*
* g - the total cost of the tile
*/
public function getG():Number
{
return g;
}
/*
* Function: getCost
* Returns the cost for the tile
*
* Parameters:
*
* None
*
* Returns:
*
* cost - the cost of the tile
*/
public function getCost():Number
{
var addCost:Number = (gUp)?1.414:1;
return cost * addCost;
}
/*
* Function: toggleWalkable
* Toggles the ability to walk on the tile
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function toggleWalkable():Void
{
this.walkable = !this.walkable;
}
/*
* Function: reset
* This method resets the tile for further use
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function reset():Void
{
this.inOpen = false;
this.closed = false;
this.closed = false;
this.parent = null;
this.h = null;
this.g = null;
this.gUp = false;
}
/*
* Function: costUp
* This method tells the node that its being accessed diagonally
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function costUp():Void
{
this.gUp = true;
}
/*
* Function: costDown
* This method tells the node that its NOT being accessed diagonally
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function costDown():Void
{
this.gUp = false;
}
/*
* Function: notice
* This method is called by A*. It tells the tile that it's in the found path.
* This can be used when you extend the tile class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function notice():Void
{
//does nothing.
}
}
Astar class:
import be.dauntless.Astar.Tile;
import be.dauntless.Astar.BinaryHeap;
class be.dauntless.Astar.Astar
{
private var startPoint:Array;
private var endPoint:Array;
private var map:Array;
public static var standardCost:Number = 1;
private var mapH:Number;
private var mapW:Number;
private var clipping:Boolean = false;
private var heap:BinaryHeap;
/*
* Function: Astar (constructor)
* Constructor, sets 'clipping'
*
* Parameters:
*
* clipping - Boolean indicating whether or not clipping is aloud.
*
* Returns:
*
* Nothing
*/
public function Astar(c:Boolean)
{
clipping = c;
}
/*
* Function: setMap
* Set the map for the engine
*
* Parameters:
*
* _map - A 2d array of references to tiles, presenting the current map
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setStartPoint>
*
* <setEndPoint>
*/
public function setMap(_map:Array):Void
{
this.map = new Array();
this.map = _map;
this.mapW = this.map[0].length;
this.mapH = this.map.length;
}
/*
* Function: newMap
* Build a new Map
*
* Parameters:
*
* x - width of the map
* y - height of the map
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setMap>
*/
public function newMap(w:Number, h:Number):Void
{
w = Math.floor(w);
h = Math.floor(h);
this.map = new Array();
for (var y = 0; y<h; y++)
{
map[y] = new Array();
for (var x = 0; x<w; x++)
{
var tile:Tile = new Tile(x, y, Astar.standardCost);
map[y][x] = tile;
}
}
this.mapW = this.map[0].length;
this.mapH = this.map.length;
}
/*
* Function: setStartPoint
* Set the stating point (player position) for the engine
*
* Parameters:
*
* x - the x position of the player
* y - the y position of the player
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setEndPoint>
*
* <setMap>
*/
public function setStartPoint(x:Number, y:Number):Void
{
this.startPoint = new Array();
this.startPoint[0] = x;
this.startPoint[1] = y;
}
/*
* Function: setEndPoint
* Set the map for the engine
*
* Parameters:
*
* x - the x position of the player
* y - the y position of the players
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setStartPoint>
*
* <setMap>
*/
public function setEndPoint(x:Number, y:Number):Void
{
this.endPoint = new Array();
this.endPoint[0] = x;
this.endPoint[1] = y;
}
/*
* Function: findPath
* find a path from the startPoint to the endPoint in the given map
*
* Parameters:
*
* None
*
* Returns:
*
* array/Boolean - array containing references of tiles presenting the path from startpoint to endpoint OR false if no path was found.
*
*
*
* See Also:
*
* <setStartPoint>
*
* <setEndPoint>
*
* <setMap>
*/
public function findPath():Object
{
//if you reuse the same tiles, the tiles will be closed. So we need to open all the tiles:
openMap();
if(!ready()) return false;
//the binary heap
heap = new BinaryHeap();
//there is no path found yet
var pathFound:Boolean = false;
//start off with the first tile (startPoint).
//set it to open, calculate its G (0, 'cause it IS the startpoint), and H.
//and add that tile to the open array
var beginTile:Tile = map[startPoint[1]][startPoint[0]];
beginTile.setOpen();
beginTile.setG(0);
beginTile.setH(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
beginTile.setParent(beginTile);
heap.addToHeap(beginTile);
//as long as there is a tile that can be visited and no path is found...
//(if open is empty, there is no solution)
while(heap.getLength() > 0 && !pathFound)
{
// the current-array always represents the current tile
var current:Tile = getLowestFTile();
//check if the path is found (current tile = end tile)
if(current.x == endPoint[0] && current.y == endPoint[1])
{
trace("Path was found");
pathFound = true;
break;
}
// remove the current object from the open-array
current.setClosed();
//find all the surrounding nodes
var neighbours:Array = findSurroundingTiles(current);
for(var i in neighbours)
{
//if this neighbour is not in the open-array
if(!neighbours[i].getOpen())
{
//add this neighbour to the open-array
//and calculate all the needed settings.
neighbours[i].setOpen();
neighbours[i].setParent(current);
neighbours[i].setH(neighbours[i].x, neighbours[i].y, endPoint[0], endPoint[1]);
neighbours[i].setG(current.getG());
heap.addToHeap(neighbours[i]);
} else {
//calculate new F. If the new F is smaller, that means the way to this node trough
//the currentnode is shorter. So update that node to make current node its parent
//and calculate new G.
var tempF:Number = neighbours[i].getH() + current.getG() + neighbours[i].getCost();
if(tempF < neighbours[i].getF())
{
var id = neighbours[i].id;
neighbours[i].setParent(current);
neighbours[i].setG(current.getG());
heap.updateList(heap.getPosition(id));
}
}
}
}
//if the open array is empty, no path was found
if(heap.getLength() == 0 && !pathFound){
trace("There is no path!");
return false;
}
//the path is calculated, so now we have to start at the end and work our way back:
var path:Array = new Array();
var px:Number = endPoint[0]; //pathX;
var py:Number = endPoint[1]; //pathY;
map[px][py].notice();
path.push(map[py][px]);
var parent:Tile = map[py][px].getParent();
px = parent.x; //pathX;
py = parent.y;
//as long as one of the two cordinates still isn't the same as the start point, continue
while (px != startPoint[0] || py != startPoint[1])
{
path.push(map[py][px]);
//notice the tile
map[py][px].notice();
//request next parent
parent = map[py][px].getParent();
//map[py][px] returns the tile; getParent() returns its parent and x the x property of the tiles parent
px = parent.x;
py = parent.y
}
//and put the startPoint in there too
path.push(map[startPoint[1]][startPoint[0]]);
//notice start tile
map[startPoint[1]][startPoint[0]].notice();
//we started at the endpoint, so now we have to reverse the array
path.reverse();
//and now we're all done so return the path!!!
return path;
}
/*
* Function: getLowestFTile
* find the tile in the open-array with the lowest F (best node to look at next).
*
* Parameters:
*
* None
*
* Returns:
*
* Tile - Reference to the tile with the lowest F
*
*/
private function getLowestFTile():Tile
{
return heap.getLowest();
}
/*
* Function: findSurroundingTiles
* find the surrounding tiles of the given tile. If clipping is set to true,
* 8 tiles will be scanned. If not: 4 tiles.
*
*
* Parameters:
*
* x - the x value of the tile
*
* Returns:
*
* array - array containing references to all the valid neighbour tiles
*/
private function findSurroundingTiles(current:Tile):Array
{
var st:Array = new Array(); //st = surroundingTiles
var x:Number = current.x;
var y:Number = current.y;
//check left, right, down and up. Check: walkable & not closed
if(map[y][x-1].walkable && !map[y][x-1].isClosed() )
{
st.push(map[y][x-1]);
map[y][x-1].costDown();
}
if(map[y][x+1].walkable && !map[y][x+1].isClosed() )
{
st.push(map[y][x+1]);
map[y][x+1].costDown();
}
if(map[y+1][x].walkable && !map[y+1][x].isClosed() )
{
st.push(map[y+1][x]);
map[y+1][x].costDown();
}
if(map[y-1][x].walkable && !map[y-1][x].isClosed() )
{
st.push(map[y-1][x]);
map[y-1][x].costDown();
}
if(clipping){
//check left-up, left-down, right-up and right-down. Check: walkable & not closed
if(map[y-1][x-1].walkable && !map[y-1][x-1].isClosed() )
{
st.push(map[y-1][x-1]);
map[y-1][x-1].costUp();
}
if(map[y-1][x+1].walkable && !map[y-1][x+1].isClosed() )
{
st.push(map[y-1][x+1]);
map[y-1][x+1].costUp();
}
if(map[y+1][x+1].walkable && !map[y+1][x+1].isClosed() )
{
st.push(map[y+1][x+1]);
map[y+1][x+1].costUp();
}
if(map[y+1][x-1].walkable && !map[y+1][x-1].isClosed() )
{
st.push(map[y+1][x-1]);
map[y+1][x-1].costUp();
}
}
return st;
}
/*
* Function: ready
* Check whether or not all the settings are correct
*
* Parameters:
*
* None
*
* Returns:
*
* Boolean - True: Game is ready, false: There was an error
*
*/
private function ready():Boolean
{
if(map == undefined){
trace("No map specified");
return false;
}
if(startPoint[0] != Math.floor(startPoint[0]) || startPoint[1] != Math.floor(startPoint[1]))
{
trace("Startpoint isn't valid");
return false;
}
if(endPoint[0] != Math.floor(endPoint[0]) || endPoint[1] != Math.floor(endPoint[1]))
{
trace("Endpoint isn't valid");
return false;
}
if(startPoint[0] < 0 || startPoint[0] >= mapW)
{
trace("=>"+mapW);
trace("Startpoint isn't valid");
return false;
} else if(startPoint[1] < 0 || startPoint[1] >= mapH){
trace("Startpoint isn't valid");
return false;
}
if(endPoint[0] < 0 || endPoint[0] >= mapW)
{
trace("EndPoint isn't valid");
return false;
} else if(endPoint[1] < 0 || endPoint[1] >= mapH){
trace("EndPoint isn't valid");
return false;
}
if(!map[endPoint[1]][endPoint[0]].walkable){
trace("Endpoint isn't walkable.");
return false;
}
if(!map[startPoint[1]][startPoint[0]].walkable){
trace("Startpoint isn't walkable.");
return false;
}
return true;
}
/*
* Function: setWalkable
* Toggle the tile to be walkable or not
*
* Parameters:
*
* x - x position of the tile
* y - y position of the tile
* walkable - boolean representing the ability to walk on the tile or not
*
* Returns:
*
* Nothing
*
* See also:
*
* <toggleWalkable>
*/
public function setWalkable(x:Number, y:Number, b:Boolean):Void
{
map[y][x].setWalkable(b);
}
/*
* Function: toggleWalkable
* Toggle the tile to be walkable or not
*
* Parameters:
*
* x - x position of the tile
* y - y position of the tile
*
* Returns:
*
* Nothing
*
* See also:
*
* <setWalkable>
*/
public function toggleWalkable(x:Number, y:Number):Void
{
map[y][x].toggleWalkable();
}
/*
* Function: setClipping
* Toggle clipping. Clipping is the ability to move diagonally.
*
* Parameters:
*
* clipping - Boolean representing the ability to use clipping
*
* Returns:
*
* Nothing
*/
public function setClipping(c):Void
{
clipping = c;
}
/*
* Function: openMap
* Opens the map (resets all the tiles)
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
private function openMap(){
for(var y = 0; y<mapH; y++){
for(var x = 0; x<mapW; x++){
map[y][x].reset();
}
}
}
}
BinaryHeap class
import be.dauntless.Astar.Tile;
class be.dauntless.Astar.BinaryHeap
{
private var heap:Array;
/*
* Function: BinaryHeap
* Constructor; makes a new heap array
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function BinaryHeap()
{
heap = new Array(null);
}
/*
* Function: addToHeap
* Adds a tile to the binary heap
*
* Parameters:
*
* Tile - The tile to be added
*
* Returns:
*
* Nothing
*/
public function addToHeap(newTile:Tile):Void
{
heap.push(newTile);
var cp:Number = heap.length-1; //cp = current position
var np:Number = Math.floor(cp/2);
//as long as the item isnt in the first position
while(cp != 1){
if(heap[cp].getF() <= heap[Math.floor(cp/2)].getF()){
//if the current F is lower than or equal to its parent, switch them!
var t:Number = heap[Math.floor(cp/2)];
heap[Math.floor(cp/2)] = heap[cp];
heap[cp] = t;
cp = Math.floor(cp/2);
} else {
//destination reached!
break;
}
}
}
/*
* Function: getLowest
* This method returns the lowest F from the heap. That F is deleted from the heap
*
* Parameters:
*
* None
*
* Returns:
*
* Tile - Tile with the lowest F
*/
public function getLowest():Tile
{
var cp:Number = 1; //current position
var np:Number; //next position
var lowest:Tile = heap[1];
if(heap.length == 2){
heap.pop();
} else {
heap[1] = heap.pop();
}
while(true){
np = cp;
if(cp*2+1 <= (heap.length -1) ){
if(heap[np].getF() >= heap[np*2].getF() ) cp = 2*np;
if(heap[cp].getF() >= heap[np*2+1].getF() ) cp = 2*np+1;
} else if(2*np <= (heap.length -1)){
if(heap[np].getF() >= heap[2*np].getF() ) cp = 2*np;
}
//check if np has changed, 'cause then we have to switch them
if(np != cp){
//switch heap[cp] & heap[np]
var t:Tile = heap[np];
heap[np] = heap[cp];
heap[cp] = t;
} else {
//position found, return!
return lowest;
}
}
}
/*
* Function: getLength
* This method returns the length of the heap
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function getLength():Number
{
return (heap.length -1);
}
/*
* Function: getPosition
* This method returns the position of the given ID in the heap
*
* Parameters:
*
* id - id of the node's position to find
*
* Returns:
*
* Number - position of id in heap
*/
public function getPosition(id):Number
{
for(var i in heap){
if(heap[i].id == id){
return i;
}
}
}
/*
* Function: updateList
* This method changes the given tile's f in the heap and resorts it.
*
* Parameters:
*
* Tile - the tile to be changed
*
* Returns:
*
* Nothing
*/
public function updateList(cp_):Void
{
var cp:Number = cp_;
var np:Number = Math.floor(cp/2);
//as long as the item isnt in the first position
while(cp != 1){
if(heap[cp].getF() <= heap[Math.floor(cp/2)].getF()){
//if the current F is lower than or equal to its parent, switch them!
var t:Number = heap[Math.floor(cp/2)];
heap[Math.floor(cp/2)] = heap[cp];
heap[cp] = t;
cp = Math.floor(cp/2);
} else {
//destination reached!
break;
}
}
}
}
Gebruik
Aangezien dit best wel wat AS is, is het misschien handiger om deze zip te downloaden:
ZIP (http://www.dauntless.be/Projects/Astar/Astar.zip)
In deze zip zit ook de documentatie.
Omdat ik het dus dynamisch wou maken is het puur code en is er niets grafisch aan. Daarom is het het handigst als je de Tile class extend. Op die manier kan je je eigen grafische elementen toevoegen. Een lijst van alle properties en methods staat in de documentatie, dus zorg ervoor dat je die niet overschrijft.
Een voorbeeld om de Tile class te extenden:
class GameTile extends be.dauntless.Astar.Tile
{
private var root:MovieClip;
private var mc:MovieClip;
private static var depth:Number = 0;
public function GameTile(x:Number, y:Number, cost:Number, root:MovieClip)
{
super(x, y, cost);
this.root = root;
mc = root.attachMovie("mc_test", "mc"+x+"_"+y, depth++);
mc._x = x * mc._width;
mc._y = y * mc._height;
mc.costt.text = cost;
}
public function notice():Void
{
mc.nextFrame();
}
}
Als je wil dat er iets met de tile gebeurt moet je de 'notice method' overschrijven. Deze wordt immers aangeroepen bij het bouwen van het pad en in de Tile Class is ze toch gewoon leeg (hier DIENT ze dus voor!). Als je de Tile class extend moet je wel altijd in de constructor super() oproepen met de x, y en cost van de tegel.
Voor zij die het niet weten: Deze class is opgebouwd met packages. Het handigste is om ergens op je computer een dir aan te maken waar je alle classes in wil gaan zetten. Daar zet je dan de 'be' map. In flash kan je bij de instellingen de classpaths aanpassen (en daar voeg je de class dir dan aan toe).
In principe kan je het gebruik helemaal uit de helpfiles nemen, maar 'k zal hier nog maar snel wat AS zetten:
import be.dauntless.Astar.*;
var a:Astar = new Astar(false);
a.newMap(10,10);
a.setStartPoint(0, 0);
a.setEndPoint(5, 8);
var path = a.findPath();
if(!path){
//er werd geen path gevonden
} else {
//hier het path
}
Het path wordt terug gegeven in de vorm van tegels. Om de x en y te krijgen doe je dus: path[i].x of path[i].y
Ik heb m'n best gedaan om hem zo snel mogelijk te krijgen (bv door die binaryHeap), maar als jullie nog dingen weten, zeg het maar. Ohja, daardoor heb ik nu ook geen closed list meer, maar is het gewoon opgeslagen in elke node. Door dit is het dus eigenlijk geen A* meer... Hmm, dan maar m'n eigen naam: D* ! :D
Voor zij die tot hier gelezen hebben: DANKU! :p Voor de anderen: grrrr :mad: (lezen ze dan toch niet :p).
Euhm,
Ik hoop op zoveel mogelijk reacties!
Greets,
Me!
Sommigen zullen dit topic nog wel kennen van het vorige forum. Wel, nu met GreenVille had ik weer een A* nodig, maar een leuke snelle dus heb ik hem terug vanaf 0 herschreven :D .Voor degene die het niet kennen: A* is een pathfinding algoritme: het zoekt de korste weg van A naar B en liefst zo snel en kort mogelijk. Omdat A* flexibel is kan je ook verschillende terrein costen per tegel zetten zodat hij bv eerder via een weg rond een moeras gaat, dan erdoor.
De Astar op zich bestaat uit 3 classes: Tile, Astar en BinaryHeap.
Ik heb hem gebrouwd zodat hij echt dynamisch is en zodat anderen (jullie) er ook gebruik van kunnen maken. Ik heb alles ook zo goed mogelijk gedocumenteerd en de documentatie staat dus ook online.
Classes
Tile class:
import be.dauntless.Astar.*;
class be.dauntless.Astar.Tile
{
private var inOpen:Boolean = false;
private var closed:Boolean = false;
private var parent:Tile;
private static var ids:Number = 0;
private var gUp:Boolean = false;
private var h:Number;
private var g:Number;
private var cost:Number = Astar.standardCost;
public var x:Number;
public var y:Number;
public var id:Number;
public var walkable:Boolean = true;
/*
* Function: Tile
* Constructor. Set the x, y and cost of the tile;
*
* Parameters:
*
* x - x position of the tile;
* y - y position of the tile;
* cost - cost of the tile;
*
* Returns:
*
* Nothing
*/
public function Tile(xp:Number, yp:Number, cost:Number){
x = xp;
y = yp;
id = ids++;
this.cost = cost;
}
/*
* Function: setWalkable
* Set the ability to walk on this the tile
*
* Parameters:
*
* walkable - Boolean value whether or not the tile is walkable
*
* Returns:
*
* Nothing
*/
public function setWalkable(b:Boolean):Void
{
walkable = b;
}
/*
* Function: setOpen
* Set this tile to be in the open-array. This is used by the findPath method of the Astar class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*
* See also:
*
* <setClosed>
*
* <setWalkable>
*/
public function setOpen():Void
{
inOpen = true;
closed = false;
}
/*
* Function: setClosed
* Set this tile to be in the closed-array. This is used by the findPath method of the Astar class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*
* See also:
*
* <setOpen>
*
* <setWalkable>
*/
public function setClosed():Void
{
inOpen = false;
closed = true;
}
/*
* Function: isClosed
* returns whether or not the tile is in the closed list.
*
* Parameters:
*
* None
*
* Returns:
*
* closed - Boolean representing whether or not its closed
*/
public function isClosed():Boolean
{
return closed;
}
/*
* Function: getOpen
* returns whether or not the tile is in the open list.
*
* Parameters:
*
* None
*
* Returns:
*
* open - Boolean representing whether or not its open
*/
public function getOpen():Boolean
{
return inOpen;
}
/*
* Function: setParent
* sets the parent of the node
*
* Parameters:
*
* Tile - Parent of the node
*
* Returns:
*
* Nothing
*/
public function setParent(tile:Tile):Void
{
parent = tile;
}
/*
* Function: getParent
* Returns the parent of the node
*
* Parameters:
*
* None
*
* Returns:
*
* The parent of the node
*/
public function getParent():Tile
{
return parent;
}
/*
* Function: setH
* sets the H for the node. H is calculated with the manhatten method.
*
* Parameters:
*
* bx - number representing the x position of the tile
* by - number representing the y position of the tile
* ex - number representing the x position of the endPoint
* ey - number representing the y position of the endPoint
*
* Returns:
*
* Nothing
*/
public function setH(bx:Number, by:Number, ex:Number, ey:Number):Void
{
h = Math.abs(ex - bx) * cost + Math.abs(ey - by) * cost;
}
/*
* Function: setG
* Sets the G (cost) for the tile
*
* Parameters:
*
* g - the cost of the parent tile
*
* Returns:
*
* Nothing
*/
public function setG(_g:Number):Void
{
g = _g + getCost();
}
/*
* Function: getF
* Returns the F of the tile (F = G + H);
*
* Parameters:
*
* None
*
* Returns:
*
* F - The F of the tile
*/
public function getF():Number
{
return this.getG() + h;
}
/*
* Function: getH
* Returns the H of the tile
*
* Parameters:
*
* None
*
* Returns:
*
* H - The H of the tile
*/
public function getH():Number
{
return h;
}
/*
* Function: setCost
* Set the cost for the tile
*
* Parameters:
*
* cost - cost of the tile
*
* Returns:
*
* Nothing
*/
public function setCost(_cost:Number):Void
{
cost = _cost;
}
/*
* Function: getG
* get the G (cost) of the tile
*
* Parameters:
*
* None
*
* Returns:
*
* g - the total cost of the tile
*/
public function getG():Number
{
return g;
}
/*
* Function: getCost
* Returns the cost for the tile
*
* Parameters:
*
* None
*
* Returns:
*
* cost - the cost of the tile
*/
public function getCost():Number
{
var addCost:Number = (gUp)?1.414:1;
return cost * addCost;
}
/*
* Function: toggleWalkable
* Toggles the ability to walk on the tile
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function toggleWalkable():Void
{
this.walkable = !this.walkable;
}
/*
* Function: reset
* This method resets the tile for further use
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function reset():Void
{
this.inOpen = false;
this.closed = false;
this.closed = false;
this.parent = null;
this.h = null;
this.g = null;
this.gUp = false;
}
/*
* Function: costUp
* This method tells the node that its being accessed diagonally
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function costUp():Void
{
this.gUp = true;
}
/*
* Function: costDown
* This method tells the node that its NOT being accessed diagonally
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function costDown():Void
{
this.gUp = false;
}
/*
* Function: notice
* This method is called by A*. It tells the tile that it's in the found path.
* This can be used when you extend the tile class.
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function notice():Void
{
//does nothing.
}
}
Astar class:
import be.dauntless.Astar.Tile;
import be.dauntless.Astar.BinaryHeap;
class be.dauntless.Astar.Astar
{
private var startPoint:Array;
private var endPoint:Array;
private var map:Array;
public static var standardCost:Number = 1;
private var mapH:Number;
private var mapW:Number;
private var clipping:Boolean = false;
private var heap:BinaryHeap;
/*
* Function: Astar (constructor)
* Constructor, sets 'clipping'
*
* Parameters:
*
* clipping - Boolean indicating whether or not clipping is aloud.
*
* Returns:
*
* Nothing
*/
public function Astar(c:Boolean)
{
clipping = c;
}
/*
* Function: setMap
* Set the map for the engine
*
* Parameters:
*
* _map - A 2d array of references to tiles, presenting the current map
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setStartPoint>
*
* <setEndPoint>
*/
public function setMap(_map:Array):Void
{
this.map = new Array();
this.map = _map;
this.mapW = this.map[0].length;
this.mapH = this.map.length;
}
/*
* Function: newMap
* Build a new Map
*
* Parameters:
*
* x - width of the map
* y - height of the map
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setMap>
*/
public function newMap(w:Number, h:Number):Void
{
w = Math.floor(w);
h = Math.floor(h);
this.map = new Array();
for (var y = 0; y<h; y++)
{
map[y] = new Array();
for (var x = 0; x<w; x++)
{
var tile:Tile = new Tile(x, y, Astar.standardCost);
map[y][x] = tile;
}
}
this.mapW = this.map[0].length;
this.mapH = this.map.length;
}
/*
* Function: setStartPoint
* Set the stating point (player position) for the engine
*
* Parameters:
*
* x - the x position of the player
* y - the y position of the player
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setEndPoint>
*
* <setMap>
*/
public function setStartPoint(x:Number, y:Number):Void
{
this.startPoint = new Array();
this.startPoint[0] = x;
this.startPoint[1] = y;
}
/*
* Function: setEndPoint
* Set the map for the engine
*
* Parameters:
*
* x - the x position of the player
* y - the y position of the players
*
* Returns:
*
* Nothing
*
* See Also:
*
* <setStartPoint>
*
* <setMap>
*/
public function setEndPoint(x:Number, y:Number):Void
{
this.endPoint = new Array();
this.endPoint[0] = x;
this.endPoint[1] = y;
}
/*
* Function: findPath
* find a path from the startPoint to the endPoint in the given map
*
* Parameters:
*
* None
*
* Returns:
*
* array/Boolean - array containing references of tiles presenting the path from startpoint to endpoint OR false if no path was found.
*
*
*
* See Also:
*
* <setStartPoint>
*
* <setEndPoint>
*
* <setMap>
*/
public function findPath():Object
{
//if you reuse the same tiles, the tiles will be closed. So we need to open all the tiles:
openMap();
if(!ready()) return false;
//the binary heap
heap = new BinaryHeap();
//there is no path found yet
var pathFound:Boolean = false;
//start off with the first tile (startPoint).
//set it to open, calculate its G (0, 'cause it IS the startpoint), and H.
//and add that tile to the open array
var beginTile:Tile = map[startPoint[1]][startPoint[0]];
beginTile.setOpen();
beginTile.setG(0);
beginTile.setH(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
beginTile.setParent(beginTile);
heap.addToHeap(beginTile);
//as long as there is a tile that can be visited and no path is found...
//(if open is empty, there is no solution)
while(heap.getLength() > 0 && !pathFound)
{
// the current-array always represents the current tile
var current:Tile = getLowestFTile();
//check if the path is found (current tile = end tile)
if(current.x == endPoint[0] && current.y == endPoint[1])
{
trace("Path was found");
pathFound = true;
break;
}
// remove the current object from the open-array
current.setClosed();
//find all the surrounding nodes
var neighbours:Array = findSurroundingTiles(current);
for(var i in neighbours)
{
//if this neighbour is not in the open-array
if(!neighbours[i].getOpen())
{
//add this neighbour to the open-array
//and calculate all the needed settings.
neighbours[i].setOpen();
neighbours[i].setParent(current);
neighbours[i].setH(neighbours[i].x, neighbours[i].y, endPoint[0], endPoint[1]);
neighbours[i].setG(current.getG());
heap.addToHeap(neighbours[i]);
} else {
//calculate new F. If the new F is smaller, that means the way to this node trough
//the currentnode is shorter. So update that node to make current node its parent
//and calculate new G.
var tempF:Number = neighbours[i].getH() + current.getG() + neighbours[i].getCost();
if(tempF < neighbours[i].getF())
{
var id = neighbours[i].id;
neighbours[i].setParent(current);
neighbours[i].setG(current.getG());
heap.updateList(heap.getPosition(id));
}
}
}
}
//if the open array is empty, no path was found
if(heap.getLength() == 0 && !pathFound){
trace("There is no path!");
return false;
}
//the path is calculated, so now we have to start at the end and work our way back:
var path:Array = new Array();
var px:Number = endPoint[0]; //pathX;
var py:Number = endPoint[1]; //pathY;
map[px][py].notice();
path.push(map[py][px]);
var parent:Tile = map[py][px].getParent();
px = parent.x; //pathX;
py = parent.y;
//as long as one of the two cordinates still isn't the same as the start point, continue
while (px != startPoint[0] || py != startPoint[1])
{
path.push(map[py][px]);
//notice the tile
map[py][px].notice();
//request next parent
parent = map[py][px].getParent();
//map[py][px] returns the tile; getParent() returns its parent and x the x property of the tiles parent
px = parent.x;
py = parent.y
}
//and put the startPoint in there too
path.push(map[startPoint[1]][startPoint[0]]);
//notice start tile
map[startPoint[1]][startPoint[0]].notice();
//we started at the endpoint, so now we have to reverse the array
path.reverse();
//and now we're all done so return the path!!!
return path;
}
/*
* Function: getLowestFTile
* find the tile in the open-array with the lowest F (best node to look at next).
*
* Parameters:
*
* None
*
* Returns:
*
* Tile - Reference to the tile with the lowest F
*
*/
private function getLowestFTile():Tile
{
return heap.getLowest();
}
/*
* Function: findSurroundingTiles
* find the surrounding tiles of the given tile. If clipping is set to true,
* 8 tiles will be scanned. If not: 4 tiles.
*
*
* Parameters:
*
* x - the x value of the tile
*
* Returns:
*
* array - array containing references to all the valid neighbour tiles
*/
private function findSurroundingTiles(current:Tile):Array
{
var st:Array = new Array(); //st = surroundingTiles
var x:Number = current.x;
var y:Number = current.y;
//check left, right, down and up. Check: walkable & not closed
if(map[y][x-1].walkable && !map[y][x-1].isClosed() )
{
st.push(map[y][x-1]);
map[y][x-1].costDown();
}
if(map[y][x+1].walkable && !map[y][x+1].isClosed() )
{
st.push(map[y][x+1]);
map[y][x+1].costDown();
}
if(map[y+1][x].walkable && !map[y+1][x].isClosed() )
{
st.push(map[y+1][x]);
map[y+1][x].costDown();
}
if(map[y-1][x].walkable && !map[y-1][x].isClosed() )
{
st.push(map[y-1][x]);
map[y-1][x].costDown();
}
if(clipping){
//check left-up, left-down, right-up and right-down. Check: walkable & not closed
if(map[y-1][x-1].walkable && !map[y-1][x-1].isClosed() )
{
st.push(map[y-1][x-1]);
map[y-1][x-1].costUp();
}
if(map[y-1][x+1].walkable && !map[y-1][x+1].isClosed() )
{
st.push(map[y-1][x+1]);
map[y-1][x+1].costUp();
}
if(map[y+1][x+1].walkable && !map[y+1][x+1].isClosed() )
{
st.push(map[y+1][x+1]);
map[y+1][x+1].costUp();
}
if(map[y+1][x-1].walkable && !map[y+1][x-1].isClosed() )
{
st.push(map[y+1][x-1]);
map[y+1][x-1].costUp();
}
}
return st;
}
/*
* Function: ready
* Check whether or not all the settings are correct
*
* Parameters:
*
* None
*
* Returns:
*
* Boolean - True: Game is ready, false: There was an error
*
*/
private function ready():Boolean
{
if(map == undefined){
trace("No map specified");
return false;
}
if(startPoint[0] != Math.floor(startPoint[0]) || startPoint[1] != Math.floor(startPoint[1]))
{
trace("Startpoint isn't valid");
return false;
}
if(endPoint[0] != Math.floor(endPoint[0]) || endPoint[1] != Math.floor(endPoint[1]))
{
trace("Endpoint isn't valid");
return false;
}
if(startPoint[0] < 0 || startPoint[0] >= mapW)
{
trace("=>"+mapW);
trace("Startpoint isn't valid");
return false;
} else if(startPoint[1] < 0 || startPoint[1] >= mapH){
trace("Startpoint isn't valid");
return false;
}
if(endPoint[0] < 0 || endPoint[0] >= mapW)
{
trace("EndPoint isn't valid");
return false;
} else if(endPoint[1] < 0 || endPoint[1] >= mapH){
trace("EndPoint isn't valid");
return false;
}
if(!map[endPoint[1]][endPoint[0]].walkable){
trace("Endpoint isn't walkable.");
return false;
}
if(!map[startPoint[1]][startPoint[0]].walkable){
trace("Startpoint isn't walkable.");
return false;
}
return true;
}
/*
* Function: setWalkable
* Toggle the tile to be walkable or not
*
* Parameters:
*
* x - x position of the tile
* y - y position of the tile
* walkable - boolean representing the ability to walk on the tile or not
*
* Returns:
*
* Nothing
*
* See also:
*
* <toggleWalkable>
*/
public function setWalkable(x:Number, y:Number, b:Boolean):Void
{
map[y][x].setWalkable(b);
}
/*
* Function: toggleWalkable
* Toggle the tile to be walkable or not
*
* Parameters:
*
* x - x position of the tile
* y - y position of the tile
*
* Returns:
*
* Nothing
*
* See also:
*
* <setWalkable>
*/
public function toggleWalkable(x:Number, y:Number):Void
{
map[y][x].toggleWalkable();
}
/*
* Function: setClipping
* Toggle clipping. Clipping is the ability to move diagonally.
*
* Parameters:
*
* clipping - Boolean representing the ability to use clipping
*
* Returns:
*
* Nothing
*/
public function setClipping(c):Void
{
clipping = c;
}
/*
* Function: openMap
* Opens the map (resets all the tiles)
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
private function openMap(){
for(var y = 0; y<mapH; y++){
for(var x = 0; x<mapW; x++){
map[y][x].reset();
}
}
}
}
BinaryHeap class
import be.dauntless.Astar.Tile;
class be.dauntless.Astar.BinaryHeap
{
private var heap:Array;
/*
* Function: BinaryHeap
* Constructor; makes a new heap array
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function BinaryHeap()
{
heap = new Array(null);
}
/*
* Function: addToHeap
* Adds a tile to the binary heap
*
* Parameters:
*
* Tile - The tile to be added
*
* Returns:
*
* Nothing
*/
public function addToHeap(newTile:Tile):Void
{
heap.push(newTile);
var cp:Number = heap.length-1; //cp = current position
var np:Number = Math.floor(cp/2);
//as long as the item isnt in the first position
while(cp != 1){
if(heap[cp].getF() <= heap[Math.floor(cp/2)].getF()){
//if the current F is lower than or equal to its parent, switch them!
var t:Number = heap[Math.floor(cp/2)];
heap[Math.floor(cp/2)] = heap[cp];
heap[cp] = t;
cp = Math.floor(cp/2);
} else {
//destination reached!
break;
}
}
}
/*
* Function: getLowest
* This method returns the lowest F from the heap. That F is deleted from the heap
*
* Parameters:
*
* None
*
* Returns:
*
* Tile - Tile with the lowest F
*/
public function getLowest():Tile
{
var cp:Number = 1; //current position
var np:Number; //next position
var lowest:Tile = heap[1];
if(heap.length == 2){
heap.pop();
} else {
heap[1] = heap.pop();
}
while(true){
np = cp;
if(cp*2+1 <= (heap.length -1) ){
if(heap[np].getF() >= heap[np*2].getF() ) cp = 2*np;
if(heap[cp].getF() >= heap[np*2+1].getF() ) cp = 2*np+1;
} else if(2*np <= (heap.length -1)){
if(heap[np].getF() >= heap[2*np].getF() ) cp = 2*np;
}
//check if np has changed, 'cause then we have to switch them
if(np != cp){
//switch heap[cp] & heap[np]
var t:Tile = heap[np];
heap[np] = heap[cp];
heap[cp] = t;
} else {
//position found, return!
return lowest;
}
}
}
/*
* Function: getLength
* This method returns the length of the heap
*
* Parameters:
*
* None
*
* Returns:
*
* Nothing
*/
public function getLength():Number
{
return (heap.length -1);
}
/*
* Function: getPosition
* This method returns the position of the given ID in the heap
*
* Parameters:
*
* id - id of the node's position to find
*
* Returns:
*
* Number - position of id in heap
*/
public function getPosition(id):Number
{
for(var i in heap){
if(heap[i].id == id){
return i;
}
}
}
/*
* Function: updateList
* This method changes the given tile's f in the heap and resorts it.
*
* Parameters:
*
* Tile - the tile to be changed
*
* Returns:
*
* Nothing
*/
public function updateList(cp_):Void
{
var cp:Number = cp_;
var np:Number = Math.floor(cp/2);
//as long as the item isnt in the first position
while(cp != 1){
if(heap[cp].getF() <= heap[Math.floor(cp/2)].getF()){
//if the current F is lower than or equal to its parent, switch them!
var t:Number = heap[Math.floor(cp/2)];
heap[Math.floor(cp/2)] = heap[cp];
heap[cp] = t;
cp = Math.floor(cp/2);
} else {
//destination reached!
break;
}
}
}
}
Gebruik
Aangezien dit best wel wat AS is, is het misschien handiger om deze zip te downloaden:
ZIP (http://www.dauntless.be/Projects/Astar/Astar.zip)
In deze zip zit ook de documentatie.
Omdat ik het dus dynamisch wou maken is het puur code en is er niets grafisch aan. Daarom is het het handigst als je de Tile class extend. Op die manier kan je je eigen grafische elementen toevoegen. Een lijst van alle properties en methods staat in de documentatie, dus zorg ervoor dat je die niet overschrijft.
Een voorbeeld om de Tile class te extenden:
class GameTile extends be.dauntless.Astar.Tile
{
private var root:MovieClip;
private var mc:MovieClip;
private static var depth:Number = 0;
public function GameTile(x:Number, y:Number, cost:Number, root:MovieClip)
{
super(x, y, cost);
this.root = root;
mc = root.attachMovie("mc_test", "mc"+x+"_"+y, depth++);
mc._x = x * mc._width;
mc._y = y * mc._height;
mc.costt.text = cost;
}
public function notice():Void
{
mc.nextFrame();
}
}
Als je wil dat er iets met de tile gebeurt moet je de 'notice method' overschrijven. Deze wordt immers aangeroepen bij het bouwen van het pad en in de Tile Class is ze toch gewoon leeg (hier DIENT ze dus voor!). Als je de Tile class extend moet je wel altijd in de constructor super() oproepen met de x, y en cost van de tegel.
Voor zij die het niet weten: Deze class is opgebouwd met packages. Het handigste is om ergens op je computer een dir aan te maken waar je alle classes in wil gaan zetten. Daar zet je dan de 'be' map. In flash kan je bij de instellingen de classpaths aanpassen (en daar voeg je de class dir dan aan toe).
In principe kan je het gebruik helemaal uit de helpfiles nemen, maar 'k zal hier nog maar snel wat AS zetten:
import be.dauntless.Astar.*;
var a:Astar = new Astar(false);
a.newMap(10,10);
a.setStartPoint(0, 0);
a.setEndPoint(5, 8);
var path = a.findPath();
if(!path){
//er werd geen path gevonden
} else {
//hier het path
}
Het path wordt terug gegeven in de vorm van tegels. Om de x en y te krijgen doe je dus: path[i].x of path[i].y
Ik heb m'n best gedaan om hem zo snel mogelijk te krijgen (bv door die binaryHeap), maar als jullie nog dingen weten, zeg het maar. Ohja, daardoor heb ik nu ook geen closed list meer, maar is het gewoon opgeslagen in elke node. Door dit is het dus eigenlijk geen A* meer... Hmm, dan maar m'n eigen naam: D* ! :D
Voor zij die tot hier gelezen hebben: DANKU! :p Voor de anderen: grrrr :mad: (lezen ze dan toch niet :p).
Euhm,
Ik hoop op zoveel mogelijk reacties!
Greets,
Me!