It’s been about a year since I started using ECS, Entity-Component Systems, in production Roblox/Luau projects. I started out making my own naive ECS because I wanted inferred strict types on my queries. Not long after, jecs released, and I switched to that. It was faster, had more features, and had the inferred strict types that I wanted. I’m glad it released when it did because a key feature of jecs, relationships, ended up being pivotal in my projects.
That aside, over my time working with ECS I’ve come to realize what works and what doesn’t. ECS fascinates me because it’s such an interesting way to organize software, and I love it! Despite this, I’ve come to notice quite a few problems and pitfalls with it, and that’s what I’m going to dive into in this post. At some point I also want to get around to writing more about what I’ve found that works well in ECS too.
Beware generalization
One of the first things people tell you to do when working with ECS is to generalize the components you create. This, at first glance, makes sense. If you were building a car with ECS, your mind would likely jump to creating a general “seat” component, so that if you need to use that seat component anywhere else, you can. While this principle is very helpful, it can also be dangerous!
I can best explain this by explaining a clarification to this principle that I think is important to note. Components should be generalized, but still have single responsibilities. Why? Well, consider a common pattern — having a “model” component to represent the physical/visual model of an object. This sounds good at first glance, but do notice how this component has multiple responsibilities. In very few cases is every model in your game going to have the same purpose — in some cases this is going to be a player model and in others, a building in the world.
If you wanted strictly typed access to the properties/ancestors/descendants of this model, it’s somewhat unhelpful. I haven’t been able to find a clean way to use a general “model” component in practice. It’s almost always better to have multiple model components that are specific to what kind of model it is.
On the other hand, consider a generalized “timer” component. It could be used for anything, but it still works well because it has a single responsibility — it’s a timer and nothing else. Its usage is general, not its implementation. The model component doesn’t follow this, and I believe the problem can be extended to any component that’s been generalized too far.
Even worse, if you have a component that is too general, then it can be hard to document or use it. Good code is often self-documenting, and having one component mean different things depending on the context sounds like a nightmare.
ECS is better at the engine level
A common pattern I’ve seen in the Roblox ECS space is to have a “transform” component that represents the exact position and rotation of an entity. The only problem is that in the engine, the “transform” property of objects won’t update whenever you change your user-level transform component. That’s fine, just create a system to reconcile your changes from ECS land into the actual representative objects.
You have just successfully created bindings to a property you can already write to! And worse yet, you’d likely need to also maintain another component on entities that represents what object the transform is supposed to be assigned on. To me, having to create this binding is frusturating. Why not just write to the respective position/rotation properties directly?
If your engine’s backend had an ECS-driven API to it, it would be great. You could just use those engine-level components alongside your user-level components and not have to worry about linking the two together manually. I find these sorts of fundamental components (transform, model, etc.) always better when the engine can provide them natively. Sadly, not every engine is going to do that because not every engine is powered by ECS. In the cases where you don’t have this support, creating this binding will be fine, but it is annoying.
Missing abstraction and encapsulation
To be clear, I love ECS and its principles. At the same time, I miss classes and otherwise “normal” code. I miss easy abstraction and encapsulation. In Luau, I’m not a huge user of classes, but it’s best to explain my point in terms of classes and OOP.
Components dont lend themselves much in terms of abstraction, and especially not in terms of encapsulating functionality. With classes, I could write something with clear behavior and hidden internals. User code doesn’t have to worry about the implementation details, they just use the abstracted class I wrote.
In ECS land, components are basically just data, and systems operate on that data. There isn’t really a way to “hide” internal logic because the data is just sitting there — that’s the whole point. I find that this complicates writing code sometimes. Instead of being able to use a simplified API to get something done, you have to understand all the components, their purpose, and what to query for. Not the end of the world obviously, but the baseline for writing and understanding code is higher. This is not ideal.
The best way I’ve figured to handle this is to have components that represent “control” data. That is, data meant purely as an input to some system which writes to internal facing components — ones that you shouldnt change manually. Unfortunately there’s no way to enforce read-only data on those internal components because your other systems need to write to them.
In ECS world there is only public, not private. This is okay, but the lack of abstraction and encapulation is disappointing. Perhaps wrapping a class or some other API around a component for reading/writing could work in certain scenarios.
Invariants are not enforced
Another thing I miss having is stronger invariants. In some respects this is a good thing for the flexibility of systems, and in others it’s detrimental. By the very nature of ECS, entities can have any arbitrary components on them, just as long as you don’t have the same component on a given entity more than once. Functionality is composed by creating components with specific purposes on an entity, forming a cohesive whole.
But what’s to stop you from sticking a component on that entity that isn’t supposed to be there? There are no easy mechanisms to enforce that. Developers could accidentally make a mistake like this very easily. What happens when you remove a component that is critical for that entity to function as a cohesive whole? Enforcing invariants on your entities that are specifically related to your game’s functionality is something I miss. To the ECS, all of these operations are valid, but in the context of a specific game, it might not be!
Generally, the solution to this is to query for groups of components and/or use relations. This doesn’t fix the root cause, though! I have written a lot of manual invariants (assertions) when retrieving components from an entity because I know that entity is supposed to have that component in a given context. The only problem is that the computer isn’t helping me check for that.
There are some seriously confusing and annoying bugs that can arise from accidentally adding/removing a single component somewhere and then your entire system mysteriously stops operating on that data you thought it was operating on. I’ve ran into a lot of these without manual assertions, and they always take on the form of not having the correct components to be successfully queried as a whole (where whole in this case represents an entity construct specific to your game).
Some short notes
I believe that debugging is more complicated compared to that of debugging non-ECS code. Your systems run each frame, so using breakpoints, a traditional debugger, or print statements to identify incorrect state or conditions becomes unintuitive — not impossible, but more annoying as a baseline. ECS-specific debuggers exist, although I personally haven’t found too much use with them (I have more to learn, as always).
I’ve also found it is difficult to initially learn how to debug in ECS. Sure you can learn it over time, but being able to teach developers how to fix bugs in their code quickly is extremely important.
Designing high-quality systems in ECS can also be more challenging. I’d argue it is more unintuitive to write ECS code in a fair number of cases compared to non-ECS code. For instance, if I need to write a round-loop for my game, I’d rather just write a literal loop that runs a “round” function each iteration than have to deal with scheduling it per-frame. A good mix of ECS and non-ECS code is ideal.