Game Development Community

dev|Pro Game Development Curriculum

Subscription Based Message Router

by Thomas \"Man of Ice\" Lund · 06/28/2004 (8:37 am) · 42 comments

Download Code File

Changelog
27th June 2004
posted resource

28th June 2004
made code handle re-entrancy by queueing messages while system is sending
fixed missing platform import statement

1st of July 2004
Due to request, I rewrote the initial parts of this resource to better explain what this is

1st of August 2004
Added object reference to the callback

7th of August 2004
Added a data field and an example "messaging trigger"

30th of October 2005
Bugfixes to prevent crashes on deleting subscribers during sending
SimObjects can now subscribe and send instead of ShapeBase objects
Rewrote script API

21th of September 2007
Wow - new update!
Fixed spelling mistake in receive vs. recieve
Partially rewrote the code to get rid of the internal delete queue. This fixes a bug where ojects could delete other objects that also were subscribers, and is more pleasing + logical.
Also fixed tiny bug with wrong error message in subscribe method

Subscription what? What the hell is this? What can I use it for?

OK OK - read through this, and you will know that you can use this in your game. Its almost guaranteed.

This resource is all about minimizing overhead - so this saves you from e.g. polling for status/ray casts and similar. it also saves you from coding dependancy into your objects about other objects. In the long run this will save you from errors and extra work.

Example:
A simple pac man game. The pac man eats a power pill and somehow the ghosts need to turn blue for a short period of time.

You can do this at least 3 ways
1) ghosts ask pac man every 50 ms about "did you eat the powerpill?"
2) pac man tells each ghost "I ate the powerpill, and thus you have to turn blue"
3) pac man sends a message to the "power pill eaten" queue, and the ghosts get the message as an event

Problem with method 1: overhead - huuuuuge overhead. It might be OK for 4 ghosts to do this, but in 99.9999% of the cases the answer is negative and thus overhead.

Problem with method 2: pac man needs to know about each and every ghost, so that he can tell them about the power pill. This creates a dependency between code objects, and if you later add a 5th ghost, you have to make sure you run through all your code and add it to where pac man talks to the ghosts = error prone process

Along comes method 3 (this resource). There is no overhead.

Only ghosts that are interested in the "power pill eaten" messages subscribe to the queue. And only when there is a message will they be bothered with it (they have a method onRecieveMessage() that is called when there is a message - and not before)

And pac man doesnt need to know about the ghosts at all - all he needs to do is send the message that he ate the pill. And the ghosts dont need to know if it was mr. pac man or mrs. pac man who ate the pill - all they need to know is that someone ate the pill and now they need to turn blue. This enables you to add more content into the game without having to run through your logic over and over again.

So event driven messaging that is subscription based saves overhead and precious CPU cycles, as well as allows for much cleaner code, as well as more complex AI.

The basic mechanism of this resource is that you (from script) create a message queue. Script objects can now subscribe to these queues and every message send to queue X is then send to a subscriber of queue X.

Some example usages:

Example: exploding rocket
A rocket is fired and it collides with something and explodes. In its onCollision() it sends out a message saying "I, the rocket, exploded at coordinates x,y,z. I have a blast radius of 20 meters, and everyone inside the radius takes 20 hitpoints damage" to the "damage" message queue

All damageable objects have subscribed to this message queue and will now recieve the message send in their onRecieveMessage(). It is now up to them to check if they are invulnerable, in range, protected against rocket damage, have armour points to spend before health etc.etc.

Example: faction system
In an RPG game all player actions are send to a "playeraction" message queue. E.g. during this particular example game the player has succesfully pick pocketed 10 NPC players. Every time a message "I succesfully pickpocketed NPC with id ###" to the queue.

The game has 2 factions. The police faction and the thieves guild faction. Both subscribe to the queue and recieve these pick pocket messages. Based on these actions they recieve each factions can adjust their AI responses to real events. In this example case the players reputation in the thieves guild has preceeded him, and he will be recieved with open arms.

In this case it would be extremely easy to add another faction - without having ANY direct coupling of the player himself and the faction AI.

Example: zoning your game
To save CPU time and get better performance in your game, you divide the world into separate zones/mission regions/trigger areas. When the player enters and exits these areas, a message is send to the "zone information" message queue, saying "player entered/exited zone X". All the NPC AI's in the zone can now go from idle to active, and process input as they have subscribed to this message queue, and been informed of the arrival of a player. While the zone you just left can now be shut down. No need to have the parrots fly around, the NPC guards doing radius checks to see if you are in range or similar if the player is on the other side of the map.

Example: game objectives
You can have a central "game objectives controller" that tracks game events. If your game for example wants you to kill 4 enemies before you win, the controller listens in on the "death queue" and increases a counter. Once counter reaches X, the game is won. This system can be altered in any way needed for game objectives - chained, parallel objectives etc.etc.

Only your imaginations sets the limits for usage of this.

