Tutorial
icon-dog7

Applying the Signal-Slot Mechanism to Implement Efficient AI Conditions

Alex J. Champandard on February 20, 2008

This article continues a series of tutorials which focuses on designing and implementing the AI for a simple simulation. In the last tutorial, you learned how to design a behavior tree for a virtual dog that responds to clicks. A good way to do this is by using AI conditions that suspend themselves and wait for an event from the input manager before reactivating themselves and terminating.

There are many ways to implement event-driven conditions in practice — each with its own advantages. But one of the simplest approach is to use the signal / slot mechanism. This article explains how this works in practice, looks into libraries that are available, and shows how everything fits together in C++ code.

Remember, These ideas apply to many other languages too. So if you don’t browse the code, be sure to check out the video that shows the resulting behaviors!

Screenshot 1: Interrupting passing behaviors with a mouse click.

Signals and Slots

The signal / slot mechanism builds on the traditional observer pattern. The idea is to use an array for registering observers (slots) that are interested in being notified and letting the event source call each of these observers when something happens (signals).

The code itself is simple enough to implement from scratch in under an hour, but you can find quite a few libraries available that provide this functionality already for you:

Specifically, here’s the bulk of the definition for the slots used for this tutorial:

/**
 * Specialization of a Slot that takes one parameter.
 */
template <typename PARAM>
class Slot<void (PARAM)>
{
protected:
  // Using delegates to keep the API simple.
  typedef typename FastDelegate<void (PARAM)> Observer;
  typedef std::vector<Observer> Observers;

  // This is where the function pointers are stored.
  Observer m_Observers;

public:
  // Dispatch the signal by calling all of the observers.
  void operator()(PARAM p)
  {
    Observers::iterator it = m_Observers.begin();
    for (; it != m_Observers.end(); ++it)
    {
      (*it)(p);
    }
  }

  // Helpers to build observers from any object and member
  // function, then add it to the array.
  template <typename OBJECT, typename FUNCTION>
  void add(OBJECT h, FUNCTION fn);

  template <typename HOLDER, typename FUNCTION>
  void remove(HOLDER h, FUNCTION fn);
};

Fast delegates are used here mainly because they allow member functions of any class to be easily created as delegates, which sides steps the usual hassle of C++ function pointers.

Basic Slots for Entity Events

Given these slots, you can then add them to your base entity class as a way for other parts of the entity to register themselves for events.

struct EntitySlots
{
  Slot<void (int)> mouseButtonClicked;
  Slot<void (int)> mouseButtonReleased;
};

class Entity
{
public:
  /* ... */

  EntitySlots m_Slots;
}

These slots provide a central coordination point with minimal dependencies, where the behavior tree can register observers when it wants to get notified, and the input manager can dispatch signals to notify these observers. Here’s what the UML diagram looks like:


Figure 2: The slots act as a central coordination point for separate modules.

Casting Rays from the Camera

When a click occurs, the OIS library provides a callback to the game code with all the information from the mouse. Then, the code uses an Ogre::RaySceneQuery to determine if there’s an entity under the mouse at the time of the click. If so, a signal is sent to the mouse click slot in the entity.

bool DogFrameListener::mousePressed(const MouseEvent& evt)
{
  // Request entity at this position of the user cursor.
  Entity* selected = raySceneQuery(evt.getX(), evt.getY());
  if (selected != NULL)
  {
    entity->m_Slots.mouseButtonClicked(evt.state.buttons);
  }
  return true;
}

Screenshot 3: Active behaviors are not interrupted by clicks.

Event-Driven Condition Tasks

To receive these events in the behavior tree, you’ll need a new task that acts as the condition. This task basically sets itself up as observer, suspends immediately, and then just waits for the signal. When the user clicks, the task fails and returns control to the parent task.

struct MouseClick : public MonitoringCondition
{
  DEFINE_CONDITION(MouseClick);

