About Hyper ECS¶
What is ECS?
ECS stands for Entity-Component-System. It is a programming architectural pattern that is very useful for game development.
Components are the most basic element. They can be thought of as containers of related data. In a 2D game, we could have a Position Component with an x and y value, for example. Components typically do not contain any logic, though they may have callbacks to deal with side effects.
An Entity is simply a generic object, which has a unique ID and a collection of Components. Entities have no inherent behavior or properties of their own, but are granted these by Components. We can add or remove Components from Entities as necessary during the simulation.
Finally, we have Systems. Systems iterate over Entities that have one or more designated Components, and perform transformations on them. For example, a Motion System could look at Entities that have a Position Component and a Velocity Component, and update the Position Component based on the Velocity Component.
Why not just use object-oriented programming?
Object orientation is an intuitive idea when it comes to building simulation-oriented applications, like video games. We can think of each “thing” in the game as a self-contained object that can be acted upon externally via access to methods. For example, in an Asteroids game, the ship is an object, the bullets the ship fires are objects, the asteroids are objects, and so on.
However, problems quickly emerge when we use object-oriented programming in practice.
As programmers we want to reuse code as much as possible. Every bit of duplicated code is an opportunity for bugs to be introduced. Object-oriented code accomplishes reuse through inheritance. But this immediately runs into problems. Suppose I have an object where it would make sense to inherit behavior from two different classes. This is common in games, because it is natural to mix-and-match behaviors. But if those classes both implement a method with the same name, now our new object has no idea what to do. Most object-oriented systems, in fact, forbid multiple inheritance, so we have to share code with helper functions or other awkward constructions. ECS accomplishes code reuse via composition, not inheritance, which avoids these problems.
Another pattern that often occurs with object-oriented code is tight coupling. Objects that reference each other directly become a problem when we change those objects in any way. If we modify the behavior of object B, and object A references object B, then we end up also having to modify object A. In a particularly poorly designed system, we might end up having to modify a dozen objects just to slightly change the behavior of one simple object.
Tight coupling is a nightmare when it comes to programming games, because games are by nature very complex simulations. The more coupling we have between objects, the more of the entire environment of the game we have to understand before we can pick apart the behavior of the game. It is also possible that we could surprise ourselves by unexpectedly changing the behavior of separate objects by changing the behavior of one particular object.
We want our architecture to encourage us to have as little coupling between different elements of the game logic as possible, so that we can look at any individual element of the game and easily understand how it behaves without needing to know anything about the other elements of the game, and modify the behavior without worrying about introducing strange behavior in other supposedly unrelated objects. ECS helps us accomplish this.
Object-orientation also makes it difficult for us to automatically test our behavior, because it encourages us to encapsulate lots of different behavior in single objects that vary based on their internal state. ECS makes it very simple to to do automated testing, because the responsibilities of each element are cleanly separated.
Issues with standard ECS
Imagine a situation where multiple systems need to manipulate the position of objects. In normal ECS, all kinds of problems could be introduced by the execution order of Systems that manipulate position. For example, let’s say we have a special ability that lets the player character teleport forward over a short distance.
In regular ECS, we might have a Teleport System that sets the x and y value of the Position Component so that it is in front of the player. But if the Teleport System runs before our regular Motion System, the Position Component will be overwritten by the regular Motion System and the teleportation behavior won’t occur.
This is known as a race condition, and it can be responsible for some very tricky bugs. This illustrates another issue with ECS, which is that in Systems that have similar behavior, we don’t really have a convenient way to share that behavior.
A new approach
Hyper ECS is a new pattern that eliminates these common problems with ECS. The core of the architecture is a 2-pass pattern for Systems. We now have multiple kinds of Systems, and a new construct called a Message. Entities and Components remain the same. I will now elaborate on the different kinds of Systems.
Detectors are responsible for reading the game world and producing Messages. A Message is fundamentally a kind of Component, but it is designed to be temporary and is discarded at the end of each frame.
As an example, let’s say we have Transform Components, which contains position and orientation data, and Velocity Components, which have an x and y component for linear motion. The Motion Detecter would simply read each Entity that has a Transform Component and a Velocity Component, and create a Motion Message, which contains a reference to the specific Transform Component, and the x and y velocity given by the Velocity Component.
In the first pass of each frame update, each Detecter runs in an arbitrary order. It does not matter which order the Detectors run in, because none of them are directly manipulating the state of the game, which would affect the other Detectors.
In the second pass, we have other types of Systems which read Messages and mutate the game world in response.
Modifiers consume a single Message prototype and manipulate game state in response. Component Modifiers, are, unsurprisingly, responsible for modifying Components. Continuing our above example, a Motion Modifier would see the Motion Message generated by the Motion Detecter, and update the data in the given Transform Component accordingly.
We also have Entity Modifiers. These are responsible for adding or removing components, or destroying Entities. In a similar vein, we have Spawners, which are responsible for creating new Entities from a given Message.
Finally there are Renderers. Renderers function similarly to Detectors, in that they iterate over Entities that contain a particular collection of components and then respond by drawing things.
At the end of the frame, all Messages are destroyed.
Components have strictly zero logic. Only Systems do, and each System should have precisely one responsibility. There should only be one Component Modifier per Component prototype. If we have two types of Component Modifiers for the same Component prototype, we introduce the possibility of race conditions. But we can easily avoid this because of the 2-pass structure.
Take our above teleportation example. We had a problem where two Systems were manipulating the same data, and the result could change based on the order of that manipulation.
In Hyper ECS, this is not a problem, since we can simply send multiple messages to the same Modifier from different Detectors. Instead of a generic Teleport System that manipulates a Position Component alongside the regular Motion System, in Hyper ECS we would know that we do not want multiple systems manipulating the same kinds of components, so we would have a Teleport Detecter create a Motion Message with the relative motion, and the Motion Modifier would process this message in the same way as it processes Motion Messages from the Motion System. This pattern allows us to easily reuse code for similar actions and avoid bugs.
encompass, a Hyper ECS implementation
You’re probably here because you are curious about using Hyper ECS in practice. These ideas would work in any language or game engine, but as a reference I have implemented a fully-featured Hyper ECS framework in Lua that I call encompass. It has many convenient features, like a built-in object pooling system and layer-based rendering.
encompass will work with any engine that has a scripting layer in Lua, but I have built a small reference game that runs on the LÖVE framework. I believe it should illustrate the kinds of patterns that you can use to build games using encompass.
If you are used to programming games in an object-oriented way, you will likely find the ECS pattern confusing at first. But once you learn to think in an ECS way, you will be shocked at how flexible and simple the pattern really is, and how quickly it lets you introduce new features. Give it a try!