🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Architecture Advice - ECS without events?

Started by
15 comments, last by Oberon_Command 6 years, 1 month ago

Hi there.

I'm looking for some quick opinions, advice or other comments on my custom engine architecture.

For better or for worse, I have ended up with an ECS engine. I didn't intend to go this way, but countless searched through Google and the forum seem to confirm that this is the case. I have entities (mere Ids), components (pure data) and systems (holding raw resources and functionality) to operate on them. To be honest, I'm fairly happy with it.

However, I have yet to implement any actual logic into my 'game', and have been looking around for details on the various ways of handling interactivity, specifically, interactively between entities and components.

A topic that comes up a lot is events and event queues. I have not liked these. I don't want to add functionality to entities or components, and I don't like the idea of callbacks or event calling firing all over the place. So, I have been puzzling over this for the last two or so days. Eventually, I gave up on the musing and came to accept that some kind of event system is going to be needed. So, I had another look at the bitSquid blog (recommended on this forum), and something occurred to me. Isn't an event really just another form of entity? If it isn't, why isn't it?

I also realised that I already have something pretty similar running in my engine now. Specifically, my (admitted quite naive) implementation works more or less like this. The scene hands a list of physicalComponents and their corresponding placementComponents, and the collisionDetection sub-system iterates through them, looking for collisions. If it finds one, it creates a collision, adds it to the list, and moves on to the next one. Once it is finished, the collisionResolution sub-system goes through the list, and handles the collisions - again, currently very naively, by bouncing the objects off of one another.

So, I am wondering if I can just use this same approach to handle logical interactions. Entities with logical requirements have a collection of components related to interactivity (the range, the effect, and so on), and the various sub-systems iterate through potential candidates. If it notices an interaction, it creates an interactionEntity (with the necessary data) and the interactions are processed by the next sub-system.

I guess I'm looking for some feedback on this idea before I start implementing it. The hope i for more granularity in the components, and the ability to add a logical scripting system which combines various components into potential interactions, and omits the need for any kind of event system. Or am I just repeating the general idea of events and event queues in a slightly more complicated way?

Additionally, any comments or commentary on this approach (ECS, and so on), would be very gratefully received. I've pretty much run out of resources at this point.

Regards,

Simon

Advertisement

I create interactions between components/entities by writing the main loop that controls the system in a procedural style, in a similar way to what you would write if you only had a single component of each type in the whole game.

e.g. If you had one homing missile made up of a laser targeting component, a rocket engine, and a proximity bomb, you might traditionally have some OO logic that makes them work together like:


Point target = entity->laserGuidance->GetTarget(); // find out where the laser wants us to go
entity->rocketEngine->SteerTowards( target, delta_time ); // steer towards it
entity->explosive->ExplodeIfWithinDistanceThreshold( target ); // blow up once we're close

To make that work for systems of components, my main loop would look like:


ArrayView<Pair<Entity,Point>> targets = laserGuidances->GetTarget( scratch ); // find out where the laser wants us to go
rocketEngines->SteerTowards( targets, delta_time ); // steer towards it
explosives->ExplodeIfWithinDistanceThreshold( targets ); // blow up once we're close
1 hour ago, SomeoneRichards said:

Isn't an event really just another form of entity? If it isn't, why isn't it?

An event is a deferred function call. What makes it an entity?

Thank you for reading and responding to my post.

I think I might be referring to a specific application of the term event, and that that might lead to some confusion (on my part).

But, in response:

Interaction between components is not a problem. In fact, it seems unlikely that someone would add a component without counterpart components and systems to act upon them. That laser targeting/guiding component, for instance, is pointless without something(s) to guide. I can get entities into my game, and I can get them to work with the current systems and tick along as they are supposed to, using the game loop to ensure that systems operate when they're intended to, and so on.

The difficult part is having entities interact with one another. What if I wanted to have nearby guards (say up to 100ft) respond to the sound of the explosion? I suppose I could have another system that goes through all of the entities to see if they heard anything, but that seems like it could get pretty messy pretty quickly. If the were many various entities with different sound emitting components, then I'd have to poll the entities with sound-aware components every time something emitted a sound that they could be interested. Alternatively, I suppose I could shuffle the order that systems operate in, but that seems equally hard work.

