1. Trang chủ
  2. » Công Nghệ Thông Tin

Advanced 3D Game Programming with DirectX - phần 5 pptx

71 347 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 71
Dung lượng 333,83 KB

Nội dung

285 Path Following Path following is the process of making an agent look intelligent by having it proceed to its destination using a logical path. The term "path following" is really only half of the picture. Following a path once you're given it is fairly easy. The tricky part is generating a logical path to a target. This is called path planning. Before it is possible to create a logical path, it must be defined. For example, if a creature's desired destination (handed to it from the motivation code) is on the other side of a steep ravine, a logical path would probably be to walk to the nearest bridge, cross the ravine, then walk to the target. If there were a steep mountain separating it from its target, the most logical path would be to walk around the mountain, instead of whipping out climbing gear. A slightly more precise definition of a logical path is the path of least resistance. Resistance can be defined as one of a million possible things, from a lava pit to a strong enemy to a brick wall. In an example of a world with no environmental hazards, enemies, cliffs, or whatnot, the path of least resistance is the shortest one, as shown in Figure 6.6 . Figure 6.6: Choosing paths based on length alone Other worlds are not so constant. Resistance factors can be worked into algorithms to account for something like a room that has the chance of being filled with lava (like the main area of DM2 in Quake). Even if traveling through the lava room is the shortest of all possible paths using sheer distance, the most logical path is to avoid the lava room if it made sense. Luckily, once the path finding algorithm is set up, modifying it to support other kinds of cost besides distance is a fairly trivial task. If other factors are taken into account, the chosen path may be different. See Figure 6.7 . 286 Figure 6.7: Choosing paths based on other criterion Groundwork While there are algorithms for path planning in just about every sort of environment, I'm going to focus on path planning in networked convex polyhedral cells. Path planning for something like a 2D map (like those seen in Starcraft) is better planned with algorithms like A*. A convex cell will be defined as a region of passable space that a creature can wander through, such as a room or hallway. Convex polyhedrons follow the same rules for convexity as the polygons. For a polygon (2D) or a polyhedron (3D) to be convex, any ray that is traced between any two points in the cell cannot leave the cell. Intuitively, the cell cannot have any dents or depressions in it; there isn't any part of the cell that sticks inward. Concavity is a very important trait for what is being done here. At any point inside the polyhedron, exiting the polyhedron at any location is possible and there is no need to worry about bumping into walls. Terminator logic can be used from before until the edge of the polyhedron is reached. The polyhedrons, when all laid out, become the world. They do not intersect with each other. They meet up such that there is exactly one convex polygon joining any two cells. This invisible boundary polygon is a special type of polygon called a portal. Portals are the doorways connecting rooms and are passable regions themselves. If you enter and exit cells from portals, and you know a cell is convex, then you also know that any ray traveled between two portals will not be obstructed by the walls of the cell (although it may run against a wall). Until objects are introduced into the world, if the paths are followed exactly, there is no need to perform collision tests. 287 Figure 6.8: Cells and the portals connecting them I'll touch upon this spatial definition later in the book when I discuss hidden surface removal algorithms; portal rendering uses this same paradigm to accelerate hidden surface removal tasks. The big question that remains is how do you move around this map? To accomplish finding the shortest path between two arbitrary locations on the map (the location of the creature and a location the user chooses), I'm going to build a directed, weighted graph and use Dijkstra's algorithm to find the shortest edge traversal of the graph. If that last sentence didn't make a whole lot of sense, don't worry, just keep reading! Graph Theory The need to find the shortest path in graphs shows up everywhere in computer programming. Graphs can be used to solve a large variety of problems, from finding a good path to send packets through on a network of computers, to planning airline trips, to generating door-to-door directions using map software. A weighted, directed graph is a set of nodes connected to each other by a set of edges. Nodes contain locations, states you would like to reach, machines, anything of interest. Edges are bridges from one node to another. (The two nodes being connected can be the same node, although for these purposes that isn't terribly useful.) Each edge has a value that describes the cost to travel across the edge, and is unidirectional. To travel from one node to another and back, two edges are needed: one to take you from the first node to the second, and one that goes from the second node to the first. Dijkstra's algorithm allows you to take a graph with positive weights on each edge and a starting location and find the shortest path to all of the other nodes (if they are reachable at all). In this algorithm 288 each node has two pieces of data associated with it: a "parent" node and a "best cost" value. Initially, all of the parent values for all of the nodes are set to invalid values, and the best cost values are set to infinity. The start node's best cost is set to zero, and all of the nodes are put into a priority queue that always removes the element with the lowest cost. Figure 6.9 shows the initial case. Figure 6.9: Our initial case for the shortest path computation Note Notice that the example graphs I'm using seem to have bidirectional edges (edges with arrows on both sides). These are just meant as shorthand for two unidirectional edges with the same cost in both directions. In the successive images, gray circles are visited nodes and dashed lines are parent links. Iteratively remove the node with the lowest best cost from the queue. Then look at each of its edges. If the current best cost for the destination node for any of the edges is greater than the current node's cost plus the edges' cost, then there is a better path to the destination node. Then update the cost of the destination node and the parent node information, pointing them to the current node. Pseudocode for the algorithm appears in Listing 6.5 . Listing 6.5: Pseudocode for Dijkstra's algorithm struct node vector< edge > edges node parent real cost struct edge node dest 289 real cost while( priority_queue is not empty ) node curr = priority_queue.pop for( all edges leaving curr ) if( edge.dest.cost > curr.cost + edge.cost ) edge.dest.cost = curr.cost + edge.cost edge.dest.parent = curr Let me step through the algorithm so I can show you what happens. In the first iteration, I take the starting node off the priority queue (since its best cost is zero and the rest are all set to infinity). All of the destination nodes are currently at infinity, so they get updated, as shown in Figure 6.10 . Figure 6.10: Aftermath of the first step of Dijkstra's algorithm Then it all has to be done again. The new node you pull off the priority queue is the top left node, with a best cost of 8. It updates the top right node and the center node, as shown in Figure 6.11 . 290 Figure 6.11: Step 2 The next node to come off the queue is the bottom right one, with a value of 10. Its only destination node, the top right one, already has a best cost of 13, which is less than 15 (10 + the cost of the edge −15). Thus, the top right node doesn't get updated, as shown in Figure 6.12 . Figure 6.12: Step 3 Next is the top right node. It updates the center node, giving it a new best cost of 14, producing Figure 6.13. 291 Figure 6.13: Step 4 Finally, the center node is visited. It doesn't update anything. This empties the priority queue, giving the final graph, which appears in Figure 6.14 . Figure 6.14: The graph with the final parent-pointers and costs Using Graphs to Find Shortest Paths Now, armed with Dijkstra's algorithm, you can take a point and find the shortest path and shortest distance to all other visitable nodes on the graph. But one question remains: How is the graph to traverse generated? As it turns out, this is a simple, automatic process, thanks to the spatial data structure. First, the kind of behavior that you wish the creature to have needs to be established. When a creature's target exists in the same convex cell the creature is in, the path is simple: Go directly towards the object using something like the Terminator AI I discussed at the beginning of the chapter. There is no need to worry about colliding with walls since the definition of convexity assures that it is possible to just march directly towards the target. 292 Warning I'm ignoring the fact that the objects take up a certain amount of space, so the total set of the creature's visitable points is slightly smaller than the total set of points in the convex cell. For the purposes of what I'm doing here, this is a tolerable problem, but a more robust application would need to take this fact into account. So first there needs to be a way to tell which cell an object is in. Luckily, this is easy to do. Each polygon in a cell has a plane associated with it. All of the planes are defined such that the normal points into the cell. Simply controlling the winding order of the polygons created does this. Also known is that each point can be classified whether it is in front of or in back of a plane. For a point to be inside a cell, it must be in front of all of the planes that make up the boundary of the cell. It may seem mildly counterintuitive to have the normals sticking in towards the center of the object rather than outwards, but remember that they're never going to be considered for drawing from the outside. The cells are areas of empty space surrounded by solid matter. You draw from the inside, and the normals point towards you when the polygons are visible, so the normals should point inside. Now you can easily find out the cell in which both the source and destination locations are. If they are in the same cell, you're done (marching towards the target). If not, more work needs to be done. You need to generate a path that goes from the source cell to the destination cell. To do this, you put nodes inside each portal, and throw edges back and forth between all the portals in a cell. An implementation detail is that a node in a portal is actually held by both of the cells on either side of the portal. Once the network of nodes is set up, building the edges is fairly easy. Add two edges (one each way) between each of the nodes in each cell. You have to be careful, as really intricate worlds with lots of portals and lots of nodes have to be carefully constructed so as not to overload the graph. (Naturally, the more edges in the graph, the longer Dijkstra's algorithm will take to finish its task.) You may be wondering why I'm bothering with directed edges. The effect of having two directed edges going in opposite directions would be the same as having one bi-directed edge, and you would only have half the edges in the graph. In this 2D example there is little reason to have unidirectional edges. But in 3D everything changes. If, for example, the cell on the other side of the portal has a floor 20 feet below the other cell, you can't use the same behavior you use in the 2D example, especially when incorporating physical properties like gravity. In this case, you would want to let the creature walk off the ledge and fall 20 feet, but since the creature wouldn't be able to turn around and miraculously leap 20 feet into the air into the cell above, you don't want an edge that would tell you to do so. Here is where you can start to see a very important fact about AI. Although a creature seems intelligent now (well… more intelligent than the basic algorithms at the beginning of the chapter would allow), it's following a very standard algorithm to pursue its target. It has no idea what gravity is, and it has no idea that it can't leap 20 feet. The intelligence in this example doesn't come from the algorithm itself, but rather it comes from the implementation, specifically the way the graph is laid out. If it is done poorly (for example, putting in an edge that told the creature to move forward even though the door was 20 feet 293 above it), the creature will follow the same algorithm it always does but will look much less intelligent (walking against a wall repeatedly, hoping to magically cross through the doorway 20 feet above it). Application: Path Planner The second application for this chapter is a fully functioning path planner and executor. The code loads a world description off the disk, and builds an internal graph to navigate with. When the user clicks somewhere in the map, the little creature internally finds the shortest path to that location and then moves there. Parsing the world isn't terribly hard; the data is listed in ASCII format (and was entered manually, yuck!). The first line of the file has one number, providing the number of cells. Following, separated by blank lines, are that many cells. Each cell has one line of header (containing the number of vertices, number of edges, number of portals, and number of items). Items were never implemented for this demo, but they wouldn't be too hard to stick in. It would be nice to be able to put health in the world and tell the creature "go get health!" and have it go get it. Points are described with two floating-point coordinates, edges with two indices, and portals with two indices and a third index corresponding to the cell on the other side of the doorway. Listing 6.6 has a sample cell from the world file you'll be using. Listing 6.6: Sample snippet from the cell description file 17 6 5 1 0 -8.0 8.0 -4.0 8.0 -4.0 4.0 -5.5 4.0 -6.5 4.0 -8.0 4.0 0 1 1 2 2 3 4 5 5 0 3 4 8 294 more cells Building the graph is a little trickier. The way it works is that each pair of doorways (remember, each conceptual doorway has a doorway structure leading out of both of the cells touching it) holds onto a node situated in the center of the doorway. Each cell connects all of its doorway nodes together with dual edges—one going in each direction. When the user clicks on a location, first the code makes sure that the user clicked inside the boundary of one of the cells. If it did not, the click is ignored. Only approximate boundary testing is used (using two-dimensional bounding boxes); more work would need to be done to do more exact hit testing (this is left as an exercise for the reader). When the user clicks inside a cell, then the fun starts. Barring the trivial case (the creature and clicked location are in the same cell), a node is created inside the cell and edges are thrown out to all of the doorway nodes. Then Dijkstra's algorithm is used to find the shortest path to the node. The shortest path is inserted into a structure called sPath that is essentially just a stack of nodes. While the creature is following a path, it peeks at the top of the stack. If it is close enough to it within some epsilon, the node is popped off the stack and the next one is chosen. When the stack is empty, the creature has reached its destination. The application uses the GDI for all the graphics, making it fairly slow. Also, the graph searching algorithm uses linear searches to find the cheapest node while it's constructing the shortest path. What fun would it be if I did all the work for you? A screen shot from the path planner appears in Figure 6.15 on the following page. The creature appears as a red circle. [...]... with the graph assert( pOut ); return pOut; } void cNode::Relax() { this->m_bVisited = true; for( int i=0; im_fWeight + this->m_fCost < pCurr->m_pTo->m_fCost ) { // relax the 'to' node pCurr->m_pTo->m_pPrev = this; pCurr->m_pTo->m_fCost = pCurr->m_fWeight + this->m_fCost; 296 } } } void cWorld::ShortestPath( sPath* pPath, cNode *pTo,... to be used in a game, state-setting functions would be called in lieu of printing out the names of the output states Listing 6.9: Sample output of the neural net simulator 309 Advanced 3D Game Programming using DirectX 9.0 Neural Net Simulator Using nn description file [creature.nn] Neural Net Inputs: -Ammo (0 1) 1 - Ammo (0 1) Proximity to enemy (0 1) 1 - Proximity to... levels are so simple, we can deal with a // linear algorithm float fBestCost = REALLY_BIG; cNode* pOut = NULL; 2 95 for( int i=0; i m_bVisited ) { if( m_nodeList[i ]-> m_fCost < fBestCost ) { // new cheapest node fBestCost = m_nodeList[i ]-> m_fCost; pOut = m_nodeList[i]; } } } // if we haven't found a node yet, something is // wrong with the graph assert( pOut );... attackEnemy 0 .5 NEURON 1 fleeToHealth 0 .5 NEURON 1 fleeToAmmo 0 .5 # # DEFAULTOUT "string" # string = the default output DEFAULTOUT "Chill out" # #EDGE x y z # x = source neuron # y = dest neuron # z = edge weight # EDGE health attackEnemy 0 .5 EDGE ammo attackEnemy 0 .5 EDGE enemy attackEnemy 0 .5 EDGE healthInv attackEnemy −0 .5 EDGE ammoInv attackEnemy −0 .5 EDGE enemyInv attackEnemy −0.6 # 312 EDGE healthInv findHealth... pFrom->m_fCost = 0.f; bool bDone = false; cNode* pCurr; while( 1 ) { pCurr = FindCheapestNode(); if( !pCurr ) return; // no path can be found if( pCurr == pTo ) break; // We found the shortest path pCurr->Relax(); // relax this node } // now we construct the path // empty the path first while( !pPath->m_nodeStack.empty() ) pPath->m_nodeStack.pop(); pCurr = pTo; 297 while( pCurr != pFrom ) { pPath->m_nodeStack.push(... succeeded The only problem with NFAs is that it's extremely difficult to encode fuzzy decisions For example, it would be better if the creature's health was represented with a floating-point value, so there would be a nearly continuous range of responses based on health I'll show you how to use neural networks to do this However, NFA-based AI can be more than adequate for many games If your NFA's behavior... fleeToHealth 0.8 EDGE enemy fleeToHealth 0 .5 # EDGE ammoInv fleeToAmmo 0.8 EDGE enemy fleeToAmmo 0 .5 # # INPUT/OUTPUT x "y" # x = node for input/output # y = fancy name for the input/output INPUT health "Health (0 1)" INPUT healthInv "1 - Health (0 1)" INPUT ammo "Ammo (0 1)" INPUT ammoInv "1 - Ammo (0 1)" INPUT enemy "Proximity to enemy (0 1)" INPUT enemyInv "1 - Proximity to enemy (0 1)" OUTPUT findHealth... The source code for the neural network simulator appears in Listings 6.11 and 6.12 313 Listing 6.11: NeuralNet.h /******************************************************************* * Advanced 3D Game Programming using DirectX 9.0 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Desc: Sample AI code * * copyright (c) 2002 by Peter A Walsh and Adrian Perez * See license.txt for modification... float cNeuralNet::cNode::GetTotal() const { return m_total; } #endif // _NEURALNET_H Listing 6.12: NeuralNet.cpp /******************************************************************* * Advanced 3D Game Programming using DirectX 9.0 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Desc: Sample AI code * * copyright (c) 2002 by Peter A Walsh and Adrian Perez * See license.txt for modification... distribution information ******************************************************************/ using namespace std; int main(int argc, char* argv[]) { // Sorry, I don't do cout printf( "Advanced 3D Game Programming using DirectX 9.0\n" ); printf( " \n\n" ); printf( "Neural Net Simulator\n\n"); 317 if( argc != 2 ) { printf("Usage: neuralnet filename.nn\n"); return 0; } printf("Using . snippet from the cell description file 17 6 5 1 0 -8 .0 8.0 -4 .0 8.0 -4 .0 4.0 -5 .5 4.0 -6 .5 4.0 -8 .0 4.0 0 1 1 2 2 3 4 5 5 0 3 4 8 294 more cells Building. pCurr->m_fWeight + this->m_fCost < pCurr->m_pTo->m_fCost ) { // relax the 'to' node pCurr->m_pTo->m_pPrev = this; pCurr->m_pTo->m_fCost = pCurr->m_fWeight. health!" and have it go get it. Points are described with two floating-point coordinates, edges with two indices, and portals with two indices and a third index corresponding to the cell

Ngày đăng: 08/08/2014, 23:20

TỪ KHÓA LIÊN QUAN