Messaging system - Handling message subject enumerations

Started by
15 comments, last by ApochPiQ 13 years, 10 months ago
Hi all,

I'm busy surgically removing the "Engine" from my latest projects and building it into its own reusable library which I can then host in its own SVN project etc etc and avoid having to maintain multiple copys across each project, this will also allow for me to post it in an open-source repo and anyone who is interested in trying out the component based entitys stuff I've been developing in it can.

One of the problems I'm not sure how to handle is the message subject enumerations, I use a message system for communication and a message is as such:

        struct Message	{		MessageSubject::ENUM Subject;		int SenderID;		int ReceiverID;		float SendTime;		void* Data;	};


And message's are defined in an enum, there is also a matching array of strings so that we can convert string - enum, or enum - string at any time, these files are generated using a tool to keep them synced.

Here's the file as in its most recent form:

/***********************************************************//* This File was Auto-generated by the Message Maker App   *//* Authors: Christopher Pepper & Scott Bevin               *//***********************************************************///AUTO GENERATED VERSION:176#include <string>#pragma once#define NO_MESSAGE 999namespace XF{namespace MessageSubject{	enum ENUM	{		//Group:Graphics API		PLEASE_SEND_RENDERER=0,	// Params -:   :- 		SENT_RENDERER,	// Params -:   :- 		//End Of Group: Graphics API		//Group:Sound API		ADD_MUSIC,	// Params -:  std::String tag, (this loads the music into memory ready to be played, paused etc it will automaticaly loop :- 		PLAY_MUSIC,	// Params -:  std::string tag of music (must be Added first :- 		STOP_MUSIC,	// Params -:  std::String tag, will remove the music from memory :- 		STOP_SOUND,	// Params -:  std::string tag (this is used for internal messageS) :- 		PAUSE_MUSIC,	// Params -:  std::string tag, will pause the music :- 		PLAY_SOUND_EX,	// Params -:  PropertyContainer (why didnt i think of this already =]) :- 							 //this is currently for the PlaySound component to use							 //it can be sent a variety of properties via the PropertyContainer							 //currently that includes the track, the position and the length of the track, the minimum distance it can be heard from							 //if it doesnt have a lengthh property it will play the sound looped until the sound is destroyed by a STOP_SOUND message		STOP_SOUND_EX,	// Params -:   :- 							 // used for stoping sounds using a specially UID, this is all managed in playsound component and sent to the sound component  		UPDATE_MUSIC_TRACK_POSITION,	// Params -:  sound data packet :- 		PLAY_SOUND,	// Params -:  std::String :- 		PLAY_3D_SOUND,	// Params -:  SoundDataPacket struct :- 		SET_LISTENER_DATA,	// Params -:  ListenerInformation struct :- 		ADD_SOUND,	// Params -:  Use only if you want to add a sound that is not pre-loaded, SEND a std::string filepath :- 		//End Of Group: Sound API		//Group:Particle API		CREATE_PARTICLE_SYSTEM,	// Params -:   :- 		//End Of Group: Particle API		//Group:Collision system		SEND_ME_ENITIES_IN_RADIUS,	// Params -:  float :- 		SEND_ME_ENITIES_IN_RAY,	// Params -:  XF::GameEntity :- 		SENT_YOU_ENTITIES_IN_RAY,	// Params -:  std::vector<XF::GameEntity> :- 		SENT_YOU_ENTITIES_IN_RADUIS,	// Params -:  std::vector<XF::GameEntity> :- 		SEND_ME_DETECTION_RADIUS,	// Params -:   :- 		SENT_YOU_DETECTION_RADIUS,	// Params -:  float :- 		//End Of Group: Collision system		//Group:GUI		SENT_GUI,	// Params -:   :- 		PLEASE_SEND_GUI,	// Params -:   :- 		PLEASE_SEND_ME_ACTION_CAMERA,	// Params -:   :- 		SENT_YOU_ACTION_CAMERA,	// Params -:  Ogre::Camera* :- 		STOP_USING_ACTION_CAMERA,	// Params -:   :- 		ESCAPE_SCREEN,	// Params -:   :- 		//End Of Group: GUI		//Group:Error Processing		ERROR_MESSAGE,	// Params -:  std::String :- 		//End Of Group: Error Processing		//Group:Cosole		SEND_ME_DISPLAY,	// Params -:   :- 		SENT_CONSOLE_DISPLAY,	// Params -:   :- 		SHOW_PAUSE_MENU,	// Params -:   :- 		TRACK_GAME_ENTITY,	// Params -:   :- 		STOP_TRACKING_GAME_ENTITY,	// Params -:   :- 		//End Of Group: Cosole		//Group:Game world -> HUD		TOWER_CLICKED,	// Params -:  GameEntity* :- 		MULITPLE_TOWER_CLICKED,	// Params -:  std::list<GameEntity*> :- 		NOTHING_CLICKED,	// Params -:  NULL :- 		SET_LEFT_PANE_CAMERA,	// Params -:   :- 		//End Of Group: Game world -> HUD		//Group:HUD OUT		TELL_ME_COST_OF,	// Params -:  string (containing filepath of towerFile) :- 		HOW_MUCH_DO_YOU_COST_TO_UPGRADE,	// Params -:   :- 		WHAT_IS_YOUR_UPGRADE_FILE,	// Params -:   :- 		WHAT_IS_YOUR_SELL_VALUE,	// Params -:   :- 		ARE_YOU_UPGRADABLE,	// Params -:   :- 		YES_I_AM_UPGRADABLE,	// Params -:   :- 		MY_UPGRADE_FILE_IS,	// Params -:  std::string :- 		SHOW_UPGRADE,	// Params -:   :- 		SEND_ME_UPGRADE_DESCRIPTION,	// Params -:   :- 		STOP_SHOWING_UPGRADE,	// Params -:   :- 		BUILD_TOWER,	// Params -:  SEND string containing filepath of towerFile :- 		UPGRADE_TOWER,	// Params -:  NULL :- 		SELL,	// Params -:  NULL :- 		UPDATE_SELECTED,	// Params -:  NULL :- 		POINT_CAMERA_AT,	// Params -:  xf::vector3 :- 		//End Of Group: HUD OUT		//Group:Entity managment 		COST_IS,	// Params -:  string (containing filepath of towerFile) :- 		MY_UPGRADE_COST_IS,	// Params -:   :- 		MY_SELL_VALUE_IS,	// Params -:   :- 		ATTATCH_CAMERA_TO_ENTITY,	// Params -:   :- 		ATTATCH_CAMERA_TO_RANDOM_ENTITY,	// Params -:   :- 		REMOVE_ENEMY,	// Params -:  GameEntity* :- 		SPAWN_NEXT_WAVE,	// Params -:   :- 		BUILD_SPACE_STATION,	// Params -:   :- 		//End Of Group: Entity managment 		//Group:Game Stuff					SPACE_STATION_SELECTED,	// Params -:   :- 		ADD_PROJECTILE_TO_WORLD,	// Params -:   :- 		ADJUST_MONEY,	// Params -:  // int (will add to money) :- 		ADJUST_LIFE,	// Params -:  int		 (will add to life) :- 		HOW_MUCH_MONEY_IS_LEFT,	// Params -:   :- 		SENT_HOW_MUCH_MONEY_LEFT,	// Params -:   :- 		MONEY_HAS_CHANGED_TO,	// Params -:   :- 		LIFE_HAS_CHANGED_TO,	// Params -:   :- 		GAME_OVER,	// Params -:  bool, if true the game was won, if false the game was lost :- 		NO_MORE_ENEMIES_AND_ALL_WAVES_SPAWNED,	// Params -:   :- 		//End Of Group: Game Stuff					//Group:Entity To Entity		REDUCE_LIFE,	// Params -:  float :- 		YOU_HAVE_BEEN_SHOT,	// Params -:   :- 		YOU_KILLED_ME,	// Params -:  GameEnity* (the enity sending the message) :- 							 //This message is sent when one entity kills another		//End Of Group: Entity To Entity		//Group:Tower Stuff		UPGRADE_YOURSELF,	// Params -:  NULL :- 		BEEN_SOLD,	// Params -:  NULL in case we want a component to make a sparkly effect or something :- 		TARGET_PRIORITY_CHANGED,	// Params -:  enum TargetPriotiry :- 		YOU_ARE_BEING_PLACED,	// Params -:   :- 		UN_CLICKED,	// Params -:   :- 		LOOK_AT,	// Params -:   :- 		CLICKED,	// Params -:   :- 		FIRE_AT_ENTITIES,	// Params -:   :- 		SET_ACTION_CAMERA,	// Params -:   :- 		FIRE_AT_NOTHING,	// Params -:   :- 		//End Of Group: Tower Stuff		//Group:Station stuff		SELL_STATION,	// Params -:   :- 		SELL_ALL_TOWERS,	// Params -:   :- 		UPGRADE_ALL_TOWERS,	// Params -:   :- 		//End Of Group: Station stuff		//Group:AI control		AI_PAUSE_THINKING,	// Params -:   :- 		AI_RESUME_THINKING,	// Params -:   :- 		AI_SET_AGGRESSIVE,	// Params -:   :- 		AI_SET_PASSIVE,	// Params -:   :- 		AI_SET_DEFENSIVE,	// Params -:   :- 		ENTITIES_DETECTED,	// Params -:   :- 		ENEMIES_DETECTED,	// Params -:   :- 		//End Of Group: AI control		//Group:Steering		STEERING_TURN_OFF_ALL,	// Params -:   :- 		STEERING_TURN_ON_ALL,	// Params -:   :- 		STEERING_SEEK_TO_TARGET_POS,	// Params -:  XF::vector3 :- 		STEERING_FOLLOW_PATH,	// Params -:  PathDataPacket :- 		STEERING_ARRIVE_AT_TARGET_POS,	// Params -:  XF::vector3 :- 		STEERING_SET_ARRIVE_DECELERATION,	// Params -:  float :- 		STEERING_WANDER,	// Params -:   :- 		STEERING_UPDATE_STEER_DATA,	// Params -:  steering data :- 		PATH_DATA,	// Params -:   :- 		UPDATE_TARGET_POS,	// Params -:   :- 		UPDATE_TARGET_ENTITY,	// Params -:   :- 		SEND_ME_DISTANCE_TO_GOAL,	// Params -:   :- 		SENT_DISTANCE_TO_GOAL,	// Params -:  float :- 		TURN_ON_STEERING_BEHAVIOUR,	// Params -:   :- 		TURN_OFF_STEERING_BEHAVIOUR,	// Params -:   :- 		ADD_STEERING_BEHAVIOUR,	// Params -:   :- 		REMOVE_STEERING_BEHAVIOUR,	// Params -:   :- 		//End Of Group: Steering		//Group:Body		APPLY_FORCE,	// Params -:   :- 		SET_POSITION,	// Params -:   :- 		ROTATE_TO_FACE,	// Params -:  vector3 :- 		//End Of Group: Body		//Group:Entity To Self		CREATE_FIRE_AND_FORGET_ENTITY,	// Params -:   :- 		DIED,	// Params -:   :- 		CREATED,	// Params -:   :- 		FIRE,	// Params -:   :- 		ROAD_MAP_CHANGED,	// Params -:   :- 		ARRIVED_AT_GOAL,	// Params -:   :- 		OOPS,	// Params -:   :- 							 //This message is sent to itself if the entity kills someone on their own team		HEALTH_CHANGED,	// Params -:   :- 		YOU_ARE_FROZEN,	// Params -:   :- 		YOU_ARE_UNFROZEN,	// Params -:   :- 		YOU_ARE_SLOWED,	// Params -:   :- 		YOU_ARE_NOT_SLOWED,	// Params -:   :- 		//End Of Group: Entity To Self		//Group:Entity Out		DECREASE_ENEMIES,	// Params -:   :- 		//End Of Group: Entity Out	}; 	std::string MessageSubjectToString(XF::MessageSubject::ENUM sub);	 XF::MessageSubject::ENUM StringToMessageSubject(std::string message);}}


