ECS: From Tool to Paradigm
This blog is about Entity Component Systems. If you’d like to learn more about ECS, see this FAQ: https://github.com/SanderMertens/ecs-faq
I usually shy away from the “P word” in conversations about ECS. Whenever it is uttered all it seems to do is divide people into two camps, which is rarely a good thing. Yet some people persist that ECS is a paradigm, whereas others are adamant it is not. So is ECS a paradigm? Does it even matter?
If you’d ask me I’d argue that no, it is not a paradigm, and yes, it matters.
The reason why it matters is that explaining what ECS is is just as important as explaining what it is not. In my opinion, labelling ECS as a paradigm creates false expectations that lead to disappointment and frustration.
The short version of why I think ECS is not a paradigm is that Object Oriented Programming (OOP) is a paradigm, and ECS is nowhere near OOP in terms of applicability, features and footprint.
OOP offers a rich set of design features such as classes, inheritance, composition, interfaces, encapsulation, generics, polymorphism, overloading, references and dynamic dispatching. These concepts are backed by a robust body of academic literature and have been applied successfully more times than can be counted in a lifetime.
To contrast this, ECS has entities, components and systems. The Wikipedia page is the closest thing to a formal definition. There are a mere handful of (known) commercially successful projects that have used it.
ECS is simple, simple is sexy, and it is easy to get excited about something that is both simple and powerful (which it is). The cold hard truth however is that most software developers do not live in a world where complex features can be elegantly and efficiently expressed with just these three concepts.
But here is the crux of the post:
I think ECS has the potential to graduate to a paradigm.
There are three things I hear over and over from ECS users which give me confidence I am not entirely wrong about this. These are:
- ECS requires you to think about problems in a different way
- It is hard to be a purist, hacks are sometimes applied to make ECS work
- ECS leads to code that is more maintainable and reusable
These comments seem to strongly suggest that there is a different way to write code that can lead to better results but is lacking capabilities.
This is obviously empirical and subjective data, but the fact that this experience is shared by so many people with widely varying skill levels across languages and frameworks makes it hard to discount. Because of it I feel comfortable arguing that ECS, at the very least, is not OOP.
If it is not (yet) a paradigm, and it is also not OOP, then what is it?
ECS is a Tool
A tool is something that is purpose-built to address a specific problem. Tools do not have to be holistic or comprehensive. They do a single thing extremely well, but if you use it for something else results may vary. To use an unimaginative example: you use a hammer to punch nails, but a house built with just a hammer is not a place where you let children play.
Tools do not have to be simple. A game engine is a very complex tool with seemingly limitless possibilities. It is still designed for a single purpose, building games, and is rarely used outside of that context. A game engine may adopt a paradigm such as OOP or functional programming, but that does not make a game engine a paradigm.
A tool can be a specific instance of something (the Unreal engine) or can refer to something in the abstract (a game engine). A tool can have rules, features, and guidelines. The list of things that describe a tool is also not a paradigm.
There is nothing wrong with being a tool, at least not in the engineering sense. Tools can be ingenious, creative, clever and above all, necessary. Tools can be exciting, spur (religious) debates and can have communities, evangelists and detractors.
You can see where I’m going with this. ECS fits this description. It is a tool for organizing game data and game logic. ECS frameworks, just like game engines, can be complex, feature rich and can be used to create a wide variety of games, but that does not make ECS a paradigm.
You would likely not use ECS if your project is not a game. Sure, there are a handful of (encouraging) exceptions where ECS was used successfully in projects that are not games. In reality though, many if not most problems do not lend themselves well for an ECS based solution today.
This all seems to support a specific point I’m trying to make.
ECS is not a Paradigm
Merriam Webster gives us this flowery definition of the word paradigm:
“a philosophical and theoretical framework of a scientific school or discipline within which theories, laws, and generalizations and the experiments performed in support of them are formulated”
That doesn’t quite match ECS. Having deep thoughts about ECS is not the same as having a philosophical framework. Tutorials, documentation and blog posts do not amount to “theories, laws and generalizations”. It is safe to say that the theoretical foundation for ECS is janky at best.
Here is another definition that is more relatable to software engineers. You won’t find it anywhere else as I made it up, but it feels right to me:
- A common set of tools for building solutions, and
- A common set of constraints for preventing problems, which are
- Language independent, and
- Domain independent
This I can easily map to OOP, procedural programming, functional programming, declarative programming, etc. Not so much to ECS.
ECS has a common set of tools, but it is not very large (entities, components, systems, done). The consensus seems to be that the tools provided by ECS alone are not sufficient for building complex solutions, which is something I have argued before.
There are no common enforceable constraints for ECS. In OOP we can create interfaces that force users to implement certain methods, make things const to prevent data from getting written, restrict access to data so it doesn’t get corrupted, and so on. This is not the case for ECS.
To be clear, I am not arguing in favor of encapsulation. ECS is all about “naked” component data and it should stay that way. It should however provide tools that can guarantee the consistency of the data. An obvious source of inspiration is how constraints are enforced in databases.
ECS is language independent, it hits that one out of the park. There are many ECS implementations written in many languages. It is definitely not domain independent though. While it is getting a foothold in game development, it still has mountains to move when it comes to other domains.
The net effect of all of this is that if you are embarking on an ECS project, you are likely to hit some rough patches. Having said all that, the future for ECS is actually looking pretty bright.
There are fantastic communities out there that are more than willing to help out people with questions. On the whole, ECS users and library authors have done a great job on writing blogs, tutorials, documentation and examples. There is real momentum behind ECS, with no signs of it slowing down.
When all of that energy and excitement is bundled and directed, I believe that ECS will be able to move on from being just a tool.
ECS can become a paradigm
One remarkable thing about ECS is that despite its shortcomings (which I will discuss in a second) people are generally able to realize its benefits. This includes code that is easier to maintain, reuse and performs better (take note that this is the first time I bring up performance).
If a scruffy upstart like ECS can compete with OOP, there must be something there. OOP seems to have an overwhelming advantage, as it is what we have been taught, is ingrained in many languages and tools, is mostly what we use professionally and has a wealth of best practices available. And yet.
The push to make a paradigm out of ECS has therefore little to do with titles, pride, or other forms of showboating. Like I said before, there is no shame in being a tool, and if that is all ECS turns out to be, that is totally fine.
The only reason for turning ECS into a paradigm is to make it more accessible and easier for people to achieve its benefits. We can do this by fixing shortcomings, conjuring up elegant and beautiful conceptual frameworks, working with academia to formalize those concepts, having lots of deep thoughts on the subject and by writing many games.
More than anything else, for ECS to become a paradigm it requires a rich set of features that enables developers to build complex solutions without having to break from the mould. Today there is a patchwork of features found across frameworks that address some of this, but in order to graduate to a paradigm, a common set of design concepts and principles are needed so that we can write down and discuss ECS based designs in a framework independent way.
So what’s missing
Let’s get specific for a moment, and look at some of the things that can complicate ECS development. This is a list I compiled from questions that have been asked on different gamedev discords, and features that have been implemented in some of the libraries in addition to “vanilla ECS”.
Hierarchies. Easily the number one thing that ECS developers run into. Hierarchies are so common in games, and ECS does so little to address them that developers find themselves again and again reinventing wheels, with not all wheels being created equally.
State Machines. Something I’ve written about before. State machines, or more generically, mutually exclusive tags are difficult to design in ECS. The reason for this is that in order to transition to a new state, you first need to know which state (tag) to replace. ECS does not make this easy.
System organization. It sounds simple and elegant. You have such and such systems that are matched with this and that component, you run those systems and voila you have a game. Not quite. Those systems have to be ordered and scheduled. ECS does nothing to specify how all of this is supposed to work, which moves the responsibility to the developer.
Component organization. Should my components be large? Should I split up a Transform component into Position, Rotation and Scale? If both approaches are valid, how do I decide which one to use in my project? Component design is the single most important thing in an ECS application. Getting it wrong is very expensive, and is also something that ECS says precious little about.
Entity initialization. Entities are not typically constructed from their individual components. A common practice is to use entity templates. Templates are usually entity hierarchies. How should an application initialize a component on an entity 2 levels down in the hierarchy? How can this information be captured in a way that allows it to be stored as an asset?
Queries. ECS queries are abstractly understood as things that match a list of components. In reality though, queries can be much more expressive, as they can exclude components, mark components as optional, select one out of a list of components, select a component from a parent entity and so on. There is no set of query operators that ECS frameworks have in common.
Runtime queries. There often is a need to filter on runtime aspects, like platoons, world chunks, NPCs that belong to a specific quest and so on. Queries are simple lists of components, which in itself are often defined at compile time. How should runtime aspects be integrated with queries so that an application does not have to reinvent its own mechanism?
Data flow programming. Not all game logic can be conveniently expressed as a system that matches with entities. Visual languages like Blueprints are examples of data flow programming, where nodes in a graph exchange immutable state, rather than mutating components. This increases reusability of code, and is an enabler for visual programming languages.
Component sharing. When a game has many similar entities, it is likely that they have overlapping component with the same values. Being able to store such components once and share them with many entities can reduce memory footprint and improve caching efficiency. Component sharing can also be useful for storing data that is computed and accessed for groups of entities, such as broad phase AABB testing.
Naming conventions. How should components and systems be named? What does it even mean to have a component? Usually people will say that an entity “has” a component (Position), but “is” a tag (Boss). Why is that? Establishing conventions can avoid confusion when discussing and sharing ECS designs.
Code organization. In OOP a single file often contains a single class, which generally produces files that are of a desired size- not too large and not too small. In ECS there is no concept with similar properties. Storing a file per system can result in lots of small files, which, depending on your preference, can be a problem.
Relationships. Games rely on lots and lots of knowledge. They are tiny mini universes with their own rules, and rules mean relationships between entities. Executing the game logic means acting on those rules, and that means accessing those relationships. This is a critical aspect in the development of many games, and ECS says nothing about it.
Generalization. ECS can be rigid in that systems have to explicitly specify the components they are interested in. There are no facilities for expressing interest in, for example, all components with a common ancestor. This causes maintenance issues, code duplication and increases boilerplate. Systems either need to keep track of an evolving list of components, or applications need to explicitly create duplicate systems when components are added.
Specialization. Related, but different from generalization. Just querying for data can sometimes match more entities than you need. Tags are a common way to subdivide datasets, but that introduces the same problems as with generalization: A system will have to provide an explicit list of all tags it wants to match with vs. for example subscribing for a common ancestor.
Constraints. In ECS an entity is a dynamic bag of components that can be added and removed at any point in time. In reality some components are never removed, should never exist at the same time, or should not be written in a certain game state. ECS specifies no such constraints, yet these can be useful in ensuring that a large team uses ECS consistently.
Warning: editorial commentary and concluding statements are imminent.
This may read a bit like a wishlist, and in some sense it is. It is definitely colored by what I would like to see in ECS. The list may contain things that some think should not be part of ECS, and it may not contain things that others think should be part of it. What the exact items are that need to be addressed is up for discussion. What is not up for discussion is that these are at least some of the challenges that developers have run into.
If it were up to me, I would get a group of motivated people together that are interested in writing, and more importantly, agreeing on proposals that address these issues head on from first principles. First principles means that we should be able to talk about ECS in terms of ECS without having to refer to framework-specific features. If we want to get serious about ECS, this is not just unavoidable, it is only the professional thing to do.
What I purposefully left out in this list is the need to categorize or somehow formalize all the different ways in which an ECS can be implemented. This should come after figuring out what good ECS design looks like. Otherwise we end up with design practices that are optimized for a set of data structures. That is in my humble opinion not only backwards, it is not durable since ECS implementations are continuously improving with ever more clever ways to store components.
Good performance should not be an excuse for bad design. Good design should not be an excuse for bad performance. We know where that one leads.
Thanks for making it all the way down here! If you like what you read, or if you don’t and you’d like to tell me why, join the Discord!
Here is some further reading in case you want to delve deeper: