AI is an important part of making games today more fun and immersive. The personality of a NPC is based upon a myriad of things, how they navigate the world, perceive the world around them and react accordingly. One of the most direct ways a designer can give an AI personality is through how it navigates the world. But the designer shouldn't have to worry about the actual movement implementation, they should think about higher level things. As it is the industry is moving away from scripted AI anyways to a more dynamic solution.
The traditional way of doing AI and how it's done currently involves sending go to commands to an NPC. Meaning, the NPC gets the destination coordinates, finds a path to the destination and scans the path for impeding obstacles to avoid. This scanning system is also used to find targets in the level and manages the AI's threat perception. It tells the AI where to spawn, in an area that's safely away from any current threats.
World Representations
There have been various ways to lay out the world for the AI to traverse including a 2D grid, way-point graphs and poly/nav meshes. A combination of those elements is used as well with nav meshes gaining considerable popularity. The A* algorithm is used typically for path finding on the laid-out, navigation world as it is the most efficient and quickest.
The Insomniac AI uses nav in the following steps:
- Game AI sets up the NPC's nav requirements, so basically the go-to command or destination of the NPC
- The nav system performs pathfinding, most likely using the A* algorithm and smooths out the result
- The steering system looks through the path to find any obstacles and how to avoid them
- The game AI then takes the steering output and turns it into animation input
- The final position and orientation of the NPC is acheived
Resistance Fall of Man used the above nav-mesh representation, where the designer would lay out the entire nav-mesh in Maya. The polys acted as nodes and the edges were the connections, used for pathfinding in A*. As it turned out, navigation was one of the big bottlenecks for the PS3 launch, and this could only support 8 navigating NPCs at one time. To fix this they implemented an AI lod system so NPCs at a distance did not use nav, but this was only during that initial ps3 experimental period, where developers were still figuring out how to program for the architecture.
The scale between ROFM and Resistance 2 also changed drastically. Three RFOM levels could make up one R2 level, and while level streaming took care of rendering the level out, the nav was way too large, almost by a poly density of 3x. So they removed the AI lod and bots could use the nav system from a distance, primarily because they were developing an 8-player co-op mode, which needed enough NPC's to entertain all the player, concurrently.
To optimize for this they removed the excess nav mesh loaded for the entire level and partitioned it into clusters so the SPU could have direct memory access. They also changed from convex, 8 sided polys to a triangular mesh representation which was better optimized. The triangle edges represented A* graph nodes and the polys were connections between them. This also opened the door for jump-up and jump-down distance customization in the search graph.
A Hierarchy
The path-finding system would use hierarchical path-finding, where the A* algorithm would calculate a coarse-path among the poly-clusters which then would be refined for the NPC's immediate path in that specific poly cluster. This ran into a problem however where it wouldn't give the shortest path the designer expected so path caching was added. This system allowed the NPC the check a request for a path against the last successful path. They also made cost estimates in A* more efficient, or rather give them more work to do like removing edge mid-point to edge mid-point and replacing it with a computed point on the edge that minimized cost. The final output paths didn't require as much smoothing as before and they just ended up throwing out the hierarchical path finding system. A* was so efficient it hardly used 10% of the navigaton SPU budget, even with a 100 NPC's on screen using only nav.
Path-finding was improved to add more parameters like the ability to jump up/down various distances, using one nav mesh for varied size NPC's, preferred corner radius etc. The game-play code itself was changed to use nav data as queries, where the AI synced the previous frame's results and set the query for the next frame, with all the code streamlined to batch all of the queries and offload it to the SPUs.
The SPU
The SPU's actual job involved several things including: finding a point on a mesh, finding a path, and looking for obstacles.
Give a volume and parameters, the mesh query algorithm outputs the closest point on the mesh, maintaining the NPC's tolerance radius from the boundary. The find path query simply used A* and path smoothing while obstacle processing gathered obstacles in each path which could be hand optimized for common routines.
The engines game loop is divided into pre-update, update and post update. In pre-update the engine reads in nav query and steering results from the previous frame and sets up the nav queries for the next frame. In the update the nav SPU job gets added, along with all the other jobs, (physics, animation). This job can theoretically run anywhere after the pre-update to the next frames pre-update, bypassing the post update. The last stage only handles syncing of animation and physics jobs before handing it off to the rederer.
For path smoothing, one big mesh is used so that string pull doesn't pull the NPC's tolerance away at corners, which is especially true for large NPCs, like the Titan, which would constantly try and cut across the edges.
Steering
The steering in R2 was very light weight and required that the NPCs be running at all times, which caused issues while cornering, because the animations would not slow down enough. The NPC needs to get to the first bend point in the path and also orient towards the next bend point. This was done in the nav SPU job which computed a bezier approach curve for the bend point.
This was done by using a swept sphere check in the current NPC facing direction to compute the start curve tangent, and doing a back projection swept sphere check from the first bend point in the opposite direction of the next bend point for the end curve tangent. These two tangents are then used to compute the bezier curve, with the NPC aimed at the mid-point for smoother approach.
For higher volumes of NPC's, like 8 player coop, a simply dynamic avoidance system was used. The SPU would just compute escape tangents for each obstacle it came across. Specifically it would look at all escape tangents spanning in a circle around the NPC. It would pick the direction based on the vacant hole closest to the direction, closest to the next path bend point. Sometimes though, it could pick the closest escape tangent pointing to a wall. If that were the case, the SPU job would flag the escape tangent, and steering would be expanded by 90 degrees.
Some levels contained about 100 grims using nav, running down narrow corridors which would cause issues while cornering (the animations again would not slow down enough). They spent nearly 80% of their time on nav, treating obstacles and flagging boundaries. This was prevalent enough to have one obstacle in ever three NPC's paths, so they introduced a special lookup cache which stored all the obstacles tangents that were 2x tolerance away from the boundary. If the obstacle was part of the cache when looked up, then the tangent flag was bypassed. These caches were rolled into the other queries and the SPU nav job now included this in its calculations.
Target Scanning
A large portion of the processing went into maintaining the tolerance away from the boundary for the NPCs. They tested out rendering the nav mash into the buffer and eroding to get the pixel threshold away from the boundary, but the designers wanted to shrink that threshold dynamically in game, so they implemented offset-polygon processing code.
Given any NPC, this code would shrink the nav-mesh polys by its threshold distance, making the resulting nav mesh smaller and thus less taxing.
Custom Links
Custom links are basically non-mesh links used for activities other than straight path-finding, like jumps, ladders, tunnels etc. They were traditionally implemented as point to point links, but the team decided to use a more logical, design-oriented approach. They used clue boxes, or just boxes that specified a link, rather than just a point, with a special front face. As long as the front face of the box intersected the nav mesh, then it was considered valid, and more designer friendly. These were also added in A* as nodes in the triangular mesh.
Two custom clues, whether clue boxes or placed in A*, could be connected by the designer to make a custom link (like a jump, or ladder climb). The NPC using the custom link has the ability to modify the link's cost in A* and subsequent path queries see that link cost until the NPC using it frees it.
The path finding query has a 64-bit field indicating custom abilities the AI using the query has, and every link type is associated with a bit. An animation hint is in the link's data, which the AI uses to blend to contextual animations that deal with traversing that link (like climbing up a ledge).
Navigation Mesh Generation
In R1 and R2 the designer and QA would initially generate the nav-mesh in Maya, but after R2 they moved to a custom nav-mesh poly layout editor, afterwards which they moved to a auto generating scheme. The auto generated nav meshes cuts a lot of designer time out and aids in iteration and allows for the generation of multiple nav meshes for different sized NPCs (which can replace the aforementioned offset-polygon processing). Insomniac adopted a "recast" navigation into their pipeline which was developed by Mikko Mononen.
The steps for nav mesh generation are as follows:
- voxelize the scene geometry
- build navigable voxels, which involved filtering and smoothing rendered voxels, followed by eroding resulting navigable voxels
- build distance transform, or closest distance from boundary for voxel, where each catchment basin becomes a mesh chunk
- trace catchment basin's contours with filtering which forms the nav mesh's boundary edges
- triangulate/tesselate to generate final nav mesh
Building from voxels can lead to issues however, like introducing discontinuities, which forms a undesired poly-soup.
Because designers like to do some customization and any post-generation tweaking is in-flexible, custom override polys are used. These custom override polys are added in by designers themselves and added into the nav-mesh rendering process. They can be used for additional things however, like removing the nav-mesh in unwanted areas, and closing holes in unwanted nav mesh, due to holes in the scene.
The generation algorithm can also be tweaked to add or remove sleeping/dynamic elements from the nav mesh.
As seen above it can support dynamically rendered nav meshes, that is nav meshes added to new geometry added at run time. They would clear the voxel data out and use the nav mesh as render geometry. This approach can provide cost-cutting benefits like saved CPU and not using as many pre-loaded nav meshes.
All images and information taken from Reddy Sambavaram's, 2011 GDC talk.