In this manner an entity is “just an id and components,” which is the common sales pitch of the ECS
I disagree - it’s not a sales pitch as much as it is a way to enforce loose coupling between the different features that are being developed. It is the essence of relational modeling, which seeks to reduce the development friction incurred by changes to the data model. The design that you have proposed, one could argue, is just an “entity system” architecture, because it does not enforce the loose coupling that the ECS architecture provides hard constraints for.
Having components entirely decoupled from the entities provides a wide array of benefits (no pun intended), most of which can drastically reduce resistance to change and tends to produce more modular and reusable code. As with relational modeling, applying normalization to your data (components) enhances the modularity and reusability of your components. You can think of each type of component as a table in a database, and each property as a column.
In your proposed design, refactoring any property on the entity will require that you also refactor it anywhere that that property is in use, which can be scattered all over the project depending on what systems use it. On the contrary, entirely decoupled components enforce a style in which, if you want to refactor some state on an entity, you only have to touch one place: where the entity is declared to have that component. The rest takes care of itself, as systems do not have to be refactored at all due to the fact that systems do not operate on entities that don’t have the required components.
This brings up another powerful side-effect of this decoupling: components and systems can be registered with the engine during runtime. I cannot even begin to describe the power that comes with runtime composition. From the ability to “hot-reload” your code while the game is running (really just redefining systems/components), all the way to assisting in the debugging of complex features by toggling systems on and off at any point while the game is running (this can help isolate the systems and features that bugs have originated from).
The ability to add/remove components to/from an entity during runtime allows you to declare whether or not an entity exhibits a specific behavior, dynamically and declaratively. If an entity does not have a component, it does not exhibit the behavior of the systems it does not meet the requirements for. In the entity system architecture you proposed, you would have to explicitly check for a property’s existence in any system that uses it to gain this useful side-effect which ECS gives for free.
The trouble in the end for every ECS is that at some point a system needs to tell another system something, thus partially reviving the type of coupling that drove most people away from whatever and towards ECS to begin with.
I think this probably the most misunderstood concept within ECS. There is a fundamentally different way to think about this, and that can be made more clear by first identifying the difference between imperative logic and declarative logic.
Imperative logic is used when writing the update loops of systems, and when defining event listeners that any given systems may be firing (e.g. engine.on('collision', (a,b) => {...})
where ...
is the imperative logic. This is usually the majority of the game’s code.
Declarative logic, on the other hand, is in use when emitting and reacting to events. Adding systems to the engine, and components to entities are also other declarative actions involved, as we are declaring the behavior that entities exhibit. Meanwhile, “under the hood” we imperatively explain exactly what that behavior makes the entity with the component do.
So the question of how systems should or should not communicate with one another is only answerable by the developer. ECS gives enough room to leave it up to the developer’s best discretion. It is entirely up to the design of the systems and how one chooses to make them work together.
Personally, I like to follow these rules when it comes to systems:
- Data in: components only
- Data out: events & components
If you need one system to convey information to another, it should either be via an event (which delegates the decision-making up to the gamemode), or via a component (which conceals the data from the gamemode logic layer, allowing another system to receive information from the component data). If you use events to delegate certain decisions to the gamemode-specific layer of code, you begin to be able to draw a line in between what is reusable for other games, and what is only specific to the game you’re creating at that time. This, of course, is just how I personally go about things. Maybe there are situations in which it is sane to have systems communicate directly via events, maybe in the case of syncing up the state between two systems (alternatively, use a service pattern, but this will couple the systems together). That begins to creep in on multi-threaded ideas, which the ECS architecture, and data-oriented design in general, are completely setup to handle.
One final benefit I will touch on is the performance aspect. Because components become essentially what are no different than C structs, pure data, the CPU can more efficiently loop over large arrays of them. This is due to the way CPU caches are designed, but in general the idea is that when the data is laid out in memory, with arrays containing structs of the exact same same shape and size, the CPU caching logic is able to better predict what it should fill the caches with next. When iterating over deep entity objects the memory can be less predictable and thus cache misses have the potential to occur much more frequently. This can be avoided in the entity system architecture you propose, but it is not enforced. That said, this is JavaScript, so there is only so much we can do about how memory is accessed. This part of ECS and data-oriented design is a more significant aspect in languages with more control over memory management.
In the end, the benefits of the design are:
- More modular code
- More reusable features
- Drastically reduced development iteration times
- Reduced and sometimes no need for refactoring when changing the behavior of the game
- Less change-resistant code
- Generally more performant code
Most of these benefits are almost exclusively due to the data-oriented design constraints, which ECS so elegantly lays on top of.
All that said, to answer OP’s question: I don’t know, but I will soon find out how possible this is!