This post describes how to make certain simple things data driven without the need for scripting languages. It’s written for engineers, and assumes some c++ knowledge.

The problem

Level designers have to populate our virtual worlds with props, enemies and such. But often their shoulders are burdened with “hooking up” stuff, stringing together logical sequences of events, and creating specific rules in a level.

These “hooking up”-stuff-tasks range from the simple: “A player entering room cause enemies to spawn.” to the brain-frying: “The whole behaviour for your NPC-sidekick in this level”

Often a scripting language, like Lua, is used to control any logic there might be, and sometimes there is something simpler for the simple cases. One example of such a scripting alternative would be the “Kismet” of Unreal engine.

A simple Event-System

In the early years of my career, I had the pleasure to work with Renderware Studio, one of the early middle-ware game frameworks. They had a simple yet effective system for broadcasting events. Since then I have implemented systems inspired by them at least twice.

The way this event-system works is that anyone (deriving from a certain abstract-class) can subscribe to an named event. One event then has a name and a list of subscribers. To send a message to subscriber you (the sender) register the event, get an event-handle back and can use that to trigger an event (called sending a message).

Let’s see if I can rephrase that in a clearer way:

1. A sender registers an event. A sender can be a volume-trigger, a timer, some button input, anything that “happens” in the game world. Often one game-object will be set up to trigger various events based on different conditions. For instance a volume-trigger can trigger one event when a player enters the volume and another event when an NPC enters.

2. A receiver subscribes to an event. A receiver can be a spawner, a sound-player, a character. Many receivers will subscribe to many events. Like a door might listen to one event for “open” and another for “close”.

3. When a sender determines something of interest happens, let’s say the player entered a volume, the sender will through the event notify all subscribing receivers.

Example:
A volume-trigger registered the event “player_enter_secret_lab”, and waits for the player to enter. An enemy-spawner would be a subscriber, waiting for the same event, connected through the name. The spawner gets notified and spawns the enemy.

The only thing actually data driven is the names of the events (“player_enter_secret_lab”) that connect the senders and receivers. the rest is written in “proper” code. So if you have a way of exposing attributes/properties of your objects, you would only need to expose an extra string for sending or subscribing to an event.

Where a designer would put the dimensions of a volume-trigger-box he would also put the “event to send on enter”.

What parts of your game can connect with events is up to the engineers but what subscriber is connected to what sender is up to the designer.

Limitations
The code below implements only the bare minimum. You can not send data (like a time, a health, or a color) with a message, but it’s actually not that hard to add. A simple (and not so type-safe) way would be to assign an event with a “type”, do some checking on register and subscribing, and pass a void ptr along with the message, and leave the casting up to the receiver.

Code
Here’s the c++ source for a simple event system like described above.

Header file: (Event.h)

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  29
 
  30
 
  31
 
  32
 
  33
 
  34
 
  35
 
  36
 
  37
 
  38
 
  39
 
  40
 
  41
 
  42
 
  43
 
  44
 
  45
 
  46
 
  47
 
  48
 
  49
 
  50
 
  51
 
  52
 
  53
 
  54
 
  55
 
  56
 
  57
 
  58
 
  59
 
  60
 
  61
 
  62
 
  63
 
  64
 
  65
 
  66
 
  67
 
  68
 
  69
 
  70
 
  71
 
  72
 
  
