Skip to main content
GameDev.net gamedev.net
🔒 Locked

ECS How systems iterates only in components that they use?

Started by testing6 Nov 25, 2021 at 7:18 PM 15 replies 20.8k views
Original Post
testing6
testing6

Hi

Im trying to implement ECS pattern in my game but I have a few questions

We have some entitites, all have position component (x and y) and sprite component, someones have a pickable component (boolean)

In the draw o rendering system, we iterate over 2 arrays, position array and sprite array, no problems

But, for instance, if I need to create a pick system (checks if a user clicks in a entity), the system needs to iterate over the position array (ok, all entities have) and need the pickable component, that no all entities uses.

I dont know what is the best aproach

  • Create arrays for each system : echa system uses the data they need, (too much arrays??) , manager adds components in each array
  • Iterate over entities : but this is not ecs pattern
  • Create arrays of ids of entities for each system: each system has an array with the ids of the entities , and all the systems looks the same array of entities with the id of the entitite (the index) and check the components
  • iterate over array of positions, and when we get the correct position, check if the entity of that component has a pickable component, I dont know if this is slow

What do you think, what is the way of doing this?

Thanks

Juliean
Juliean

testing6 said:
Iterate over entities : but this is not ecs pattern

First of, you should try to get that out of your head. You don't need to be afraid that the ECS-police is going to arrest you if you stray from what the core-pattern is about ?

I've personally always had this pattern for systems from the get-go:

void System::Update(void)
{
	for (const auto& [position, sprite, collision] : GetComponentPacks<Position, Sprite, Collision>())
	{
	}
}

The first implementation did actually just iterate over all entities and build this list each frame. This is of course pretty slow, but even that was fast enough for most use-cases.

My current, optimized approach builds a cache of those components, interally using some template-magic, but in essense, I create an array of the requested components which I update when entities are created/updated/removed. Essentially, its interally like this:

std::vector<std::tuple<std::reference_wrapper<Position>, std::reference_wrapper<Sprite>, std::reference_wrapper<Collision>>>;

And “GetComponentPacks” returns an interator into this structure.
The first iteration btw simply cleared the entire cache whenever anything mandated its layout to chance, the current applies updates after all systems are done iterating. You could make the process of updating a bit faster by not storing the components itself but indices, but this will also need updates (when components are removed in between) and I have found that the speedup from not having to do lookups in the different component-stores far outweight the cost of updating the cache (which can be mitigated alltogether by using standard-practices like pooling).

And thats why I originally said - don't worry too much about if this or that implementation is strictly ECS or not. If you have a well-enough interface you can change implememtations to meet your requirements, and you can just start with something that works instead of having to come up with the perfect solution (which rarely exists anyways).

brebarth221
brebarth221

Juliean said:
I create an array of the requested components which I update when entities are created/updated/removed.

That sounds basically like what I do too. I have a set of interest_lists, which contain the entityid's of those entities that have an interest in being handled because of their component x. Those are in a global scope, accessible from every system. They are managed by a system which adds, removes entity id's as needed. Only that one system is allowed to change the content of these interest_lists.

Essentially this is a question about how you want to do the bookkeeping.

hplus0603
hplus0603

testing6 said:
the system needs to iterate over the position array

Why?

It's not uncommon for each system to have its own array of components. The “pickable” system will have components only for objects that are pickable. Thus, you iterate over the pickable components. The pickable component then needs to expose some interface to know its position. There are many ways to do that:

  1. Whenever the object moves, copy the position into a field in the pickable component.
  2. The pickable component contains a pointer to the position component of its representative entity.
  3. The pickable component contains a pointer to the entity, and you can then get the position from there.

Also, when I say “pointer” I could just mean “index into array” if you don't want to use direct pointers for whatever reason (savegames are easier without pointers, for example, as are Rust games.)

Also, it's totally OK to make the law that “every entity has a position, so you there's no separate position component – position is just a field in the entity record itself.”

enum Bool { True, False, FileNotFound };
Shaarigan
Shaarigan

Unity as one of the most prominent users of ECS(-ish systems) is doing exactly that when they introduced BURST. An example was a system which first created an array of entities and filled that array by iterating over all entities present in the game by certain criteria. Then that entities who for example match a set of components were put into that array and the array was passed every time BURST called for an update. So it is absolutely fine to iterate entities in order to find those who match certain needs of a system. You should take care to do that only once something changes or you initialize a new system to be added to the update loop, don't do that on every frame. Or you register some kind of update pipeline whenever a component is attached to an entitty or removed from it, different systems listen to that pipeline and either add or remove that entity to/from their internal array.

