7
\$\begingroup\$

I want to ask if the following is an effective way to architect event propagation using an ECS?

Here is a hypothetical collision scenario using an ECS.

Components:

class Collider {
    public var rect:Rectangle;
}

and

class Health {
    public var health:Int;
}

Now for the behaviour logic:

We can create entities with Collider and/or Health components.

Some ColliderSystem handles entities (more specifically, nodes) with Collider components. It deals with handling collision logic and determining if two Collider component rectangles overlap.

Moreover, if an entity with a Collider component has a Health component, the Health component's value should decrease.

How would I implement this behaviour?


I see three ways to approach it, 2 bad, 1 good.

  1. Bad idea 1: We could check for a collision on all ColliderHealthNodes in a HandleCollisionsSystem each frame, but this seems like a waste of computation. The value likely won't be changing every frame.

  2. Bad idea 2: Instead of an events system however, we can use the ECS to pass this logic via system registration. Our HandleCollisionsSystem could add a Collided component (really just a tag) to the Collider component's entity. Then our HandleCollisionsSystem would be registered to our ECS with an aspect of <Collider, Health, Collided>.

    Besides this looking like an overuse/abuse of ECS, it also silently couples logic between systems (how do we know that CollisionSystem will add Collided component tags to the entities of the ColliderNodes it processes?) How do we know that our HandleCollisionsSystem needs to have an aspect that includes a Collided tag? CollisionSystem and HandleCollisionsSystem need to know each others inner workings and so become logically, but invisibly, coupled.

  3. Good idea (?): The approach that makes the most sense in my mind is to use an event-listener pattern and to have our CollisionSystem broadcast CollisionEvents, while our HandleCollisionsSystem can listen to CollisionEvents. The implementation is described below:


We'd create an enum:

enum CollisionEvent {
    BEGIN_COLLISION;
    // ... could have more types
}

And we create our CollisionSystem like so

//aspect:<Collider>
class CollisionSystem {

    // some collision determining logic to determine if a Collider
// is being collided with -> calls the broadcastCollisionEvent function
// on those Colliders that are colliding
    
    function broadcastCollisionEvent(event:CollisionEvent, collider:Collider){
        eventSystem.broadcastEvent(event, collider)
    }
}

We'd then have an interface

interface CollisionEventListener {
    function handleCollisionEvent(event:CollisionEvent, collider:Collider);
}

And our HandleCollisionsSystem would look like

//aspect:<Collider, Health >
class HandleCollisionsSystem implements CollisionEventListener
    
    public function handleCollisionEvent(collisionEvent:CollisionEvent, collider:Collider) {
        colliderHealthNode = findNodeForColliderComponent(collider)
        switch(collisionEvent){
            case BEGIN_COLLISION:
                assocNode.health.health -= X; // some computation
            // other cases
        }
    }

Our HandleCollisionsSystemthen gets registered as a listener for CollisionEvents to the EventSystem. When the CollisionSystem broadcasts a CollisionEvent, the EventSystem will propagate that call to the HandleCollisionsSystem to handle.

Is this the best approach for handling event propagation between systems?

If it isn't: is there a better way?

If it is: how would we implement findNodeForColliderComponent()?:

Would we always register ColliderHealthNodes within a Collider->ColliderHealthNode map within our HandleCollisionsSystem upon registration to be looked up later?


Note 1 Specifically referring to a ECS to mean pure ECS: that is, all behaviour would be encompassed by systems and all components are data-only. Systems don't have access to entities directly and only operate on "nodes"- bags of components that match their respective aspect; ie ColliderSystem operates on "nodes" with only a Collider component, and HandleCollisionsSystem operates on "nodes" with both a Collider and Health component.

Note 2 In this simplification, it may look like merging CollisionSystem and HandleCollisionsSystem would make sense, but the separation is there to allow extension to a more complex scenario where we have multiple different HandleCollision systems that care about CollisionEvents and affect different aspects.

\$\endgroup\$
3
  • \$\begingroup\$ Danny, I have to comment at this stage that this question is, and always has been, overly broad. Usually such a question would be flagged for closure but because this is a bountied question, that is prevented by the system. You need to be more succinct and to-the-point in what you are presenting here, or the question is not going to get sufficient attention and your bounty will likely go to waste. As it is (and I may not be one to speak, but I had to mirror the openness of your question in my responses) there is simply a wall of text here and too many edge cases to address in a concise answer. \$\endgroup\$
    – Engineer
    Commented May 31, 2017 at 13:38
  • \$\begingroup\$ I suggest you spend more time over the next couple of days thinking about what you are really asking here, before editing again. Do not leave large open solution spaces where respondents can go astray. Restrict the scope tightly. Then whittle your question text down to reflect that. Good luck. \$\endgroup\$
    – Engineer
    Commented May 31, 2017 at 13:39
  • 3
    \$\begingroup\$ I beg to disagree. Because ECS means different things to different people, I have to elaborate on the flavor of ECS I'm talking about. Then, I provide a concrete scenario to keep the question from being overly vague. The question specifically asks about architecting event propagation between systems in this flavor of ECS. The answer I'm looking for is whether there is a better way to architect event propagation (again, in this flavor of ECS) \$\endgroup\$ Commented May 31, 2017 at 13:53