#include <string>
 
  #include <vector>
 
  #include <map> // could be hash_map
 
   
 
  typedef unsigned int uint32;
 
   
 
  class Object;
 
  class Event;
 
  class EventRegistry;
 
  class SenderEventHandle;
 
  class ReceiverEventHandle;
 
   
 
  // derive to be able to receive a message
 
  class MessageReceiver
 
  {
 
  public:
 
      virtual void ReceiveMessage( Event* e, Object* origin ) = 0;
 
  };
 
   
 
  // this is the event
 
  class Event
 
  {
 
  public:
 
      Event();
 
      std::string mName;
 
      std::vector< MessageReceiver* > mReceivers;
 
      uint32 mSenderCount;
 
   
 
      // to be able to unregister yourself
 
      EventRegistry* mReg;
 
  };
 
   
 
  // one global of these...
 
  class EventRegistry
 
  {
 
  public:
 
      void Register( std::string& eventName, SenderEventHandle* eh );
 
      void Subscribe( std::string& eventName, ReceiverEventHandle* eh, MessageReceiver* receiver );
 
      void Unregister( SenderEventHandle* eh );
 
      void Unsubscribe( ReceiverEventHandle* eh, MessageReceiver* receiver );
 
  private:
 
   
 
      // could be hash_map
 
      std::map< std::string, Event > mEvents;
 
  };
 
   
 
  // keep one of these for sending
 
  class SenderEventHandle
 
  {
 
  public:
 
      SenderEventHandle();
 
      ~SenderEventHandle();
 
   
 
      void SendMessage( Object* origin );
 
  private:
 
      friend EventRegistry;
 
      Event* mEvent;
 
  };
 
   
 
  // keep one of these for receiving
 
  class ReceiverEventHandle
 
  {
 
  public:
 
      ReceiverEventHandle();
 
      ~ReceiverEventHandle();
 
   
 
  private:
 
      friend EventRegistry;
 
      Event* mEvent;
 
      // to be able to auto-unregister
 
      MessageReceiver* mReceiver;
 
  };

Source file: (Event.cpp)

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  29
 
  30
 
  31
 
  32
 
  33
 
  34
 
  35
 
  36
 
  37
 
  38
 
  39
 
  40
 
  41
 
  42
 
  43
 
  44
 
  45
 
  46
 
  47
 
  48
 
  49
 
  50
 
  51
 
  52
 
  53
 
  54
 
  55
 
  56
 
  57
 
  58
 
  59
 
  60
 
  61
 
  62
 
  63
 
  64
 
  65
 
  66
 
  67
 
  68
 
  69
 
  70
 
  71
 
  72
 
  73
 
  74
 
  75
 
  76
 
  77
 
  78
 
  79
 
  80
 
  81
 
  82
 
  83
 
  84
 
  85
 
  86
 
  87
 
  88
 
  89
 
  90
 
  91
 
  92
 
  93
 
  94
 
  95
 
  96
 
  97
 
  98
 
  99
 
  100
 
  101
 
  102
 
  103
 
  104
 
  105
 
  106
 
  107
 
  108
 
  109
 
  110
 
  111
 
  112
 
  113
 
  114
 
  115
 
  116
 
  117
 
  118
 
  119
 
  120
 
  121
 
  122
 
  123
 
  124
 
  125
 
  126
 
  127
 
  128
 
  129
 
  130
 
  131
 
  132
 
  133
 
  134
 
  135
 
  136
 
  137
 
  138
 
  139
 
  140
 
  
