Jump to content
  • Advertisement
Sign in to follow this  
AShinyAcorn

Event Handling within an ECS

This topic is 917 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

First off, this site has been a great resource and it's helped me many times!

I've developed an Entity Component System, where Entities hold a collection of Components, and Systems hold a reference to the Entities that they operate on. Systems additionally can take in certain events to add or remove Entities. 

The problem I've gotten hung up on is how to handle events that pertain specifically to Entities rather than Systems. For example, a collision event where if the entity collided with is of type A, the velocity is transferred. But if the Entity is type B, then it is destroyed instead. 
I am not sure how to handle this. 

 

I could write a System that does nothing in it's Update() function, but instead just listens for specific Events. The System then performs some logic on the affected Entities.
I do not like this approach, as I feel a System should be responsible for dictating how an Entity would act at every Update(), not just a select few whenever a specific Event rolls through.

 

Another idea I had was to have a Component store a function pointer. Then, when an Event rolls in, all Entities with the Component that corresponds to the Event calls the function it is storing. This way, Entities can act in a variety of ways even though they are all responding to the same event. 

Again, I do not like this method because I am trying to keep all logic out of Components, and make them strictly data-oriented. 

 

The final idea I had was to just have each Entity contain a collection of function pointers, mapped by Event IDs. I am leaning towards this idea, but I am not sure how well I'll like it down the road (I want to completely eliminate the idea of an "Entity" and reduce it to just an ID, with the same ID between systems being the same "Entity".)

I am not sure how to develop this out, and resources I've found on the subject have just barely touched on the subject, preferring to focus more on the ECS side. What have you guys done, if you have implemented such a thing? 

Share this post


Link to post
Share on other sites
Advertisement
http://bitsquid.blogspot.com/2011/01/managing-coupling.html -- read all four parts of that series. It covers what Hodgman is talking about with more technical details, as well as some related topics. Read the whole blog, really. That one and the Molecular Musings blog.

I kinda disagree with both Hodgman and the blogs on this topic, but if you're going to go head-first into "by the book" DoD I'd definitely read through them.

Share this post


Link to post
Share on other sites

A common theme in ECS is batching component processing together, which contradicts with callbacks and immediately-processed events. Another common theme is focusing on the data flows of the program, which contradicts opaque "update" functions.

If I am understanding you correctly, I am already doing that. For example, when it is time to simulate physics, I go through and all by xVel and then yVel. Then I go through all Entities registered who have a RigidBody component, and check for collisions between them, generating the needed information, etc.
I queue up events, so they are processed at the start of each frame, I don't immediately try to resolve them.

 

I'm confused at how to handle events for different types. Take Arkanoid/Breakout, for example.
A ball hits a brick, a CollisionEvent is generated and Queued. Then in the next frame, while processing the events, the CollisionEvent comes up. I'm not sure how to handle that.

Should I make some collection of Systems that do nothing but listen for CollisionEvents? Various Entities are subscribed to them, so each Entity handles a CollisionEvent differently? IE, DeleteBrickOnCollisionSystem deletes the brick object upon receiving the CollisionEvent, while BounceBallOnCollisionSystem just reverses the balls y-velocity when it receives a CollisionEvent? And then, what if I wanted to go a step further and have some bricks bounce off other bricks? I'd need to make another system that would only listen for these CollisionEvents, check if it hit a ball or a brick, then do something based on that. 

 

 

http://bitsquid.blogspot.com/2011/01/managing-coupling.html -- read all four parts of that series. It covers what Hodgman is talking about with more technical details, as well as some related topics. Read the whole blog, really. That one and the Molecular Musings blog.

I kinda disagree with both Hodgman and the blogs on this topic, but if you're going to go head-first into "by the book" DoD I'd definitely read through them.

 

I've read a bit of bitsquids stuff before, it's very helpful! The articles you are suggesting in particular are great, but they aren't exactly what I am looking for. I'll take a peek at Molecular Musings blog, though. I haven't read anything there!

Share this post


Link to post
Share on other sites
Why do you have to wait until next frame to resolve collisions? Usually you move objects, resolve collisions, then draw them.
Note that in most physics engines, resolving collisions means that you have to move objects for a second time that frame.

