Game Development Community

dev|Pro Game Development Curriculum

Dead Simple Multicasting

by Charlie Patterson · 01/10/2012 (1:04 pm) · 7 comments

The following script, multicast.cs, provides a simple way to pass messages between objects without them knowing about each other. The idea is similar to the Observer Pattern, and specifically it is similar to signals/slots in Qt.

As a simple example of the value of "multicasting," let's suppose you have a game rule where you get a smart bomb every 1,000 points and the level ends at 20,000 points. The first way to program this is to make a score class and teach it all the scene objects that are affected by score changes:

// version 1
// note: I'm taking many liberties on the details here.  think of this as a pseudocode example
Score::updateScore(%this, %pointChange) {
   %this.score += %pointChange;

	 if (%this.score >= 20000)
	    myLevel.endLevel();

   %this.nextSmartBomb += %pointChange;
   if (%this.nextSmartBomb >= 1000) {
	    ship.addSmartBomb();
	    %this.nextSmartBomb = 0;
	 }
}

In the above code, the score class has to know about each object that will be affected (namely the smart bomb and the level.) It even has to keep up with the time until the next smart bomb and the end of the level. A better idea would be for the score class to inform the level class each time the score is updated and let the level class encapsulate when the level is over. Also send the ship each score update and let the ship decide when it gets a smart bomb. For example:

// version 2
Score::updateScore(%this, %pointChange) {
   %this.score += %pointChange;

   myLevel.scoreUpdated(%this.score);
   ship.updatedScore(%this.score);
}

Now, the ship and the level have to have the function scoreUpdated(%this, %newScore), although I am not showing them here. This is better, but the score class still has to know which simObjects want to receive a notice on score updates. Can we do better? Introducing multicasting! The score class should not have to know which SimObjects care about the new score. It should just "signal" a score change if any object cares.

// version 3 (final version)
Score::updateScore(%this, %pointChange) {
   %this.score += %pointChange;

   multicast(%this, scoreUpdated, %this.score);
}

The score class has no concern which objects pick it up. It just knows that score changes are pretty important in a game. For all it knows, no other object will pick it up and that's fine. What remains is how do we teach the ship and the level to receive these signals? Like so:

Ship::onLevelLoaded(%this, %scene) {
	 receive(%this, 0, scoreUpdated);
}

Level::onLevelLoaded(%this, %scene) {
	receive(%this, 0, scoreUpdated);
}

The ship and level do not know where this signal comes from. The signal may come from multiple places! All the ship and level know is that they would like to be informed if the score changes, and they will be informed as long as objects that mess with the score are kind enough to shout it out with multicast.

Again, the ship and level each need game logic in a method named scoreUpdated(%this, %newScore), but the wires connecting multicasters and receivers are accomplished with multicast() and receive().

One more note for now. You may wonder about the '0' in the receive call above -- receive(%this, 0, scoreUpdated). This is a little practical hack I added. If your object's call to receive specifies 0, then you will receive all multicasted calls of scoreUpdated(). If you put a SimObject reference, then you will tie this ship to a specific SimObject multicaster. This might be useful for limiting the player 1 ship to receiving signals from a 'score1' object and player 2 would be limited to hearing 'score2' signals. It's up to you.

The following is the simple version of the code. If you want to make it more robust with validity checks, speed performance upgrades, and the like, go for it!

---------------------------

// game/gameScripts/multicast.cs

// I call the following init in game/main.cs in the initializeProject() function
function multicast_init()
{
   new SimSet(multicast_receivers);
}

// I call the following init in game/main.cs in the shutdownProject() function
function multicast_destroy()
{
   multicast_receivers.delete();
}

// Where you put this will probably vary, but I want to clear the list of multicasters/receivers at
// level end, so I put it in my t2dSceneGraph's onLevelEnded() method.
function multicast_clear()
{
   for (%i = 0; %i < multicast_receivers.getCount(); %i++) {
      multicast_receivers.getObject(%i).delete();
   }
   multicast_receivers.clear();
   echo("multicast_clear" SPC multicast_receivers.getCount());
}

// call this to inform of an important change to any object that is listening
function multicast(%this, %function, %arg1, %arg2, %arg3)
{
   // assume up to three arguments.  we could make more if it would be useful
   // the multicast call does not have to have all three args
   if (%arg1 !$= "") %args = %arg1;
   if (%arg2 !$= "") %args = %args @ ", " @ %arg2;
   if (%arg3 !$= "") %args = %args @ ", " @ %arg3;

   for (%i = 0; %i < multicast_receivers.getCount(); %i++) {
      %receiver = multicast_receivers.getObject(%i);

      if ((%receiver.from !$= 0)  && (%receiver.from.getId() != %this.getId()))
         continue;
      if (%receiver.func !$= %function)
         continue;
      
      %eval = %receiver.to @ "." @ %receiver.func @ "(" @ %args @ ");";
      // echo( "muticast: calling '", %eval, "'" );
      eval(%eval);
   }
}

// called this for an object when it wants to start listening for signals
function receive(%this, %from, %function)
{
   %receiver = new SimObject();
   %receiver.to = %this;
   %receiver.from = %from;
   %receiver.func = %function;
   multicast_receivers.add(%receiver);
}

---------------------------

Hope someone gets a kick out of this! I'm using it for keeping track of friendly and baddie counts, and to keep GUI buttons up to date with the game's internal state.

[1/29/2012] Note: I changed line 38 above to fix a bug.

#1
01/10/2012 (2:46 pm)
This looks useful, it never even occurred to me to do anything other than the hardwired setup for events. It might also help to do a networked multicast to, for example, have the server inform clients when to switch GUIs.
#2
01/10/2012 (7:04 pm)
Thats always cool to have charlie, well done.

You have it on the engine already, although poorly documented, in TGEA and T2D.
I didnt checked the T2D thread in depth, it may not be working...
#3
01/12/2012 (12:38 pm)
perfect ADD-ON for T3D then, as its only there on TGEA and T2D right ?!
#4
01/12/2012 (1:12 pm)
Cool! This is like pubsub for Python. Very nice for GUI apps!

Good job!
#5
01/12/2012 (1:56 pm)
Thanks @Jeff! I'm not sure I follow but this script file should work for T3D, I think. I do not own T3D but I can't see why it wouldn't work.

Thanks @Frank! "pubsub" is another name for this type of message passing.
#6
01/12/2012 (3:26 pm)
Haha, I did not know that. I thought it was just the name of library: pubsub.sourceforge.net/

Talk about a library name being ultra literal!
#7
01/29/2012 (12:34 am)
Just found a subtle bug in my code. On line 38, it should read:

if ((%receiver.from !$= 0) && (%receiver.from.getId() != %this.getId()))


As it was, if you accepted casts from a named sender (instead of a sender with just an id), the code would get confused and multicast wrong.