Game Engine part 3

In this tutorial I will share an approach that allows for event-response like programming in C++. The technique uses both global statics and a couple of layers of indirection, so it may not be the most elegant of implementations, but it does allow for very pretty code. A sample:

...(some includes)...
struct MyEvent {
  int value;
};

struct TriggerHandler {
  void operator()(const MyEvent& evt) {
     std::cout << "Event received: " << evt.value << "\n";
  }
};

int main() {
  TriggerHandler handler;
  Channel.add(&handler);
  MyEvent aaa { 123 };
  Channel.broadcast(aaa); // transmit object
  Channel.broadcast(MyEvent { 456 }); // pass anonymous object
}

Which outputs exactly what you would expect. Notice however, the lack of inheritance and the absence of strict coupling between the objects. Many game engines I’ve seen implement some type of global messaging system, but usually they either use a signal-slot approach which requires pretty serious coupling of the components, or a base message with destination flags are used. In the latter case, all objects are considered as possible recipients of a signal, not just the objects that can actually handle the message in question.

The system I’ll describe here creates a separate queue at compile time for each separate type of message that can be transmitted. This encourages using small or even empty structs as a message, but really any message will do because this is template-based instead of inheritance. Even though it is template based, when creating an infrastructure system like this it’s preferred to start out with a concrete type and then generalize it using templates. So we start out with defining a class Channel that contains a list of handlers for the type MyEvent:

class Channel {
public:
  void add(void* handler) { // the actual handler type could be anything, but still needs to be an object
    mHandlers.push_back(handler);
  }

  void broadcast(const MyEvent& evt) {
    for (auto& handler: mHandlers)
      (*handler)(evt);  // this won't compile, because the stored type can't be dereferenced :(
  }

private:
  std::vector mHandlers;
};

Immediately, this presents an issue — accepting any type won’t allow the desired function to be called. There are a couple of ways around this, but it is more difficult than one might think. One solution, which I prefer is to capture the handler in a lambda, and then store it in a std::function as follows:

class Channel {
public:
  template 
  void add(T* handler) {
    mHandlers.push_back(
      [handler] (const MyEvent& evt) {
        (*handler)(evt);
      }
    );
  }

  void broadcast(const MyEvent& evt) {
    for (auto& handler: mHandlers)
      handler(evt); // ok now, each handler has an appropriate operator()
  }

private:
  std::vector> mHandlers;
};

The lambda’s stored capture the original pointer at compile time, preserving the original type. But the lambdas themselves are stored using std::function, which uses type erasure. By using this style, already all objects that have an operator() for MyEvent will work now. Generalizing to all message types is just one template away:

template 
class Channel {
public:
  template 
  void add(tHandler* handler) {
    mHandlers.push_back([handler] (const tMessage& msg) { (*handler)(msg); });
  }

  void broadcast(const tMessage& msg) {
    for (auto& handler: mHandlers)
      handler(msg);
  }
};

While the channel is very generic now, there is still the issue of coupling, for which I use static instances of the Channel for each message type used. This way, exactly those objects that want to respond to a message type will be reached, with the only dependency between the objects being the message communicated:

class Channel {
public:
  template 
  static void add(tHandler* handler) { 
    // typically, the handler type is derived while the message type would be explicit
    // e.g. Channel::add(this);
    InternalChannel::instance().add(handler); //forward to the appropriate queue
  }

  template 
  static void broadcast(const tMessage& message) { 
    // usually no need to be explicit, the message type can be derived at compiletime
    InternalChannel::instance().broadcast(message);
  }

private:
  template 
  class InternalChannel {
  public:
    static InternalChannel& instance() {
      static InternalChannel result;
      return result; // return a reference to an internal static
    }

    template 
    void add(tHandler* handler) {
      mHandlers.push_back([handler](const tMessage& msg) { (*handler)(msg); });
    }

    void broadcast(const tMessage& msg) {
      for (auto& handler: mHandlers)
        handler(msg);
    }

  private:
    InternalChannel() {} // private constructor -- only access through static interface

    std::vector> mHandlers;
  };
};

Now the snippet will actually perform as we’d hope and we can adress the fact that, while there is an ‘add’ there is no ‘remove’ in this class. Because the actual stored object can no longer be compared to the originally submitted pointer we’re in a bit of a bind. The solution I’ve come up with is to just go and store the original pointer in a list that is kept in sync with the handler list. There are some disadvantages to this, as long as we’re keeping this class ‘straightforward’ they should be manageable at least.

 Handler;
    std::vector mHandlers;
    std::vector mOriginalPtrs;

Pretty nice, if I do say so myself. However, this does introduce the possibility that a handler would unregister itself upon handling a message. Doing this will invalidate the iterator in the broadcast function, so that needs a little patch:

...(InternalChannel)...
    void broadcast(const tMessage& msg) {
       auto localQueue = mHandlers; // create a local copy

       for (auto& handler: localQueue)
         handler(msg);
    }

Because I plan to use this heavily to communicate between threads, it needs to be threadsafe. The simplest way to do this is to just introduce a mutex that guards access to the handler list. All message types are separate queues anyway, so actual blocking should be reduced to a minimum.

...(InternalChannel)....
    template 
    void add(tHandler* handler) {
      std::lock_guard lock(mMutex);
      mHandlers.push_back([handler](const tMessage& msg) { (*handler)(msg); });
      mOriginalPtrs.push_back(handler);
      assert(mHandlers.size() == mOriginalPtrs.size());
    }

    template 
    void remove(tHandler* handler) {
      std::lock_guard lock(mMutex);
      auto it = std::find(mOriginalPtrs.begin(), mOriginaPtrs.end(), handler);
      if (it == mOriginalPtrs.end())
        throw std::runtime_error("Tried to remove a handler that was not in the handler list");

      auto idx = (it - mOriginalPtrs.begin()); // convert iterator to index

      mHandlers.erase(mHandlers.begin() + idx);
      mOriginalPtrs.erase(it);
    }

    void broadcast(const tMessage& msg) {
      std::vector localQueue(mHandlers.size()); // don't copy just yet, but allocate enough space
      { // lock for as little time as possible
        std::lock_guard lock(mMutex);
        localQueue = mHandlers; // *now* copy
      }

      for (auto& handler: localQueue)
         handler(msg);
    }
...

This allows message structs to be communicated between threads, but take heed of the fact that broadcasting a message occurs fully in the thread that is doing the broadcast call. There is no buffering or message queue at the receiving end or anything. One of the strengths of this approach is that the message type does not have to be copyable as only const references are passed to the receiving objects. The main disadvantage is that it uses global objects with mutexes and two layers of indirection, so it may not be quite optimal performance-wise.

Interestingly the design suggests not quite a base class but more of a contract that an object should fulfill to be considered a valid message handler. The contract can be implemented as an abstract base class (but it’s not required):

template 
class MessageHandler {
public:
  MessageHandler() {
    Channel::add(this);
  }

  virtual ~MessageHandler() {
    Channel::remove(this);
  }

  virtual void operator()(const tMessage&) = 0;
};

The complete code can be downloaded here.

I think I’ll leave it at that for this tutorial. As usual, let me know if there are any bugs and/or suggestions with the presented code 🙂

Until next time!

3 thoughts on “Game Engine part 3

  1. You forgot to lock access to mHandlers in the remove function

    You also have a typo, mOriginaPtrs instead of mOriginalPtrs. Not the missing L in orginiaL.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.