1
\$\begingroup\$

I see all the upside in entities registering automatically for basic components. If an entity has a Renderable component, it should be registered to a list that's used by a Renderable System. If an entity has a Physics component, it'll be registered to a list of physics entities that gets used by a Physics System.

But beyond that, say we have an entity that is created with both a Physics and a Renderable component.

I fully understand how ECSs normally handle this case; If there was a system that worked on entities that have both a Physics and Renderable component, most Entity Component Systems (ECSs) would then automatically register that Entity to that system because the signature matches (in other frameworks, if the aspect matches, etc).

Moreover, if you add any components from an entity at runtime, most ECSs will then loop through all systems and try to register this entity if its new signature matches the systems' requirements (similarly unregistering the entity when removing components).

In practice, this seems to have many downsides (specifically talking about cases of where a signature of MORE than 1 component auto matches a system):

1. All entities with a particular set of components will automatically register to a system that the programmer may not have expected/remembered. Perhaps it makes sense that an entity with a physics and a visual component should be operated on by a physics-visual system, but perhaps not. Every time we add components to an entity, it may be difficult to foresee all the systems that it'll register to and may have unexpected consequences (similarly for removing components). If we have multiple people working on creating systems, the likelihood of unexpected consequences goes up. [Logic errors at runtime, maintainability problems]

2. In the case that we have entities like the ones above that fit a signature but we don't want to register it to a particular system, the best solution revolves adding a tag to the entity. Then we'd add an exclusion to the aspect of the system that would have operated on it - but this couples the behaviour of EntityCreators and System registrations [High Coupling]. Otherwise, we'd check for a dynamic tag on each entity on the update loop of the system and perform a noop on certain tags [Unneeded work, not-exactly 'pure' ECS]

3. We need to create a bitmask system or some other equivalent to manage signatures, and always loop through almost all systems every time an entity changes. Of course, all of this depends on implementation, but some form of loop-check is always required. In many cases, for complex aspects or when dealing with a large number of systems, this can become super inefficient and constrain how you build up your aspects/systems. [Lots of Work]

4. Sometimes ECS is implemented in a way that allows systems access to all of an entities components which may or may not be part of the systems requirements. [easy to break Encapsulation]. Now this may not be a big deal if everyone's on board with the intricacies of 'pure' ECS, but given the number of questions surrounding type casting entities and checking entity tags at runtime, its common enough that it can lead to taking shortcuts in systems and creating more [Runtime Errors].

Really, problems 1, 2, and 4 produce problems inherent similar to those that appear with duck-typing and dynamic casting in general.


Instead of automatically registering entities to systems that match a signature (via whole entities or via nodes), wouldn't it be more type-safe and maintainable, yet as extensible, to directly register component-bag nodes in EntityCreators / EntityModifier classes?

If our entity which we create has a Physics component and a Renderable component, and matches in our minds what a PhysicsRenderable object does, then we would add a component called PhysicsRenderable which is constructed with our physics and renderable components injected. By explicitly saying that our entity matches what we believe a PhysicsRenderable should do, we solve 1.

If it doesn't match what we believe a PhysicsRenderable is, we only add the Physics and Renderable components, solving 2. Note: this does have the opposite effect of 1 that we have to remember to register it as a PhysicsRenderable if we want systems that work on PhysicsRenderable entities to update us, but this can be more easily debugged and fixed than the reverse situation.

Our entity would then have the components [Physics, Renderable, PhysicsRenderable]. Moreover our systems would then register to only one type (Physics, Renderable, PhysicsRenderable) and then the logic for registering the entity would become much simpler than using a bitmask or other method described in 3. And because we explicitly built the entity with a PhysicsRenderable, this can act as the type for the node-list traversed in our system that operates on PhysicsRenderables (no passing around full entities anymore, solving 4).

Added detail: removing a component on an entity that's composed in another component, ie, removing Physics from an entity with a PhysicsRenderable, would require an extra listener to also unregister the PhysicsRenderable component, but it's not much different, if not simpler, than what is already done in ECSs.


