The terms event and message seem to be used interchangeably by most programmers. These two concepts are quite different even if their implementations can be identical. In order to illustrate the differences, as I see them, I am going to evolve a simple code block from a procedural implementation, through a message-based implementation, and finally into an OO event-based implementation.
Procedural Approach
Here is a straightforward implementation of the feature “When a gun fires a bullet appropriate effects occur at the barrel of the gun.” This is implemented about how I would expect to see it in most game engines.
if (bulletFired)
{
ParticleManager::createEmitter(gun.getMuzzlePosition(), “MuzzleFlash.xml”);
SoundManager::playSound(gun.getMuzzlePosition(), “ShotFired.snd”);
TracerManager::createTracer(gun.getMuzzlePosition(), “BulletTracer.xml”);
}
This implementation is tightly coupled with the three
systems that do the work of making appropriate effects when the bullet is fired.
It also displays little cohesion since if we follow this pattern there will be
code that creates Emitters, Sounds, and Tracers spread all over the place.
Improved Procedural Approach
Even if we data drive which assets are used for the effects
and inject the managers we still end up with code that is only slightly less
coupled and slightly more cohesive:
if (bulletFired)
{
mParticleManager.createEmitter(gun.getMuzzlePosition(), mMuzzleFlashEmitter);
mSoundManager.playSound(gun.getMuzzlePosition(), mShotFiredSound);
mTracerManager.createTracer(gun.getMuzzlePosition(), mBulletTracer);
}
Procedural Messaging Approach
An approach that is often taken to reduce the coupling found is this type of code is to send messages. This is what I would expect the message-based version to look like:
if (bulletFired)
{
CreateEmitterMessage emitterMessage(gun.getMuzzlePosition(), mMuzzleFlashEmitter);
mMessageManager.sendMessage(emitterMessage);
PlaySoundMessage soundMessage(gun.getMuzzlePosition(), mShotFiredSound);
mMessageManager.sendMessage(soundMessage);
CreateTracerMessage tracerMessage(gun.getMuzzlePosition(), mBulletTracer); mMessageManager.sendMessage(tracerMessage);
}
While this removes #include dependency, there is still all of the conceptual coupling that the earlier version had. For instance, if the SoundManager changed to use ints instead of strings to identify sounds this code (as well as all code that played sounds) would have to change. Cohesion is just as low as it was in the previous examples for the same reasons.
Object Oriented Message Sending
The basic OO/event-based could look like this:
if (bulletFired)
{
LocationMessage bulletFiredMessage(BulletFiredMessageId, gun.getMuzzlePosition());
mMessageManager.sendMessage(bulletFiredMessage);
}
This is obviously more cohesive because all of the code relates to the concept of a bullet firinf. It is also more loosely coupled because there is no #include or conceptual dependency on any of the systems that react to the event.
Events have characteristics that distinguish them from other types of messages:
1) They are ‘eventful’ in the sense that they only get sent when something special/exceptional happens.
2) They are named and parameterized in the language of the application from the user’s point of view. A user can understand the concept of a bullet firing but would never describe part of the results as “a tracer appears at the end of the gun muzzle”.
3) One message is sent per event.
4) The creator of the event does not require any particular result to happen because of the event being sent. They should also not be aware of how the event is being handled. This means that the payload of the event should not have anything to do with how the event is handled. The position of the gun muzzle is critical to describing the event, while the name of the sound to be played is not intrinsic to the event itself and belongs with the code that handles playing the sound.
Object Oriented Message Handling
if (bulletFired)
{
ParticleManager::createEmitter(gun.getMuzzlePosition(), “MuzzleFlash.xml”);
}
This is a mapping of the detection of a particular condition to the creation of a particular particle emitter. In the OO code snippet we broke the mapping in half. The part that is handled is mapping the condition into a particular message. So all that is left to do is map that message to the creation of a particular particle emitter.
ParticleManager::ParticleManager(void)
{
// We could easily data drive this is we want to
mMessageToEffectMap[BulletFiredMessageId] = “MuzzleFlash.xml”;
}
ParticleManager::handleMessage(const Message& message)
{
MessageToEffectMap::iterator messageIdEffectPair = mMessageToEffectMap.find(message.Id());
if (messageIdEffectPair != mMessageToEffectMap.end())
{
// I am using an ugly down-casting style messaging system for simplicity
LocationMessage* locationMessage = static_cast< LocationMessage*> (&message);
createEmitter(locationMessage->getLocation(), messageIdEffectPair.second());
}
}
We can repeat this pattern for the Sound and Tracer systems. Each of these classes is cohesive because all of the code that relates to its domain lives within that class and is loosely coupled because each class deals only with concepts within its domain.
This pattern has immediate benefit beyond the vague coupling and cohesion ones:
- It is easy to data drive because all of the map initialization happens in one place.
- It removes code and data duplication because multiple objects do not have to remember the mapping from the same condition to the same result.
- It is easy to refactor, remove, or replace any of these systems because they are the only classes that understand the details of how a {Sound/Particle Emitter/Tracer} is created.
Patterns like this tend to take a little more effort when adding the first (or second) feature that uses them, but my experience has been that they quickly pay off by making all later features easier to implement.