How to add to the engine
Easy to add. The code is not complicated at all.


First of all dump the attached code into your engine\game folder.

Then we need to create a new script callback in the SimObject.

open console/simBase.h and before this
class SimEvent;
class SimObject;
add
struct Message;

then find this
bool isHidden();
   void setHidden(bool b);
and add this after the above
/// Added for Message Router callback method
   void onReceiveMessage(char* queueName, Message message);
   /// End add

Now open simBase.cc. In the top after this
#include "console/consoleInternal.h"
#include "console/typeValidators.h"
include this
#include "game/messageRouter.h"

And all the way in the end of the file add this
void SimObject::onReceiveMessage(char* queueName, Message message)
{
	// If this object has a datablock, then send to that instead of the object itself
	GameBase* gameBaseObj = static_cast<GameBase*>(this);
	if (gameBaseObj) {
		Con::executef(gameBaseObj->getDataBlock(), 6, "onReceiveMessage", gameBaseObj->scriptThis(), queueName, message.mSenderId, message.mMessage, message.mData);
	} else {
		Con::executef(this, 5, "onReceiveMessage", queueName, message.mSenderId, message.mMessage, message.mData);
	}
}

This now means that ALL SimBase objects can override the onReceiveMessage in script. Since not all have datablocks, we have 2 kinds of callbacks. I need the datablock for my game, so thus both are in there.

Compile the engine and thats it.

Script API

In the examples below I have a global variable on the server side holding the MessageRouter object. This might not be what you want.

To create a new Message Router object, you could do the following:
$MessageRouter = new MessageRouter(char* routerName) {};

To create/delete a message queue or to clear/remove all existing subscribers from a queue you use
bool $MessageRouter.createMessageQueue(char* queueName);
bool $MessageRouter.deleteMessageQueue(char* queueName);
bool $MessageRouter.clearMessageQueue(char* queueName);

To subscribe or unsubscribe %simbaseObject from a queue you use
bool $MessageRouter.subscribeMessageQueue(%simbaseObject, char* queueName);
bool $MessageRouter.unsubscribeMessageQueue(%simbaseObject, char* queueName);
where %simbaseObject is your object that you want to subscribe

To send a message you use
bool $MessageRouter.sendMessage(%simbaseObject, char* queueName, char* message, char* data);
where %simbaseObject is the object that sends the message.

Lastly there is a debug function that you can use to see all queues and their subscribers. Simply type
void dumpMessageQueues();
on the console.


Message Trigger
For my own games I've made a special type of generic trigger. What is does is than when a player enters it, a predefined message is sent to all who listen.

The code is here

//-----------------------------------------------------------------------------
// Message Trigger
// Sends a message into the "MessageTriggerQueue" when the player enters
// Takes the values from the "message" and "data" fields defined in the 
// world editor for this trigger and sends it to the "MessageTriggerQueue"
//-----------------------------------------------------------------------------


datablock TriggerData(MessageTrigger)
{
   // The period is value is used to control how often the console
   // onTriggerTick callback is called while there are any objects
   // in the trigger.  The default value is 100 MS.
   tickPeriodMS = 100;
};

function MessageTrigger::onEnterTrigger(%this,%trigger,%obj)
{
   if (isObject(%obj.client.player)) 
   {
	$MessageRouter.sendMessage(%obj, "MessageTriggerQueue", %trigger.message, %trigger.data);
   }
}

Its generally useful for everything. For e.g. applying damage to the player, for triggering sounds - for anything you can use triggers for without having to code game functionality into the triggers themselves.

Where to go from here
The code itself still needs just a little more work to be truly generic usable. Currently the message can only hold 2 fields containing information (the message and the data). This is useful for maybe 90% of the uses you can think of, but it should be expanded to "unlimited" data fields. Its easy to add to the message router if your game needs it.
E.g. in the rocket explodes situation the message should contain 4 fields:
"rocket explosion" text as a type you can filter on
x,y,z coordinates
blast radius
damage count

One can do this with the 2 field and then cut it up using the getWord function, but its not as robust and can be error prone.

Another thing the code lacks is security. You might not want clients to be able to listen in on the messages and/or send messages in a multiplayer game to prevent cheating.

Other than that, its fully usable (I hope) for everyone. Enjoy!
Page «Previous 1 2 3 Last »
#1
06/28/2004 (2:03 am)
Super fat dope! Has been needed for a long time by many I bet...

-s
#2
06/28/2004 (3:37 am)
This is definetly worth while for the way I'm planning on doing some AI! I'll have to check it out!

Insead of chaning the NULLs to 0s, I just put
#define NULL 0

near the top of llist.h and it complied for me.

-Joe
#3
06/28/2004 (4:38 am)
:O
#4
06/28/2004 (7:45 am)
You can use StringTableEntry's for fast comparison purposes; a string in the table will always point to the same memory location, so the equality/inequality operators will actually work!