#include "Event.h"
 
  #include <algorithm>
 
   
 
  void ASSERT( bool ok )
 
  {
 
      if ( !ok )
 
      {
 
          _exit(-1);
 
      }
 
  }
 
   
 
  Event::Event()
 
  {
 
      mReg = NULL;
 
      mSenderCount = 0;
 
  }
 
   
 
   
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
   
 
   
 
   
 
  void EventRegistry::Unregister( SenderEventHandle* eh )
 
  {
 
      Event* e = eh->mEvent;
 
      if ( NULL!= e )
 
      {
 
          std::map< std::string, Event >::iterator it = mEvents.find( e->mName );
 
          --(e->mSenderCount);
 
          ASSERT( 0 <= e->mSenderCount ); // non-negative
 
          if ( 0 == e->mSenderCount && e->mReceivers.empty() )
 
          {
 
              mEvents.erase( it );
 
          }
 
          eh->mEvent = NULL;
 
      }
 
  };
 
   
 
  void EventRegistry::Unsubscribe( ReceiverEventHandle* eh, MessageReceiver* receiver )
 
  {
 
      Event* e = eh->mEvent;
 
      if ( NULL!= e )
 
      {
 
          std::map< std::string, Event >::iterator eit = mEvents.find( e->mName );
 
          ASSERT( eit != mEvents.end() ); // found
 
   
 
          std::vector<int>::iterator it;
 
          std::vector< MessageReceiver* >::iterator rit = std::find( e->mReceivers.begin(), e->mReceivers.end(), receiver );
 
          ASSERT( rit != e->mReceivers.end() ); // existed
 
   
 
          if ( 0 == e->mSenderCount && e->mReceivers.empty() )
 
          {
 
              mEvents.erase( eit );
 
          }
 
   
 
          eh->mEvent = NULL;
 
          eh->mReceiver = NULL;
 
      }
 
  };
 
   
 
  void EventRegistry::Register( std::string& eventName, SenderEventHandle* eh )
 
  {
 
      // 1. Unreg
 
      Unregister( eh );
 
   
 
      // 2. Re-reg
 
      Event& e = mEvents[ eventName ];
 
      e.mName = eventName; // if added, make sure name is set
 
      e.mReg = this;
 
      ++(e.mSenderCount);
 
   
 
      eh->mEvent = &e;
 
  };
 
   
 
  void EventRegistry::Subscribe( std::string& eventName, ReceiverEventHandle* eh, MessageReceiver* receiver )
 
  {
 
      // 1. Unsub
 
      Unsubscribe( eh, receiver );
 
   
 
      // 2. Re-reg
 
      Event& e = mEvents[ eventName ];
 
      e.mName = eventName; // if added, make sure name is set
 
      e.mReceivers.push_back( receiver );
 
      e.mReg = this;
 
   
 
      eh->mEvent = &e;
 
      eh->mReceiver = receiver;
 
  };
 
   
 
   
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
  //-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--//-//-//-//-//-//--
 
   
 
   
 
  // keep one of these for sending
 
  SenderEventHandle::SenderEventHandle()
 
  {
 
      mEvent = NULL;
 
  };
 
   
 
  SenderEventHandle::~SenderEventHandle()
 
  {
 
      if ( NULL != mEvent )
 
      {
 
          ASSERT( NULL != mEvent->mReg );
 
          mEvent->mReg->Unregister( this );
 
      }	
 
  };
 
   
 
  void SenderEventHandle::SendMessage( Object* origin )
 
  {
 
      if ( NULL != mEvent )
 
      {
 
          std::vector<MessageReceiver*>& rv = mEvent->mReceivers;
 
          int cnt = rv.size();
 
          for ( int i = 0 ; i != cnt ; ++i )
 
          {
 
              rv[i]->ReceiveMessage( mEvent, origin );
 
          }
 
      }
 
  };
 
   
 
   
 
  ReceiverEventHandle::ReceiverEventHandle()
 
  {
 
      mEvent = NULL;
 
      mReceiver = NULL;
 
  };
 
   
 
  ReceiverEventHandle::~ReceiverEventHandle()
 
  {
 
      if ( NULL != mEvent )
 
      {
 
          ASSERT( NULL != mEvent->mReg );
 
          mEvent->mReg->Unsubscribe( this, mReceiver );
 
      }	
 
  };

Minimal test case:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  
#include "Event.h"
 
   
 
  struct Obj1 : public MessageReceiver
 
  {
 
      // to get message
 
      void ReceiveMessage( Event* e, Object* origin )
 
      {
 
          printf( "message from %p to %p\n", origin, this );
 
      };
 
   
 
      ReceiverEventHandle mEh;
 
  };
 
   
 
  int main(int,char**)
 
  {
 
      EventRegistry reg;
 
   
 
      std::string en( "testing" );
 
      Obj1 obj[4];
 
      for ( int i = 0 ; i != 4 ; ++i )
 
          reg.Subscribe( en , &obj[i].mEh, &obj[i] );
 
   
 
      SenderEventHandle eh;
 
      reg.Register( en, &eh );
 
   
 
      eh.SendMessage( (Object*)NULL );
 
  };

Conclusion
I personally enjoy developing in my favourite language and not so much fixing/optimizing non-programmers code. So any way of keeping non-programmers away from programming should be considered.

The above technique is way simpler than a language or kismet, but provides a non-script way of hooking up the simplest stuff. By adding objects that trigger one event after receiving a certain number of another event, you can see how you can build pretty advanced logic using this framework.

If you find yourself supporting very advanced logic you might want to consider re-solving the problem in script of code though. The point of this simple system should not be to replace script/code where it’s actually needed, but to allow some things to be done faster and less overhead.

I’d love to hear other developers experience with non-scripting systems.