2 Answers 2

11
+100
\$\begingroup\$

Merging systems, such as collision and collision response, is a bad idea. The reason being that there can be many different responses to collisions: Physics based, and game-logic based.

In most game engines I have seen (ECS and non-ECS and all the greys in-between), there is some kind of inter-system communication system, because this is what makes the creation of games possible.

As far as communication goes, you want your engine to handle two basic message types: events, and commands.

Events get queued up, and processed at the beginning of the next frame(synchronously), or handled asynchronously (as soon as they are received). Which you choose is up to you (but I recommend async).

Commands are for something that does not affect the game state, but modifies the engine state (startup/shutdown/change settings/log error).

Both can be routed very similarly, but this distinction is very important: An event is something that occurred in the past, and a command is something that is to occur in the future. What is also very important is whether you let the processing of one event generate other events. If handled incorrectly, you may end up blowing the program stack.

My personal approach, is for the highest level of the engine to have a central event queue, which is emptied at the beginning of the update cycle, and filled up again during the update cycle, as game events occur. Each system has it's own event queue, but the events that end up in there are filtered by each systems responsibilities.

When these events are processed by each system, they can generate additional events which are passed back up to the engine level, and en-queued.

Once this is in place, you can then have multiple collision event handler systems, each of which behaves differently (and could be run off scripted behaviours if you want).

Now, here is where we get to the meat of the question:

How do we handle things like health from say, a bullet hitting the player?

Well, that's simple enough. Both entities should have some kind of GameObjectData component, which, to keep this simple, contains a type (string, enum, whatever). The player has a CharacterData component too.

So, when a collision event is received by our "GameCollisionHandler" system, we can query a couple of things, like so:

GameCollisionHandler::handleCollisionEvent(CollisionEvent& e, Engine& engine){
    //find entities by unique id provided by event


    auto entityA = findEntity(e.first.getID());
    auto entityB = findEntity(e.second.getID());

    //Query if we have a bullet/player match
    if ( entityA->hasComponent<GameObjectData>() ){

        auto data = entityA->getComponent<GameObjectData>();
        if ( data->getType() == "Player"){

            if ( entityB->hasComponent<GameObjectData>() ){

                auto data = entityB->getComponent<GameObjectData>();
                if ( data->getType() == "Bullet"){

                    if (entityA->hasComponent<CharacterData>()){

                        entityA->getComponent<CharacterData>()->decrementHealth(10);
                        // and let the engine know that the bullet is to be removed
                        engine.sendCommand(RemoveEntityCommand(e.second.getID());

                        if ( entityA->getComponent<CharacterData>()->getHealth() <= 0){
                            engine.sendEvent(PlayerDeath(e.first.getID());
                       } 
                   }
                }
            }
        }
    }
}

This is a very crude example, but I think it illustrates the idea reasonably well.

So, to more concisely answer your question:

Is this the best approach? Possibly, there are no silver bullets in programming. I believe so, but could be wrong.

Is there a better way? I don't believe so.

How would we implement findNodeForColliderComponent()? I have no I idea what you mean by that, except that it is some form of search function. Your engine, at this stage, should already have some form of multi-function search ability, so that should cover it.

\$\endgroup\$
4
  • \$\begingroup\$ It seems like you're advocating to keep the event logic at a high-level above the ECS (to be dealt with by managers that know about the entities and can query for entity components). The nuance in what I'm asking is if it makes sense to put this event logic at a level below that- have events dealt with by systems subscribed to the ECS that deal with only their aspects' components. This allows us to bypass having to query for the components per entity. Hence the findNodeForColliderComponent() call and the passing of the associated Collider component. Is this more hassle than it's worth? \$\endgroup\$ Commented Jun 1, 2017 at 16:41
  • 2
    \$\begingroup\$ The event logic is handled by the ECS systems. The high level queue is simply a way to prevent over-propagation of events. in the bullet/player example I mentioned, each system should have a findEntity which would search for and return the proxy that contains the information needed to process the event properly, IF the system is interested in that event. \$\endgroup\$
    – Ian Young
    Commented Jun 1, 2017 at 21:23
  • 1
    \$\begingroup\$ I don't think you actually mean for the events to be handled asynchronously, as that would mean that you'd need to execute the code in parallel without blocking at all. I think what you really want is for events to be processed instantly, while blocking. If you ran the event callback async, then the system that is responding to the event might even execute while it's own callback to the event is still running. The distinction isn't between async or not, but rather whether you hold events and process them until a later time, or if you directly execute the response callbacks for each event type. \$\endgroup\$
    – nenchev
    Commented May 16, 2019 at 13:59
  • \$\begingroup\$ Correct. All events are handled by each system on the main thread, but the update cycle for each system is performed on worker threads. With an event queue there are no callbacks; The events are propagated down to the systems during the update cycle, i.e. the queue tells the subsystems "This what happened last frame, deal with this now". \$\endgroup\$
    – Ian Young
    Commented May 17, 2019 at 17:14
3
\$\begingroup\$

Your question boils down to

Is it a good idea to use a publisher-subscriber-based GUI and to keep this subsystem distinct from my ECS (while enabling interop between them)?

Yes, keep your UI and your ECS systems totally separate.

Notice what I'm not saying here: I'm not saying you should not have mouse-clickable entities. What I am saying is that it would be ideal to keep your primary UI system totally separate from your clickable entities system. But first... Why is it a good idea to keep them distinct?

  • You are obeying the Single Responsibility Principle
  • Low coupling brought by pub-sub (event dispatchers and handlers)
  • "Silent coupling" that you mentioned is avoided - you never, ever want to think about side effects of what a system is doing. You want systems that do the minimum possible to fulfill their role, whether that is at the function, class, or module level. "Do the job and go home" - see FP.
  • You indeed want to avoid polling if you can help it, as it is costly and unnecessary in most cases. Handle things as they happen, this means function call stacks which means your event system.
  • Btn components - bad, bad idea. You are smearing (for lack of a better term) one layer of logic into another, embedding an aspect of the UI system into your ECS. It quickly becomes unclear who should be controlling whom, and that's a bad place to be at in a codebase.

So how can you achieve this?

Option A There's the play area, and superimposed over it, the UI. If a click misses everything in UI, it gets delegated down to the play area / game world viewport. At this stage, your entity system takes over entirely, and uses its own custom approaches to handling clicks. Notice that what is shared between these systems (UI and ECS) is fairly trivial - primarily the ability to decide who, if anyone, gets the click. It's generally a lot easier in the game viewport however, because you usually haven't got nested elements to contend with, as in UI. So it's a simple box, circle, and/or pixels based collision check that returns an entity ID. And away you go.

Don't be taken in by that little voice that says, "but... but... I could reuse so much of the architecture if I could only make the entities use the UI system!" It's a seductive lie that never works out. YAGNI. Better to copy some logic from one to the other where needed, and avoid mangled architecture.

Option B Create a simple UI element (button?) with bounds to represent each entity and have these track the entity positions each frame, only for those entities that are in view and thus clickable. In order to do this, your UI system should listen for additions, removals, and positional changes of individual entities as broadcast by the ECS core (preferably not from individual entities themselves). This way as far as the UI system is concerned, it is dealing only with its own known types (UI elements) and not some foreign types (Entities) which it would then have to be re-written to handle (tight coupling).

P.S. I made a joke on another ECS post today that "In order to discuss ECS, you first have to define ECS... in full." That holds true here as well. I am not telling you how ECS is supposed to work. Everyone has a different view of that, and some approaches are considered more "pure" than others. I am giving you the benefit of experience - don't mix the primary subsystems of your game. ECS is a direct submodule of your larger game logic, and game logic should never be dealing with raw input or rendering, which is exactly what UI is. That's why you are advised to have the UI system, as a View (in the MVC sense), subscribe to the ECS system which is a Model (in the same sense) in B, ensuring your core game logic concerns remains clean, vs. being polluted by unrelated concepts.


EDIT

Let's look at your proposals again (now that they've been updated via the comments).

  • Use an ECS in the pure sense of having supercontrollers make changes to, and read state from, data elements; (consequently) have pure data elements with no functions.
  • Not separate your logic between view/render/UI at the ECS components level,
  • Yet separate it at the supercontroller level (BtnSystemvs. BtnGraphicSystem where you have your pub-sub).

Focusing on ECS, I suggest that you read Mick West's Evolve Your Hierarchy, the seminal text on pure ECS. Mick does not disallow mixing of view vs logic components; whereas for the record, I do.

On my (deleted) answer you mentioned:

...all behaviour would be encompassed by systems... For an ECS-based GUI, Btn would be a component (data-only). Btn would not have an OnDown function and BtnSystem (which would have a aspect) would handle all entities with Btns behaviour. Thats why the BtnSystem would receive events from an InputMgr and handle logic per Btn. A BtnGraphicSystem would receive events passed from a BtnSystem to apply graphic changes to entities who have both Btn and Graphic components.

You are planning to implement several new classes just to implement this one, tiny, new bit of functionality. That's a code smell. Good engineering primarily centres around one thing: Keep moving parts to a minimum. Every interface between parts is a potential point of failure.

In addition to the above wants, you presumably also need a generalised UI system that will be generic to all sorts of UI elements (menu buttons, inventory etc.), that will in any case replicate some of the functionality of clickable entities. If you already have such a system that amply accommodates UI elements, and/or can be easily extended to accommodate a new sort of UI element while still remaining utterly generic (i.e. no knowledge of Btn* at all), then why would you introduce new classes to do what the UI system can already do? Let the regular UI system handle button state changes, as it would for any other button. For the UI supercontroller to find out about entities, have a proxy, be it your main game loop or a distinct UIEntityProxy controller, subscribe to events dispatched by the ECS supercontroller, which it then routes to your regular UI system (which could be anything from QT to DOM to your own custom but encapsulated OpenGL-based system). That is, the proxy will inform the UI to add remove entities for the current viewable area of the world. The UI system can then act to add clickable elements for each entity, as appropriate, and maintain graphical state for these.

However, I think you are proposing that the graphics are instead handled by the ECS supercontroller. Also, I think entities sound like your primary interest here, and not generic UI systems, so let's ignore the latter. ECS system then handles all clicks, game logic changes and graphics. The pub-sub approach is unnecessary, and I do see a problem with the separation into BtnSystemvs. BtnGraphicSystem; since your entity components may be either view or game logic related, then you may as well conflate this logic in your supercontroller as well, and avoid class proliferation (fewer moving parts). Doing Controller vs. View in once place, yet not in another, just leads to confusion. It can be as simple as the following (pseudocode ahead - interpret rather than taking literally):

ECS.BtnSystem
{
    //register for event from your OS, platform, or library
    Initialise()
    {
        SomeAPI.RegisterMouseDownCallback(OnMouseDown);
    }

    //receive callback from your OS/platform/library indicating that mouse device state has changed
    OnMouseDown(pos)
    {
        entity = GetEntityAtClick(pos);
        entity.UpdateModelState(); //cache game logic state of the entity as caused by mouse interaction
        entity.UpdateGraphicState(); //cache render (or "frame") state induced
    }
}

//main game loop
...
{
    ...
    //by here, OnMouseDown should already have run and stored some state - both logical and graphics - for the following steps to use.
    RunGameLogic(); //use game logic state
    ...
    RenderBackground();
    ECS.RenderEntities(); //use stored graphic state
    ...
}

Why set up event systems when you can just do everything where the device input event comes in?

\$\endgroup\$
7
  • \$\begingroup\$ Great response, although I'm not convinced that your arguments to never treat UI elements as entities hold weight. Yes, for a scenario where your UI is simple and can simply overlaid, perhaps making UI entities is overkill. But games are rarely this simple; you can have complex overlaying logic. You may have to bind multiple entities in multiple ways to different interactions; at which point when you say As far as the UI system is concerned, it is dealing only with its own known types (UI elements)- makes it sound like entities with UI components handled by UI Systems in your ECS would work. \$\endgroup\$ Commented May 30, 2017 at 15:43
  • \$\begingroup\$ My question is really IF I do choose to create the game GUI via components in an ECS manner, would the method I've shown for events make sense? You don't make much of a case for why btn components - bad, bad idea. With a complex multi-touch/swipe/etc interactive game, this logic needs to happen somewhere. It isn't unclear who should be controlling whom as the UI System is the system that would register with the ECS and would handle those entities with UI components. Moreover the "registering" in option B would be handled by Systems subscribed to entities with UI components. \$\endgroup\$ Commented May 30, 2017 at 16:05
  • \$\begingroup\$ @DannyYaroslavski I've made a third and final contribution to this, and have deleted the other answer as it was apparently utterly invalid. I've edited your answer with those additional specs, to make it explicit, since as I said, "to discuss ECS, you must define (your brand of) ECS", which wasn't done in your question. \$\endgroup\$
    – Engineer
    Commented May 31, 2017 at 7:49
  • \$\begingroup\$ wrt the new answer: I think a lot of the answer stems from disallowing mixing view vs logic which, for the record, I'm not advocating for. I think we have mismatching views on ECS (I imagine pure ECS to be modelled similarly to a framework like Ash). My view entails not have direct entity access; you can only operate on nodes- containers of components- passed to a system that match the system's aspect); making your pseudocode solution inapplicable. It does seem like some form of proxy object would be required to hook in UI-component entities into the UISystem however. \$\endgroup\$ Commented May 31, 2017 at 12:08
  • \$\begingroup\$ Like I commented on the deleted answer, I'll consider creating an equivalent question without the UI context that may better highlight the actual question at hand; which is really about passing events about specific components between systems in ECS. \$\endgroup\$ Commented May 31, 2017 at 12:10

You must log in to answer this question.

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