Now as you can see, this file is buryed deep inside the engine, and requires modifying every time a new message is needed, not very good for compiling the library into a .lib and jsut using that instead of recompiling it from source every time.

Now, I would rather steer clear of the obvious "change messageSubject to a string" if possible, so does anyone have any suggestions as to how I can change this?

Thanks all, Scott

Code posted to google code: https://code.google.com/p/xframeworkcomponentsengine/

[Edited by - gameXcore on June 1, 2010 5:24:43 PM]
Game development blog and portfolio: http://gamexcore.co.uk
Advertisement
You might want to use classes or a structs to represent messages in a type hierarchy. Then you do not need to keep the definitions in one file. Users of the library can add new message types in any file they choose as long as they link in the header of the base message type.
Using that suggestion how would I go about determining the type of the message when it is received? The main reason I dont want to use std::strings as the message subject is that I would rather not go through and rewrite 3 projects worth of statements which switch on an enumeration in every handle message function to statements which perform an if...if else...else chain.
Game development blog and portfolio: http://gamexcore.co.uk
I don't think there's really any good way around this. It sounds like you have it set up such that message types are checked in the message handlers, which means that if you want to change the way you store message types, you're going to have to change all your message handlers.

A good way to avoid this is to move the message type checking into the message dispatching code. You could, for instance, have listeners register individual functions to individual message types. This way, the message dispatcher is tasked with matching message types to functors. Of course, this would still require a major rewrite at this point.
OP, object-oriented programming was invented to solve exactly the kind of problem you are facing, i.e. the problem of rigidity. This is the property of code where it is extremely difficult to reuse or extend it without doing a complete rewrite.
Writers of library code try to steer clear of huge enumerations when designing with modularity and extensiblity in mind. Since you are dealing with code that was not designed with these goals, it is unlikely you will be able to just cut it up nicely and easily in to reusable chunks, but will rather have to do major refactoring to get it right. But it should very interesting software design challenge.
Use a message struct, and use the std::type_info as the subject.
So, opening my mind to rewriting all my message handlers, I guess the only way around this is to use strings, given strings are rather hard to maintain tho, I'm going to have to come up with someway of keeping things in order.

