Is The "entity" Of Ecs Really Necessary?

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

Having logic in the components themselves means that components will need to know about other kinds of components, or about all of the other components of the same type. And you end up with these kinds of questions in the forums "What do I put in my components?" "How can I make component X relate to component Y?" "Omg I cant make the component logic isolated from other components! ECS sucks!". What if the same component is used for two different things? You just shoehorn the two different functions in the component and call it a day? It opens a whole can of worms.

The "system" part deals with that issue, providing a platform where you can place these dependencies between components and between entities.


That just moves the problem into a new place, and gives you 2 headaches (how do I carve my game up into orthogonal systems and components) instead of one (how do I carve my actors up into components).

The main reason newbies are confused about what goes in components is because they're trying to obey blog posts about how games should be structured before they've made a game and therefore before they understand how they would do things differently.

I just said T-Machine blog is a nice source to learn what an ECS is, not what is good for.


It's a nice source to learn what one particular person wants these approaches to be. I don't think we should accept that one particular definition gets to be called "Entity Component System" and other approaches, which also have entities and components, do not. (Especially since the blog tries to attach the label 'entity system', which is woefully underspecific.)
Advertisement

That just moves the problem into a new place, and gives you 2 headaches (how do I carve my game up into orthogonal systems and components) instead of one (how do I carve my actors up into components).
Its a "headache" that separates the concerns better. It separates component ownership better. It separates component usages better. It separates the system dependencies better. If separating concerns is multiple headaches for you, then put everything in a god class. Only one big headache there.

I already listed the kind of issues you have to think when stuffing the logic in the components. Moreover I already listed the issues people come here with when they do so. I'm just not seeing your POV here. Could you explain further the "two headaches" you mention?

I don't think we should accept that one particular definition gets to be called "Entity Component System" and other approaches, which also have entities and components, do not.
Then don't complain latter than there isn't a "clear definition" of what an ECS is. Its like handing you a MVC definition, then saying you dont like it because there are other architectures that use models and views, but not controllers. I know, thats why its called MVC, not MV.

I agree on the "Entity System" label though. It is vague, and stupid. Quite hard to come up with an architecture in a game that doesn't uses some form of entity, actor, or similar.

"I AM ZE EMPRAH OPENGL 3.3 THE CORE, I DEMAND FROM THEE ZE SHADERZ AND MATRIXEZ"

My journals: dustArtemis ECS framework and Making a Terrain Generator

That just moves the problem into a new place, and gives you 2 headaches (how do I carve my game up into orthogonal systems and components) instead of one (how do I carve my actors up into components).

It actually serves a couple of purposes:

- Moving logic into a higher level piece of code is a common way to address undesired cross-dependencies (which, as someone mentioned, is a common question on these forums when people attempt to implement an ECS "incorrectly").

- Moving logic into a place that knows about *all* the components of a certain type allows that logic to reason over them in an intelligent way (say, for optimizing spatial location queries).

There's no reason you can't do both; have logic in components that only operate on itself, and have systems that operate on more than 1 component.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

There's no reason you can't do both; have logic in components that only operate on itself, and have systems that operate on more than 1 component.

So much this.

Just making up component names for these examples, but it is something I've done hundreds of times over the years.

It is straightforward and easy to write a component that hooks up to various commands and then only manipulates itself.

But for interplay, you could write something that detects another component before working. All the systems I've worked with have had a way for components to identify their containing parent in the hierarchy, and also provided functions to scan for contained components, either as direct children or as nested elements. Perhaps you'll build a component scans it's parent for a PhysicsObject component as a direct child.

Or maybe you write a mesh deformation component that gets it's parent, then requests all Mesh components from the parent. You may want to require the parent object contains exactly one Mesh component, and logs errors every time it is called and the attached components aren't found.

Or maybe you want to write an AI component that searches the parent for both an AiController component that you wrote and a AiLocomotor component. You can then derive your own specialized AiControllers or AiLocomotor types from interfaces, check that there are exactly one component implementing those interfaces attached, then either log the error or run the code using the detected components.

This type of automatic connection can be an alternative to directly-specified values. That alternative is to have a system that injects the target. You might have public methods to get and set the target component which validate that the component meets the interface you're interested in. Then the target can be injected by the game engine at load time, or be injected by your own code that replaces the component while running, or some other means. Automatically detecting makes it slightly easier for anyone manipulating the world and slightly less error prone in case someone forgets to specify the target.

Its a "headache" that separates the concerns better. It separates component ownership better. It separates component usages better. It separates the system dependencies better. If separating concerns is multiple headaches for you, then put everything in a god class.


The way I see it, these 'systems' are God classes. They typically handle large amounts of functionality and cut across various concerns in order to make various components work together.