The common response I have seen - and I could easily be misunderstanding here - is to use some kind of an event system to notify registered entities (or components thereof) that something they are interested in has occurred. So I have looked at various implementations of event/messaging queues/buses, and attempted to adapt my own. What I have noticed is, in my planning an implementation, is that I don't need an event/message, as such, since the behaviour more or less mimics that of my other components. But I could be wrong about events here. In other implementations events I suppose an event could be a deferred function - in that it has the capacity to lead to a function call - but with an event queue that processes over the events, they seem to function the same way as any other component. In my own planned implementation, the 'interaction' is really just data, and the logic system iterates over them, in the same way that any other system does.

But I might just be confusing things (and myself) here.

In the above example, how would you implement an interaction between entities? In the case of guards hearing the explosion?

I have had some prototype in the past made with Unity that targets exact these scenario :D

We have had a NoiseTracker component added to our guards that was bound to an event system. This event system has had a noice channel that fires each frame when the player does an action that may lead to noise, its strength and position. The NoiseTracker registered to this specific event channel and then decided depending on it's character component's data (Position,AttentionLevel ..) if it should ship the event call to it's character.

This was C# and now comes C++; To implement a generic event handling system into my engine I took a look at what an event system needs to do and come to the conclusion that it is simply a static sync or async processed list of data that is dropped to certain audience. So what my event system does is exactly that, recording some kind of data that depends to the channel it is put to in a list and at some point, iterate through that list to execute all registered function delegates by passing each single data entry. To have the possibility to support several channels and also channels of the same data, I made a template for it that is of signature


template<int ChannelId, returnType (Parameter, Parameter ...)> struct Dispatcher

This way, we can not only define the signature of the function call that is an allowed recipient but also the structure of the data that can be passed to it. The ChannelId parameter lets one specify also different channels of the same data as described above.

I've used it in a long-term test with different things but mostly Input handling with very good results

21 hours ago, SomeoneRichards said:

If it notices an interaction, it creates an interactionEntity (with the necessary data) and the interactions are processed by the next sub-system.

While you can implement this an entity, why create an entity when you can also just add it as a component? I've seen approaches before where people add the events that occurred as a component to the entities involved. The problem with creating a new entity is that it somehow needs to reference the old entity. That's of course not impossible, but you could have just as well attached it to the entity right away.

Regardless, the problem with having it either as an entity or component is that you need to keep track of whether it was processed. If you have two systems that are interested in knowing whether a collision has occurred, it becomes difficult to remove this Collision component/entity afterwards. None of the systems interested can assume another system that will process that entity afterwards is no longer interested in that Collision component/entity, so they cannot delete it. That means the system that added the Collision component/entity is also responsible for deleting it, otherwise the other systems may process it multiple times!

Again that's something that might seem easy to fix, until you want to have your physics system update at a different rate than the other systems. Now, let's say your physics system processes collisions at 20hz and the other systems run at 60hz. Obviously, that creates a problem; now your collision components/entities persist for too long and might get processed multiple times. You can bring solutions, but in the end whatever created these event entities/components are now likely to rely upon the rate of which the systems process it. 

Your approach is more of a Model View Controller approach where the view (system) checks the model (the (presence of a) entity/component). It's not event based; you are likely to be creating scenarios where the events are not received. With this Model View Controller approach you are checking a state. The event of a collision having occurred should likely not be considered a state, but as it suggests, an event, i.e., a callback.

Thanks again for responses. Again, I am probably confusing myself, but:

23 minutes ago, AthosVG said:

While you can implement this an entity, why create an entity when you can also just add it as a component? I've seen approaches before where people add the events that occurred as a component to the entities involved. The problem with creating a new entity is that it somehow needs to reference the old entity. That's of course not impossible, but you could have just as well attached it to the entity right away.

Doesn't that remove a lot of the decoupling between systems? If my physical collisions require a particle effect (some sparks) and a sound effect (like a clang) and audio responders (like guards), doesn't my physics system need to be about to create and add those components, or call into a higher level system to do so?

25 minutes ago, AthosVG said:

Your approach is more of a Model View Controller approach where the view (system) checks the model (the (presence of a) entity/component). It's not event based; you are likely to be creating scenarios where the events are not received. With this Model View Controller approach you are checking a state. The event of a collision having occurred should likely not be considered a state, but as it suggests, an event, i.e., a callback.

It's why it should be an event that confuses me. If I had 35 homing missiles, wouldn't I have the following:

Calculate the target for 35 missile. // Good

Move 35 missiles. //Good

Work out if 35 missile explodes //Looks good, however, on each missile exploding, I now have to fire off a load of events which take me out of the iteration to process for each individual system.

Obviously I am going to have to do this for explosions, but can't I save my structure by doing:

Calculate the target for 35 missile. // Good

Move 35 missiles. //Good

Work out if 35 missile explode. If they do, create an interaction to add to a higher level list // Good

Work out the effects of each explosion according to the higher level system. Remove each interaction as it occurs. If the interaction results in another interaction, add it to the end of the list so it gets resolved in due time. Still requires mixing systems and potentially updating components all over the place, but its now being handled by a higher level system designed to do this.

1 hour ago, Shaarigan said:

This way, we can not only define the signature of the function call that is an allowed recipient but also the structure of the data that can be passed to it. The ChannelId parameter lets one specify also different channels of the same data as described above.

I've used it in a long-term test with different things but mostly Input handling with very good results

I think this is pretty much what I am suggesting. I'm probably just quibbling over terminology.

But rather than having an event/message template, Im'm just talking about using the same entity system, and make the various data fields components. This way I can make different kinds of events/interaction through scripting. However, here I think I am just generalising and complicating the system completely needlessly...

28 minutes ago, SomeoneRichards said:

Doesn't that remove a lot of the decoupling between systems? If my physical collisions require a particle effect (some sparks) and a sound effect (like a clang) and audio responders (like guards), doesn't my physics system need to be about to create and add those components, or call into a higher level system to do so?

I'd expect you to create some CollisionComponent or an entity that has such a component. You have a system that iterates over the CollisionComponents (or entities) and emits particles for each component that exists. I don't see how changing this to a component adds coupling compared to using an entity. The physics system does not need to know what happens to it (in fact, it shouldn't), it just tells that a collision happened.

32 minutes ago, SomeoneRichards said:

Work out if 35 missile explodes //Looks good, however, on each missile exploding, I now have to fire off a load of events which take me out of the iteration to process for each individual system.

Nothing prevents you from making something that manages the events in such a fashion that they will always be processed after a system has updated. You can delay the events, but you should also wonder why this even matters. Is it a problem that events are immediately fired off upon a collision? And be careful to not make too many theoretical situations here. It's hard to find a silver bullet immediately for handling all the cases where those events could interfere with the update of the system sending these events.

36 minutes ago, SomeoneRichards said:

Work out if 35 missile explode. If they do, create an interaction to add to a higher level list // Good

Work out the effects of each explosion according to the higher level system. Remove each interaction as it occurs. If the interaction results in another interaction, add it to the end of the list so it gets resolved in due time. Still requires mixing systems and potentially updating components all over the place, but its now being handled by a higher level system designed to do this.

So if I understand you correctly, you have a list of interactions that trigger once you figured an explosion has occurred, which you can register to? And you remove interactions as they occur? How are you adding the interaction back to that list exactly? Also, given that you have a list of interactions that are triggered after the explosion as to have to be executed, how is this different from an event that is simply called after the update? Sounds like an event to me :)

28 minutes ago, AthosVG said:

I'd expect you to create some CollisionComponent or an entity that has such a component. You have a system that iterates over the CollisionComponents (or entities) and emits particles for each component that exists. I don't see how changing this to a component adds coupling compared to using an entity. The physics system does not need to know what happens to it (in fact, it shouldn't), it just tells that a collision happened.

I wasn't arguing for the choice of component over entity. I'm more thinking about which system (or level of system) is responsible for it, giving the intended scope and flexibility.

I want a physics system that just identifies collisions, with a separate 'system' to handle the collision. I don't want the outcome of the collisions to be as flexible as possible (I might not always want particles).

28 minutes ago, AthosVG said:

And be careful to not make too many theoretical situations here. It's hard to find a silver bullet immediately for handling all the cases where those events could interfere with the update of the system sending these events.

This is almost definitely my problem, but, until my mind settles on something, it is a situation I'm stuck in...

28 minutes ago, AthosVG said:

So if I understand you correctly, you have a list of interactions that trigger once you figured an explosion has occurred, which you can register to? And you remove interactions as they occur? How are you adding the interaction back to that list exactly? Also, given that you have a list of interactions that are triggered after the explosion as to have to be executed, how is this different from an event that is simply called after the update? Sounds like an event to me :)

