Is The "entity" Of Ecs Really Necessary?

Started by
48 comments, last by Shannon Barber 7 years, 8 months ago

But if a system is handling multiple components, then you have a many-to-many relationship between systems and components, which has implications for what data goes into which components. You always have this problem of deciding which data fields are related to others in order to create a high cohesion unit, but now the code that operates on that data lives in these separate system objects which have their own cohesion concerns, all while dipping in to whichever components are necessary to perform its task.

It's fairly common to have two components that should clearly be separate (i.e. it's totally valid for an entity to have just one of them), and yet have code somewhere that needs to deal with the relationship between these two components.

For instance, say you had a Health component and an Inventory component which contains the amount of food you have. If you want the entity to starve if they run out of food, that requires code that knows both about the food in Inventory, and the Health. Where is the best place for that logic?

Advertisement
Going back a bit:

The way I see it, if you think it makes more sense to divide your system up along system lines - and there's nothing wrong with that, as a standard procedural approach - then why bother batching the data into components? You may as well just have a long list of property fields since there's no explicit relationship between any 2 data fields except what a specific service might require.


Why bother?

1. See the bolded phrase. Splitting the state into separate systems promotes encapsulation. If a system doesn't need to look at a particular bit of data, it doesn't have to - and shouldn't. Systems don't even have to *see* each other's state, never mind be able to interact with it.
2. If the data are small enough, they can be packaged contiguously so that a particular system's most common operations (eg. go through the active status effects and update them) don't thrash the cache unnecessarily.
3. Having the state be in separate components allows those components to be added and removed dynamically without changing the base object. This can be done with traditional composition patterns - at the expense of storing the components in the object and having to make your components implement an interface, which means virtual function calls and (depending on how much is in your component base class) the brittleness that comes with implementation inheritance. And again, anyone who asks can query your component list on your object for a particular component = less encapsulation.
4. Operations of the same type tend to get applied homogeneously, in a well-defined (and named) order, and at the same time. This can encourage systems to only do certain kinds of operations at specific points, which means that component mutation can be easier to track, which means bugs are easier to track down. In the movement system example above, for instance, all characters are moved at once, in one specific spot. So far I've found this approach has made debugging substantially easier.

With this approach, there are no centralized game objects, there are only systems and the state they manage. It's not just dividing the code up along system lines, it's dividing the data as well. What you seem to be describing is an approach where game objects are just buckets of properties, and systems operate on the game objects, and I'm not sure I understand why you think this is relevant.

Oberon_Command: the majority of your state is locked away in your systems, and they also contain your game logic. So basically you have a very similar component system that I advocate - data packaged up with the code that acts upon it - except you call them systems, and I call them components. :)You have some transient data objects but they seem more like messages than components. However I do take the point about 'existence-based processing' blurring the distinction between the two.

"component mutation can be easier to track, which means bugs are easier to track down" - in certain cases, yes. On the other hand, the more complex problems come from the interactions, which you now perform asynchronously via your shared data structures, rather than through explicit function calls, and that makes those issues harder to debug. You win some, you lose some.

"What you seem to be describing is an approach where game objects are just buckets of properties, and systems operate on the game objects, and I'm not sure I understand why you think this is relevant" - it wasn't clear to me that the majority of your data is locked away inside the systems. (When most people are talking about entity-component systems with separate components and systems, they have all the component data exposed and systems access whatever they need to - eg. http://www.gamedev.net/page/resources/_/technical/game-programming/understanding-component-entity-systems-r3013)

Phil_t: I addressed this above: "any sort of system that operates on other components is just implemented as another component." In your specific example I probably wouldn't have a Health component since that's just data; I might have a LifeCycle component which checks Inventory for food and decreases the health part of CharacterStats accordingly. Partitioning components is always going to be about the compromise between one massive Actor class at one extreme or an unmanageable morass of micro-components at the other - I just prefer to not make it a 2-dimensional problem by adding Systems into the mix as well. :)

(When most people are talking about entity-component systems with separate components and systems, they have all the component data exposed and systems access whatever they need to - eg. http://www.gamedev.net/page/resources/_/technical/game-programming/understanding-component-entity-systems-r3013)


I didn't think that way was all that common. Now that I read some of the other ECS-threads you've replied in, I can kind of see where you're coming from. Those threads don't portray ECS as I understand it, at all.

The ECS I'm describing is just what I thought a "proper" ECS was from the start. There was a thread here on GDNet a few years back advocating exactly putting state + behaviour in systems. A lot of the "data-oriented design" blogs tend to advocate something similar, as well. I might be venturing into "no true Scotsman" territory here, but I suggest that folks who explain ECS as completely separating the components and their systems misunderstood the original idea, as all the early references to ECS I can recall portray ECS as both component state and behaviour living together in cohesive systems. Of course, even technical language evolves - Alan Kay has complained that object-orientation is really about messaging and not all that other stuff, so maybe this suggestion doesn't matter much.

This is why I don't actually use the term "ECS" much anymore, and why I don't describe my implementation as ECS even though it looks a lot like it. It's too easily misunderstood, and taken dogmatically is a bit rigid.

I might have a LifeCycle component which checks Inventory for food and decreases the health part of CharacterStats accordingly.


This is likely how I would first approach this, as well, with the difference that what you call a "LifeCycle component" is where I would actually store health, as well. There wouldn't be a "CharacterStats" component in the first place. A life cycle system might look like this:

struct LifecycleSystem
{
    // ...
    void ApplyInventoryEffects(InventorySystem& inventory);
    void CheckForDeath(std::vector<CharacterHandle>& outDeadCharacters);
    // ...

private:
    struct LifecycleDatum
    { 
        float baseHealth; // initialized when an entity is created
        float currentHealth; // updated as gameplay progresses
        float poisonTimeRemaining; // for example...
    };

    // typed pool is an object pool that enforces that handles to data in the pool are marked with a specific type ID and use count
    TypedPool<ObjectType::Character, LifeCycleDatum> m_data;
   
    // active data list so we don't have to iterate through the entire data pool;
    // would probably store this externally, at least initially, but putting it here gives us a new option: 
    // not all characters actually need to have have health!
    // implementing TypedPool<> as a sparse array would make this unnecessary, of course
    std::vector<TypedHandle> m_charactersWithHealth;
};

// one way
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
    for (const TypedHandle& character : m_charactersWithHealth)
    {
       if (!inventory.HasFood(character))
       {
           character.GetMutableFrom(m_data).currentHealth -= k_starvationDepletionPerFrame * dt;
       }
    }
}

// another way - more efficient, but splits concerns up differently
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
    // std::vector<TypedHandle> InventorySystem::charactersWithNoFood;
    for (const TypedHandle& character : inventory.charactersWithNoFood)
    {
       if (auto characterInstance = character.TryAndGetMutableFrom(m_data))
       {
           characterInstance->currentHealth -= k_starvationDepletionPerFrame * dt;
       }
    }
}


// still another way - likely less efficient, but better separation of concerns
void LifecycleSystem::ApplyInventoryEffects(float dt, InventorySystem& inventory)
{
    // a handle to a character is in this list if the character's inventory was updated this frame
    // std::vector<TypedHandle> InventorySystem::recentlyUpdatedCharacters;
    for (const TypedHandle& character : inventory.recentlyUpdatedCharacters)
    {
       if (!inventory.HasFood(character))
       {
           if (auto characterInstance = character.TryAndGetMutableFrom(m_data))
           {
               characterInstance->currentHealth -= k_starvationDepletionPerFrame * dt;
           }
       }
    }
}
edit: grammar and code example, haven't had coffee yet.

Phil_t: I addressed this above: "any sort of system that operates on other components is just implemented as another component."

Where do you put logic that needs to reason over a all of components of a particular type? Code that provides some spatial organization of transform components, for instance. Clearly that doesn't belong in a single component.

The ECS I'm describing is just what I thought a "proper" ECS was from the start. There was a thread here on GDNet a few years back advocating exactly putting state + behaviour in systems. A lot of the "data-oriented design" blogs tend to advocate something similar, as well.

That makes sense for components that are only used within one system (or for per-entity private state a system may manage) - but if they're used by multiple systems then you'd need to create an inter-system dependency where one requests the data from another... which is fine I guess. Or you could just store all component state in some World object and have the systems request what they need.

Where do you put logic that needs to reason over a all of components of a particular type?


Not sure what Kylotan meant, but I would put said logic in the system that owns the components.

The ECS I'm describing is just what I thought a "proper" ECS was from the start. There was a thread here on GDNet a few years back advocating exactly putting state + behaviour in systems. A lot of the "data-oriented design" blogs tend to advocate something similar, as well.


That makes sense for components that are only used within one system (or for per-entity private state a system may manage) - but
1. if they're used by multiple systems then you'd need to create an inter-system dependency where one requests the data from another... which is fine I guess.
2. Or you could just store all component state in some World object and have the systems request what they need.


1 is the preferred solution there. With 1, the dependencies are explicit in the control flow. That's a feature, not a bug.
2 devolves to everything referencing everything else - a big ball of mud. That's part of what we're trying to avoid in the first place.

Going with 1 helps you prevent the outcome of 2 by making changes that lead to a ball of mud stand out - and harder to make in the first place.
Im not sure i follow the systems are God class argument - it seems pretty clear that systems should deal with one aspect of game logic - so that the logic is encapsulated within the system. I guess they are God classes in the sense that they know about multiple game objects.

With systems responsible for specif portions of game logic, components can then be used as a method for intersystem communication without having to use messaging - so that no system is dependent on another system (though i usually end up having intersystem dependencies of some sort anyway - but they are well defined meaning a the game wont compile if a system isnt there that another system needs). If game objects dont have some component needed by a system to execute logic then the system does not process that game object - or it acts in some well defined way (such as no material component then use a default material that makes it obvious that a material is missing)

The only logic within components is logic pertaining to itself - this could be setting a needs processing flag for example on changing a component value - this separation is really just syntactic sugar

I dont have the actual component data live within systems - my components essentially live within the entities for all intents and purposes of API. Internally they live in component type specific arrays and the entities keep indices in to these arrays - and systems have access to these arrays so they can loop through and process them, but adding and removing components is only done through the entities' interface.

Creating a new component type is then a separate operation from creating a new system, but often they do coincide. The granularity is left up to the user - you could theoretically create a single system that processes all components - a true "God class" - but thats not what it is meant for.

Systems are updated in a well defined but customizable order, with as many or as few as needed to make the code both work and remain maintainable.

I just dont see a clean way to have global state without "systems", and you definitely need global state. If not systems then either your game object components need to know about eachother, or you have to have some kind of objects in place with the larger game state that the components have access to. It seems like you would then need to worry about the order that game objects are updated, or loop through the objects multiple times calling different functions each time - which is essentially what systems are doing in a nutshell.

Maybe doing that is clean and fine - i did things that way at first following the unity model - i didnt like it and found myself frequently getting confused on how exactly i should implement game logic that required inputs from many game objects at once.

At the end of the day though i guess its just a matter of preference which one way to do things is "better".
1 is the preferred solution there. With 1, the dependencies are explicit in the control flow. That's a feature, not a bug. 2 devolves to everything referencing everything else - a big ball of mud. That's part of what we're trying to avoid in the first place. Going with 1 helps you prevent the outcome of 2 by making changes that lead to a ball of mud stand out - and harder to make in the first place.

The thing is, you're making an arbitrary decision about which system owns which component. That may be obvious for some components, but not always. Who owns Transform? Who owns Inventory? It's not always clear. You're also creating a needless dependency of one system on another (which can make testing more difficult, for one thing). Have a system depend on another isn't bad thing if that dependency is because one system depends on anothers logic/behavior (for instance, to make efficient spatial queries). But if it just needs component data, it doesn't make sense to me.

Ideally, the system only knows that it needs an array/list/whatever of component A, B or C. It shouldn't care where they came from (grabbing them from some World object was perhaps a poor example - they can simply be passed in to the constructor of the system).

The thing is, you're making an arbitrary decision about which system owns which component. That may be obvious for some components, but not always. Who owns Transform? Who owns Inventory? It's not always clear.


The transform system would own transforms.
The inventory system would own inventories.

Have a system depend on another isn't bad thing if that dependency is because one system depends on anothers logic/behavior (for instance, to make efficient spatial queries). But if it just needs component data, it doesn't make sense to me.


And yet, as you point out, the data has to live somewhere. The dependency of certain behaviour on certain data will always be there. That's not a reason not to put components in systems.

Ideally, the system only knows that it needs an array/list/whatever of component A, B or C. It shouldn't care where they came from (grabbing them from some World object was perhaps a poor example - they can simply be passed in to the constructor of the system).


1. Not a bad idea. So: expose the components to your glue code, and have the glue code marshal the components between systems?
2. But if nothing else references the components, why not put them in the system? If multiple systems need access to the same data, pull the data out into its own system and then those multiple systems can reference that one. A system that just acts as a collection of state is still a system, it's just a very simple one.

In general, I think you can solve a lot of the dependency problems by "kicking them upstairs" to the glue code that owns the systems.

I doesn't make sense to me to create a system just for the sake of storing an array of components.

How do you manage adding/removing components from entities at runtime? Does whomever needs to do that require a dependency on the particular system that owns the component?

This topic is closed to new replies.

Advertisement