Tutorial
icon-dog5

Monitoring Assumptions for Behaviors Using Polling Conditions

Alex J. Champandard on January 23, 2008

In last week’s tutorial, you learned how instantaneous conditions can check information from the world to affect how decisions are made in a behavior tree. However, using this approach, once a decision is made the behavior will run until it terminates on its own — which reduces the responsiveness of the AI.

This article shows you how to build conditions that run continuously to monitor information from the world. These conditions can help terminate behaviors immediately when certain assumptions are broken. For example, you’ll see how to design a behavior that reacts to another dog nearby, and then keeps moving backwards until it’s out of range.

Monitoring Mode

Conditions that operate in monitoring mode are basically tasks that execute over multiple frames instead of terminating immediately. The implementation must ensure that they:

  1. Keep executing if the condition is currently true. This is done by returning the RUNNING status code.

  2. Fail immediately if the condition is false. This is achieved by passing back the FAILED status code.

These conditions, like any other condition, are also used as leaves in the behavior tree.

Monitoring Behaviors with Proximity Conditions

Screenshot 1: A terrible trio of dogs growling at each other.

Polling for Game Data

The easiest to implement this kind of condition is to use polling. Polling means that the code for the condition gathers the data from the engine on a regular basis, and makes the decision whether to keep running based on this information.

The general logic for a polling condition only needs to be written once, as long as the custom logic for checking information from the world is abstracted into a separate match() function which returns a boolean. Here’s what a base class for a Condition task would look like:

struct MonitoringCondition : public Task
{	
  // To be implemented by the derived condition.
  virtual bool match() = 0;

  // Main entry point for condition tasks.
  Status execute()
  {
    return (match() ? RUNNING : FAILED);
  }
};

In the case of the EntityNearby example from last week, the code for scanning the entities and testing the nearest against the threshold would be implemented inside the match() function of that class.

Supporting Both Types of Conditions

Ideally, to reduce the amount of engine code (in this case, C++) and keep the behavior scripts as simple as possible, it’s a good idea to implement both types of condition in one class. Then, all that’s required is an option to specify how the condition behaves.

For example, this would be a way to define the basic settings of every condition object:

DEFINE_SETTINGS(SettingsCondition)
{
  // Flag to determine if the task should keep running.
  DEFINE_PROPERTY(bool, Monitor);

  // By default, it's an instant condition.
  SettingsCondition()
  : m_Monitor(false)
  {}
};

Then, the main entry point for the condition task should be updated to take into account.

Status Condition::execute()
{
  if (!match())
  {
    return FAILED;
  }
  return (m_Settings.getMonitor() ? RUNNING : COMPLETED);
}

This is the simplest way to approach the implementation of such conditions. However, instant conditions use more memory with this approach since they’ll store the same data structures as monitoring conditions.

Sensory System with Conditions using Polling

Screenshot 2: Moving away from the other dogs until out of range.

Monitoring Conditions within Parallel Behaviors

The main usage for conditions that monitor data in the game engine is to preserve assumptions of behaviors while they are running. For example, if the AI starts growling at another dog, it should stop if the dog goes out of range.

The best way to achieve this is to put the behavior into a parallel behavior, and attach multiple conditions that are responsible for monitoring that behavior — as discussed in more details in this article about using conditions in the sensory system (see Figure 4). I call this read-only concurrency in the third video of my GDC lecture on behavior trees.

In practice, this would be created like this in the type-safe tree builder:

Node* root = TreeBuilder()
  // The parallel for monitoring conditions.
  .composite()
    // A continuous distance check.
    .execute()
      .Monitor(true)
      .Operator(LessThan)
      .Threshold(200.0f)
    .end()
    /* Main sub-behavior defined here. */
  .end();

Now for a real example using the dog behaviors…

Behaviors with Assumptions

Last week’s tutorial extended the behaviors with a reaction when other dogs were nearby. The dogs would growl for a bit, then back away from the situation to prevent them from growling forever. However, the dogs would only back away for a randomly chosen amount of time, so it’s possible they could start growling again if they were still in range.

Using parallel conditions attached to the main behavior, it’s possible to make the backing off behavior repeat until the other dog is out of range. (Note: The range of this condition should most likely be larger than the first condition so that the dog backs away a little further than necessary.)

Behavior Tree with Parallel Assumptions Monitored by a Condition



Figure 3: This behavior reacts to other dogs. It is enabled when the first instant condition matches, then performs a growling animation with sound, and finally backs away until the other dog is out of range.

Summary

When conditions are built as tasks that run continuously, they become useful for monitoring changes in the world. These conditions can be executed in parallel with other behaviors

In practice, almost half of your conditions will be in monitoring mode, and the rest will be instant checks. These kinds of conditions are very useful once you understand how they work. Here are a few examples of things you can do with such conditions:

  1. Immediately abort a PlayAlone behavior tree once another dog is within range. (This uses one parallel node in the tree, and one extra condition).

  2. Stop doing a growl behavior n seconds after a dog moves out of range, using a WaitFor action in a sequence after the condition.

Stay tuned until next week to find out how to use events to implement conditions. If you have any questions, comments, suggestions or requests, feel free to post them below!

Discussion 3 Comments

3emeType on January 24th, 2008

Why do you have to specify ".Monitor(true)" ? Isn't a condition in a parallel node always monitored ?

alexjc on January 24th, 2008

[B]3emeType[/B], Yes, a condition in a parallel [I]should[/I] always be in monitoring mode, but the question is how you tell it to behave that way. When you set Monitor to false, you can use the same condition in a sequence. You can implement this with two different classes if you prefer... Alex

3emeType on January 24th, 2008

I would prefer the monitoring to be done by the parallel node, so a condition would not have to specify its mode (because its mode depends only on its parent node).

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!