Plan for Martin "Muerte" Schultz
by Martin Schultz · 06/09/2005 (8:50 pm) · 17 comments
I'm breezing A* now! This was the first time ever I implemented an A* algorithm and I must say I'm blasted how cool it works. I followed this fantastic tutorial here, took a look at the A* tutorial for T2D, starting reading, thinking and coding and finally after fixing some weird self-produced bugs I got it finally working. Wheeeee! :-)
This image here shows two smileys in the maze and their path marked by the arrows.
I already got 15 AI players running around in the maze choosing random destination nodes without any major performance hit. Amazing how fast A* can be although it does lots of list processing (iterating through SimSets). Ok, to be honest and fair I have to note that I don't have very huge amounts of nodes. The biggest number of connected nodes was about 200, so A* had a maximum of 200 nodes to look at. But anyway, good and fast for games.
This little video shows the A* in action. Please note that in the video the game seems to hang some time. This is not the A* processor cost, it is simply the video catching software not running very smooth on my notebook (but better than nothing :-)
Oh, and finally a hint for anyone who also wants to implement A*: When you're doing a while loop when the path is found just like written in the tutorial, make sure you somehow limit the while loop not to run forever. This took me 2 full days to figure out, that when (by accident) the start node and the end node are the same, this while loop runs infinit. Argh, this was hard to find. :-)
Thanks for stopping by and reading my stuff. Have a nice day! :-)
Martin
This image here shows two smileys in the maze and their path marked by the arrows.
I already got 15 AI players running around in the maze choosing random destination nodes without any major performance hit. Amazing how fast A* can be although it does lots of list processing (iterating through SimSets). Ok, to be honest and fair I have to note that I don't have very huge amounts of nodes. The biggest number of connected nodes was about 200, so A* had a maximum of 200 nodes to look at. But anyway, good and fast for games.This little video shows the A* in action. Please note that in the video the game seems to hang some time. This is not the A* processor cost, it is simply the video catching software not running very smooth on my notebook (but better than nothing :-)
Oh, and finally a hint for anyone who also wants to implement A*: When you're doing a while loop when the path is found just like written in the tutorial, make sure you somehow limit the while loop not to run forever. This took me 2 full days to figure out, that when (by accident) the start node and the end node are the same, this while loop runs infinit. Argh, this was hard to find. :-)
Thanks for stopping by and reading my stuff. Have a nice day! :-)
Martin
#2
Nick
06/10/2005 (8:48 am)
That's excellent Martin! I have read that A* tutorial in the past in it's really good. Path finding is a must in my opinion in many games. So do you consider this project completed? Keep us posted when you do more rigorous testing on it.Nick
#3
Martin
06/10/2005 (9:55 am)
Thanks. No, this project is still ongoing, but it makes huge steps now, as more and more major parts are completed. Sure, I'll keep you updated. Martin
#4
are you planing to implement a navigation graph? it's really the hardes part?
and did you implement this in c++ or using script?
thnks
06/11/2005 (7:13 am)
thants greate keep going martinare you planing to implement a navigation graph? it's really the hardes part?
and did you implement this in c++ or using script?
thnks
#5
thanks. :-) I already implemented a nav graph, but it's not visible on the screenshot. All the linked nodes which make the nav graph are created when creating the maze through a so called maze-editor. Once a maze is loaded, all nodes are calculated, from which node it's possible to get to other linked nodes and so on. All these data is then stored into the .mis file.
As you can see in the screenshot, this is how it looks in the editing mode. I unfold the nodes group which makes my nav graph. As you can see, there's a list of connected nodes in each node. I gave each node 8 directions of movement and for each direction it is checked, if there's a wall or not.
The whole A* was completly implemented in Torque script.
06/11/2005 (9:01 am)
Hi Firas,thanks. :-) I already implemented a nav graph, but it's not visible on the screenshot. All the linked nodes which make the nav graph are created when creating the maze through a so called maze-editor. Once a maze is loaded, all nodes are calculated, from which node it's possible to get to other linked nodes and so on. All these data is then stored into the .mis file.
As you can see in the screenshot, this is how it looks in the editing mode. I unfold the nodes group which makes my nav graph. As you can see, there's a list of connected nodes in each node. I gave each node 8 directions of movement and for each direction it is checked, if there's a wall or not.
The whole A* was completly implemented in Torque script.
#6
colud you please give some hints how can I do somthing similar I really like to do somthing like this but I don't know how to do it
and agine what about the nav graph is it implemented on c++ or script?
thanks
06/11/2005 (8:32 pm)
thats really greate I like your workcolud you please give some hints how can I do somthing similar I really like to do somthing like this but I don't know how to do it
and agine what about the nav graph is it implemented on c++ or script?
thanks
#7
It's not so easy to describe the whole process in a few words. The nav graphs are self created script objects. In my case, as seen in the screenshot just above, it contains 8 possible references to adjacent nodes. So each of the 8 references describe the directions north (1), north-east (2), east (3), south-east (4), south (5), south-west(6), west (7) and finally north-west (8). When creating a mission, I divide my mission area into equal squares. Then I iterate through all squares and check, if each neighbour (adjacent) square is walkable from this node for all directions meantioned above. If a direction is walkable, I store it as a "1" for that direction, like "NodeD1 = 1" (D = direction, 1 = North). If it's not walkable, I store a "NodeD1 = 0". If a node direction is 1, I store also which row and cell the adjacent is, like "row2cell5" to later access it easily. This is how the preprocessing is done (roughly). After I iterated through all nodes, I have a complete nav graph - meaning, I know for each node which direction is walkable or not from this node and what is den identification for the corresponding adjacent node.
If you have a data structure like this, you could easily apply everything from the tutorial I mentioned above. When I start to do a A* search, I first try to find out, which is the nearest node to my player position. Then I choose a random node where my smiley should go and do the A* calculation. The A* takes the start node, looks at all adjacents and chooses the one with the lowest F value. Ok, I should stop here, this is better described in the tutorial link above. But that's more or less how I've done it. And it works pretty perfect.
Hope this helps a bit.
Martin :-)
06/11/2005 (9:59 pm)
The nav graph is pure Torque script - no C++. It's not that hard to implement (at least in my case - there are surely more complicated environments where it's harder to implement).It's not so easy to describe the whole process in a few words. The nav graphs are self created script objects. In my case, as seen in the screenshot just above, it contains 8 possible references to adjacent nodes. So each of the 8 references describe the directions north (1), north-east (2), east (3), south-east (4), south (5), south-west(6), west (7) and finally north-west (8). When creating a mission, I divide my mission area into equal squares. Then I iterate through all squares and check, if each neighbour (adjacent) square is walkable from this node for all directions meantioned above. If a direction is walkable, I store it as a "1" for that direction, like "NodeD1 = 1" (D = direction, 1 = North). If it's not walkable, I store a "NodeD1 = 0". If a node direction is 1, I store also which row and cell the adjacent is, like "row2cell5" to later access it easily. This is how the preprocessing is done (roughly). After I iterated through all nodes, I have a complete nav graph - meaning, I know for each node which direction is walkable or not from this node and what is den identification for the corresponding adjacent node.
If you have a data structure like this, you could easily apply everything from the tutorial I mentioned above. When I start to do a A* search, I first try to find out, which is the nearest node to my player position. Then I choose a random node where my smiley should go and do the A* calculation. The A* takes the start node, looks at all adjacents and chooses the one with the lowest F value. Ok, I should stop here, this is better described in the tutorial link above. But that's more or less how I've done it. And it works pretty perfect.
Hope this helps a bit.
Martin :-)
#8
but where can I find a tutorial about nav graph structure?
and about devide mission area to a equal squres, is this can be done in script? if yes please some hints.
thanks for your cooperation.
06/12/2005 (5:00 am)
thanks for your information but where can I find a tutorial about nav graph structure?
and about devide mission area to a equal squres, is this can be done in script? if yes please some hints.
thanks for your cooperation.
#9
It is more something you do in mind. Imagine your level. What areas would make sense to divide them into a node area (in my case: squares). The easiest thing would be: you simply define that your whole level is divided into 5x5 squares (in my case 6x6). Then you automatically have for each node the width and depth values, know that for the row0cell0 node that row0cell1, row1cell0 and row1cell1 are the adjacent nodes. You could do a line-of-sight test from the row0cell0 node to all adjacents if they are "visible". If so, mark those directions as walkable (in my case: mark for example row0cell1 as "1" for walkable). Once you iterated through all available nodes, you have your nav graph.
But you're right. The above mentioned tutorial doesn't cover how to set up your mission area with nodes. But on the other side the tutorial couldn't handle this as it depends too much on your own type of game. Here are the first three nodes as example from the level above.
Martin
06/12/2005 (9:00 am)
Unfortunately I don't have any tutorials about nav graphs. But if you start thinking about how you will implement your own sqaure tiles, you will automatically have your nav graph. It is more something you do in mind. Imagine your level. What areas would make sense to divide them into a node area (in my case: squares). The easiest thing would be: you simply define that your whole level is divided into 5x5 squares (in my case 6x6). Then you automatically have for each node the width and depth values, know that for the row0cell0 node that row0cell1, row1cell0 and row1cell1 are the adjacent nodes. You could do a line-of-sight test from the row0cell0 node to all adjacents if they are "visible". If so, mark those directions as walkable (in my case: mark for example row0cell1 as "1" for walkable). Once you iterated through all available nodes, you have your nav graph.
But you're right. The above mentioned tutorial doesn't cover how to set up your mission area with nodes. But on the other side the tutorial couldn't handle this as it depends too much on your own type of game. Here are the first three nodes as example from the level above.
new SimGroup(Nodes) {
new ScriptObject(Row0Cell0Node) {
cell = "0";
posY = "3";
NodeD8 = "0";
NodeObjD1 = "0";
NodeObjD3 = "MissionGroup/Maze/Nodes/Row0Cell1Node";
NodeObjD5 = "MissionGroup/Maze/Nodes/Row1Cell0Node";
row = "0";
posX = "-3";
NodeObjD7 = "0";
NodeD1 = "0";
NodeD3 = "1";
NodeD5 = "1";
NodeD7 = "0";
NodeCount = "2";
H = "0";
NodeObjD2 = "0";
NodeObjD4 = "0";
G = "0";
NodeObjD6 = "0";
NodeObjD8 = "0";
F = "0";
Parent = "0";
NodeD2 = "0";
NodeD4 = "0";
NodeD6 = "0";
};
new ScriptObject(Row0Cell1Node) {
cell = "1";
posY = "3";
NodeD8 = "0";
NodeObjD1 = "0";
NodeObjD3 = "MissionGroup/Maze/Nodes/Row0Cell2Node";
NodeObjD5 = "0";
row = "0";
posX = "-9";
NodeObjD7 = "MissionGroup/Maze/Nodes/Row0Cell0Node";
NodeD1 = "0";
NodeD3 = "1";
NodeD5 = "0";
NodeD7 = "1";
NodeCount = "2";
H = "0";
NodeObjD2 = "0";
NodeObjD4 = "0";
G = "0";
NodeObjD6 = "0";
NodeObjD8 = "0";
F = "0";
Parent = "0";
NodeD2 = "0";
NodeD4 = "0";
NodeD6 = "0";
};
new ScriptObject(Row0Cell2Node) {
cell = "2";
posY = "3";
NodeD8 = "0";
NodeObjD1 = "0";
NodeObjD3 = "MissionGroup/Maze/Nodes/Row0Cell3Node";
NodeObjD5 = "MissionGroup/Maze/Nodes/Row1Cell2Node";
row = "0";
posX = "-15";
NodeObjD7 = "MissionGroup/Maze/Nodes/Row0Cell1Node";
NodeD1 = "0";
NodeD3 = "1";
NodeD5 = "1";
NodeD7 = "1";
NodeCount = "3";
H = "0";
NodeObjD2 = "0";
NodeObjD4 = "0";
G = "0";
NodeObjD6 = "0";
NodeObjD8 = "0";
F = "0";
Parent = "0";
NodeD2 = "0";
NodeD4 = "0";
NodeD6 = "0";
};Martin
#10
now what I won't to know is what method did you use to distribute these nodes on the maz?
and thanks for you time :)
06/12/2005 (10:06 am)
thanks a lot man thats relly greate and helpfull now what I won't to know is what method did you use to distribute these nodes on the maz?
and thanks for you time :)
#11
06/12/2005 (12:33 pm)
What exactly do you mean by "distributing the nodes in the maz"?
#12
I mean there should be a procedure or function you call it and this procedure make some calculation and place the nodes in ordered way like the picture above, and also give them the names (row0cell0node, row0cell1node,....).
that what I really need to know.
06/13/2005 (6:03 am)
I mean the "Placment process" of the nodes and puting them in the way you do inside the maze, if i'm not wrong this is an automation process (you didn't do that by hand).I mean there should be a procedure or function you call it and this procedure make some calculation and place the nodes in ordered way like the picture above, and also give them the names (row0cell0node, row0cell1node,....).
that what I really need to know.
#13
A maze with a closed block somewhere next to the middle. That's more or less how my mazes look like, when I import them. Now I start the the top-left block and check, if it has a left wall and if it has a top wall. The top-left block would be for me the:
I create a node with the name Row0Cell0 and give it two attributes: hasLeftWall and hasTopWall. If they have both (like in this example), I set them to "1". Then I look at the next one, Row0Cell1 and check the same. Each node I add to a mission group called "/missions/maze/nodes".
That's a simplified view on the method I'm using. I'll do some more processing for each node, but that's not so important for this example now. Part of the advanced stuff is, that I check also for each node if it has a bottom wall and a right wall and if all directions are walkable.
Does this explanation help?
06/13/2005 (9:17 am)
Ah, got it now. Yes, it's an automated process. I created a maze editor and this maze editor displays the whole possible maze. Once the user finishes editing and saves the maze, a function is called which goes through all squares and creates a node for each square. Because the function knows which square it currently processes, it knows also the current row and also the current cell of the maze. Think of such a maze:+--------------+ | | | +-+ | | | | | | +-+ | | | +--------------+
A maze with a closed block somewhere next to the middle. That's more or less how my mazes look like, when I import them. Now I start the the top-left block and check, if it has a left wall and if it has a top wall. The top-left block would be for me the:
+- |
I create a node with the name Row0Cell0 and give it two attributes: hasLeftWall and hasTopWall. If they have both (like in this example), I set them to "1". Then I look at the next one, Row0Cell1 and check the same. Each node I add to a mission group called "/missions/maze/nodes".
That's a simplified view on the method I'm using. I'll do some more processing for each node, but that's not so important for this example now. Part of the advanced stuff is, that I check also for each node if it has a bottom wall and a right wall and if all directions are walkable.
Does this explanation help?
#14
file: astar.cs
file: astarHelper.cs
02/15/2008 (9:16 am)
For anyone who's interested, here's the 2 A* files I created for SuperMaze. Not super clean, but works. Use it if you like. Some code inside is specific to the game SuperMaze, but you'll quickly see which parts once you dived in.file: astar.cs
// *****************************************************************************
//
// This A* stuff is based on this tutorial:
// http://www.policyalmanac.org/games/aStarTutorial.htm
//
// *****************************************************************************
function Player::aStarTest(%this)
{
%nodesRoot = nameToID("MissionGroup/Maze/Nodes");
%this.findShortestPath(%nodesRoot.getObject(5), %nodesRoot.getObject(%nodesRoot.getCount() - 1) );
}
function Player::findShortestPath(%this, %startNode, %destNode)
{
if (!isObject(%this.scriptObject))
{
error("No Ninja object found - no data to do pathfinding!");
return;
}
if (%startNode == %destNode)
{
echo("Startnode ("@%startNode@") and endnode ("@%destNode@") are the same (" @ %this.scriptObject.name @ ")!");
return;
}
//error("findShortestPath: startnode: "@%startNode@", destNode: "@%destNode@" for " @ %this.scriptObject.name);
if (!isObject(%this.scriptObject.aiOpenList))
{
error("aiOpenList object not found - no data to do pathfinding (" @ %this.scriptObject.name @ ")!");
return;
}
if (!isObject(%this.scriptObject.aiClosedList))
{
error("aiClosedList object not found - no data to do pathfinding (" @ %this.scriptObject.name @ ")!");
return;
}
if (!isObject(%this.scriptObject.aiPath))
{
error("aiPath object not found - no data to do pathfinding (" @ %this.scriptObject.name @ ")!");
return;
}
// DEBUG: set a destination marker in the color of our smiley
//if (%this.aiDestMarker > 0)
// %this.aiDestMarker.delete();
//%this.aiDestMarker = addAIEndPointMarker(%destNode.posX SPC %destNode.posY SPC "106", "1 0 0 90", "DestOf"@%this@"Slot"@%this.teamID, %this.teamID);
// first of all clear the lists for this client
%this.scriptObject.aiOpenList.clear();
%this.scriptObject.aiClosedList.clear();
%this.scriptObject.aiPath.clear();
%this.clearAStarPathMarker();
//error("start node: "@ %startNode @ "/" @ %startNode.getName() );
//error("dest node: "@ %destNode @ "/" @ %destNode.getName() );
%maxWhileRetries = 300;
%currentWhileRetries = 0;
%this.addNodeToOpenList(%startNode);
%this.calcInitialFValue(%startNode, %destNode);
while (%this.scriptObject.aiOpenList.getCount() > 0)
{
//echo("while.... for " @ %this.scriptObject.name);
%currentNode = %this.getOpenListLowestFNode();
if (%currentNode == -1) // error retrieving current node. break now!
{
error("findShortestPath: failed to retrieve current node from open list for " @ %client.nameBase);
break;
}
// check if the destination node is on the open list.
if (%this.isNodeOnOpenList(%destNode) )
{
// we found our destination node
//error("findShortestPath: DESTINATION NODE FOUND!!! " @ %destNode @ "/" @ %destNode.getName() );
// add the whole path from dest to start to the ai path list.
%this.scriptObject.aiPath.add(%destNode);
%nextBack = %destNode.parent;
%currentTry = 0; // for security: do now allow the while loop to run forever!
while (%nextBack != %startNode)
{
%this.scriptObject.aiPath.add(%nextBack);
%nextBack = %nextBack.parent;
// for safety let a second counter validate our while loop.
%currentTry++;
if (%currentTry == 1000)
{
error("Reached maximum retries in while loop! player: "@%this.scriptObject.name @", Startnode: "@ %startNode @ ", destNode: "@ %destNode);
%this.scriptObject.aiPath.clear();
return;
}
}
%this.scriptObject.aiPath.add(%startNode);
%this.addAStarPathMarker();
//error("parent node:" SPC %destNode.parent);
break;
}
//echo("findShortestPath: processing currentnode: " @ %currentNode @ "/" @ %currentNode.getName() );
%this.moveNodeFromOpen2ClosedList(%currentNode);
// check each adjacent node to the current node.
// if its not on the open list and not on the closed list and if so,
// add it to the open list and calc the F value.
for (%i = 1; %i < 9; %i++)
{
//error("adjacent node " @ %i @ ": " @ %currentNode.NodeD[%i]);
if (%currentNode.NodeD[%i] == 1) // there's a walkable node in that direction
{
%adjacent = nameToID(%currentNode.NodeObjD[%i]);
if (%this.isNodeOnClosedList(%adjacent) )
{
// this node is on the closed list so ignore it completly!
//echo("adjacent: "@ %adjacent @ "/" @ %adjacent.getName() @ " is on the close list - so ignoring it!");
}
else if (%this.isNodeOnOpenList(%adjacent) )
{
//error("IS ON OPEN LIST. HIER WEITER CODEN!!!");
//echo("OPENLIST adjacent: " @ %adjacent @ "/" @ %adjacent.getName() );
//error("adjacent.G: " @ %adjacent.G);
//error(" current.G: " @ %currentNode.G);
//echo("old G " @ %adjacent.G );
//echo("old H " @ %adjacent.H );
//echo("old F " @ %adjacent.F );
if (%adjacent.G < %currentNode.G)
{
%adjacent.parent = %currentNode;
// recalc G value for this node now
if (%i == 1 || %i == 3 || %i == 5 || %i == 7)
%adjacent.G = %adjacent.parent.G + 10;
else
%adjacent.G = %adjacent.parent.G + 14;
// calc the H (heuristic) by using the manhatten method
%this.calcHValue(%adjacent, %destNode);
// F = G + H
%adjacent.F = %adjacent.G + %adjacent.H;
//echo("new G " @ %adjacent.G );
//echo("new H " @ %adjacent.H );
//echo("new F " @ %adjacent.F );
}
}
else
{
// process this node now: It's not on the closed list and not on the open list.
// calc the F/G/H values and add it to the open list.
//echo("adjacent: " @ %adjacent @ "/" @ %adjacent.getName() );
// make the current node the parent node of this adjacent
%adjacent.parent = %currentNode;
// use a G value of 10 for orthogonal directions (1,3,5,7) and 14 for diagonal directions (2,4,6,8)
if (%i == 1 || %i == 3 || %i == 5 || %i == 7)
%adjacent.G = %adjacent.parent.G + 10; // TODO Martin: mit parent.G???? %adjacent.G = %adjacent.parent.G + 10;
else
%adjacent.G = %adjacent.parent.G + 14; // %adjacent.G = %adjacent.parent.G + 14;
//echo("G " @ %adjacent.G );
// calc the H (heuristic) by using the manhatten method
%this.calcHValue(%adjacent, %destNode);
// F = G + H
%adjacent.F = %adjacent.G + %adjacent.H;
//echo("F " @ %adjacent.F );
%this.addNodeToOpenList(%adjacent);
}
}
//error(" ");
}
//echo(" ");
%currentWhileRetries++;
if (%currentWhileRetries == %maxWhileRetries)
{
// stop this infinite loop here. We didn't find a way!
error("findShortestPath: REACHED MAX RETRIES. NO WAY FOUND for " @ %client.nameBase);
%this.aiThinkState = $AI_NO_PATH_FOUND;
break;
}
}
//error("ai pathing done for " @ %this.scriptObject.name);
}file: astarHelper.cs
// Calculate the H value for a given node
function Player::calcHValue(%this, %adj, %destNode)
{
// calc the H (heuristic) by using the manhatten method
%Hrow = %destNode.row - %adj.row;
if (%Hrow < 0)
%Hrow *= -1;
%Hcell = %destNode.cell - %adj.cell;
if (%Hcell < 0)
%Hcell *= -1;
%adj.H = (%Hrow + %Hcell) * 10;
//echo("H " @ %adj.H );
}
// Calculate the F value for a given node
function Player::calcInitialFValue(%this, %sourceNode, %destNode)
{
%sourceNode.G = 10;
%Hrow = %destNode.row - %sourceNode.row;
if (%Hrow < 0)
%Hrow *= -1;
//error("calcFValue: Hrow " @ %Hrow );
%Hcell = %destNode.cell - %sourceNode.cell;
if (%Hcell < 0)
%Hcell *= -1;
//error("calcFValue: Hcell " @ %Hcell );
%sourceNode.H = (%Hrow + %Hcell) * 10;
//error("calcFValue: H " @ %sourceNode.H );
%sourceNode.F = %sourceNode.G + %sourceNode.H;
//error("calcFValue: F " @ %sourceNode.F );
}
// gets the node with the lowest f value from the clients open list
function Player::getOpenListLowestFNode(%this)
{
%count = %this.scriptObject.aiOpenList.getCount();
%lowestF = 99999999;
%resultNode = -1;
if (%count == 0) // no nodes available
{
error("getOpenListLowestFNode: open list has no entries in it: " @ %this.scriptObject.aiOpenList.getCount() );
return -1;
}
for (%i = 0; %i < %count; %i++)
{
%node = %this.scriptObject.aiOpenList.getObject(%i);
//error("getOpenListLowestFNode: node: " @ %node @ ", F: " @ %node.F);
if (%node.F < %lowestF)
{
%lowestF = %node.F;
%resultNode = %node;
}
}
return %resultNode;
}
// moves a node from the clients open list to his closed list
function Player::moveNodeFromOpen2ClosedList(%this, %node)
{
%this.removeNodeFromOpenList(%node);
%this.addNodeToClosedList(%node);
}
// removes the node from the open list.
function Player::removeNodeFromOpenList(%this, %node)
{
if (!%this.isNodeOnOpenList(%node))
{
echo("AStar - Unable to remove node from open list because node " @ %node @ " is not on the list.");
return;
}
%this.scriptObject.aiOpenList.remove(%node);
}
// Returns TRUE if node is on the open list, FALSE if not
function Player::isNodeOnOpenList(%this, %searchNode)
{
%count = %this.scriptObject.aiOpenList.getCount();
for (%i = 0; %i < %count; %i++)
{
%node = %this.scriptObject.aiOpenList.getObject(%i);
if (%node == %searchNode)
{
return true;
}
}
return false;
}
// removes the node from the closed list.
function Player::removeNodeFromClosedList(%this, %node)
{
if (!%this.isNodeOnClosedList(%node))
{
echo("AStar - Unable to remove node from closed list because node " @ %node @ " is not on the list.");
return;
}
%this.scriptObject.aiClosedList.remove(%node);
}
// Adds the node to the open list.
function Player::addNodeToOpenList(%this, %node)
{
if (%this.isNodeOnOpenList(%node))
{
echo("AStar - Attempted to add duplicate node " @ %node @ " to open list for player " @ %this.scriptObject.name @ ".");
return;
}
%this.scriptObject.aiOpenList.add(%node);
}
// Returns TRUE if node is on the closed list, FALSE if not
function Player::isNodeOnClosedList(%this, %searchNode)
{
%count = %this.scriptObject.aiClosedList.getCount();
for (%i = 0; %i < %count; %i++)
{
%node = %this.scriptObject.aiClosedList.getObject(%i);
if (%node == %searchNode)
return true;
}
return false;
}
// Adds the node to the closed list.
function Player::addNodeToClosedList(%this, %node)
{
if (%this.isNodeOnClosedList(%node))
{
echo("AStar - Attempted to add duplicate node " @ %node @ " to closed list for player " @ %this.scriptObject.name @ ".");
return;
}
%this.scriptObject.aiClosedList.add(%node);
}
// debug test function to visualize the A* route
//function addAIMarker(%pos, %rot, %name)
//{
// %aStarMarkerRoot = nameToID("MissionGroup/Maze/AStar");
// %nm = new Marker(%name) {
// position = %pos;
// rotation = %rot;
// };
// %aStarMarkerRoot.add(%nm); // add the A* marker to the mission group.
//}
function addAIEndPointMarker(%pos, %rot, %name, %slot)
{
%aStarMarkerRoot = nameToID("MissionGroup/Maze/AStar");
%obj = new InteriorInstance(%name) {
position = %pos;
rotation = %rot;
parent = %parent;
interiorFile = "supermaze/data/interiors/node/arrow"@%slot@".dif";
};
%obj.setScale("3 3 3");
%aStarMarkerRoot.add(%obj); // add the A* marker to the mission group.
return %obj;
}
// debug test function to visualize the A* route
function dumpAStarValues(%id)
{
%aStar = nameToID("MissionGroup/Maze/AStar/AStar" @ %id);
if (-1 == %aStar)
{
error("Wrong A* ID given. No A* Node found!");
}
else
{
echo("AStar" @ %id @ ".G: " @ %aStar.parent.G);
echo("AStar" @ %id @ ".H: " @ %aStar.parent.H);
echo("AStar" @ %id @ ".F: " @ %aStar.parent.F);
}
}
//function marker2test()
//{
// %aStarMarkerRoot = nameToID("MissionGroup/Maze");
// %nm = new Marker2(martin) {
// position = "1 1 102";
// rotation = "1 0 0 0";
// scale = "4 4 4";
// red = "1";
// green = "0";
// blue = "0";
// };
// %aStarMarkerRoot.add(%nm); // add the A* marker to the mission group.
//}
function Player::clearAStarPathMarker(%this)
{
for (%c = 0; %c < 5; %c++)
{
for (%x = 0; %x < %this.scriptObject.aiPathMarkers.getCount(); %x++)
{
%obj = %this.scriptObject.aiPathMarkers.getObject(%x);
%obj.delete();
}
}
%this.scriptObject.aiPathMarkers.clear();
}
function Player::addAStarPathMarker(%this)
{
// TODO: comment it for debugging
return;
%lastMarker = 0;
%lastNode = 0;
for (%x = 0; %x < %this.scriptObject.aiPath.getCount(); %x++)
{
%node = %this.scriptObject.aiPath.getObject(%x);
%marker = addMarker2(%node.posX, %node.posY, %this.scriptObject.teamID, %lastMarker, %lastNode);
%this.scriptObject.aiPathMarkers.add(%marker);
%lastMarker = %marker;
%lastNode = %node;
//error("lastMarker: "@%lastMarker);
}
}
function addMarker2(%posX, %posY, %slot, %lastMarker, %lastNode)
{
%val = getSlotColor(%slot);
%colorR = getWord(%val, 0);
%colorG = getWord(%val, 1);
%colorB = getWord(%val, 2);
//%rotation = "1 0 0 0";
%rotation = "1 0 0 90";
%zHeight = "100.5";
%zHeight = %zHeight + (0.1 * %slot);
// if (%lastMarker != 0)
// {
// %oldPos = %lastNode.posX SPC %lastNode.posY SPC "100.5";
// %thisPos = %posX SPC %posY SPC "100.5";
// %dot = vectorDot(%oldPos, %thisPos);
// %norm = vectorNormalize(%dot);
// error("oldPos: "@ %oldPos);
// error("thisPos: "@ %thisPos);
// error("dot: "@ %dot);
// //error("norm: "@ %norm);
// %rotation = "0 0 1" SPC %dot;
// error("dot2: "@ %norm);
// }
%nm = new Marker2("Slot"@%slot) {
position = %posX SPC %posY SPC %zHeight;
rotation = %rotation;
scale = "2 2 2";
red = %colorR;
green = %colorG;
blue = %colorB;
parentMarker = %lastNode;
};
%aStarMarkerRoot = nameToID("MissionGroup/Maze/NodeMarkers");
%aStarMarkerRoot.add(%nm); // add the A* marker to the mission group.
return %nm;
}
//function testslot(%num)
//{
// %val = getSlotColor(%num);
// %colorR = getWord(%val, 0);
// %colorG = getWord(%val, 1);
// %colorB = getWord(%val, 2);
// error("Color: R="@%colorR@", G="@%colorG@", B="@%colorB);
//}
function getSlotColor(%slot)
{
switch (%slot)
{
case 0: // 255 255 0
return "1 1 0";
case 1: // 0 255 99
return "0 1 0.38823529411764705882352941176471";
case 2: // 255 0 0
return "1 0 0";
case 3: // 0 53 255
return "0 0.2078431372549019607843137254902 1";
case 4: // 255 205 0
return "1 0.8039215686274509803921568627451 0";
case 5: // 23 232 232
return "0.090196078431372549019607843137255 0.90980392156862745098039215686275 0.90980392156862745098039215686275";
case 6: // 244 10 244
return "0.95686274509803921568627450980392 0.03921568627450980392156862745098 0.95686274509803921568627450980392";
case 7: // 116 155 102
return "0.45490196078431372549019607843137 0.6078431372549019607843137254902 0.4";
case 8: // 180 75 105
return "0.70588235294117647058823529411765 0.29411764705882352941176470588235 0.41176470588235294117647058823529";
case 9: // 255 104 0
return "1 0.4078431372549019607843137254902 0";
case 10: // 161 0 255
return "0.63137254901960784313725490196078 0 1";
case 11: // 238 153 191
return "0.93333333333333333333333333333333 0.6 0.74901960784313725490196078431373";
case 12: // 160 160 160
return "0.62745098039215686274509803921569 0.62745098039215686274509803921569 0.62745098039215686274509803921569";
case 13: // 0 255 171
return "0 1 0.67058823529411764705882352941176";
case 14: // 165 189 65
return "0.64705882352941176470588235294118 0.74117647058823529411764705882353 0.25490196078431372549019607843137";
case 15: // 255 255 232
return "1 1 0.90980392156862745098039215686275";
}
error("getSlotColor: Failed to find slot color! slot: " @ %slot);
}
#15
Whats about Nav Graph in your project? Can you send me Nav Graph script?
email: deewahyu@yahoo.com
Thanks
05/28/2009 (11:44 am)
Hi... martin, i want to implement your Astar script in may student project. But i dont know how to placing nodes at my mission file. Is this using WayPointMarker or PathMarker? Whats about Nav Graph in your project? Can you send me Nav Graph script?
email: deewahyu@yahoo.com
Thanks
#16
05/28/2009 (11:51 am)
Sorry, I haven't used Torque since a long time and can't remember anymore.
#17
05/28/2009 (12:55 pm)
any one can help me?
Associate Justin DuJardin
I dig the ninja smilies.
-justin`