This is a tough one. I'll just try to tackle some of the questions based on my particular experiences (YMMV):
Components must access data of other components. E.g. the render component's draw method must access the transform component's position. This creates dependencies in code.
Don't underestimate the amount and complexity (not degree) of coupling/dependencies here. You could be looking at the difference between this (and this diagram is already ridiculously simplified to toy-like levels, and the real-world example would have interfaces in between to loosen the coupling):
... and this:
... or this:
Components can be polymorphic which further introduces some complexity. E.g. There might be a sprite render component that overrides the render component's virtual draw method.
So? The analogical (or literal) equivalent of a vtable and virtual dispatch can be invoked via the system rather than the object hiding its underlying state/data. Polymorphism is still very practical and feasible with the "pure' ECS implementation when the analogical vtable or function pointer(s) turns into "data" of sorts for the system to invoke.
Since the polymorphic behaivour (e.g. for rendering) has to be implemented somewhere, it is just outsourced into the systems. (e.g. the sprite render system creates a sprite render node that inherits render node and adds it to the render engine)
So? I hope this is not coming off as sarcasm (not my intent though I've been accused of it often but I wish I could communicate emotions better through text), but "outsourcing" polymorphic behavior in this case doesn't necessarily incur an additional cost to productivity.
The communication between systems can be difficult to avoid. E.g. the collision system might need the bounding box which is calculated from whatever concrete render component there is.
This example seems particularly weird to me. I don't know why a renderer would be outputting data back to the scene (I generally consider renderers read-only in this context), or for a renderer to be figuring out AABBs instead of some other system to do this for both renderer and collision/physics (I might be getting hung up on the "render component" name here). Yet I don't want to get too hung up on this example since I realize that's not the point you're trying to make. Still the communication between systems (even in the indirect form of read/writes to the central ECS database with systems depending rather directly on transformations made by others) shouldn't need to be frequent, if at all necessary. That's contradicting some of what I wrote immediately below about the importance of determining order of evaluation upfront but that's with practical needs for user response rather than "correctness" (it's not necessarily a temporal coupling issue but a user-end design issue of ensuring frames output the latest results without lagging behind).
This may lead to preblems if the order of calling the system's update functions is not defined.
This absolutely should be defined. The ECS is not the end-all solution to rearrange system processing evaluation order of every possible system in the codebase and get back exactly same kind of results to the end user dealing with frames and FPS. This is one of the things, when designing an ECS, that I'd at least strongly suggest should be anticipated somewhat upfront (though with a lot of forgiving breathing room to change minds later provided it's not altering the most critical aspects of the ordering of system invocation/evaluation).
However, recalculating the entire tilemap every frame is expensive. Therefore, a list would be needed to keep track of all the changes made to then update them in the system. In the OOP way this could be encapsulated by the tile map component. E.g. the SetTile() method would update the vertex array whenever its called.
I didn't quite understand this one except that it's a data-oriented concern. And there are no pitfalls as to representing and storing data in an ECS, including memoization, to avoid such performance pitfalls (the biggest ones with an ECS tend to relate to things like systems querying for available instances of particular component types which is one of the most challenging aspects of optimizing a generalized ECS). The fact that logic and data are separated in a "pure" ECS doesn't mean you suddenly have to recompute things you could have otherwise cached/memoized in an OOP representation. That's a moot/irrelevant point unless I glossed over something very important.
With the "pure" ECS you can still store this data in the tile map component. The only major difference is that the logic to update this vertex array would move to a system somewhere.
You can even lean on the ECS to simplify the invalidation and removal of this cache from the entity if you create a separate component like TileMapCache
. At that point when the cache is desired but not available in an entity with a TileMap
component, you can compute it and add it. When it's invalidated or no longer needed, you can remove it through the ECS without having to write more code specifically for such invalidation and removal.
The dependencies between components still exist although being hidden away in the systems
There's no dependency between components in a "pure" rep (I don't think it's quite right to say that dependencies are being hidden here by the systems). Data doesn't depend on data, so to speak. Logic depends on logic. And a "pure" ECS tends to promote the logic to be written in a way so as to depend on the absolute minimal subset of data and logic (often none) a system requires to work, which is unlike many alternatives which often encourage depending on far more functionality than required for the actual task. If you're using the pure ECS right, one of the first things you should appreciate is the decoupling benefits while simultaneously questioning everything you ever learned to appreciate in OOP about encapsulation and specifically information hiding.
By decoupling I specifically mean how little information your systems need to work. Your motion system doesn't even need to know about something far more complex like a Particle
or Character
(the developer of the system doesn't necessarily even need to know such entity ideas even exist in the system). It just needs to know about the bare minimum data like a position component which could be as simple as a few floats in a struct. It's even less information and fewer external dependencies than what a pure interface like IMotion
tends to carry along with it. It's primarily due to this minimal knowledge that each system requires to work that makes the ECS often so forgiving to handle very unanticipated design changes in hindsight without facing cascading interface breakages all over the place.
The "impure" approach you suggest somewhat diminishes that benefit since now your logic isn't localized strictly to systems where changes don't cause cascading breakages. The logic would now be centralized to some degree in the components accessed by multiple systems which now have to fulfill interface requirements of all the various systems that could use it, and now it's like every system then needs to have knowledge of (depend on) more information than it strictly needs to work with that component.
Dependencies to Data
One of the things that's controversial about the ECS is that it tends to replace what might otherwise be dependencies to abstract interfaces with just raw data, and that's generally considered a less desirable and tighter form of coupling. But in the kinds of domains like games where ECS can be very beneficial, it's often easier to design the data representation upfront and keep it stable than it is to design what you can do with that data at some central level of the system. That's something I've painfully observed even among seasoned veterans in codebases that utilizes more of a COM-style pure interface approach with things like IMotion
.
The developers kept finding reasons to add, remove, or change functions to this central interface, and each change was ghastly and costly because it would tend to break every single class that implemented IMotion
along with every since place in the system that used IMotion
. Meanwhile the entire time with so many painful and cascading changes, the objects that implemented IMotion
were all just storing a 4x4 matrix of floats and the whole interface was just concerned with how to transform and access those floats; the data representation was stable all the way from the beginning, and a lot of pain could have been avoided if this centralized interface, so prone to change with unanticipated design needs, didn't even exist in the first place.
This could all sound almost as disgusting as like global variables but the nature of how the ECS organizes this data into components retrieved explicitly by type through systems makes it so, while compilers can't enforce anything like information hiding, the places that access and mutate the data are generally very explicit and obvious enough to still effectively maintain invariants and predict what sort of transformations and side effects go on from one system to the next (actually in ways that can arguably be simpler and more predictable than OOP in certain domains given how the system turns into a flat sort of pipeline).
Lastly, I want to ask the question of how I would handle animation in a pure ECS. Currently I have defined an animation as a functor that manipulates an entity based on some progress between 0 and 1. The animation component has a list of animators which has a list of animations. In its update function it then applies whatever animations are currently active to the entity.
We're all pragmatists here. Even in gamedev you'll probably get conflicting ideas/answers. Even the purest ECS is a relatively new phenomena, pioneering territory, for which people haven't necessarily formulated the strongest opinions on how to skin cats. My gut reaction is an animation system which increments this sort of animation progress in animated components for the rendering system to display, but that's ignoring so much nuance for the particular application and context.
With the ECS it's not a silver bullet and I do still find myself with tendencies to go in and add new systems, remove some, add new components, change an existing system to pick up that new component type, etc. I don't get things right at all the first time around still. But the difference in my case is that I'm not changing anything central when I fail to anticipate certain design needs upfront. I'm not getting the rippling effect of cascading breakages that require me to go all the over the place and change so much code to handle some new need that crops up, and that's quite the time saver. I'm also finding it easier on my brain because when I sit down with a particular system, I don't need to know/remember that much about anything else besides the relevant components (which are just data) to work on it.