Does anyone have any suggestions or bright ideas, I've spent the last couple of days trying to come up with the golden solution, but nothing works, or feels right. I'm thinking the solution is probably going to involve some pre-processor code generation magic.
Game development blog and portfolio: http://gamexcore.co.uk
Quote:Original post by gameXcore
Does anyone have any suggestions or bright ideas, I've spent the last couple of days trying to come up with the golden solution, but nothing works, or feels right. I'm thinking the solution is probably going to involve some pre-processor code generation magic.

Let me be more concrete.

We'll use the FastDelegate library to do this. If you want, you can use boost::function and boost::bind. I find FastDelegates do less damage to compile times, so I prefer them for this sort of thing. boost::bind is far, far more flexible, however. Also, keep in mind that where you say messages, I say events.

I define an Event as follows:
struct PlaybackStartedEvent{	PlaybackStartedEvent(Track* track) :		Track(track)	{	}	Track*	Track;};

Note the lack of a base class. You can have as many fields as you want in the event. It carries all the data that is needed to update other system components when a track has started playing.

Now, this data is useless without a way of notifying everyone about it. So, let's do that:
#include <algorithm>#include <map>#include <vector>#include <cassert>#include "Delegate.hpp"class EventBus{public:	~EventBus()	{		for(HandlerTable::iterator i = _table.begin(); i != _table.end(); ++i)			delete i->second;	}public:	template<typename Event>	void Publish(const Event& e)	{		Handlers* handlers = this->GetHandlers<Event>(false);		if(! handlers)			return;		Delegate<void (const Event&)> target;		for(Handlers::iterator i = handlers->begin(); i != handlers->end(); ++i)		{			target.SetMemento(*i);			target(e);		}	}	template<typename Event>	void Subscribe(void (*target)(const Event&))	{		assert(target != 0);		if(target == 0)			return;		Handlers* handlers = this->GetHandlers<Event>(true);		handlers->push_back(Delegate<void (const Event&)>(target).GetMemento());	}	template<typename Class, typename Event>	void Subscribe(Class* instance, void (Class::* target)(const Event&))	{		assert(instance != 0);		assert(target != 0);		if(instance == 0 || target == 0)			return;		Handlers* handlers = this->GetHandlers<Event>(true);		handlers->push_back(Delegate<void (const Event&)>(instance, target).GetMemento());	}		template<typename Event>	void Unsubscribe(void (*target)(const Event&))	{		assert(target != 0);		if(target == 0)			return;		Handlers* handlers = this->GetHandlers<Event>(false);		if(! handlers)			return;		Handlers::iterator i = std::find(handlers->begin(), handlers->end(), Delegate<void (Event&)>(target).GetMemento());		if(i == handlers->end())			return;		handlers->erase(i);	}	template<typename Class, typename Event>	void Unsubscribe(Class* instance, void (Class::* target)(const Event&))	{		assert(instance != 0);		assert(target != 0);		if(instance == 0 || target == 0)			return;		Handlers* handlers = this->GetHandlers<Event>(false);		if(! handlers)			return;		Handlers::iterator i = std::find(handlers->begin(), handlers->end(), Delegate<void (Event&)>(instance, target).GetMemento());		if(i == handlers->end())			return;		handlers->erase(i);	}private:	typedef std::vector<Internal::DelegateMemento>	Handlers;	typedef std::map<const char*, Handlers*>		HandlerTable;private:	template<typename Event>	Handlers* GetHandlers(bool createIfNotPresent)	{		const char* key = typeid(Event).raw_name();		HandlerTable::iterator i = _table.find(key);		if(i == _table.end())		{			if(! createIfNotPresent)				return 0;			Handlers* handlers = new Handlers();			handlers->reserve(4);	// wild-ass guess.			std::pair<HandlerTable::iterator, bool> result = _table.insert(std::make_pair(key, handlers));			i = result.first;		}		return i->second;	}private:	HandlerTable _table;};