  /**
   * Called just before the task starts.
   */
  void setup(Entity::Slots& slots)
  {
    // Setup the callback via the entity's slots
    slots.mouseButtonClicked.add(this, &MouseClick::OnSignal);
    m_Slots = &slots;

    // By default, this task then suspends itself.
    m_Task.bind(&suspendedTask);

    // Also make sure to deregister the observer later.
    m_Observer.bind(this, &MouseClick::shutdown);
  }

  /**
   * Once the task has finished, deregister the observer.
   */
  void shutdown(alive::Status status)
  {
    if (m_Slots != NULL)
    {
      m_Slots->mouseButtonClicked.remove(this,\
            &MouseClick::OnSignal);
    }
    m_Slots = NULL;
  }

  /**
   * This observer closes the task down when the user clicks.
   */
  void OnSignal(int button)
  {
    m_Scheduler->resume(this, &failedTask);
  }

  Entity::Slots* m_Slots;

  DECLARE_MONITORING_NODE(MockMonitoringCondition);
};

You can use a simpler approach that doesn’t require a scheduler also. Instead, the task would just keep checking if data is available from the event, and return a RUNNING status. See the last tutorial last for more information.

Video 4: When the dog is passive, either lying or sitting, the loud clicks interrupt its behavior. Another behavior is chosen again randomly. When the dog is actively moving, it does not get interrupted by the user’s clicks (muted).

Further Analysis

This approach is a great way to get up and running with events. Using a very simple signal-slot implementation you can get the most out of an event-driven architecture, which keeps the code simple and reduces the amount of unnecessary polling. The process of adding new functionality is simple:

  1. Identify new events that you want the AI to support and add them as a slot.

  2. Build AI conditions that register themselves as observers in these slots.

  3. Implement the logic for signaling events to the observers in the slots.

The only down side of this approach is that it can introduce dependency problems. For example, if you want to pass custom data from the input manager to the condition in the behavior tree, then this data-structure needs to be forward declared in the base entity class. This may sound acceptable, but can break the layering of your codebase and prevent you from using modular libraries.

An alternative to this would be to use a message data-structures for each event, and let the implementation in each observer interpret the messages the best they can. This fully blown implementation of an event system has its advantages, but requires more work upfront.

In the next tutorial in this series, you’ll learn about visual debugging techniques for behavior trees and AI logic in general. In the meantime, if you have any questions about events or signal-slot libraries, feel free to post them below.

Discussion 9 Comments

dmail on February 21st, 2008

Just Two questions Alex. What is the void for in the specialisation of the template class? Is the following code a typo? // Dispatch the signal by calling all of the observers void operator(PARAM p)() and should read void operator()(PARAM p)

alexjc on February 22nd, 2008

Yes, you're right. It was a copy/paste then reformatting error. Fixed. The void is the return status. It's using something similar to the boost preferred syntax for the slots. Alex

Ian Morrison on September 17th, 2008

Alex, are you planning to do the next tutorial in this? I'm very much interested in visual debugging techniques... if not a tutorial, could we get an article on this instead?

alexjc on September 17th, 2008

Short answer, yes -- at some stage! If you can tell me what you need I can focus in and provide something quicker... Alex

kfields on September 18th, 2008

Since Ian brought it up ... Is there some reason the doggie demo isn't included with GameAI? Is it an art license issue? Game AI [I]really[/I] needs a reference application until you guys have a decent sandbox.

alexjc on September 18th, 2008

Yes, it's pretty much a licensing issue. That's one of the reasons I have to invest more time and money into the sandbox to make sure we actually have the rights to distribute it! Alex

kylotan on August 7th, 2009

The code for the slots appears to be mangled, perhaps because the less-than and greater-than signs used for template declarations have become HTML tags. Any chance of it being fixed? :)

alexjc on August 7th, 2009

The C++ syntax was an import problem. Should work fine now! Alex

cod3monk3y on August 29th, 2014

Again, great articles. Looks like there's a missing 's' in the declaration for the observers in your Slot class: Should "Observer m_Observers" actually be "Observers [...]"?

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!