Entity component system or short ECS is one of the coolest, brightest, and most beautiful paradigm in programming I have ever met. It is data-driven and different from object-oriented programming most people know nowadays. It is mainly used in game development. The entity component system approach brought me back the feeling of the old days hacking games on a zx81 with a 4kbyte ram extension.
The "All Fucked Up" game relays heavily on entity component system and makes it super cool to develop.
But What is ECS
Looking at the name we can identify three parts.
- Entity
- Component
- System
Entity
An entity is just an identifier. Nothing more than that. In my case a number.
Component
An entity can have one or many components. A Component is just data, like the position or the model name or health. It has no functionality like walk, spell or any other activity. It might have getters. I use getters.
System
The System subscribes for entities with certain components and implements what should happen with added, updated or removed entities.
Basically, you subscribe for entities with a certain set of components, and in the update loop of your system, you get all the added, updated, and removed entities.
So let’s say you are interested in all entities which do have a position and a model component. Let’s further assume that there are already entities "1" and "2" with a position and a model component. In the current frame you add entity "3", update the position of entity "2" and remove entity "1". In the update loop, you get entity "3" as added, entity "2" as updated, and entity "1" as removed. Your system does now load and display the new entity "3" to the given position, move the entity "2" to the new position and removes entity "1" from the screen.
Note: The system in this example only sees the entities with a position and a model. If you remove either the position or the model from an entity, this entity will appear as removed in this example.
Let’s deep dive
I love code examples. My click experience was to use ECS for displaying the objects on the screen and move them around. I will provide some real code in a simplified form and explain the parts.
jMonkeyEngine and Zay-ES
I use jMonkeyEngine with Zay-ES written in Java. You might use another game engine and even another language, but that doesn’t really matter, as I’m sure you guys are all smart enough to make the link to your game engine as they don’t differ that much.
The System
First of all, I use app states as my Systems provided by the jMonkeEngine. All app states update method are called every frame in the order they are registered. This might look different for your game engine and maybe you even have to introduce a construct like this.
This app states here is the System to display the models on the screen.
public class ModelViewState extends BaseAppState {
private Node modelRoot;
private Main main;
private EntityData ed;
private ModelContainer modelContainer;
@Override
protected void initialize(Application app) {
...
}
@Override
protected void onEnable() {
...
}
@Override
protected void onDisable() {
...
}
@Override
public void update(float tpf) {
...
}
private class ModelContainer extends EntityContainer<ModelData> {
...
}
}
Lets add some flesh to this skeleton…
Initialization
In the initialization method I set up the model root, a pointer to the main app, and the entity data. The entity data is in my case an own app state, I like to organize the stuff in-app states, makes it clearer and the code gets less cluttered.
@Override
protected void initialize(Application app) {
modelRoot = new Node();
main = (Main) app;
ed = main.getStateManager().getState(EntityDataState.class).getEntityData();
}
Enable
The application state features an enable method, that might be different for your game engine. I use this to let the models appear by adding my model root node. It’s not really relevant but helps to understand the software a bit better. So no magic.
What is relevant is the model container we instantiate and start, this is an ECS container. The model container is described in all details further down.
@Override
protected void onEnable() {
main.getRootNode().attachChild(modelRoot);
modelContainer = new ModelContainer(ed);
modelContainer.start();
}
Disable
The application state also features a disable method. I use this to remove all the models by simply remove the model root from its parent. Very handy. It’s as well not really relevant to explain the ECS architecture.
The ECS model container is stopped and destroyed as well.
@Override
protected void onDisable() {
modelContainer.stop();
modelContainer = null;
modelRoot.removeFromParent();
}
Update
In the update method, we update the started model container. For this example this is it, nothing else is needed for the update loop. For more advanced stuff you need to iterate over all entities and for example calculate collision detection or other logic you may need to make entities interact with each other.
@Override
public void update(float tpf) {
modelContainer.update();
}
ECS Model Container
The ECS container from Zay-ES makes it really simple to handle added, removed and update entities on every update() call.
Data Class
I always define a data class, in this case, it is very simple and holds just the loaded spatial. That’s it.
private class ModelData {
Spatial model;
}
The ECS Container
The ECS container does subscribe for all entities which have a ModelType and a Position component. This is now the important stuff. We are interested in entities that have a model and a position component.
private class ModelContainer extends EntityContainer<ModelData> {
public ModelContainer(EntityData ed) {
super(ed, ModelType.class, Position.class);
}
Add Entities
The addObject method is called for all new entities which do have a ModelType and a Position component. For every entity, I create a model data object and fill in the loaded model. Only the model name must be stored in the component and not the spatial itself. This leads to very decoupled software, you can even have another system that also is interested in the model and the position but does for example display it in a minimap. To store the spatial in the component would be an anti-pattern. I also call the updateObject method to place the added model. Of course to make it visible we have to attach the model to the model root.
@Override
protected ModelData addObject(Entity entity) {
modelData = new ModelData();
String modelName = entity.get(ModelType.class).getModelName();
modelData.model = main.getAssetManager().loadModel(model);
updateObject(modelData, entity);
modelRoot.attachChild(modelData.model);
return modelData;
}
Update Entities
The updateObject method is called if either the ModelType or the Position of an entity has changed. In this example, I’m only interested in the position changes. A position holds the new location and the new rotation. So if your controller stated wants to move your hero around, it simply updates the position component of the hero entity.
@Override
protected void updateObject(ModelData modelData, Entity entity) {
Position position = entity.get(Position.class);
modelData.model.setLocalLocation(position.getLocation());
modelData.model.setLocalRotation(position.getFacing());
}
Remove Entities
Finally, we have the removeObject method, which is simply plain to remove the models, because either the Position and/or the ModelType component was removed by another system, for example, your dead system.
@Override
protected void removeObject(ModelData modelData, Entity entity) {
modelData.removeFromParent();
}
}
Place Models and Move it
The code we have so fare wants entities with ModelType and Position components, so we need to place that. For this, I have a level app state which places the initial stuff including my hero. The initial stuff is done in the app state enable method, where I load the level or build up the level, that depends. Ok real quick
public class ModelViewState extends BaseAppState {
private Main main;
private EntityData ed;
private EntityId myHero;
...
@Override
protected void onEnable() {
myHero = ed.createEntity();
ed.setComponents(myHero, new ModelType("MyFancyModel.j3o"), new Position(new Vector3f(0, 0, 0)));
}
@Override
public void update(float tpf) {
ed.setComponent(myHero, newPosition(newVector3f(tpf, 0, 0)));
}
...
}
Yes that simple, this moves the object to the right in my case with a speed of 1 unit per second. And java is pretty good with very short-living objects and never was an issue for me. I have up to 2500 or more frames per second if I do not run it in sync mode. And even with a lot, and I mean a lot of entities (up to 10’000 objects) it is not an issue…
And right the position component is a new instance everytime you change it, java seems to be smart and efficient enought to handle short living objects.
The wow moment
Just to displaying and moving the stuff around wasn’t really my wow moment, it was just the now-I-understand moment. It’s even like why should I use ECS for this? What is the benefit? The real wow moment was the decay system for me. It is a pattern you probably have in every game.
What is a decay system exactly? The decay system is interested in all entities with the Decay component, which basically says how long this entity shall live. Very handy to clean stuff up like bullets, which are probably very short living in almost all games. And you have tons of them and as well different kinds of them. But as well any entity which is dying or has to be removed after a while. You even can display the decay state with another system that is also interested in the decay component. The only thing you have to do is to attach the Decay component to the entity which should vanish after a while. No inheritance, no direct component linking, just attach the Decay component.
You can also think of a health system to give something health and it doesn’t matter if that is a dog, a tree, a house, a stone, or your hero. And you can add it anytime. Maybe you started with stones without health, but then your game designer shows up and wants that the stones have health and the hero can destruct them with a tool. No big deal it is a one-line addition by just adding Health component to the stone entity and it has health. If you have already a health visual system in place the health of the stones will be displayed like any other thing with health. And that’s really cool.
If you made it this far
Put your questions and suggestions in the comments below. Thanks for reading.