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.