The implementation is a bit long, but it isn't too bad. The EventBus maintains a table of event handlers. These are functions the client has requested should be called when a particular event occurs. In the EventBus parlance above, the Event is Publish()'d to everyone who has Subscribe()'d to it previously.

The Subscribe() and Unsubscribe() methods each come in two flavors: free (non-member) and member functions. They're just syntactic sugar and conceal the fact that the FastDelegate class is used to hold the event handler.

Internally, a map is maintained that converts the message type to the list of handlers. FastDelegate has a so-called "memento" feature that we exploit. It lets you destroy the concrete type information associated with the callback and store a generic function pointer. The caveat is you have to cast it back to the correct type before you use it. This may seem useless, but it lets the EventBus class store delegates without caring what the exact type of the argument is. We just need a way to restore that type when we go to use it. Luckily, C++ is up to the task.

The GetHandlers() method is templated on the Event type. This template parameter is carried through by the public methods. However, somewhat surprisingly, inside of GetHandlers(), we turn around and use RTTI on it. The reason for this is RTTI can (usually!) give us something to use as unique key for this particular type. Look up the std::type_info object if you aren't familiar with it. The name() field can serve as a unique identifier for this particular type!

Knowing this, let me be a downer and mention that this code is tailored toward MSVC in two ways:

1. it uses std::type_info::raw_name() instead of std::type_info::name(). This is because the latter tends to generate false positives in the VC memory leak checker. If you're not on MSVC, feel free to substitute name() with no ill effects, assuming...

2. it relies on the fact that each of the raw_name() should point to one (and only one) unique name for each type (hence using the pointer as a key). IIRC, the standard doesn't require implementations to honor this requirement. I think only the most cheap-ass implementation would skip providing type names.

However you go about it, the problem is tricky to solve in C++ (as you discovered). You need to mix a compile-time construct (templates) with a run-time one (RTTI) in order to get something that is elegant. RTTI is preferable to requiring users to hardcode a key because it means the event class doesn't need to fulfill any sort of contract. In more dynamic languages, this sort of construct is much easier to implement.

Anyway, let's see how it's actually used, first from the handler side:
class UIComponent{    UIComponent(EventBus* bus)    {        // Tell the bus we're interested in receiving these events.        // The event type is inferred from the argument list of OnPlaybackStarted.        bus->Subscribe(this, &UIComponent::OnPlaybackStarted);    }    void OnPlaybackStarted(const PlaybackStartedEvent& e)    {	_presenter.OnPlaybackStarted(e.Track);    }


When some other component calls Publish() with a PlaybackStartedEvent, UIComponent::OnPlaybackStarted will be called with the instance of PlaybackStartedEvent that was passed to Publish().

And now from someone who wants to Publish() the Event:
// Passing events as const-ref allows us to create and fire in a single line:_bus->Publish(PlaybackStartedEvent(currentTrack));
Hey Matt,

Sorry for taking so long to reply, and wow that was a long descriptive post. What I gather from it however is that it is a proposal of using a callback system, where a class would have to register with every message it would ever be interested in. Almost the same as how I notify when Property's change, I use an events system like you would find in C#, basically a list of delegates, when I create a component, for example the "Renderable" component, it registers a callback with the "Position" property, which is called when "Position" changes, this way the Renderable component can update the view.

The problem with this is how much code would have to be changed. And also it doesn't feel as intuitive or easy to use as simply sticking an extra case into a switch statement.

I have just come up with one idea, which while a little ugly and hacky does seem to work and requires barely any modifications to the existing code-base.

Here's my solution:

First off I change the Message class to accept an int as the subject, as opposed to XF::MessageSubject::Enum.

