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.
Bad idea 1: We could check for a collision on all
ColliderHealthNodes in aHandleCollisionsSystemeach frame, but this seems like a waste of computation. The value likely won't be changing every frame.Bad idea 2: Instead of an events system however, we can use the ECS to pass this logic via system registration. Our
HandleCollisionsSystemcould add aCollidedcomponent (really just a tag) to theCollidercomponent's entity. Then ourHandleCollisionsSystemwould 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
CollisionSystemwill addCollidedcomponent tags to the entities of theColliderNodes it processes?) How do we know that ourHandleCollisionsSystemneeds to have an aspect that includes aCollidedtag?CollisionSystemandHandleCollisionsSystemneed to know each others inner workings and so become logically, but invisibly, coupled.Good idea (?): The approach that makes the most sense in my mind is to use an event-listener pattern and to have our
CollisionSystembroadcastCollisionEvents, while ourHandleCollisionsSystemcan listen toCollisionEvents. 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.