For Breakout, there's two Entity templates/prototypes for blocks. One has Sprite+AABB+Bouncy and one has Sprite+AABB+Breakable.

When checking for collisions, you have a loop that reads Ball components and AABB components. Instead of having one global events list, the AABB component could have a property that's used for event routing. The AABB's in the first type of block entity could route it's touch events to the BouncySystem, while the second type could route to the BreakableSystem.
e.g.
struct BallTouchEvents { vector<pair<Ball*,Entity*>> events; }
struct BouncySystem { BallTouchEvents ballEvents; ... }
struct AABB { int x,y,w,h; BallTouchEvents* onTouch; };
void CheckBalls( vector<Ball> balls, vector<AABB> bounds)
{
 foreach balls, foreach bounds
   If ball and bounds touch then
     bound->onTouch.push_back( make_pair(ball, bound->Owner()));
}

Share this post


Link to post
Share on other sites

A solution I've done before is t simply have a Collision Component added to entities that are colliding.  This component has which other entity is being collided with, and has a flag noting whether it's an initial collision, a sustain collision, or a just separated collision.  You can then have your collision system operate on entities with collision components, and call the functions required when collisions happen.

 

Just another way of doing things, not the right way, or the only way, just another possibility.

Share this post


Link to post
Share on other sites

Why do you have to wait until next frame to resolve collisions? Usually you move objects, resolve collisions, then draw them.
Note that in most physics engines, resolving collisions means that you have to move objects for a second time that frame.

For Breakout, there's two Entity templates/prototypes for blocks. One has Sprite+AABB+Bouncy and one has Sprite+AABB+Breakable.

When checking for collisions, you have a loop that reads Ball components and AABB components. Instead of having one global events list, the AABB component could have a property that's used for event routing. The AABB's in the first type of block entity could route it's touch events to the BouncySystem, while the second type could route to the BreakableSystem.
e.g.

struct BallTouchEvents { vector<pair<Ball*,Entity*>> events; }
struct BouncySystem { BallTouchEvents ballEvents; ... }
struct AABB { int x,y,w,h; BallTouchEvents* onTouch; };
void CheckBalls( vector<Ball> balls, vector<AABB> bounds)
{
 foreach balls, foreach bounds
   If ball and bounds touch then
     bound->onTouch.push_back( make_pair(ball, bound->Owner()));
}

A little more miscommunication on my part, but I think I see what you are saying! I am resolving the collisions as far as moving things back so they are no longer colliding, but I am not applying "what happens" immediately. That is what I want the queued event to do. If there is a collision between ball and brick, move the ball so it is not colliding and draw that frame. Next frame, process the collision event, queue the brick for destruction, and "bounce" the ball off of the destroyed brick. Process everything, queue up more events, then draw. 

But, I think I understand your example. Each AABB could belong to a different system. When a collision happens, it creates & pushes a CollisionEvent to that system, who would then operate on the collection of CollisionEvents when the time comes to process events?
But then I'm with another problem I mentioned earlier: having a System that does nothing but handle events, and it's Update() function does nothing. 
I suppose instead of pushing the events to a particular system, I could queue up a bunch of callbacks and execute them in-order sometime during the next execution.
You mentioned the virtual void Update() antipattern - are you talking about the Open/Closed principle? Should I be passing a function pointer to each System on it's creation, then having the Update() call that function pointer, passing in it's collection of Entities? I don't see how else you could do specific operations on a collection from system to system without overriding the Update() function or passing it a function pointer. 

Share this post


Link to post
Share on other sites
Keep in mind with physics - so far as a generalized pattern goes - you really can't do delayed handling of collisions like this. Physics resolution is non-trivial and requires the physics engine to do a number of iterations over the contacts. If you want to filter out a contact, you must do it during a very specific phase of the physics resolution. With most middleware, you don't get a lot of choice other than to do this all with callbacks. You could setup the callbacks to push the contacts into a queue and then use the general pre-resolution callback to ask all interested Systems to process that stack of collision events, but you certainly can't delay the handling of them until next frame.

Of course, if you're writing your own special-purpose physics for a simple game like Breakout you have a lot more flexibility. Just keep in mind that the pattern you're using is fairly limiting and not applicable to many other types of games.

