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
ColliderHealthNode
s in aHandleCollisionsSystem
each 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
HandleCollisionsSystem
could add aCollided
component (really just a tag) to theCollider
component's entity. Then ourHandleCollisionsSystem
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 addCollided
component tags to the entities of theColliderNode
s it processes?) How do we know that ourHandleCollisionsSystem
needs to have an aspect that includes aCollided
tag?CollisionSystem
andHandleCollisionsSystem
need 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
CollisionSystem
broadcastCollisionEvent
s, while ourHandleCollisionsSystem
can listen toCollisionEvent
s. 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 HandleCollisionsSystem
then gets registered as a listener for CollisionEvent
s 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 ColliderHealthNode
s 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 CollisionEvent
s and affect different aspects.