Article

Sharing Data for Efficient AI Tasks

Alex J. Champandard on June 14, 2007

A major advantage of creating modular behaviors is reusability. So it’s important to reduce the cost of reusing these behaviors to a strict minimum, particularly in terms of memory usage. If two identical behaviors are running on two different actors, you can often do better than twice the memory consumption.

To get this right, you need to split your data into two categories:

  • Instance-specific data which needs to be unique for each task (for example, keeping track of the current position in a sequence).

  • Shared data that can be reused by multiple tasks (for example, the structure of the behavior tree, or various behavior settings).

For the implementation, it helps to think about the AI logic as a set of generic behavior Node objects; these are the same nodes that form the tree of hierarchical logic, with actions and conditions as leaf nodes. A tree of nodes cannot be executed directly on an actor; it must be used to instantiate a set of tasks that are bound to that actor. There are two software patterns that can help get this right in C++.

Template Method

The template method pattern breaks down code into two parts: an abstract algorithm which relies on specific methods to be provided, and the concrete implementation which exposes these methods. Typically, this is done by having the algorithm in a base class, and the derived class implements the necessary virtual functions.

In the case of AI tasks, you’re not so much interested in making it easy to reuse code, but instead reuse data. So rather than have virtual functions to implement the external methods, these can be normal member functions in an object known at compile time. Instead, this object will just be shared among the multiple tasks.

class MyNode
{
public:
    // Template methods go here.
private:
    // Shared data goes here.
};

class MyTask : public Task
{
public:
    virtual Status execute()
    {
        // Use shared data from m_Node.
    }

protected:
    MyNode* m_Node;
};

Factory Method

Now, the AI engine will not know the specific types of nodes or tasks — since they are implemented by the user in a separate library. So to make it work in a generic way, you can use the factory method pattern. This pattern defines an abstract factory that can create objects, which can be overridden by concrete factories to create derived objects.

Essentially, each node in the behavior tree should be responsible for creating a task that is bound to an actor.

class MyTask : public Task
{
public:
    MyTask(MyNode& node)
    : m_Node(&node)
    {
    }
protected:
    MyNode* m_Node;
};

class MyNode : public Node
{
public:
    virtual Task* create()
    {
        return new MyTask(*this);
    }
};

So once you have an unbound behavior tree as a Node, you can simply call create() to make a new task that’s bound to your actor.

In practice, these two patterns can save a lot of memory — even when there’s only one actor. As soon as there’s a duplicate task that runs inside the AI, all it’s invariant data can be shared with other active tasks.

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!