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.
I consider using messages this way to be analogous to
run-time linking. The message type defines which function gets called and the
message parameters correspond to the function parameters. This approach trades
frame-rate for compile time without making the code significantly more
flexible. Sometimes this is a good trade-off but it is rarely a clear win.
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
Now we just need to handle the event. If you look back to
the very first code snippet you will see something like this:
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.