For those pipelines I started to like the reactive programming pattern https://gist.github.com/staltz/868e7e9bc2a7b8c1f754​​​ . You however need to implement that in a way that is good for games performance and memory wise

Juliean
Juliean

hplus0603 said:
Also, it's totally OK to make the law that “every entity has a position, so you there's no separate position component – position is just a field in the entity record itself.”

The one downside of this is that it couples the entity with the position. In my engine, 2D and 3D are entirely separate implementations with separate transforms (Transform2D/Transform3D). And while its true that every entity always has a Transform, it is not given which, at least on the entity-framework level.

I personally did simply make a separate type of component, which is specialized for always existing on every entity. This uses a separate store, and a O(1) array-lookup for an entity, but it still allows me swap those transforms, alongside some other properties.

Now you can decide whether or not this decoupling makes sense to you (for my design it does), but if you don't think you need it, then sure, it can also be a field as you described.

Dirk Gregorius
Dirk Gregorius

The problem I have with ECS is that people seem under the assumption that you always iterate all entities with a specific combination of components. From my experience this is very far from what game engines do and you only want to iterate a very small subset of components in practice . Namely those which actually changed or those which are actually updating. E.g. In a physics system you don't iterate the rigid body component after you updated the physics world (think Havok or PhysX), but you only update those entities where the rigid body in your physics systems moved. And this is very often none.

Here is an example:

// Instead of this...
void PhysicsSystem::Update( float dt )
{
	mWorld->Step( dt );
	
	// This can be a couple of 10.000 components...
	for (const auto& [RnRigidBody] Body: GetComponentPacks<RnRigidBody>())
	{
		RnTransform Transform = Body->GetTransform();
		Body->GetEntity()->SetTransform( Transform );
	}
}

// ... you actually do this
void PhysicsSystem::Update( float dt )
{
	mWorld->Step( dt );

	// This is most often in the 10s or maybe 100s
	for ( RnBody* Body : mWorld->GetActiveBodies() )
	{
		RnTransform Transform = Body->GetTransform();
		Body->GetEntity()->SetTransform( Transform );
	}
}

Another example would be animation. You might want to update the animation graph and move the character, but you only create a pose if the character is actually going to be rendered on the screen. Also that assumes that animation is updated every frame, but the NPC graphs might be on much slower clocks in practice. In my opinion this is all an over simplification → namely everything is an array.

Juliean
Juliean

Dirk Gregorius said:
The problem I have with ECS is that people seem under the assumption that you always iterate all entities with a specific combination of components. From my experience this is very far from what game engines do and you only want to iterate a very small subset of components in practice .

This might be true to an extend. The main counter-argument to that is that the way ECS is setup, having to iterate over a bunch of entities that are not really truly active has a tiny overhead, if you don't do any work. So if the code did this:

void PhysicsSystem::Update( float dt )
{
	mWorld->Step( dt );
	
	// This can be a couple of 10.000 components...
	for (const auto& [RnRigidBody] Body: GetComponentPacks<RnRigidBody>())
	{
		if (Body.isActive)
		{
			RnTransform Transform = Body->GetTransform();
			Body->GetEntity()->SetTransform( Transform );
		}
	}
}

In a well-optimized ECS, then having to iterate over 10000 enities when only 10 are active has less of an overhead then you might expect. Sure, it is still O(N) for 10000 instead of 10/100 whatever, but its usually tolerable.

Furthermore, an ECS doesn't prevent you from creating more optimized data-structures for more specific processing-requirements. There is nothing keeping you from creating a link between the active bodies and the components, and using the active-list from your example #2 instead of iterating over all entities. The much more common example in my own codebase is actually scripting. I have a top-down rpg with complex, custom interactions on pretty much any map. Of course I don't write my scripts with a GetComponentPacks-loop when I need to change the texture of a chest that has been opened. This is also happening on an individual entity basis.

Does that overall mean that ECS doesn't fit all the requirements of a game-engine? Yeah, sure. But it doesn't have to. A game-engine is a complex enough beast that it requires lots of different patterns and structures to work, ECS in my opinion can fit quite well as a ground-level foundation/glue between other systems. You still need enough flexibility to stray from the pattern where its required, so as long as you don't treat ECS absolutely literal like in its definition, and don't try to apply it literally to everything, it works quite well (at least in my experience).

Dirk Gregorius
Dirk Gregorius

@Juliean: The main counter-argument to that is that the way ECS is setup, having to iterate over a bunch of entities that are not really truly active has a tiny overhead, if you don't do any work

