Article

Managing Custom Behaviors Using Visitors

Alex J. Champandard on July 7, 2007

When you start adding more behaviors to your actors, you’ll find yourself creating many tasks with custom memory. For example, there would be some specific C++ code for PlayAnimation, FollowPath or UseObject.

To preserve some layering in your engine, your AI library cannot depend directly on game-specific behaviors. So all game behaviors must conform to a common interface defined in the library. But managing all the different tasks and their custom memory through one interface is difficult.

  • How do you keep the common API, and hence the virtual table, as small as possible?

  • How can you isolate extensions to the game code, instead of modifying the AI library?

The temptation in C++ when adding functionality to a base class, is to have many virtual functions. The problem is every object takes more memory, and you need to change the library every time a new feature is needed. This could be for initializing a behavior, accessing custom data, debugging or even logging and so on.

Extensions in the Client Code

Instead of having entry points for every extension in the base class, you can define only one extension point, and let the client code plug into that. There are three main ways you can do this:

C-style type identifier
Each object would have an extra integer as a type identifier, which you create manually. Then, you can treat these objects differently based on their identifier. The advantage is that you don’t need a virtual table at all, and you control all the implementation — so it can be very fast and specific.
C++ dynamic_cast operator
This is effectively the same approach, except you rely on built in features of C++ (like RTTI) to do the hard work for you.
C++ visitor pattern
The visitor uses double indirection to do all this in a type-safe, object-oriented way. So you define your custom functionality in the visitor, then ask the base object to call a member function with the correct type. It’s also very easy to deal with hierarchies of classes for behaviors, at the cost of runtime efficiency.

Most often, the implementation for these three solutions can be hidden in macros and helper functions. So it’s best to go for the most flexible approach first and then optimize where necessary.

Acyclic Visitor

The best way of doing this in practice is an acyclic visitor (PDF). Typically with the visitor pattern in C++, you must forward declare all the client extensions in the base library — which beats the point of having layers in your codebase. Instead what you want is an visitor without cyclic dependencies.

A good implementation of the acyclic visitor is in the Loki C++ Library. (You can include just the Visitor.h header if you remove the TypeList support, which isn’t mandatory.) Then, all you have to do is make your custom objects visitable, and create a visitor for that object:

struct MyBehavior : public Behavior
{
    DEFINE_VISITABLE();
    // Custom methods and data members here.
};

struct ProcessBehavior
:    public BehaviorVisitor
,    public Visitor
{
    virtual void visit(MyBehavior& behavior)
    {
        // Custom extension for MyBehavior here.
    }
};

Then, to use this code, you must do this:

// Base-class pointer created elsewhere.
Behavior* behavior = new MyBehavior;
// Custom functionality is defined here.
ProcessBehavior visitor;
// Call the visitor if the type matches. 
behavior->accept(visitor);

Notice you don’t need to add a virtual process() function in your base behavior class to add this functionality. It’s entirely contained within the client library. It also makes it very easy for any behavior to extend any other behavior.

The library code is equally simple, and rather elegant compared to other implementations. See the documentation for more details.

Optimizations

Now, this level of indirection is very useful generally, but it it does have its price. The tendency of hardware today is to have more and more processors that are less friendly towards virtual functions. A visitor causes two virtual calls, and an acyclic visitor has a dynamic_cast as well. You definitely want them, but be wise about using them.

The best solution is to limit visitors to establishing a (type-safe) connection between two arbitrary objects, for example, during initialization. After that, you can use direct access to keep things fast. So all your main loops would be free of indirection.

Discussion 0 Comments

If you'd like to add a comment or question on this page, simply log-in to the site. You can create an account from the sign-up page if necessary... It takes less than a minute!