	struct Message	{		int Subject;		int SenderID;		int ReceiverID;		float SendTime;		void* Data;	};


Now, I utilize the scoping of enums to allow the user to "add" extra enumerations to the list, first I modify my internal message enum so that it starts counting from some high value:

namespace XF{namespace MessageSubject{	enum ENUM	{		//Group:Graphics API		PLEASE_SEND_RENDERER = 10000,	// Params -:   :- 		SENT_RENDERER,	// Params -:   :- 		//End Of Group: Graphics API


Scoping means that you access these using XF::MessageSubject::PLEASE_SEND_RENDERER, as opposed to XF::MessageSubject::ENUM::PLEASE_SEND_RENDERER. And as you can see, this enum counts up from 1000.

So here's the hacky bit, in my project I create a second enumeration, starting at 0, but in the same namespaces "XF::MessageSubject" as follows:

namespace XF{	namespace MessageSubject	{		enum EXT_ENUM		{			BOB,			CHARLES,			WOOP,		};	}}


Now the XF::MessageSubject namespace all contains the following:

PLEASE_SEND_RENDERER,
SENT_RENDERER,
BOB,
CHARLES,
WOOP

And providing the user doesn't go over 10000 message enums, hey all have unique integer values meaning I can switch on them happily, as an added bonus (instead of just declaring an enum in any namespace, which would work) I can use intellisense to give me a list of possible message types all int he same namespace. [That bit doesn't make much sense, but I can't think of a better way to phrase it].

Anyway, packaged with a tool for streamlining the creation of the enumeration as well as a list of string values to convert between enum and string representation this could just work.

I'm very interested to here reactions and responses to this admittedly hacky approach, here's the full listing of my little test program I wrote to try it:

(MessageSubjects.h)
namespace XF{	namespace MessageSubject	{		enum EXT_ENUM		{			BOB,			CHARLES,			WOOP,		};	}}


(MainProg.cpp)
#include "MessageSubjects.h"#include <XFramework.h>#include <iostream>class TestClass : public XF::Messenger{public:	bool OnHandleMessage(const XF::Message &message)	{		switch(message.Subject)		{		case XF::MessageSubject::BOB:			{				std::cout << "bob\n";				break;			}		case XF::MessageSubject::PLEASE_SEND_RENDERER:			{				std::cout << "Renderer\n";				break;			}		}		return true;	}};int main(){	XF::Message message;	message.Subject = XF::MessageSubject::BOB;	TestClass c;	c.HandleMessage(message);	message.Subject = XF::MessageSubject::PLEASE_SEND_RENDERER;	c.HandleMessage(message);	system("pause");	return 0;}


(Program output)
bobRenderer
Game development blog and portfolio: http://gamexcore.co.uk
If you want to guarantee unique subjects, you can always use GUIDs. They are 8 bytes though, and can take much longer to compare. Another alternative is to use char pointers. I like this the best because debugging is simple, you can still switch on them, and the user doesn't have to worry about trampling the system ones.

Either way it sounds good. If you use an enum and cast it to int you lose some info that can be used during debugging, so I started thinking along those lines.

This topic is closed to new replies.

Advertisement