This is unfortunately not true. I understand that there is a common understanding that if just everything is in a array iteration is free. Say your overhead is 500us (micro seconds) which is not unlikely in the above example since your innocent looking Body.isActive is not a simple boolean in the cache but will query the state of the PhsicsSDK body through a pointer. This seems not much, but it is not tolerable and will add up. Assume you have 10 systems like this then a third of your frame time is wasted for nothing.

When we shipped Half-Alyx the major performance work in the end was to get rid of these tolerable overheads.

Actually in one place I worked the animation programmer was of the opinion that if he just has all bones linearly in memory (which you absolutely should!) he could just iterate over all skeletons and naively build poses. Well, guess what, it didn't work and the first thing we added was a callback from the rendersystem to skip this step if the character is not rendered. So I would argue a thing like ‘tolerable overhead’ does not exist.

Juliean
Juliean

Dirk Gregorius said:
This is unfortunately not true. I understand that there is a common understanding that if just everything is in a array iteration is free. Say your overhead is 500us (micro seconds) which is not unlikely in the above example since your innocent looking Body.isActive is not a simple boolean in the cache but will query the state of the PhsicsSDK body through a pointer. This seems not much, but it is not tolerable and will add up. Assume you have 10 systems like this then a third of your frame time is wasted for nothing.

Well if its not a boolean but a query then sure, the overhead is going to be much larger. I was assuming that you are able to mark the components as active/inactive when this state actually changes. If thats not then case then sure this adds up, but than thats the overhead of querying the physics-system and not having to iterate over inactive components. If it can work like in my above example with a bool, I'm pretty confident the overhead should be next to nothing, and never add up close to a third of frame-time (based on my own extensive testing of my own system).

Dirk Gregorius said:
So I would argue a thing like ‘tolerable overhead’ does not exist.

Now we are getting into a larger debate; but if overhead is never tolerable than everybody would always write perfectly hand-crafted assembly since obviously you cannot waste the 0.1us from some instruction that the compiler generated that you wouldn't have? Of course the first priority should be having code/tools that are easy to work with, and as long as you can hit your target-framerate with some a lot of leeway, then everything else is IMHO absolutely tolerable.

As an example, my (ECS-based) 2D collision-detection currently uses O(N^2) collision-checking, since I have at max 50-100 collidable entities on screen. But my game-build still runs at up to 3000 FPS if left uncapped. So yes, I'd again say that this is absolutely overhead that is acceptable. Until I need to have more collible units, then I'd need to write a more sophisticated algorithm.

hplus0603
hplus0603

Juliean said:
The one downside of this is that it couples the entity with the position

Yes, of course! Hence, why you may or may not want to make that simplification.