I think you're saying that and interaction is an event, and so I don't need the interaction, and I am saying that an event is an interaction, and so I don't need the event.

I am also confusing things by using interactions to mean a scripted coupling between two entities/components, and an ad-hoc instance of an interaction in a general way. I think the latter is the event that you're talking about. So, that confusion aside, I'm essentially talking about handling events as a synchronous list, within the entity/component framework, which I think I have resolved in my head...

On 5/15/2018 at 10:36 PM, SomeoneRichards said:

Interaction between components is not a problem. The difficult part is having entities interact with one another.

If entities are just ID's, they dont communicate with each other. Only components do. So if interaction between components is not a problem, there is no problem :)

On 5/15/2018 at 10:36 PM, SomeoneRichards said:

In the above example, how would you implement an interaction between entities? In the case of guards hearing the explosion?

I do it without an event framework. Just batch processing of systems in a procedural manner. Batch, batch, batch.


struct Noise
{
  float     volume = 1;
  NoiseType type = Explosion;
  Vector3   position;
  Entity    source;
};
struct SplashDamage
{
  float damage = 100;
  Vector3   position;
};
struct ExplosionSystemResults
{
   ArrayView<Noise> noises;
   ArrayView<SplashDamage> damages;
};
...
  
OnTick:
vector<ArrayView<Noise>> noises;
vector<ArrayView<Noise>> splashDamage;
...
ArrayView<Pair<Entity,Point>> targets = laserGuidances->GetTarget( scratch ); // find out where the laser wants us to go
rocketEngines->SteerTowards( targets, delta_time ); // steer towards it
auto explosionResults = explosives->ExplodeIfWithinDistanceThreshold( targets ); // blow up once we're close
noises.push_back( explosionResults.noises );// add explosion noises to the collection of all noises this tick
splashDamage.push_back( explosionResults.damages );// add explosion damage to the collection of all splash damage events this tick
...
healths->TakeSplashDamage( splashDamage );// pass splash damage events to health system
aiListeners->ReactToNearbyNoises( noises );// let AI respond to noise events
5 hours ago, Hodgman said:

If entities are just ID's, they dont communicate with each other. Only components do. So if interaction between components is not a problem, there is no problem

I do it without an event framework. Just batch processing of systems in a procedural manner. Batch, batch, batch.



struct Noise
{
  float     volume = 1;
  NoiseType type = Explosion;
  Vector3   position;
  Entity    source;
};
struct SplashDamage
{
  float damage = 100;
  Vector3   position;
};
struct ExplosionSystemResults
{
   ArrayView<Noise> noises;
   ArrayView<SplashDamage> damages;
};
...
  
OnTick:
vector<ArrayView<Noise>> noises;
vector<ArrayView<Noise>> splashDamage;
...
ArrayView<Pair<Entity,Point>> targets = laserGuidances->GetTarget( scratch ); // find out where the laser wants us to go
rocketEngines->SteerTowards( targets, delta_time ); // steer towards it
auto explosionResults = explosives->ExplodeIfWithinDistanceThreshold( targets ); // blow up once we're close
noises.push_back( explosionResults.noises );// add explosion noises to the collection of all noises this tick
splashDamage.push_back( explosionResults.damages );// add explosion damage to the collection of all splash damage events this tick
...
healths->TakeSplashDamage( splashDamage );// pass splash damage events to health system
aiListeners->ReactToNearbyNoises( noises );// let AI respond to noise events

Thank you. That's pretty much exactly what I was going for. Except with confusing the terminology and attempting to over-generalise things.

This topic is closed to new replies.

Advertisement