I prefer encapsulating behaviour inside the components themselves, the way Unity does it. Sure, occasionally you need to add a coordinating component on top. For me, trying to decide how to split along 'system' lines just doesn't make any sense because each system is usually so broad as to be full of conditionals or so narrow as to probably be a good candidate to be specific to one component. And how do you decide what goes in individual components if there are an arbitrary number of different systems acting upon the component? Why have components at all in that case, and not just a long property list?

Then don't complain latter than there isn't a "clear definition" of what an ECS is.


I don't want a clear definition. There are many ways to use composition in making game software and trying to attach a fairly generic label to just one of many approaches means people assume that is the 'right' or the 'pure' way of approaching it - which is bad for people new to games.

Its like handing you a MVC definition, then saying you dont like it


To be fair, MVC is a good example of people twisting the term to mean something else. Hardly anybody uses that term in the way it was intended for the original Smalltalk system. (Mostly because it doesn't fit well with web apps.)

Its a "headache" that separates the concerns better. It separates component ownership better. It separates component usages better. It separates the system dependencies better. If separating concerns is multiple headaches for you, then put everything in a god class.


The way I see it, these 'systems' are God classes. They typically handle large amounts of functionality and cut across various concerns in order to make various components work together.

I prefer encapsulating behaviour inside the components themselves, the way Unity does it. Sure, occasionally you need to add a coordinating component on top. For me, trying to decide how to split along 'system' lines just doesn't make any sense because each system is usually so broad as to be full of conditionals or so narrow as to probably be a good candidate to be specific to one component. And how do you decide what goes in individual components if there are an arbitrary number of different systems acting upon the component? Why have components at all in that case, and not just a long property list?


I'm not sure you're using "system" to mean the same thing thing Chubu is, or that I would. When I think of "components" and systems" in the context of ECS, I think of a "system" as an object that owns the data and provides the behaviour for all components of a particular type. It's essentially the same pairing of data and behaviour that drives OO, but the behaviour is moved to the "system" level to cut down on the need for dependency management on individual data, and the state of a particular "game object" is spread across multiple systems. If your systems look like god objects, it's probably time to split them out into smaller sub-systems that actually map to a particular bit of a data.

In my own code - which does not use an ECS - a "system" is simply an aggregation of game state and some methods to act on it collectively. With this kind of "system", you generally don't have to think too hard about both which systems are orthogonal and which components are orthogonal because those are the same problem. "Component" may not be the best word here. I actually prefer the word "datum" because "component" implies that the component owns its own behaviour and exists as a separate "thing" in isolation, but with this approach it doesn't. The design process for a data-oriented "system" should ideally go something like:
- determine what the desired behaviour is
- determine what data is needed to implement the behaviour
- implement a system that owns the needed data and applies the desired behaviour to the data

With this approach, roughly-speaking each system should match to a particular feature of the game, though sometimes multiple systems need to use the same data, which means that you can split the shared data off into its own system. In general, systems exist to perform specific tasks. A system owns the data it needs to perform a particular task.

Here are some examples of my systems:
SpriteObjectSystem - owns all the data and behaviour needed to animate and render a sprite
CharacterSystem - owns all the data and behaviour needed to represent a character; each datum is essentially a character sheet that other gameplay systems can examine to drive their behaviour
MovementSystem - owns all data relevant to character movement; which characters are moving and how far, in what direction, and whether the movement was voluntary or not
CombatSystem - owns combat state data like how long a particular character has left in its swing, what attack it's using, etc.
CollisionSystem - owns data needed to test for collisions between characters and the terrain and characters and other characters

Updating the systems looks something like this (though in the actual game this update is spread across several "layers" that serve to aggregate related systems):
// somewhat abridged so as not to show the systems I didn't mention above
characters.RegenerateHealth(dt);
characters.ResetDisabledActions();

movement.ApplyAnimations(characters, spriteObjects);
movement.ComputeNextPositions(dt, characters, spriteObjects, collision);
movement.ApplyMovement(dt, characters, spriteObjects);

collision.ClearColliders();
characters.BuildCharacterColliders(collision, spriteObjects);

combat.BuildHitboxes(characters, spriteObjects);
combat.UpdateCombat(dt, characters, movement);
combat.ResolveAttacks();

characters.SetNextAnimations(spriteObjects);
spriteObjects.Tick(dt);
Note that each specific sub-task a subsystem carries out is its own method, rather than throwing everything in a big clunky "Update" function. This means that each method only needs to reference the specific sub-systems it needs, making dependency management substantially easier than having each system store references to one another. In addition, if I want to insert a step in between two particular tasks, I can easily do so. I can even put these into a job system if I want.

I'm not sure you're using "system" to mean the same thing thing Chubu is, or that I would. When I think of "components" and systems" in the context of ECS, I think of a "system" as an object that owns the data and provides the behaviour for all components of a particular type.


If behaviour is outside the component, and a system is handling the behaviour of exactly one component type, then that just sounds like normal OOP except written in a procedural style. It doesn't seem worthwhile unless you have a strong dislike for OOP. This seems to be what the T-Machine blog advocates in some places without ever making a good argument for why this would be better. You iterate over the same things and pass in the same arguments, just with the loop ordering transposed.

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.

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. Think of it as infinitely granular components, if that helps. ;)

