0
\$\begingroup\$

I have created the following architecture in C++:

GameObject (uint id, Transform transform, Renderer renderer, Rigidbody rigidbody, CustomComponent[] comps; Update(), ReceiveMessage()) -> Scene (Update(), ReceiveMessage()) -> Core (Update(), ReceiveMessage())

ReceiveMessage has two important parameters:

  1. Scope, which defines the scope, like game object, scene or core. This determines how far the message will be forwarded. For example, core messages will be processed by Core, and game object messages will processed by GameObject.
  2. Event, which defnes the name of the event, as an enum. This might include FIND_GAMEOBJECT_IN_RANGE, PLAY_SOUND, etc.

If I accidentally send an event that can only be handled by Core with the game object scope, instead of core, it is only checked at runtime. Currently, it is possible to send a message like ReceiveMessage(GAMEOBJECTS, PLAY_SOUND, ...);. This would be forwarded to every game object, and won't be handled, because the PLAY_SOUND event is only properly handled by Core.

How do I redesign my messaging system so that the compiler will give an error or warning when I try to send a message with an invalid scope-event pair?

\$\endgroup\$
2
  • \$\begingroup\$ I could think of a solution for this, but to write it I need more information about two details: 1. Are there any messages which are valid in more than one scope? 2. When a higher-scope object gets a message through ReceiveMessage, can/must it forward that message to its children? \$\endgroup\$
    – Philipp
    Dec 18, 2016 at 23:49
  • \$\begingroup\$ 1. I don't think so, I don't plan on doing that. 2. If tthe targeted scope is children, then yes, if the targeted scope is the higher-scope, then no, it will be processed there. \$\endgroup\$
    – Tudvari
    Dec 19, 2016 at 8:11

3 Answers 3

1
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ I don' t have time right now, so I just run through your answer: 1. I want to save time by having checked my typeos compile time, not runtime. 2. Separate enums look good to me. 3. Necessity: I don't think that directly accessing big systems from gameobjects is a good practice. It makes the low hiearchy element "gameobjct" dependent on a high hierarchy element. Or do I just overcomplicate it? If I include the different systems, and it works fine, but destroys modularity. If I include the core (which handles the different systems), it will make a big circular dependency through the hierarchy \$\endgroup\$
    – Tudvari
    Dec 19, 2016 at 8:16
  • \$\begingroup\$ So I shouldn't mess with messaging, when the recipent is a "singleton" (not literally) object (like scene, audio, etc)? Should I make methods in the hiearchy, so I can pass something from one end to another (without directly accessing ie. the core from the gameobjects), Or that's just overcomplicating too, and directly accessing them would work just fine? \$\endgroup\$
    – Tudvari
    Dec 19, 2016 at 8:29
  • 1
    \$\begingroup\$ @Tudvari: I wouldn't bother with messaging if you have a single well-known target object, no, unless you can present a valid use case for the added complexity. The hierarchy of the core to game objects is a big topic with a lot of possible answers, all off topic to this question and not suited to comments. :) \$\endgroup\$ Dec 19, 2016 at 8:49
2
\$\begingroup\$

In a comment you said that every message is only valid for one scope. When that's the case, you actually have three separate message systems here and should have separated infrastructures for them.

Create separate enum's for GameObjectMessageId, SceneMessageId and CoreMessageId. Also create three separate methods ReceiveGameObjectMessage(GameObjectMessageId, ...), ReceiveSceneMessage(SceneMessageId, ...) and ReceiveCoreMessage(CoreMessageId, ...) (If your programming language supports method overloading, you can call them all just ReceiveMessage and have them differ by their parameter). Have each class of event receiver only implement those methods which are relevant for it.

When you are using a programming language which has type safety for enumerations at compile-time, you will be prevented from passing an event to an object which is not supposed to handle it.

\$\endgroup\$
1
  • \$\begingroup\$ Yep, the other answerer also mentioned it, and I liked it, but I think I have serious design issues. \$\endgroup\$
    – Tudvari
    Dec 19, 2016 at 8:40
0
\$\begingroup\$

I got some ideas, might be in random order:

  1. I personally do not prefer too much use of enum, especially when it's tied to classes, i.e. GAMEOBJECTS if designed for class GameObject. The tying makes it harder to expand/modify the code base, So my first move would be remove the enum and do not rely on it.

  2. It looks like ReceiveMessage(Scope scope, EventName name, ...) would do some if (name == xx) or switch(name) work inside, which I personally feel tedious. Also you are concerning not to receive irrelevant messages. So the second move would be find a way to let event dispatcher directly fire the event to exactly who wants to receive it.

So, anyway, I suggest to implement the event system like the signal & slot system mentioned by Sean Middleditch. And I suggest to check the Unreal Engine 4's event system -LINK HERE-, which is also written in C++, so it might help you a lot. Note that UE4 uses the term "delegate" for the event dispatcher. For the event receiver to receive the message, they bind their listener function to the delegate, and it's done. By the way you can also check C#'s delegate architecture, they are very alike.

The advantage of such system is that:

  1. The event dispatcher would be a member variable, so it looses inheritance tree. You don't have to let every class who wants the ability to fire/receive event to inherit anything, just declare a member and it's done.

  2. Since it's a member, you can have multiple members, each of them represent different messages.

  3. Since each of them represent different messages, you can have different parameters and types and whatever for them.

  4. The dispatchers just fire, don't care about the receivers, which is very easy task for the dispatchers.

  5. The receivers "bind" themselves to the dispatcher, and can "unbind" anytime they want, so you can control when/how to react to the messages.

  6. Since the receivers have to bind to each dispatcher, the listener function can be separated, and they can have different parameters (basically the same as the delegate's parameters).

  7. In this design, the event scope and name won't be needed, since the dispatcher's variable name, variable type, and parameters fired directly represent the scope and name, and in a better way.

I'll give some conceptual example codes of the suggested solution:

class MyObject1
{
public:
    // this is UE4's syntax, which creates a new type of dispatcher
    // this line creates a dispatcher of type `MyMessageType1`, and 1 parameter of string
    DECLARE_DELEGATE(MyMessageType1, string);
    MyMessageType1 message;

    void DoSomething()
    {
        string result = "";

        // ... do many things
        // things are done and good, send a message

        result = "good!".
        message.Broadcast(result);

        // note here, the listener can be anyone, 
        // and the dispatcher doesnt care, just fire.
    }
}

class MyObject2
{
public:
    void Initialize()
    {
        MyObject1* obj1 = SomehowYouHaveThePointerOfSomeObj1();

        // bind this object and its function to the dispatcher,
        // and the message would be received when dispatcher do the `Broadcast()`.
        obj1.message.Bind(this, &MyObject2::OnReceiveObj1);
    }

    void OnReceiveObj1(string result)
    {
        if (result == "good!")
        {
            // whatever
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ But my problem is that my hierarchy is bidirectional. So the scene canmessages to the game objects, and the game objects can also send message to the scene (which will be maybe forwarded to other game objects) But maybe your suggestion is also good for this situation too, and I just didn't understand it :( \$\endgroup\$
    – Tudvari
    Dec 23, 2016 at 15:49
  • \$\begingroup\$ The point of my suggested solution is to let each dispatcher-listener pair to be independent. So that you won't care about "direction", so there is no "bidirectional". The dispatcher sends messages, and listeners who want to receive just bind their selves to the dispatcher. The dispatcher and listener can be anyone, separately, so there's no "direction" involved, which may lighten your burden of design :p \$\endgroup\$
    – Marson Mao
    Dec 26, 2016 at 1:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .