The Goal
Given a dynamic moving field of objects, leverage the existing capabilities of the physics engine to allow an entity to navigate from any position to any other position while appearing to realistically avoid the objects that may collide with the entity.
That is an awful lot to say. It is also a pretty tall order when you look at the video (below) and realize that there are dozens of objects all moving around all the time while the entity is plotting paths through them.
Why use the physics engine?
If you take apart the pieces that you need to put a game scene together where the AI has some chops, you are going to find you need:
- A way to partition the space and make queries about what is where to reduce CPU cycles.
- A way to make the bodies move about under your control and look reasonable while they do it.
- A way to tell you when collisions occur and have the involved parties take action.
All of these features are present in the Box2D physics engine. The model for it is relatively easy to use and the execution speed is very good. Writing a cell space partition and basic physics system might be fun, but there are better things to do with the time (for now).
What is meant by "realistically avoid"?
When
Han Solo flew the
Millennium Falcon through the asteroid field in
The Empire Strikes Back, he makes the famous quote "Never tell me the odds!". And as we all know, the odds are not good. So maybe "realistically avoid" is a bit of an overstatement, because if the ship can do that, it's a bit more than you should expect.
The effect that we are looking for is to
not overtly cheat in how the ship files around the moving obstacles and have it fly around them using the physics it is endowed with (mass, momentum, finite turn torque, etc.).
- The collision should not be avoided by magic such as having the rocks pass through the ship or having the ship teleport around them.
- The rocks should not have their velocity reduced to such a low speed that they are essentially fixed objects.
- The rocks should not move in predefined paths (although this is an interesting option)...if they get bumped they need to behave with the real physics of the situation.
The entity should be able to plot a path through the rocks that it sees near it and also a "general path" towards its final goal, if it can get there.
If the entity is sitting idly by and a rock comes hurtling at it, if it can move in time, it should get out of the way and escape to a safe location.
Sometimes collisions should occur. The AI should not (and probably cannot) be perfect without forcing the rocks to avoid the ship or otherwise "cheating". NOTE: "Cheating" is not a bad thing, the end goal is to have the effect look good; if the player is entertained, they don't usually don't care how the effect was achieved.
The Big Picture
The diagram below shows the major components of the system.
Main Scene
The Cocos2d-x framework, along with the classes on the left side of the image, make up the "View" of the
Model View Controller (MVC) model for the system. The Main Scene acts as the "Controller", taking the user input and sending it directly down to the model (the stuff on right side of the image).
The Viewport approach has been discussed in
other blog entries. In previous versions, the Viewport was manipulated (scrolled, pinched, zoomed) inside of the Main View. In this incarnation, a "camera" was created to hide all the guts of making this work, making it more portable to other applications.
We're not going to talk about the View or Controller too much more in this article.
The General Approach
The Box2D engine allows for objects to be "solid" and have normal collision responses, but also allows for objects to pass through each other in various ways (see
this link and
this link for a good tutorial on this...yes, we reference other developers...they have good stuff). One special class of "intangibility" for Box2D are the "sensors". If you mark a fixture as a sensor, then everything will pass through it, but you will still get notified about each object that collides with it.
Given the above, here is the general strategy:
- Lay out an arrangement of closed convex geometrical objects that are all marked as sensors. A "grid" of squares was chosen this time, but it could be circles, hexagons, rectangles etc., They don't have to be on a grid either, they can be in any arrangement that meets your needs. What they have to do is reasonably cover the area that you want to navigate through.
- Create a special contact handler that, for each sensor, counts up when a contact begins and counts down when a contact ends. If the numbers line up (and they appear to), then the space inside the shape is "empty" if the count is 0 and "not empty" otherwise.
If you watch the video, you can see that "under" the asteroids are squares that appear/disappear as the asteroids move. This is the
GraphSensorContactLayer which shows the sensors that are "not empty" and hides them when they are "empty". There are also flags associated with the entities themselves that the contact listener uses to say "ignore me". This lets the ship fly around without tripping the sensor counts (this is important for the graph search).
So, we have a grid of sensors and as the asteroids dance around, the spaces which are traversable are known and dynamically updated.
When the sensors are created, they are placed into a graph, along with information about which sensors are adjacent to each other, and a distance between them (Euclidean).
Together, this gives you a graph of the space you want to navigate across that updates using the physics engine automatically to tell you which parts of the graph can be used.
The last part, is to develop a series of search algorithms that use the graph and also are aware that nodes can be "not empty" so that they are effectively skipped in the search. This is simply a "blocked" flag on the node (or the edge, though this may complicate the search when you consider which direction the edge is going).
What the Other Pieces Do
The
Model part of MVC is filled in part by the Box2D engine. On top of that are these other major components, all of which play a different part. All of these are "singletons" with a well defined responsibility in the execution model. Some of them interact with only a few of the others, some are ubiquitous.
Notifier
The
Notifier has been used in many designs (see
other blog entries). This singleton is an efficient global one-to-many event communication mechanism for the system. Register for an event and your can receive them as long you derive from an abstract base class and implement the interface.
EntityManager
The
Entity objects are the pieces of the game/system that get instantiated and derived from. This class is a combination container (with destruction responsibility) and phone directory for the entities. In a more complex (and realistic) system, pointers to entities are not held by other entities; an ID/reference is held and when you need to communicate with that entity, you send it a message by its
ID. This avoids the challenge of dead/reused pointers.
EntityScheduler
When this code base was first being developed, the asteroids got an "update" call for their AI every frame. This lead to a lot of wasted CPU cycles since they only really need to be updated once a second or so. The
EntityScheduler is a class that schedules the calling of the AI updates for the entities. Each frame, it executes the
Update(...) method on all the entities scheduled for that frame. By spreading out the calls to update the asteroids across multiple frames, the overall load on the system was reduced.
GraphSensorManager
The
GraphSensorManager loads the graph sensors into the a sensor graph and serves as the single point called by the Box2D framework (through the
SystemContactListener) to update sensors about contacts. This singleton was originally created with a much larger use in mind, but has really be reduced in scope significantly. It's most important function now is to provide a mechanism to debug which sensors have been changed. It will probably be removed (or morph into something else) in future designs.
The information about the
GraphSensorManager has been discussed here to highlight a
very important design/development consideration:
Just because it is good in your head does not mean it will be good in the design. You should always look back on your design and ask yourself "Did this really give me what I wanted?" and "Can I do it another way (probably in the future) that will work out better?"
SystemContactListener
The last of the big components is the
SystemContactListener. This is the callback used by Box2D when collisions occur to mark which items have been hit. Based on the size of the squares, there are many (or many more for smaller sensors) collisions every single update of the physics. This is where your CPU cycles go for the most part and the part of the design that really needs the most scrutiny in the future.
The Code
Get the Source Code for this article hosted on GitHub by clicking
here.
Conclusion
This article presented a technique (and a video and a link where you can download the code) for using a physics engine integrated with the pathfinding system to dynamically avoid objects in a changing environment. The initial testing (and demo) is very promising for future use. One "fly in the ointment" is that the CPU load, while not intolerable, is a bit higher than I would like.
Article Update Log
30 Oct 2014: Initial release
For a contractor, i had developed a system similar to yours for robot navigation using dynamic voronoi cell generation, it worked pretty well, even if that was a rather performance costly solution i think that this will be implemented in a couple of years also in the gaming area.