Personally, if I am going to attempt to package up data into cohesive parcels, it makes sense to me to bundle the logic in there too. So I prefer an approach similar to what Beernutts and Frob are discussing, which allows logic in components, and where in my case any sort of system that operates on other components is just implemented as another component.

Just to address your edit...

Here are some examples of my systems:
CharacterSystem - owns all the data and behaviour needed to represent a character; each datum is essentially a character sheet that other gameplay systems can examine to drive their behaviour
MovementSystem - owns all data relevant to character movement; which characters are moving and how far, in what direction, and whether the movement was voluntary or not
CombatSystem - owns combat state data like how long a particular character has left in its swing, what attack it's using, etc.
CollisionSystem - owns data needed to test for collisions between characters and the terrain and characters and other characters


I appreciate natural language is imprecise, but you've got several different systems competing over different aspects of a character here. Movement and combat have to coordinate with each other, and I'm sure there are aspects of the CharacterSystem that matter too. I can't see a clean way in which these dependencies get managed short of throwing extra properties into the character objects which are passed around from one system to the other, but which are not clearly owned by any system in particular. Deciding on how to manage that sort of thing, especially as the number of systems grows, is the 'headache' I referred to originally. I'd rather start from a position of encapsulating some of those responsibilities clearly inside component logic that operates on private data.

I don't have time to respond in full, but for now:

I appreciate natural language is imprecise, but you've got several different systems competing over different aspects of a character here. Movement and combat have to coordinate with each other,


I don't know if "competing" is the word I would use. There isn't a whole lot of overlap. The movement system encapsulates movement; the combat system encapsulates combat. The combat system only knows about the movement system because it tells the characters to move forward when they attack and backwards when they block.

With this codebase, the movement system only knows about the characters, sprites (because characters use their associated sprites for positioning - I may change this in the future), and the collision system. The only state the movement system tracks is this:
private:
// These aren't "components", hence why I prefer the term "datum" rather than "component." 
// These particular data are used more like events - the CharacterMovement instances only exist as long as the character is actually trying to move.
struct CharacterMovement
{
    TypedHandle characterHandle;
    sf::Vector2f newDirection;
    float modifier;
    SpriteAnimationName animationName;
};

struct CharacterMoveResult
{
    TypedHandle characterHandle;
    sf::Vector2f newPosition;
};

std::vector<CharacterMovement> m_voluntaryMovementQueue;
std::vector<CharacterMovement> m_involuntaryMvementQueue;
std::vector<CharacterMoveResult> m_results;
The movement system works like this:
- Other systems (input, AI, combat, status effect) queue up movements
- the movement system filters the movements by which ones are allowed by the collision system (ie. walkable terrain and character colliders) to build the move result list
- the movement system applies the move result list to characters

The combat system has quite a bit more state than this, and could use a refactoring, but it follows a similar sort of design. As much as possible, state only actually exists if it needs to, and if no other system needs it, then that state is completely private to the system. Nothing but the combat system needs to know that the character attacked last frame; therefore, it isn't on the character, which is the only state that's shared between systems.

This is what a character datum looks like:
struct CharacterDatum
{
    const CharacterDefinition* definition;
    TypedHandle spriteHandle;
    SpriteDirection facingDirection;
    const SpriteAnimation* nextAnimation;
    float remainingHealth;
    float remainingStamina;
    FlagSet<CharacterAction> disabledActions;
};
All other character state is private to the systems that deal with the various features of the game.

and I'm sure there are aspects of the CharacterSystem that matter too.


Sure - to the movement system, and the combat system, but in general, dependencies only flow in one direction. The character and sprite systems don't actually know about the movement system. Nor does the movement system know about anything other than sprites, characters, and collision. Arguably, it doesn't even really need to know about sprites - I could improve this decomposition somewhat, and very easily.

I can't see a clean way in which these dependencies get managed short of throwing extra properties into the character objects which are passed around from one system to the other, but which are not clearly owned by any system in particular.


I suggest you read this link. The bit on "existence based processing" and the bit on "component-based objects" is particularly relevant. This may be a better explanation of the approach I'm taking. The key is that a lot of the properties are only transient - again, the movement system doesn't bother with a character if it isn't actually trying to move.

I'd rather start from a position of encapsulating some of those responsibilities clearly inside component logic that operates on private data.


But that's exactly what this does. It's just that responsibility is decided at the system level rather than the component or object level. :)

This topic is closed to new replies.

Advertisement