|
Title:
Using A* for 3D Pathing in Q2
Difficulty: Fairly Difficult
By: Maj.Bitch
Email: peblair@frontier.net
Date: 09-26-99
Note: Please give credit where credit is due.
======================================================
I spent quite a bit of
time searching the web for stuff to
bootstrap me into the 3D pathing realm but I didn't find
much. There were some good things but, as always, in the
end I had to figure it out myself. Wasn't too difficult
but took some time. So, for all the other coders out there
I've decided to put together a tutorial in implementing the
A* (pronounced A-Star) algorithm for path routing in Q2.
Okay, I'm gonna spend
some time upfront talking about this
algorithm for it was developed in the 60's and has been the
subject of quite a bit of research in the computer science
fields and has proven to be the premier routing algorithm
of its kind. After reading this tutorial, you'll see why.
NOTE: If you don't know
linked lists, then I'll tell you upfront
that most, if not all, of this tutorial won't make any sense to
you...
What does the A*
algorithm do?
The A* algorithm does a
bunch of stuff to find the shortest path
from start to destination by conducting a pre-determined search
on neighboring locations (nodes) and accumulating a cost for
each node along the way. The path with the least total cost from
start to destination is determined to be the shortest (and best)
path to take. The implementation uses a Stack (for temp storage
of past nodes for backtracking) and an OPEN list (for temp storage
of nodes not yet searched) and a CLOSED list (for temp storage of
nodes already search so you don't go over them again and again).
The Stack and OPEN/CLOSED lists are Linked Lists created dynamically
as the search for the shortest path moves merrily along. In the end,
a linked list (in reverse order) is returned which contains the path
from end to start. That right.. End to start because that's the way
it works. So, when the path is returned you have to copy the linked
nodes in reverse order to get the path from start to end. You'll see
why this is as you begin to understand what is happening.
=======================================================================
=======================================================================
What does the data
structure look like?
You'll need to have this
struct NODE declared.
<c code>
#define NUMCHILDS 6 // I'll explain this later...
struct NODE {
int g; // how far we've already gone from start to here
float h; // heuristic estimate of how far is left
float f; // is total cost (estimated) from start to finish
// ANYTHING YOU WANT GOES HERE
// DEPENDING ON YOUR IMPLEMENTATION
struct NODE *Child[NUMCHILDS];
struct NODE *PrevNode;
struct NODE *NextNode;
};
</c code>
So, ALL of the above fields YOU MUST HAVE but you can add other fields
too. For the implementation below, I will add a field called nodenum so
my struct NODE looks like this.
<c code>
struct NODE {
int g; // how far we've already gone from start to here
float h; // heuristic estimate of how far is left
float f; // is total cost (estimated) from start to finish
int nodenum; // My own field here...
struct NODE *Child[NUMCHILDS];
struct NODE *PrevNode;
struct NODE *NextNode;
};
</c code>
=======================================================================
=======================================================================
What does my own
structure look like?
You can have any kind of
structure you want which contains your
own information about the map. For this tutorial, I will use
the following:
<c code>
typedef struct {
int nodenum; // index number of this node
vec3_t origin; // location in [x,y,z]
} node_t;
node_t node;
int numnodes; // Number of node[]
</c code>
Once I know how many nodes I've got (using whatever technique you
want) then, I create my huge node struct array by doing this:
<c code>
node=(node_t *)malloc(numnodes*sizeof(node_t));
</c code>
So, I've gotten all the information about all the nodes that I need
for pathing. You'll have to figure out what works best for your
own implementation.
I then have a huge array
of node[0..numnodes-1] with node[i].nodenum=i;
Note that my huge array
of nodes is sequential and I'm using the nodenum
for each node in my struct NODE array for cross reference. You have to
have some way of allowing the A* algorithm to be able to cross reference
your data (no matter how you may want to do it). Here, the NODE field
called nodenum will correspond directly to the node[i].nodenum field.
This way, when the A* algorithm returns the linked list of nodes along
the shortest path, I can cross reference this with my node[i].origin.
Don't get confused by
this.. You'll see what I'm talking about later on
down in this tutorial.
What does the A*'s Stack
look like?
The A* algorithm uses a
Stack like this:
<c code>
struct STACK {
struct NODE *NodePtr;
struct STACK *StackPtr;
};
struct STACK *Stack=NULL;
</c code>
Note that the Stack is merely a linked list of pointers. One pointer on
each link of the Stack connects to a NODE struct and the other links to
the next Stack node in the Stacks linked list..
=======================================================================
=======================================================================
What does the Stack do?
The A* algorithm uses the
stack to keep track of past nodes for the following
reason. The A* slowly creeps along going from node to node (location to
location)
in the direction of the shortest path. Say the A* algorithm slowly works its
way
down a long and narrow hallway which turns out to be a dead-end. The A*
algorithm
didn't know this at first because it just creeps in the direction of the
shortest
path. So when it gets to the dead-end it has to slowly work its way backward
to find an alternative side-route and creep that way to see where that leads.
If
that also is a dead-end then it has to again work backward until it finds
another
possible path to head down. The Stack is used to keep track of these
backtracking
nodes so it can get out of trouble if the path it is creeping down turns out
to
not lead anywhere.
There are 2 routines for
using the Stack. These are:
<c code>
//=========================================
// Push Node to front of the linked list
//=========================================
void Push(struct NODE *Node) {
struct STACK *STK;
STK=(struct STACK *)malloc(sizeof(struct STACK));
STK->StackPtr=Stack; // NULL at start
STK->NodePtr=Node; // Tie the Node
Stack=STK; // Set to start of Stack
}
//=========================================
// Pop Node from front of the linked list
//=========================================
struct NODE *Pop(void) {
struct NODE *tNode;
struct STACK *STK;
STK=Stack; // Start of Stack
tNode=Stack->NodePtr; // Grab this Node.
Stack=Stack->StackPtr; // Move Stack Pointer
free(STK); // Free this node
return (tNode);
}
</c code>
Note that the A* algorithm keeps pushing past nodes onto the front of
the Stack and popping nodes from the front of the Stack. It does this
for a simple reason. If it has to backtrack, the last node it visited
should be the next node on the Stack. This way, the order of nodes
visited down any particular path is maintained. When the A* algorithm
has to backtrack, it simply pops the last node it just pushed onto the
Stack. I have rebuilt these 2 routines and they are rock-solid stable.
You can mess with them if you like but do so at your own risk.. I do
recommend that you spend at least a few minutes understanding how NODES
are pushed onto the Stack and popped from the Stack just so you'll have
a feel for what is happening there..
You can put these
routines anywhere you want so long as they are made
accessible to the main A* routines..
=======================================================================
=======================================================================
What about the OPEN and
CLOSED Lists?
You'll need to have the
following declarations made global to your A*
routines:
<c code>
struct NODE *OPEN=NULL; // Start of OPEN List
struct NODE *CLOSED=NULL; // Start of CLOSED List
</c code>
The OPEN list is a linked list of NODES indicating which Nodes have not
been visited yet. Once a particular NODE has been visited, it is then
immediately removed from the OPEN linked list and placed onto the CLOSED
linked list. This way, the A* routines don't keep searching from the
same nodes over and over again...
So, every time the A*
algorithm creeps over to another NODE along its
search path, it'll check to see if this particular NODE is on the OPEN
list (to be searched) or the CLOSED list (already searched).
This routine is used to
check both lists (rebuilt):
<c code>
//=====================================================
// Check the desired OPEN/CLOSED LIST for NodeNum..
//=====================================================
struct NODE *CheckLIST(struct NODE *LIST, int NodeNum) {
struct NODE *tNode;
tNode=LIST->NextNode; // Start of OPEN or CLOSED
while (tNode)
if (tNode->nodenum == NodeNum) { // My test!!
return (tNode); } // Found it!
else
tNode=tNode->NextNode;
return NULL; // NodeNum NOT on LIST.
}
</c code>
When you want to check if a NODE with the nodenum (NodeNumS) is on the
OPEN list then you pass into the CheckList routine OPEN. Likewise,
if you want to see if the NodeNum node is on the CLOSED list then you
pass in the CLOSED list. Easy enough. You'll see this routine in use
a little later on..
Since my struct NODES
cross-reference my node[i] locations, I am
testing for the existence of the node on either list by checking if
any NODES on the list have the same NodeNum. If they do then I found
it on the list. If not, then NULL is returned..
NOTE: If I was going to
use a vec3_t in my struct NODE instead of an
index number of some type, I'd have a new field called something like
vec3_t origin; added into my struct NODE and my CheckList() routine would
then become the following:
<c code>
//=====================================================
struct NODE *CheckLIST(struct NODE *LIST, vec3_t location) {
struct NODE *tNode;
vec3_t vtmp;
tNode=LIST->NextNode; // Start of OPEN or CLOSED
while (tNode) {
VectorSubtract(tNode->origin,location,vtmp);
if (VectorLength(vtmp) == 0.0) { // New test here!!
return (tNode); } // Found it!
else
tNode=tNode->NextNode; }
return NULL; // NodeNum NOT on LIST.
}
</c code>
So my new test would have to compare 2 vec3_t vectors. However you want
to make your test, you do it right inside of this routine...
Okay, lets move on..
=======================================================================
=======================================================================
Now, add these helper
functions into your AStar.c file.
<c code>
//=========================================
// Free allocated resources for next search
//=========================================
void FreeStack(struct NODE *PathNode) {
struct NODE *tNode;
struct STACK *STK;
while (PathNode) {
tNode=PathNode;
PathNode=PathNode->PrevNode;
free(tNode); }
while (CLOSED) {
tNode=CLOSED;
CLOSED=CLOSED->NextNode;
free(tNode); }
while (OPEN) {
tNode=OPEN;
OPEN=OPEN->NextNode;
free(tNode); }
while (Stack) {
STK=Stack;
Stack=Stack->StackPtr;
free(STK); }
}
//===============================================
// Propagate Old node's values to Nodes on Stack
//===============================================
void PropagateDown(struct NODE *Old) {
struct NODE *POPNode;
int g,c;
for (c=0;c < NUMCHILDS;c++)
if (Old->Child[c])
if (Old->g+1 < Old->Child[c]->g) {
Old->Child[c]->g=Old->g+1;
Old->Child[c]->f=g+Old->h;
Old->Child[c]->PrevNode=Old;
Push(Old->Child[c]); } // Push onto Stack
while (Stack) {
POPNode=Pop();
for (c=0;c < NUMCHILDS;c++) {
if (!POPNode->Child[c]) break; // No more valid Child nodes!
if (POPNode->g+1 < POPNode->Child[c]->g) {
POPNode->Child[c]->g=g=POPNode->g+1;
POPNode->Child[c]->f=g+POPNode->h;
POPNode->Child[c]->PrevNode=POPNode;
Push(POPNode->Child[c]); } } } // Push onto Stack
}
</c code>
// We'll need this Macro!!
<c code>
#define vDiff(b,a) sqrt((a[0]*a[0]-b[0]*b[0])+(a[1]*a[1]-b[1]*b[1])+(a[2]*a[2]-b[2]*b[2]))
</c code>
The following routine is the main function which A* will use when it
moves to a new node as it creeps along its search path. The parameters
are a StartNode, which is the Parent Node that A* is searching from!
NodeNumS is the nodenum of the node it is checking out, and NodeNumD
is the nodenum of the final destination (this stays the same throughout).
What happens is that as
the A* creeps along, all valid new nodes it finds
in its search pattern (NUMCHILDS) become child nodes of the current node.
So for example, I have a search pattern that is forward, back, left, right,
up and down. Then my NUMCHILDS is 6 because I could have a maximum of 6
valid child nodes (new directions) from any one particular node along the
path. You'll see a good example of a search pattern later on... As the A*
moves merrily along, if, from its current node, only 2 directions are valid
(the rest are in solid or lava/slime or A* already searched that node)
then that current node will only have 2 child nodes associated with it.
Hope this makes sense to you..
NOTE: If the search
pattern I want (you can make your own) is 8 different
directions from the present node then I must have NUMCHILDS = 8. If I want
to have a fancy search that searches in 20 different directions from each
present node then my NUMCHILDS = 20. If the number of searches per current
node does not match your NUMCHILDS value than you'll step off one of your
pointers and kaboom!! This has also been rebuilt..
<c code>
//===========================================
// Successor Nodes all pushed onto OPEN list
//===========================================
void GetSuccessorNodes(struct NODE *StartNode, int NodeNumS, int NodeNumD) {
struct NODE *Old,*Successor;
struct NODE *tNode1,*tNode2;
int g,c;
float h;
//================================
// Has NodeNumS been Searched yet?
//================================
Old=CheckLIST(OPEN,NodeNumS);
if (Old) {
for (c=0;c < NUMCHILDS;c++)
if (!StartNode->Child[c]) break; // Find first empty Child
StartNode->Child[((c < NUMCHILDS)?c:(NUMCHILDS-1))]=Old;
if (StartNode->g+1 < Old->g) {
Old->g=g=StartNode->g+1;
Old->f=g+Old->h;
Old->PrevNode=StartNode; }
return; }
//==================================
// Has NodeNumS been searched yet?
//==================================
Old=CheckLIST(CLOSED,NodeNumS);
if (Old!=NULL) {
for (c=0;c < NUMCHILDS;c++)
if (StartNode->Child[c]==NULL) break;
StartNode->Child[((c < NUMCHILDS)?c:(NUMCHILDS-1))]=Old;
if (StartNode->g+1 < Old->g) {
Old->g=g=StartNode->g+1;
Old->f=g+Old->h;
Old->PrevNode=StartNode;
PropagateDown(Old); }
return; }
//=======================================
// It is NOT on the OPEN or CLOSED List!!
//=======================================
// Make Successor a Child of StartNode
//=======================================
Successor=(struct NODE *)malloc(sizeof(struct NODE));
Successor->nodenum=NodeNumS;
Successor->g=g=StartNode->g+1;
// NOTE: the heuristic estimate of the remaining path from this node
// to the destination node is given by the difference between the 2
// vectors. You can come up with your own estimate..
Successor->h=h=abs(vDiff(node[NodeNumS].origin,node[NodeNumD].origin));
Successor->f=g+h;0
Successor->PrevNode=StartNode;
Successor->NextNode=NULL;
for (c=0;c < NUMCHILDS;c++)
Successor->Child[c]=NULL;
for (c=0;c < NUMCHILDS;c++)
if (StartNode->Child[c]==NULL) break; // Find first empty Child[]
StartNode->Child[((c < NUMCHILDS)?c:(NUMCHILDS-1))]=Successor;
//=================================
// Insert Successor into OPEN List
//=================================
tNode1=OPEN;
tNode2=OPEN->NextNode;
while (tNode2 && (tNode2->f < Successor->f)) {
tNode1=tNode2;
tNode2=tNode2->NextNode; }
Successor->NextNode=tNode2;
tNode1->NextNode=Successor;
}
</c code>
This next routine returns the nodenum of the node[i]
which contains this particular origin.
<c code>
//=========================================
// What is nodenum of this origin, if any?
//=========================================
int GetNodeNum(vec3_t origin) {
vec3_t vtmp;
float dist;
int i;
// Search all of my node[i]'s
for (i=0;i < numnodes;i++) {
VectorSubtract(node[i].origin,origin,vtmp);
dist=VectorLengthSqr(vtmp);
if (dist < 1.0F)
return node[i].nodenum; }
return -1;
}
</c code>
NOTE: If the origin does NOT exist in my huge array of node[i]'s then
it returns -1. Else, if it finds that origin in the node array it
returns that node's nodenum. This way, you can go from any origin in
the map and see if it is a valid node (one in your pre-determined node
array) and get its corresponding nodenum..
You'll also need this
helper routine. Since all nodes placed onto the
OPEN list are sorted by their f value (estimated distance from start to
destination) with the ones with the shortest paths at the front of the
OPEN list, then the next best node will always be the first node on
the OPEN list. This function retrieves that node from the OPEN list and
places it on the CLOSED list. This has been rebuilt too.
<c code>
//=========================================
// Pull FIRST node from OPEN, put on CLOSED
//=========================================
struct NODE *NextBestNode(void) {
struct NODE *Node;
Node=OPEN->NextNode; // Pull from FRONT of OPEN list
if (!Node) return NULL;
OPEN->NextNode=OPEN->NextNode->NextNode;
Node->NextNode=CLOSED->NextNode;
CLOSED->NextNode=Node; // Put at FRONT of CLOSED list
return Node; // Return Next Best Node
}
</c code>
=======================================================================
=======================================================================
Okay, we're all done with
the helper stuff. Now let's get into the guts
of the A* algorithm itself.
Like I mentioned before,
you determine the type of search pattern you want
the A* algorithm to conduct from node to node to node.. The total number
of searches it does per node must equal the NUMCHILDS value as discussed
before... Above, my NUMCHILDS was equal to 6. So, I want the A* algorithm
to search 6 different directions from each node along the path like so.
<c code>
//=========================================
//
// +Z +Y
// | /
// | /
// | /
// -X -----o----- +X
// /|
// / |
// / |
// -Y -Z
//
// 0=+x, 1=-x, 2=+y, 3=-y, 4=+z, 5=-z,
//=========================================
void NextNode(int i, vec3_t v) {
int x=32; // 32 units each x direction
int y=32; // 32 units each y direction
int z=24; // 24 units each z direction
switch (i) {
case 0: v[0]+=x; break;
case 1: v[0]-=x; break;
case 2: v[1]+=y; break;
case 3: v[1]-=y; break;
case 4: v[2]+=z; break;
case 5: v[2]-=z; break;
} // end switch
}
</c code>
So, for each node, it'll check up,down, left,right, forward,backward
for the next node along the path which brings me closer to the final
destination node.. Remember, you can have A* go 3 forward and 2 back
and 5 up and 3 down etc etc etc. Whatever you think is the best
searching pattern that you want. Set up your NextNode() function
accordingly and your A* will be on its merry way. Be imaginative!!
The above NextNode()
routine is called from this routine:
<c code>
//==========================================================
void ComputeSuccessors(struct NODE *PresentNode, int NodeNumD) {
int c,NextNodeNum;
vec3_t vtmp;
for (c=0;c < NUMCHILDS;c++) {
VectorCopy(node[PresentNode->nodenum].origin,vtmp);
NextNode(c,vtmp); // Note: vtmp is changed inside NextNode()
NextNodeNum=GetNodeNum(vtmp); // location in node[] array?
if (NextNodeNum == -1) continue; // vtmp Node is invalid
GetSuccessorNodes(PresentNode,NextNodeNum,NodeNumD); }
}
</c code>
What is happening here? Well. for the present node where A* is
currently at (still heading for its NodeNumD destination node)
for each possible pattern (NUMCHILDS) you've set for yourself,
you copy the present node location and send that vector into the
NextNode() routine for some "adjustment". It adjusts the vector
location according to your criteria and returns the adjusted
vector. Now, you need to do some OPEN/CLOSED list searching so
you'll need to find out what NodeNum corresponds to a vector
with this particular origin, if it even exists! If your GetNodeNum
routine fails to find that origin in your node[i] array, then
it must be an invalid location and it returns -1. You check to see
if the adjusted location is valid. If it is then you pass it into
the GetSuccessorNodes() routine for searching on OPEN/CLOSED list
or making it a Successor Node (because it is valid) and then putting
it on the OPEN list.. You repeat this for each of the searches in
your search pattern. If ALL directions are valid from the PresentNode
then the PresentNode will have all 6 of its children nodes filled up..
So.... from each
PresentNode, A* will search the surrounding area
based on the criteria you've established and generate possible valid
Successor nodes (child nodes of PresentNode) and put them onto the
OPEN list for later searching...
NOTE: that when a node is
put onto the OPEN list it is sorted by
its estimated distance from start to destination (f-value)... All
the valid Successors from the present node are placed onto the OPEN
list (if not already there AND not on the CLOSED list) and the above
function NextBestNode() pulls the next best node (first node on the
OPEN list) as the best possible direction to destination from the
current present node. Get that? If not, go over the source a few
times in your mind...
Now comes the MAIN
findpath() function for A* (also rebuilt)..
You call this FindPath()
function giving it a start and destination
vector and it returns the number of nodes along the path...
I use the following 2
vars.
<c code>
int *Waypoint=NULL; // Integer array of nodenum's along the path
int numpts=0; // Number of nodes in the path..
//=========================================
int FindPath(vec3_t start, vec3_t destination) {
struct NODE *StartNode;
struct NODE *BestNode;
struct NODE *tNode;
int NodeNumD;
int NodeNumS;
int g,c,i;
float h;
vec3_t tstart,tdest;
VectorCopy(start,tstart);
VectorCopy(destination,tdest);
// Get NodeNum of start vector
NodeNumS=GetNodeNum(tstart);
if (NodeNumS==-1) return 0; // ERROR
// Get NodeNum of destination vector
NodeNumD=GetNodeNum(tdest);
if (NodeNumD==-1) return 0; // ERROR
// Allocate OPEN/CLOSED list pointers..
OPEN=(struct NODE *)malloc(sizeof(struct NODE));
OPEN->NextNode=NULL;
CLOSED=(struct NODE *)malloc(sizeof(struct NODE));
CLOSED->NextNode=NULL;
//================================================
// This is our very first NODE! Our start vector
//================================================
StartNode=(struct NODE *)malloc(sizeof(struct NODE));
StartNode->nodenum=NodeNumS;
StartNode->g=g=0;
StartNode->h=h=abs(vDiff(start,destination));
StartNode->f=g+h;
for (c=0;c < NUMCHILDS;c++)
StartNode->Child[c]=NULL;
StartNode->NextNode=NULL;
StartNode->PrevNode=NULL;
//================================================
OPEN->NextNode=BestNode=StartNode; // First node on OPEN list..
for (;;) {
tNode=BestNode; // Save last valid node
BestNode=(struct NODE *)NextBestNode(); // Get next node from OPEN list
if (!BestNode) {
BestNode=tNode; // Last valid node..
break;}
if (BestNode->nodenum==NodeNumD) break;// we there yet?
ComputeSuccessors(BestNode,NodeNumD);} // Search from here..
//================================================
if (BestNode==StartNode) { // Start==End??
FreeStack(StartNode);
return 0; }
BestNode->NextNode=NULL; // Must tie this off!
// How many nodes we got?
tNode=BestNode;
i=0;
while (tNode) {
i++; // How many nodes?
tNode=tNode->PrevNode; }
if (i <= 2) { // Only nodes are Start and End??
FreeStack(BestNode);
return 0; }
// Let's allocate our own stuff...
numpts=i;
Waypoint=(int *)malloc(numpts*sizeof(int));
// Now, we have to assign the nodenum's along
// this path in reverse order because that is
// the way the A* algorithm finishes its search.
// The last best node it visited was the END!
// So, we copy them over in reverse.. No biggy..
tNode=BestNode;
while (BestNode) {
Waypoint[--i]=BestNode->nodenum;
BestNode=BestNode->PrevNode; }
// NOTE: At this point, if our numpts returned is not
// zero, then a path has been found! To follow this
// path we simply follow node[Waypoint[i]].origin
// because Waypoint array is filled with indexes into
// our node[i] array of valid vectors in the map..
// We did it!! Now free the stack and exit..
//================================================
FreeStack(tNode); // Release ALL resources!!
return (numpts);
}
</c code>
I've included these helper routines for you.
<c code>
//======================================================
void G_Spawn_Splash(int type, int count, int color, vec3_t start, vec3_t movdir, vec3_t origin) {
gi.WriteByte(SVC_TEMP_ENTITY);
gi.WriteByte(type);
gi.WriteByte(count);
gi.WritePosition(start);
gi.WriteDir(movdir);
gi.WriteByte(color);
gi.multicast(origin, MULTICAST_PVS);
}
//==================================================
void DrawPath(void) {
int i=0,k;
vec3_t zvec={0,0,0};
for (k=0;k < numpts;k++) {
i=Waypoint[k];
// fprintf(fp,"PathNode[%d]=[%.0f,%.0f,%.0f]\n",i,node[i].origin[0],node[i].origin[1],node[i].origin[2]);
G_Spawn_Splash(TE_LASER_SPARKS, 8, 0xe2e5e3e6, node[i].origin, zvec, node[i].origin);
}
}
</c code>
To use these, you have to have a global var like so:
<c code>
int showpath=0;
</c code>
Inside of your ClientThink function near the top put this:
<c code>
if (showpath) DrawPath();
</c code>
Then, at the bottom of your ClientCommands() in your g_cmds.c file put this:
<c code>
else
if (!Q_stricmp(cmd, "drawpath")) {
VectorCopy(ent->s.origin,start);
VectorCopy(SOME_DESTINATION_POINT, dest);
if (FindPath(start,dest)>0)
showpath=true; }
else
if (!Q_stricmp(cmd, "pathOFF"))
showpath=false;
</c code>
You bind some key to "drawgrid" in your autoexec.cfg file and
come up with a way of getting some destination vector (using
a findradius or something like that). Make a call to FindPath
with your origin and the destination vectors. If FindPath returns
non-zero, then a path exists. You turn on the showpath var for
ClientThink() and the nodes along the path will be displayed!
If you want to turn them
off then bind another key to "pathOFF"
and when you hit that key, ClientThink() will stop drawing the
nodes along the path for you. Cool effects too!
Well... That's it for now..
Now, get out there and
find a path to the BASTARDS and KILL THEM!
Have Fun!
Maj.Bitch
|