Very odd that NULL isn't defined. I thought it was a standard symbol. Are you including the platform layer?
#5
06/28/2004 (7:59 am)
Thx Ben - that was it. Included a #include "platform/platform.h" and problem went away. Duh.

Working on solving the re-entrancy problem at the moment. I queue up messages that arrive while the system is sending, and then process them afterwards in the order of arrival.

But for an hour or so I've been running in rings around a problem with the sender id being overwritten somewhere.

Before that is solved, I cant update the resource. So hang on - I hope to have it done tonight.

BTW - anyone know how to send an array of char* from C++ to the script callback?
#6
06/28/2004 (9:54 am)
Code updated.

Added queueing of messages that are sent while another message is sent (confused?). If recieving a message triggers sending of another message, then the new message is held in a queue until sending the first one is done. Then subsequent messages are handled in the order of sending.

Also fixed the missing platform import statement, where NULL was defined. So no messing with llist.h anymore.
#7
06/29/2004 (3:26 pm)
Here's one that probably falls under the dumb newbie dept..

Why not pass an object instead of a string?
That way you can pack as much data into the object as you need for all the subscribers.

-Jerry
#8
06/30/2004 (1:57 am)
Jery: as far as I know, the only datatypes allowed in script callbacks are simple types - int and char*. No examples I've seen in the engine sources can send complex objects to the script layer.

With that said, I was pointed to the execute() method in the Console, and that one sends an array to scripts - so very soon I'll have the array of char* in place.
#9
07/02/2004 (5:06 am)
Message queues rock :)
#10
07/29/2004 (5:22 pm)
Quote:anyone know how to send an array of char* from C++ to the script callback?
I haven't yet looked into this resource, but I remember this other one that may work well here.
#11
08/01/2004 (12:28 pm)
I'll look into that Erik - thanks for the link

I updated the resource with a tiny little thing making a huge difference.

While implementing this today in a game project of mine, I discovered that I couldn't reference the object that subscribed to a queue on a message recieved.

So I added the reference to the object itself in the onRecieveMessage() callback.

What was
onRecieveMessage(%this, %queueName, %senderId, %message);
is now
onRecieveMessage(%this, %obj, %queueName, %senderId, %message);

A very small change to shapeBase.cc - Just change the callback to
void ShapeBase::onRecieveMessage(char* queueName, Message message)
{
   Con::executef(mDataBlock, 5, "onRecieveMessage", scriptThis(), queueName, message.
mSenderId, message.mMessage);
}

Thats it - making the entire resource much more usable.
#12
08/07/2004 (4:05 am)
I uploaded a new version of the code, where I added a single data field to the message. So now one can send e.g "bomb exploded", "x,y,z coordinates"

I also added a script for a message trigger, that (when the player enters the trigger) sends out a message+data to a certain queue.
#13
08/31/2005 (7:27 pm)
Does anyone have comments on how this would react if thousands of objects are subscribed to a single message queue and a message is sent to all of the objects? Would this work, is it feasible to assume it would not bog the system down?
#14
09/01/2005 (12:27 am)
At some point it will bog the system down. The code as is will send the message to all subscribers in that queue in one go.

If you truly need thousands of subscribers, you will need to insert some throttle in the processing. E.g. simply insert a counter in the queue handling to e.g. 100 objects per tick. Then only process that amount of subscribers per tick and take another 100 in the next tick etc.
#15
09/05/2005 (1:06 pm)
I found a couple bugs:

- if you delete an object that is subscribed to a queue, then send a message to that queue, the game will crash nicely, because the object didn't unsubscribe before being deleted.

- If an object unsubscribe itself from a queue as the result of recieving a message, the game will crash.

I managed to add subscription support for simObject-derived types quite easily (it was required for our project). It just executes the onRecieveMessage on the object itself, not on the datablock. Objects deriving from shapebase and on use the default behavior. I'll try to post it soon, but it should be easy for you to figure out.
#16
09/06/2005 (12:15 am)
Cool Manoel - thanks. I'll fix those buggers + try to do the simobject fix.
I'm using the messaging extensively in my game, and it has some random crashes. Havent gotten to the "track and fix bugs" phase yet
#17
10/28/2005 (2:48 am)
Any word on those last couple bugs and the simobject fix?
#18
10/28/2005 (5:40 am)
Sorry yes. All bugs are fixed, and I have recoded things a bit to use simobjects + made the message router a console object and other stuff.

The interface to subscribing and sending messages has changed a bit too due to this, so I'm a bit unsure how to post this best. New resource or update here.

How many are actually using this resource besides me?
#19
10/28/2005 (6:33 am)
I'm not using it yet, but i'd like to.

I say update this resource.
#20
10/28/2005 (7:57 am)
I have been tinkering with it but not enough to run into any real problems. I plan to use it more when I get into a more solid phase of my (slow) project.
I wouldn't mind you presenting an update to this resource. Maybe it would be best to go with a completly new resource, a 2.0 so to speak.
Page «Previous 1 2 3 Last »