From: gamedev.net Author: Patrick Lester [pwlester@policyalmanac.org] Translation: Sun Jun [tlwanan@gmail.com] Although a * (reading a star) algorithm for beginners is more esoteric, but once you find the path, it will become very simple. There are a lot of articles on the Internet that explain a * algorithm, but most of them are for those who have a certain foundation. What you see here is actually for cainiao. This article does not want to make some authoritative arguments about this algorithm, but describes its basic principles and lays the foundation for your understanding of more relevant materials and discussions. Some good links are provided at the end of the article, which are placed after the "advanced reading" section. Finally, this article is not a programming specification. You may compile the content described here into any computer language. At the end of this article, I also provide a download link for the sample package, which may be your intention. In this package, there are two versions of program code: C ++ and blitz basic. If you just want to see how the * algorithm works, this package also contains files that can be directly executed for your research. We still need to surpass ourselves (to understand algorithms). So, let's start from scratch!
Preliminary: Search for regions
Let's assume that a person is going to arrive at point B from point A, and a wall separates the two points, as shown in. The green part indicates the start point A, and the red part indicates the end point B, the blue square area represents the wall.
[Figure 1]
First of all, you will notice that we have divided this search area into squares, which simplifies the search area and is the first step in searching for paths. This method simplifies our search area into a common two-dimensional array. Each element in the array represents a square. The status of the square is marked as passable and inaccessible. The path between AB is obtained by finding the square passing through from point A to point B. After finding the path, the person can move from the center of a grid to the center of another grid until he reaches the destination. The midpoint of these grids isNode. When you see something about finding a path elsewhere, you will often find people discussing nodes. Why don't they be called squares directly? Because you do not have to separate your search areas into squares, rectangles, hexagonal shapes, or any other shapes. What's more, the node may be in any of these shapes? In the middle, by the side, or something. We use this setting because, after all, this is the simplest case.Start searchAfter we simplify the search area to some nodes that are easy to operate, we need to construct a search to find the shortest path. In the * algorithm, we start from vertex A, check its adjacent nodes in sequence, and then continue and expand outward until we find the destination. We can use the following methods to start searching: 1. From vertex A, add Vertex a to an "open list" that stores the square to be checked. This open list is a bit like a shopping list. There is only one element in the current list, but there will be more in a moment. The square contained in the list may be the square you want to go through, or not. In short, this is a list containing squares to be checked. 2. Check all reachable or accessible squares adjacent to the starting point a, and add them to the open list without wall, water, or any other ineffective terrain. For each adjacent square, point A is saved as their "parent square ". When we want to trace the path, the parent Square is a very important element. We will explain it in detail later. 3. Remove square a from the open list and add a to a "closed list. Closed lists store squares that you don't have to consider now. In this case, you will get the result as shown in. In this figure, the dark green square in the middle is your starting square. All adjacent squares are currently on the open list and are marked in bright green. Each adjacent square has a gray pointer pointing to its parent square, that is, the start square.
[Figure 2]
Next, we select an adjacent square in the open list and repeat the process several times as described above. But which one should we choose? The one with the minimum F value.Path sortingThe key to determining which squares will form the path is the following equation: F = G + H here
- G = the overhead of moving from start a along the generated path to a given square.
- H = estimated moving overhead from the given square to the target square. This method is often called testing. It is a bit confusing. In fact, it is called the test method because it is just a guess. We don't actually know the actual distance before finding the path, because everything may appear on the road (walls, water, or something ). This article provides a method for calculating the H value. There are many other articles on the Internet that introduce different methods.
The path we want is generated by repeatedly traversing the open list and selecting a square with the minimum F value. This article will discuss this process in detail later. Let's take a further look at how to calculate that equation. As mentioned above, G is the moving overhead from starting point a along the generated path to a given square. In this example, we specify 10 for each horizontal or vertical movement, the overhead of diagonal movement is 14. Because the actual distance between the diagonal lines is the square root of 2 (don't be scared), or 1.414 times the overhead of horizontal and vertical movement. For simplicity, we use the Values 10 and 14. The ratio is probably the same, so we avoid the calculation of the square root and decimal places. This is not because we are stupid or do not like mathematics, but because such numbers are much faster for computers. Otherwise, you will find that the search path is very slow. We need to calculate the G value of a given square in a specific path by finding out the G value of the parent square of the square, and according to the relative position (angle or non-angle direction) with the parent square) to add 14 or 10 to the G value. In this example, this method will be used more and more as the number of squares is getting farther and farther from the start point. There are many ways to estimate the H value. The Manhattan method is used to calculate the H value obtained by multiplying the number of squares that reach the destination through horizontal and vertical translations by 10. The Manhattan method is called because it is like calculating the number of city blocks that move from one place to another, and you usually cannot cross the blocks diagonally. It is important that you do not consider any obstacle when calculating the H value. This is an estimate of the remaining distance rather than the actual value (it is usually necessary to ensure that the estimated value is not greater than the actual value ). This is why this method is called a test method. Want to know more? Here are more about the multimodal model and additional instructions on the test method. F is obtained by adding G and H. Shows the search result. F, G, and H values are marked in each square. As shown in the square on the right of the Start square, F is displayed in the upper left corner, G is in the lower left corner, and H is in the lower right corner.
[Figure 3]
Let's look at these squares. In the square with letters, G = 10, because it is only one square away from the start point in the horizontal direction. The start point is close to the top, bottom, and left of the same G value of 10. The G value of the diagonal line is 14. The H value is obtained by estimating the Manhattan distance from the red target square. The square on the right of the start point obtained by this method has three squares, and the H value of the square is 30. The square above has four squares (note that they can only move horizontally and vertically), and H is 40. You can probably see how the H value of other squares is calculated. The F value of each square is, of course, the sum of G and H values.Continue searchTo continue searching, we simply select a square with the minimum F value from the open list, and then perform the following operations on the selected square: 4. remove it from the open list and add it to the closed list. 5. Check all adjacent squares and ignore the squares that fail or are already in the closed list. If the adjacent square is not in the open list, add it. Set the selected square to the parent square of the New Square. 6. If an adjacent square already exists in the open list (meaning it has already been detected, and the parent square-translator has already been set), check whether there is a better path to the square. That is to say, whether the G value of the selected square will be smaller from the selected square to the square. If not, no operation is performed.
On the contrary, if the G value of the new path is smaller, the parent square of the adjacent square is reset to the selected square. (In the middle, change the pointer direction to the selected square. Finally, recalculate the F and G values of the adjacent square. If you are confused, the following illustration will be provided. Let's take a look at the specific example. In the initial nine squares, when the start square is added to the closed list, there are eight squares left in the open list. Among the eight squares, the square on the right of the Start square has a minimum value of 40. So we chose this square as the next Central Square. It is highlighted in blue.
[Figure 4]
First, we remove the selected square from the open list and add it to the closed list (So mark it in bright blue ). Then test its adjacent nodes. The square on the right side of the square is a wall, so no matter what they are. The left side is the start square, and the start square is already in the closed list, so we don't care about it. The other four squares are already in the open list, so we need to check whether it is better if the path goes through the selected square. Of course, G value is used as a reference. Let's take a look at the square in the upper right corner of the selected box. Its current G value is 14. If we reach that square through the current node, the value of G is 20 (the value of G to the current square is 10, and then 10 is added when the grid is moved up ). The G value of 20 is larger than 14, so this path is not better. You can easily understand the graph. Obviously, moving from the starting point to the angle is more direct than moving one grid horizontally and then moving one grid vertically. When we test all the four squares in the open list in sequence based on the above process, we will find that the current square will not form a better path, so we will keep the current situation unchanged. Now we have processed all adjacent squares. Prepare the next square. Let's traverse the open list. Currently, there are only seven squares. Let's pick the minimum F value. Interestingly, in this case, there are two squares with a value of 54. So how should we choose? In fact, it doesn't matter which one you choose. If you want to consider the speed, it will be faster to select the one you recently added to the open list. When it is getting closer and closer to the destination, it is more inclined to select the last detected square. In fact, this really doesn't matter (the difference in processing this makes two versions of a * algorithm get different paths with equal length ). Select the one below, that is, the one shown on the right of the Start square.
[Figure 5]
This time, when we test the adjacent square, we find that the one next to the right is the wall, and it doesn't matter. The one next to the above is also ignored. We don't care about the square under the right wall. Why? Because it is impossible for you to switch through the corner to reach the grid directly. In fact, you have to go down first and then pass through the square. This process is centered around the corner. (Note: the rule that passes through the corner is optional, depending on how your nodes are placed .) Then there are five other adjacent squares. The two below the current square are not in the open list, so we will add them and use the current square as their parent square. Two of the other three are already in the closed list (the two are marked with bright blue in the figure, the start square, and the square above), so you don't have to worry about it. At last, the left side of the current square is located. Check whether the G value will be lowered from the current node. The result does not work, so we have processed it again, and then checked the next grid in the open list. Repeat this process until we add the target square to the open list. It looks like this at that time.
[Figure 6]
Did you notice? The position of the two cells under the START square, where the lattice is different from the previous figure. Previously, its G value is 28 and points to the square in the upper right corner. Now its G value is 20 and points to the square above. This change occurs when the G value of a new path is verified. Then its parent square is reset and the G and F values are recalculated. In this example, this change does not seem very important, but in many cases, this change will make the best path to the target very different. So how can we automatically obtain the actual path? It is very simple. As long as the red target square begins to move in the direction of the pointer of each square, it will arrive at their parent square in sequence, and eventually it will surely arrive at the start square. That's your path! As shown in. Moving from square a to square B is about to move from the center of each square (node) along this path to the center of another square until the terminal is reached. Easy!
[Figure 7]
A *Algorithm Summary1. put the Start Node in the open list (both F and G values of the Start Node are regarded as 0); 2. repeat the steps below: I. find the node with the minimum F value in the open list and use the nodeCurrent NodeII.Current NodeDelete from the open list and add it to the closed list. III. PairCurrent NodeEach adjacent node performs the following steps in sequence: 1. IfAdjacent nodesUnavailable orAdjacent nodesIf it is already in the closed list, no operation is executed and the next node is verified. 2. IfAdjacent nodesIf the node is not in the open list, add the node to the open list andAdjacent nodesThe parent node of isCurrent NodeAnd saveAdjacent nodesG and F; 3. IfAdjacent nodesIn the open list, it is determined thatCurrent NodeArrive at thisAdjacent nodesWhether the G value of is smaller than the previously saved G value. If it is smallerAdjacent nodesThe parent node of isCurrent NodeAnd resetAdjacent nodesG and F values of. IV. cycle end condition: WhenEnd NodeWhen the node is added to the open list as the node to be verified, it indicates that the path is found and the cycle should be terminated. If the open list is empty, it indicates that no new node can be added, if no end node exists in the verified node, the path cannot be found and the cycle ends. 3. slaveEnd NodeStart traversing along the parent node, and save the coordinates of the entire traversal node. The retrieved node is the final path;A little emotionForgive me for my digress. However, it is worth noting that when you see a lot of things about a * path search algorithms on the Internet and forums, occasionally, some people may encounter situations where the * algorithm is not really. In fact, to apply the true a *, we need to include the elements we discussed above: Specialized open list and closed list, and sort the path statistics by F, G, and H values. There are many other path search algorithms, but none of them are a *, and a * is generally considered to be the best among these algorithms. The reference document at the end of this article contains a discussion of these algorithms by Bryan stout. Some algorithms may be better in certain situations, but you must at least understand what you are going to do. Okay, it's almost the same. Let's go back to the topic.Precautions for implementationNow that you have understood the basic algorithms, we will discuss some things you should consider when writing your own program. The following materials are related to the program I wrote in C ++ and blitz basic, but those points apply to any other language. 1. Maintenance of Open list: it is actually the most time-consuming factor in the * algorithm. Each time you process the open list, you need to find the square with the minimum F value. There are several ways to do this. You can save the required path elements and traverse the list to find the square with the minimum F value. This method is very simple, but it will be very slow when the path is very long. It can also be improved to maintain an ordered list. In this way, you only need to obtain the first element of the ordered list when you need the smallest F-value square. I started to use this method when I was writing my program.
This method is suitable for maps that are not big. But this is not the fastest solution. A serious programmer who really pursues speed often uses a binary heap. This is also the method I use in my code. In my experience, this method is at least two to three times faster than most solutions, and faster (more than 10 times faster) when the path is long ). If you are interested in learning more about binary heap, read this article, using binary heaps in a * pathfinding. 2. Other units. If you read my sample code carefully, you will notice that it completely ignores other units on the map. My Pathfinder actually passes through each other directly. It depends on the game. If you want to consider other units on the map and they can be moved, I suggest you ignore them in the path search algorithm and write some code to check whether they have collided. When a collision occurs, you can immediately generate a new path or apply some standard moving rules (for example, always move right) until there are no obstacles or obstacles on the path, then generate a new path. Why do you consider these units when you first calculate the path? The reason is that other units are moving, and their positions are constantly changing. This will lead to some incredible results. For example, a certain unit will suddenly turn to avoid another unit that is not actually there, or hit a unit that is subsequently moved to its predefined path.
Ignoring other units in the pathfinding algorithm means you have to write some separate code to avoid conflicts. This is entirely determined by the game features. So I leave the solution for you to think about. At the end of this article, refer to the section Bryan stout which provides several solutions (such as robust tracking. 3. some speed-up tips: when you develop your own a * program or change the one I wrote, you will always find that this path-Finding Algorithm consumes a lot of CPU, especially when there are a large number of Pathfinder and map on the map. If you read some materials online, you will know that this is no exception for a professional game like Starcraft or the age of empire. When you find that your pathfinding algorithm slows down your writing, you can refer to the following methods to speed up:
- Use a smaller map or less Pathfinder.
- Do not compute paths for multiple Pathfinder instances at the same time. It is better to put them in a queue and scatter them in several game loops. If your game is running at most, for example, 40 cycles/second, no one will notice it. However, if a bunch of Pathfinder compute paths at the same time and slow down the game in a short time, it will be very noticeable.
- Divide the map blocks into larger ones. This will reduce the total number of blocks in the search path. If you are strong enough, you can design more than two pathtracing systems to adapt to different path lengths. This is exactly what those professionals do. When the path is long, large blocks are used. When the path approaching the target is short, cell blocks are used for exact search. If you are interested in this concept, read my article two-tiered A * pathfinding.
- Consider using a roadmap system to handle long paths, or pre-process paths that are rarely changed for the game.
- Preprocessing the map and calculating which areas cannot be reached from other places. I call these regions "Island ". In reality, these areas may be islands or walls. A * one drawback of the algorithm is that when you find a path in such an inaccessible area, you will almost search the entire map, it will not stop until every block on the map is searched. This will waste a lot of CPU time. The solution to this problem is to determine in advance which areas are not reachable (through flood or other methods), record the data with an array and check before finding the path. In my blitz version code, I created a map Preprocessor to complete this process. This pre-processor can also find dead corners that can be ignored in the pathfinding Algorithm in advance, thus greatly improving the algorithm speed.
4. overhead of terrain diversification: In this getting started tutorial and my accompanying program, there are only two kinds of terrain and landform: accessible and inaccessible. But if there are still some terrain, it is possible to pass, but the mobile overhead is a little bigger? What about swamp, Hill, and ladder in the dungeon? These are examples of accessible terrain, but they have higher overhead than open flat. Similarly, highway terrain has less overhead than suburban roads.
This problem can be easily solved. You only need to add the overhead of the terrain when calculating the G value of a given block. When an extra overhead is added to a block, the * algorithm is still valid. In the simple example I described, the terrain can only be divided into two types: Pass and not pass. The A * algorithm will find the most direct and shortest path. However, in a diversified terrain environment, the minimum overhead path may be a long journey. For example, bypassing the road is obviously less costly than directly passing through the swamp.
Another interesting way is that professionals call "inductive ing. Just like the diversified terrain overhead described above, you can create an additional point system and apply it to the AI path. Suppose there are a bunch of guys on a map guarding a mountain road. Every time a computer sends someone through that road, they will be trapped. Then you can create an inductive map to increase the overhead of those blocks in which many combat conflicts occur, so that you can tell your computer to select a safer path, and avoid continuing to dispatch personnel through a path based on the shortest path (but more dangerous. 5. Deal with areas not explored: Have you ever played PC games that always know how to go, or even those that haven't been explored by maps? From this point of view, it seems unrealistic to search for an algorithm in this path. Fortunately, this problem can be easily solved.
The method is to create an independent "known Passable" array for different players and computers (not every unit, otherwise it will consume a lot of memory. Each array contains information about the regions that players have explored and unknown regions, and treats all unknown regions as accessible areas unless other terrain is discovered later. Using this method, the unit moves around to the dead corner or makes similar errors until they discover the path around them. Once all the maps have been explored, the path search algorithm runs normally. 6. Smooth path: the * algorithm will get the path with the minimum overhead and the shortest path, but it cannot get the seemingly smoothest path. Take a look at the final path of the example (Figure 7). The first step of the path is the square at the bottom right of the starting point. If the first step is the square at the bottom, isn't the obtained path much smoother?
There are several ways to handle this problem. For example, when calculating a path, you can add an additional overhead to the squares that change the path direction to increase the G value. In this way, you can follow the obtained path to see which choices can make your path look smoother. For this topic, you can refer to toward more realistic pathfinding (which is free, but you need to register it) to learn more. 7. Non-square search area: In the above example, we use a simple two-dimensional square layout. You can choose not to do this, as well as irregular areas. Think about the countries in the board game risk and risk. You can design a path-finding level. To do this, you have to create a search table to store the neighboring countries of each country and the overhead g for moving from one country to another. You also need to select a method for calculating the estimated value H. Other processing methods are similar to the previous example. Only when a new region is verified, that is, when a new project is added to the open list, the neighboring countries are queried from the table instead of the neighboring square.
Similarly, you can create a Road Map System for a map with fixed terrain. A road sign is usually a point that crosses a path. It may be on the road or in the main tunnel of a dungeon. For game designers, these road signs need to be set in advance. If there is no obstacle between the straight lines formed by two road signs, it is called "adjacent ". In the risk game example, you store adjacent information in a search table and use the search table when adding an open list element. Then you will use the relevant g values (which may be obtained through the straight line distance between the two nodes) and H values (which may be obtained through the straight line distance from the node to the end) other operations are similar to common operations.
For another example of an archive RPG Game that uses a non-square search area, see my article two-tiered A * pathfinding.
Advanced Reading
Okay! Now you have mastered the basic knowledge and have some impressions on advanced concepts. Here I suggest using my code for research. The compressed package has two versions: C ++ and blitz basic. Their comments are very detailed and I think they should be easy to understand. Below is the link:
- Sample Code: A * Pathfinder (2D) version 1.71
If you cannot use C ++ or blitz basic, two program files can be run directly in C ++. The Blitz basic version can only run without downloading the free demo version of blitz basic 3D on the blitz basic website. Here is an online example written by Ben o'neill. You should also read the following webpage. These should be well understood after you read this article.
- Amit's a * pages: This is a widely cited article by Amit Patel. However, if you do not read this article first, you may be confused. It is worth reading. In particular, Amit has a unique view on this.
- Smart moves: intelligent path finding: This article by Bryan on the gamasutra.com website needs to be registered for reading. However, registration is free of charge and it is worth the trouble for this article. Bryan used the program written in Delphi to help me understand a *, which is also the source of inspiration for my A * program. This article also describes some variants of *.
- Terrain Analysis: This is a high-level article, but it is interesting. It was written by Dave Pottinger, an expert at ensemble Studios. This guy is responsible for coordinating the development of age of kings II. It is impossible to understand everything here, but it may bring some interesting ideas to your project. This article also includes some advanced AI/path searching concepts about texture maps, inductive ing, and others. The discussion about flood filling is a source of inspiration for my map preprocessing code, such as dead corners and islands. This is included in my blitz BASIC program ..
Other articles worth reading:
- Aiguru: pathfinding
- Game AI Resource: pathfinding
- Gamedev.net: pathfinding
Okay! If you use any of the above ideas when writing a program, I will be very happy. You can contact me via email:
Good luck!