Now I realize that this goes against what some people may find to be a core principle of ECS (automatic signature registering), but it still uses a lot of the other core ideas (automatically registering entities to systems that process components that are expected on those entities).

Are there more downsides to this method that I'm not seeing? Do any ECSs use this sort of framework that requires explicit signatures on entities?

\$\endgroup\$
1
  • \$\begingroup\$ I had autoregistering objects for a while; a physical autoregistering with the physics engine etc. Then I wanted to know if there was the required was space for an object at a point, and then decide what to do based on that. The fact I couldn't instantiate a physical and say to the physics engine "will this fit" without all sorts of hidden processes happening in the background was very annoying \$\endgroup\$ May 25, 2017 at 16:39

2 Answers 2

5
\$\begingroup\$

TL;DR Entities SHOULD NOT auto-register to systems based on component signatures; prefer instead to explicitly declare component sets/nodes to register your entities to systems that operate on specific component signatures.

It’s been almost a year since I’ve asked the question, and I’ve played with both approaches to take a step away from theory and see how things pan out in practice. I’ve tried both:

1) The more-common auto registering entities based on type signatures (ie if we add a physics component and a renderer component to an entity, and we have a system that operates on (physics, renderer) signatures/nodes, the entity would register to this system automatically)

2) Explicitly registering entities based on components only- if we have a physics component and a renderer component, as well as a system that operates on entities with a (physics, renderer) signature, it WILL NOT register the entity UNLESS we ourselves also create a (physics,renderer) component and add it to our entity directly.

Now, YMMV, but I’ve come to find that all the issues I brought up in the original question do in fact exist with 1) and that by going down route 2), your code becomes more modular, less coupled and easier to reason about, not to mention solves all the issues addressed in the original question 1-4.

Is this more work/more painful? In reality, as you assemble most entities you would like to have fine grained control of how each entity behaves stored in one place. Going down route 2 would put that logic solely at entity creation/modification sections. Route 1 would put that logic at both entity creation/modification and at system registration- and becomes a real pain to diagnose.

I highly recommend route 2 as its explicit nature comes with a lot of benefits and little downside.

\$\endgroup\$
1
  • \$\begingroup\$ You made the right key choice. Ian Young's advice on the answer above is bad (understatement). \$\endgroup\$
    – KeyC0de
    Sep 9, 2018 at 2:22
3
\$\begingroup\$

I think you may be overthinking this.

The point of ECS is to be simple:

An entity is nothing more than an aggregate of components. A system is only interested in a subset of those components. For example: A rendering system would only be interested in Transform and Graphics data components,and a physics system would be interested in Position, Motion and Body (maybe Transform too).

Registering an entity with a system on creation is also simple:

  1. Check if the entity has the components the system is interested in. If not, ignore it.
  2. Once viability is established, create a proxy for the entity, and store pointers to those components. This is the systems' working data set for that entity.

During work cycles, iterate over the proxies, and do work.

Now, in this example, the components are stored within the entity itself, however, I've heard tell that some engines do it in a way that the entity just stores id's of components. I believe Unity does this, but don't quote me on that.

In any event, I will attempt to address the points you raise, in order:

  1. If multiple developers are working on different systems, it shouldn't matter. Each system is a self contained module, and the only coupling that should be happening is between the entity interface and system. (System needs to know how to query the entity for component information).

  2. The simplest solution is to simply have a vector, or list, of booleans within the entity, the position of which can be translated to a component type. In this way, the existence and recovery of an entity can be performed in O(1) time.

  3. Solution for 2 deals with this point nicely.

  4. It's good that you consider things like encapsulation in general, but bear in mind that an ECS system is not "pure" OO design. Don't make the mistake of trying to adhere to OO too strictly. Use OO when it helps, and don't let it force you to code yourself into difficult situations. Typecasting of entities can be handled in some pretty neat ways, depending on the language used, I code in C++, and you can use template functions to handle all the typecasting, and component storage. I'll show you an example:

