Posts by Jonas-Norberg
  1. Scripting with no Scripts ( Counting comments... )
  2. Yet Another Post about Gamma Correction ( Counting comments... )
  3. Texture Compression Using The Discrete Cosine Transform ( Counting comments... )
Game Design / Technology/ Code /

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.