The draw-back with having entities with different kinds of transforms (or maybe MULTIPLE transforms – I'm a “forest” and I have 100 tree positions!") is that everything that talks about entities now needs to be aware.

For example, what happens if I add a PhysicsComponent to an entity that only has a GuiTransform? Now, you end up with cross-component dependencies, which can totally be solved mechanically using runtime asserts and such, but THEN you end up with an ordering problem. Not to mention, I prefer it when my compiler tells me I'm wrong :-)

Yet another option is to bake the component into the entity class using multiple inheritance. Each inherited component would be called with the main entity as an argument, and the components would use compile-time type checking to make sure the entity has the right dependent components. The draw-back there is that the storage for components now lives in the entity object, not in a nice flat array managed by the component system, plus all the lifetime shenanigans that come with that.

The main trick is to understand what parts are most important to your engine (iteration speed, number of entities supported, flexibility, runtime performance, …) and optimize for those parts in the engine. And, no, most indie engines do not want to optimize for runtime performance as the top priority – there are much more important barriers to finishing and delivering a fun and working game for an indie developer!

enum Bool { True, False, FileNotFound };
Juliean
Juliean

hplus0603 said:
The draw-back with having entities with different kinds of transforms (or maybe MULTIPLE transforms – I'm a “forest” and I have 100 tree positions!

Sure, that can be highly problematic. For example in Unity where a GameObject might now all the sudden have a RectTransform which is kind of a Transform, but also not really. Oh yeah, thats also the example you brought on a second read.

In my own application, I don't really have that problem, as I decided to make it an eigther/or situation globally. Users will have to decide whether they want a Transform2D or Transform3D via a core-plugin, and thats all they get. After that, every entity has exactly that transform. The only main difference is that I can move this code into plugins (you could also do something like this of course):

using TransformT = std::conditional_t<IsUsing3D, Transform3D, Transform2D>;

class Entity
{
	TransformT& transform;
};

But I have my 2D/3D specific implementations entirely decoupled, thus why I mentioned it. So my actual implementation for using those transforms when using the plugins then is:

// never null, always valid
const auto& transform = Transform2D::Get(entity);

Which is perhaps a little more awkward to write, but does about the same as your idea (so I'm not totally opposed to it, just wanted to share a slightly different approach that solves the coupling).

hplus0603 said:
For example, what happens if I add a PhysicsComponent to an entity that only has a GuiTransform? Now, you end up with cross-component dependencies, which can totally be solved mechanically using runtime asserts and such, but THEN you end up with an ordering problem. Not to mention, I prefer it when my compiler tells me I'm wrong :-)

I don't treat GUI as entities at all. You can place a widget in a world with a WidgetComponent, but that only wraps that widget (kind of like unreal does it). For that exact reasons, and I also find it incredible awkward to work with such a system in say unity. Though I also haven't fully solved that problem in my own implementation; there are components that depend on other components, and I havn't found a nice way to enforce this constraint (since it might also change at runtime which is why I cannot enforce it entirely in the toolchain).

hplus0603 said:
The main trick is to understand what parts are most important to your engine (iteration speed, number of entities supported, flexibility, runtime performance, …) and optimize for those parts in the engine. And, no, most indie engines do not want to optimize for runtime performance as the top priority – there are much more important barriers to finishing and delivering a fun and working game for an indie developer!

Absolutely agreed! Though in practice unfortunately performance can be a major issue especially in engines like Unity (I had even more extrem issues in Rpg-Maker), so indies might have to spend some time optimizing, unless they roll their own tech entirely (which I quess is ok for small titles? But an indie probably shouldn't write their own fully functionaly engine, wouldn't you agree? :D)

hplus0603
hplus0603

Juliean said:
I don't treat GUI as entities at all.

Then you're probably OK with thinking that every entity has a position.

(You'd need something more light-weight for objects that aren't physical, like inventory items and such.)

This is the path Unreal went with AActor versus UObject (and UActorComponent.) All AActors have a position, and that works fine for them.

enum Bool { True, False, FileNotFound };
Juliean
Juliean

hplus0603 said:
(You'd need something more light-weight for objects that aren't physical, like inventory items and such.)

Inventory-items are not part of the ECS at all (well, kind of). For non-physical objects I have the concept of (non-ECS) “systems”, so the inventory is a “system” which lives at game-scope. The item-entries themselves are managed via prefab-asset pointers (or class-types in Unreal). When the item is used by the player, it is spawned (to execute its scripted logic), but just for displaying the item in a menu I can query the properties (texture, etc…) from the prefab.
That means that I'm even more fine with every entity having transform, since everything that doesn't simply lives in the system-space.

Alberth
Alberth

testing6 said:
What do you think, what is the way of doing this?

Following any design system rigorously without compromise will inevitably leads to major trouble near the edges of the domain. All design systems claim to fully cover everything you ever need to do, but nothing actually does. Don't try to take on the world with exactly one tool, use everything in your toolbox to get the best of all worlds.

frob
frob

Getting back to the pickable component of the topic rather than the weeds of what is or is not ECS, or components that have nothing to do with the question

Remember it is little more than a collection. Objects have a collection holding zero or more things.

It is okay to ask the container about its properties.

It is risky to start making assumptions about other items in the collection, such as that there is only one of a certain type, or that the collection also contains exactly one of another type.

With that in mind, you need to look at the problem being solved.

In this specific case, the goal is to have some objects pickable by the mouse. It's a good idea explore it a bit.

If it is a HUD or UI element, a UI approach of nested objects can be better. If you have a few sparse objects, a search of intersections with a ray or mouse volume may be the most efficient. If you have dense pickable objects a separate list might make more sense.

For the sparse objects, it would be (for my own interpretation) easiest to do a ray test for objects that might touch the mouse, and for each object, send the command / event to each component of that type it contains, which may be zero or more. If a thing has five components of that type all five get activated.

That can be quite easy if the mouse ray is likely to only intersect a few items. If it might hit five or ten things, send the event to all their components.

For dense objects it may be worth it to cull the list with spatial tools and also against if any of the components exist in the region before doing testing against the mouse cursor. Basically the opposite direction of above, depending on the density details of which is more expensive to do.

If the mouse is clicked on a tree and you are in a forest where the ray in the frustum might cross hundreds of trees, probably do something different for the dense instances.

What you do with the results is up to you. You might get several objects, multiple hit detection components all triggering. Maybe each component has a priority field and you look at the highest priority of the results. Maybe you want the nearest the camera, or the farthest. Maybe you select every object. Each has a place depending on the goal, and you can probably find even more scenarios where you take other actions.

The point is to think of components as items in a bag attached to an object. Try not to focus on the other elements in the container too much, building things so it works for zero, for one, and for more than one.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.