Entity has a member variable:

std::map<std::type_index, Icomponent*> m_Components;

then you have a generic insert function:

template<typename T>
void add(Icomponent* component)
{
    m_Components.insert(std::type_index(T), component);
}

To query (O(1)):

template<typename T>
bool hascomponent()
{
    auto it = m_Components.find(std::type_index(T));
    if (it != m_Components.end())
        return true;
    return false;
}

and to retrieve a component in constant time:

template<typename T> get()
T* get()
{
    auto it = m_Components.find(std::type_index(T));
    if (it != m_Components.end())
        return dynamic_pointer_cast<T>(it.second);
    return nullptr;
}

If the dynamic cast fails, a null pointer will be returned, so the calling code just has to check for this, which indicates a fail.

Yes, this gives all systems potential access to all components, but this is actually a good thing, as in the game play systems, you may wish to retrieve and modify any number of the entities components at run-time. This generic method is also highly maintainable, as you can continue adding component types for game mechanics without ever having to change the entity code, and all systems can work from the same interface.

This also simplifies the "signature" issue, as all any system needs to do is query the entity on creation for the presence of appropriate components that it cares about.

Beyond this, anything further could be construed as opinion, and as such, not helpful, so I'll stop there.

\$\endgroup\$
12
  • \$\begingroup\$ TY for the reply. That said, you show how ECSs work in the ways that I'd already described. The examples you give just accept that runtime type-checking are fine. Your solution to 2 doesn't really solve 2 and applying 3 using 2 over component signatures is nowhere near constant time. Dynamic type extrapolation (using type here to refer to a multi-component signature) via casts results in more runtime error handling and thus runtime bugs. My question entails explicitly declaring multi-component types on entities to improve maintainability in ECSs. \$\endgroup\$ May 25, 2017 at 11:17
  • \$\begingroup\$ If you're having to query for a component in a system, you're probably not using ECS the way it was intended; your systems should only register those entities on which they'll operate. Please re-read 2. If you don't go the dynamic casting route (which opens you up to runtime errors) you go the other route, which forces you to know the interplay between your entities and your systems. If you accept that runtime checking your entities is no big deal, still realize that every type check you do in your systems is actually coupling logic about potential entities too. \$\endgroup\$ May 25, 2017 at 11:37
  • \$\begingroup\$ @DannyYaroslavski That isn't run-time type checking. It's template (generic) programming, which is evaluated at compile time. And while the code i posted is not in constant time, that is only one possible implementation. You could just as easily feed an enumerator into the entity (which translates to a vector index), to query or recover a component, which would absolutely be in constant time. \$\endgroup\$
    – Ian Young
    May 25, 2017 at 14:01
  • \$\begingroup\$ @DannyYaroslavski As to your comments on 2/3: It depends on how you view the system. In my view, you "add" or "remove" entities from systems. If the entity has the components a system cares about, it is "registered", i.e. retained.The only looping required is iterating through systems to add/remove entities, or update the engine state. It is becoming clear however, that you have something in mind, so perhaps if you were to illustrate that idea more clearly, I may be better placed to comment. \$\endgroup\$
    – Ian Young
    May 25, 2017 at 14:06
  • \$\begingroup\$ Say you create an entity with components <A,B,C,D> and have a list of systems with requirements <A, B>, <F>, <A, F>,<A,C>,<A, B, C>. In most ECS designs we have to loop through all systems to find which systems to register to- O(linear) in the number of systems. By specifying that the entity should register to <A,B> and <A,B,C> directly, there's no check-loop required, we simply lookup the associated system list and register the entity. We even get the choice to not register this entity to <A,C> systems if our entity, although has an A and C, doesn't behave like what we imagine an <A,C> is. \$\endgroup\$ May 25, 2017 at 14:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .