Tutorial
icon-dog6

Building Event-Driven Conditions for an Asynchronous Sensory System

Alex J. Champandard on February 6, 2008

This article continues a series of tutorials building simple dog behaviors for a simulation game. Last time, you learned how to use polling to build tasks that monitor conditions in the world, for example to keep the growling behavior running while another dog is nearby.

In practice, there’s also another way to implement these kinds of condition using asynchronous events. Instead of the condition task actively probing for information from the world, another piece of code in the simulation passes the requested information back when it’s available.

On the rest of this page, you’ll learn about events, when to use them, and what it takes to implement them into an AI engine. Specifically, you’ll see how to get dogs to stop mis-behaving or become more active when the user clicks on them. In the following Wednesday tutorial on AiGameDev.com you’ll find out exactly how it’s implemented.

An Overview of Event-Driven Architectures

Using the event-driven approach, the logic of the program is written to react to external calls rather than operate deliberatively according to a linear program. As such, an event driven program is usually split up into many small event handlers; these are small bits of code that should react to the events when they occur.

In practice, there are many ways to implement this kind of system, but these two are the most popular:

  • Callbacks or observers, which use traditional function parameters to pass data to the event handlers. This approach is very simple, and only requires a mechanism for registering callbacks for particular events. Read more about the observer design pattern for a more in depth overview.

  • Data structures known as messages which can be dispatched indirectly to the corresponding handlers. In this case, there’s usually an event loop (a.k.a. message pump) responsible for processing the events and finding the corresponding function to call.

The first approach requires less work upfront and remains easily extensible, so it’s a great way to get started.

Asynchronous Sensory System

Screenshot 1: Events or polling; two ways to implement the same behaviors.

When to Use Events

Just like everything else, using an event-driven approach vs. polling depends on the problem. Here are the major arguments for either approach to help you make a more informed decision:

Advantages
The code typically becomes more modular using events, especially if messages are used. This helps decouple the different parts of the system. Also, it can be more efficient as its possible to optimize sensory operations centrally and avoid re-checking for data unnecessarily.
Disdvantages
The down side of event-driven architectures is that they are harder to implement; the code can look “inside out” since it has to react to many external events rather than proceed purposefully executing statements from a single procedure. Thus, this approach can be much harder to debug.

Chances are you’ll need both events and polling within your engine. So once you have an event system in place, use it when it helps speed things up or keeps the code modular — knowing that it’ll involve a little more work to setup.

Who Generates the Events?

Certainly, events can help keep the codebase simple. However, it won’t magically resolve all your problems; all the sensory system logic still needs to be implemented somewhere! The question is where best to put it…

For example, instead of having each dog probing for other entities in range, you could either:

  1. Use a central manager to perform distance checks and notify dogs who have requested to be notified of the proximity of other entities. Generally, this centralized approach makes sense when it takes a lot of logic to prepare the events, and computation can be reduced by regrouping everything together.

  2. Allow each AI entity individually to send events as it executes its behaviors, for example to notify other dogs during the growling behavior. This decentralized solution makes more sense for occasional events that don’t require too much overhead of computation.

Once your event architecture is in place, start with a decentralized implementation as it’s often easier. Then build up certain subsystems (e.g. nearest neighbor checks, line of sight) when efficiency becomes a problem. Also note that external libraries are best considered as a centralized source of events that you don’t need to worry about.

Mouse Click Event-Driven Conditions

Screenshot 2: Mis-behaviors can be interrupted with a mouse click.

Implementation Strategies

The last tutorial used a form of polling to build conditions, which involves gathering data on a regular basis using complex queries (a.k.a. probing). To switch to an event driven implementation, you have two main approaches:

  1. You can still use polling in combination with event driven solution. Each update, tho condition checks to see if it has received data from the event handler. The disadvantage here is that you still have the cost of updating each conditions, also known as busy waiting.

Status ConditionA::update()
{
  // Quick check every frame.
  if (m_Event != NULL)
  {
    /* Process data once here. */
    return COMPLETED;
  }
  // Keep going in busy wait.
  return RUNNING;
}
  1. Ideally, you need a way to suspend the condition altogether until the event handler has data. This typically requires you to implement a scheduler that only updates the tasks that are necessary. The scheduler keeps track of tasks that require execution, and skips the ones that don’t need updating.

Status ConditionB::update()
{
  // Request future notifications.
  setCallback(this, &ConditionB::handleEvent);
  // Tell the scheduler to no longer call update.
  return SUSPENDED;
}

void ConditionB::handleEvent(EventB& evt)
{
  /* Process data here. */
  m_Scheduler->halt(*this, COMPLETED);
}

In the next article in this series, you’ll see how to implement this second approach.

Quick Design for User Interaction

An example that’s always used for event systems is user interfaces. Since it’s also a good way to make the dog behaviors more interactive, it makes sense to add support for using the mouse in the application. These events will be plugged into the behavior tree using conditions, exactly like in the last tutorial.

The basic idea is that the high-priority bodily behaviors can be punished by the user with single click. The corresponding tasks are aborted immediately when this happens. Likewise, when the user clicks on a dog when it’s passive, it will prematurely terminate those behaviors too in order to stimulate some activity.

Event-Driven Behavior Tree

Figure 3: Whole subtrees are wrapped into parallel nodes that wait for a click then bail out.

Ultimately, a centralized user input system could monitor the mouse and generate events used by the behavior tree. This is also a good example of the ideas discussed earlier in the article! Stay tuned for more details about the implementation in a following Wednesday.

If you have any questions about event-driven architectures, asynchronous sensory systems, or anything else discussed in this article feel free to post a comment below or in the forums.

Discussion 1 Comments

bknafla on February 18th, 2008

Another thought: to enable an agent to only subscribe to events of interest it registers a sensor interest whenever a condition that needs these sensor inputs is found during the BT evaluation. The moment the conditions or sensor nodes aren't inside an "active" subtree their sensor interest is resigned from the sensor system. Such a system would lead to deferred sensor reception because of the need to register with a sensor service before receiving events. However there might be another way to overcome at least some latency in registering an interest and receiving events: scout traversal. Scout traversal of a BT (based on the idea of scout threads as used by Sun's next processor with codename "Rock") could traverse the tree without activating/processing any nodes but just looks ahead what nodes might be evaluated in the future. Every condition/sensor/event interested node found registers an interest into the associated event even before the node is really evaluated/processed. For nodes that a scout traversal can find, latency for registering for events and receiving events is eliminated. Another method is needed to detect when interests are lost to unregister. Using a history of event interests of the last frame, the current frame, and the interests scouts find could be used to implement a hysterese that fights frequent registering and resignation of sensor/event interest. Cheers, Bjoern PS: An "interest" is like a callback or handler registered with an event source. The "interest" triggers that events are send to the interest target.

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!