You didn't specify language, but there's some options that should work regardless. The first question to ask yourself is if this is a problem that actually needs solving, though; is this a hypothetical problem you have with your code or are you wasting considerable time/money by frequently sending events to the wrong places?
Signals and Slots
One is to move away from a generic event broadcaster and to use direct message types and registrations. Think Qt's signals and slots. In order to send an event you must call an explicit handleEventType(EventType event)
method; if it doesn't exist, you can't call it.
This tends not to scale well for things like game objects because a system emitting an event doesn't know which objects care about. However, it works very well for systems receiving events, since there tends to be a small number of those.
Signals Variant
A variation of that would be to invert your event flow. Instead of sending messages to receivers, send messages from emitters, and anyone interested in the event finds the appropriate event emitter and registers.
This works very well for any kind of event, so long as the event's emitter is well-known; e.g., it doesn't work well for events emitted by game objects. For example, it works well for events emitter by systems that an object or another system is interested in.
The combination of these two approaches can potentially solve both the sending of events to systems from systems/objects and the sending of events from systems to systems/objects, but neither approach handles well the case of sending events from objects to object. You can keep your existing approach for that case though and solve your original question by only allowing the generic message system to be valid for objects and never your core systems.
Event Queues
You could move away from immediate-mode events and instead to generate event lists during system updates, and the lists persist until next frame. Subsequent systems can then iterate over all the event lists they may care about. This is the approach that many data-oriented enthusiasts prefer.
This scales fairly well and is easy to extend. It will work very poorly if your game objects or components have their own update
methods and you're not keeping the meat of your game logic in systems that operate on collections of components/objects. Given your use of UML diagrams (hello 1990's, welcome back!) I'd bet strongly that this approach isn't going to work well with your architecture. :)
Separate enums
You mention enums for types. You could use different enums for each context and make the handler only accept appropriate enums. If a message is valid in multiple context, just add its key to both enums; if you make them use the same value, you can even forward messages dynamically. You can use explicit ranges of message identifier values to denote which range is shared so you can quickly check that any dynamic message is multi-context.
Basically, Core::HandleMessage(ECoreMessage type, ...)
vs GameObject::HandleMessage(EObjectMessage type, ...)
.
Tag Types
This is a more advanced version of the enum
approach that relies on there being specific types for each message, e.g. a struct
or something that contains event payload.
You can use inheritance or C++ type traits to augment any message with one or more context tags that denote which context(s) it is compatible with. This can be enforced via the type system to make it a compile-time error to send the wrong kind of message to the wrong context.
This can be tricky to implement and embeds a lot of data into the type system in very awkward ways. If you're looking for a compile-time fix to an object-based dynamic message system, this might be your best bet, but I'd consider it a last resort.
Revisit the Necessity
I want to stress again that you should really be sure you need any of this. The problem you're describing has come up so rarely in years of working on an engine that allows it, in my experience. Some of your examples also, to me, seem like they're better uses for functions and not events. If the only possible receiver of a PlaySound
event is the audio system, why are you sending an event instead of just audio->playSound(...)
?
If the argument is decoupling object relationships (I see this one a lot), just keep in mind that converting functions into messages and tossing them through a generic message router doesn't decouple anything; your whole question is basically about how you can statically check the relationship between a message and its receiver.