You mentioned the virtual void Update() antipattern - are you talking about the Open/Closed principle? Should I be passing a function pointer to each System on it's creation, then having the Update() call that function pointer, passing in it's collection of Entities? I don't see how else you could do specific operations on a collection from system to system without overriding the Update() function or passing it a function pointer.


Virtual Update is usually more frowned upon when used in contexts of individual game objects or components rather than high-level managers or systems. You'll possibly have hundreds of entities and thousands of components but only dozens of systems in a small game, so looping through all components to call Update is painful while looping through all systems to call Update is fine. The other big problem of the general virtual Update pattern is that the naive implementation limits your ability to control the order of when systems are updated which is usually pretty important; this is solvable in a number of ways, though.

That said, you don't need function pointers for event queue processing. One solution is to let Systems have dependencies on each other, e.g. the BreakableBlockSystem is allowed to know about the CollisionSystem and directly call a CollisionSystem::ReadCollisionEventQueue() or the like. It's totally acceptable to have dependencies within code that is naturally coupled; in that case you also want to very explicit about the dependencies and avoid _implicit_ coupling (which is what leads to difficult-to-understand code and bugs).

You could avoid dependencies between systems and have only dependencies on event types, too. For instance, if you have some general event queue manager that tracks all queues of events, your System need only depend on that event queue and the specific type of event it needs. The way to look at this is that BreakableBlockSystem has no reason to care about how collisions were detected (so it doesn't need to know if collision events came from CollisionSystem or from DebugReplaySystem or whatever) but only cares about the collisions themselves (hence it only depends on EventSystem::Queue<CollisionEvent> or the like). This starts moving a little more toward implicit dependencies but is still more than "explicit enough" for most purposes.

Share this post


Link to post
Share on other sites

Keep in mind with physics - so far as a generalized pattern goes - you really can't do delayed handling of collisions like this. Physics resolution is non-trivial and requires the physics engine to do a number of iterations over the contacts. If you want to filter out a contact, you must do it during a very specific phase of the physics resolution. With most middleware, you don't get a lot of choice other than to do this all with callbacks. You could setup the callbacks to push the contacts into a queue and then use the general pre-resolution callback to ask all interested Systems to process that stack of collision events, but you certainly can't delay the handling of them until next frame.

Of course, if you're writing your own special-purpose physics for a simple game like Breakout you have a lot more flexibility. Just keep in mind that the pattern you're using is fairly limiting and not applicable to many other types of games.
 

You mentioned the virtual void Update() antipattern - are you talking about the Open/Closed principle? Should I be passing a function pointer to each System on it's creation, then having the Update() call that function pointer, passing in it's collection of Entities? I don't see how else you could do specific operations on a collection from system to system without overriding the Update() function or passing it a function pointer.


Virtual Update is usually more frowned upon when used in contexts of individual game objects or components rather than high-level managers or systems. You'll possibly have hundreds of entities and thousands of components but only dozens of systems in a small game, so looping through all components to call Update is painful while looping through all systems to call Update is fine. The other big problem of the general virtual Update pattern is that the naive implementation limits your ability to control the order of when systems are updated which is usually pretty important; this is solvable in a number of ways, though.

That said, you don't need function pointers for event queue processing. One solution is to let Systems have dependencies on each other, e.g. the BreakableBlockSystem is allowed to know about the CollisionSystem and directly call a CollisionSystem::ReadCollisionEventQueue() or the like. It's totally acceptable to have dependencies within code that is naturally coupled; in that case you also want to very explicit about the dependencies and avoid _implicit_ coupling (which is what leads to difficult-to-understand code and bugs).

You could avoid dependencies between systems and have only dependencies on event types, too. For instance, if you have some general event queue manager that tracks all queues of events, your System need only depend on that event queue and the specific type of event it needs. The way to look at this is that BreakableBlockSystem has no reason to care about how collisions were detected (so it doesn't need to know if collision events came from CollisionSystem or from DebugReplaySystem or whatever) but only cares about the collisions themselves (hence it only depends on EventSystem::Queue<CollisionEvent> or the like). This starts moving a little more toward implicit dependencies but is still more than "explicit enough" for most purposes.

 

I see! That makes a lot more sense. Back to the drawing board for me! Thank you all, these replies have been super helpful!!

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!