Hey guys, the reason for this post is what devlog 104 was about: Unity is changing in a fundamental way, where they want to better support ECS.
This is going to be a long post, so strap in
What is ECS?
Well, before we get into that, i want to take a relevant sidestep first.
How to program things and their behavior?
I want to use a fuel truck and a bus as examples throughout this post.
So, you want fuel trucks. Now, you could just write each fuel truck from scratch in code, but that gets real tedious real fast. So how is it done?
In programming you have several ways of going about this.
The most used one (and the one normally taught at school) is Inheritance.
So let’s take our fuel truck and our bus. They both are cars.
so with inheritance you would make a template, let’s call it Car. (In programming this is called a Class)
This template would contain all logic needed for the car to move, find its way, etc.
Then we would make a template for Fuel Truck, and let it inherit from Car.
This would mean that all behavior we put in Car, is now also in Fuel Truck.
In Fuel Truck we add the behavior for holding and dispensing fuel.
The same goes for Bus. We add behavior for holding people, while it inherits all the behavior from Car.
This is nothing special, and as i said before, this is basically how 90% of programs are written.
Now there is another way. This is called Composition.
Let’s again look at our Fuel Truck and our Bus. But now we look at what makes them… them.
So, a Fuel Truck can move, as can a Bus. So we put all the information and logic we need for moving into
a separate Class. A Fuel Truck contains and dispenses Fuel. So we put all the information we need for that
in a separate Class as well. The Bus contains people, so the information for that in another class. This class can then be reused in other places where we want the same behavior.
In games as ACEO, composition actually has an advantage. What exactly? Well, suppose you want to simulate fuel usage. With inheritance you would code all that in the Car Class.
With composition you make it a separate Class that you add to your Fuel Truck and Bus.
Inheritance is still simpler, right? But what if we want to simulate fuel usage for aircraft.
We can’t inherit an aircraft from a car, their behavior is too different. And we can’t inherit from multiple Classes (There are languages where it is possible, but let’s not get into that, it’s ‘bad’ programming)
With composition however, it’s easy: We just add the Fuel Class we added to the Bus, to the aircraft as well.
This still has some drawbacks however, as the Fuel Class contains the logic and it might differ for aircraft and cars.
Now that we have all this out of the way, i can finally answer the question.
So what IS ECS?
It Stands for Entity Component Systems. Looking online you will find many, many, many, many different explanations
which all differ (mostly) in nuances. What you will find here is my interpretation of what an ECS should be (which corresponds with most of the articles you will find).
An entity is i.e. our beloved Fuel Truck. But it is nothing more than a collection of Components.
Everything, every behavior a Fuel Truck can have, is pulled out. (In a pure ECS system, an entity is not even a Class anymore, but nothing more than a unique ID).
A Component is a bag of data. No behavior, just data. So Current Fuel level, Fuel transfer rate, etc.
Components might also contain no data at all, in that case it acts more like a flag.
And a System is where the bahavior is at. The system is responsible for checking we still have fuel to give.
It will take all entities with Component X (Or Component X and Y) and process them.
So an expanded example (though still very much simplified):
For component we could have a FuelCarrier component. It would contain information on the current and maximum fuel level. Anything with this component has fuel.
Another component would be a FuelProvider. It would contain information on i.e. transfer rate. Anything with this component can give fuel to others.
And yet another would be FuelConsumer. It would contain information on how much fuel it consumes. Anything with this component will use up fuel. (It will most likely also contain a FuelCarrier component)
And the last one would be a FuelRequest component. It contains nothing.
The accompanying systems would be as follows:
- A FuelUsage system. It would take all entities with a FuelConsumer and FuelCarrier component and update those. So The FuelCarrier.CurrentFuel would get reduced by FuelConsumer.RateOfFlow.
- A RefuelLinker system. It would look at all entities with a FuelRequest component (i.e. an aircraft). If we added a FuelProvider component to the stand, it would link these together, otherwise it would request the overarching planning system to get a Movable FuelProvider to it
- A Refuel system. This would look at all FuelProvider and FuelCarrier components that are linked and actually transfer the fuel, increasing FuelCarrier.CurrentFuel with FuelProvider.RateOfFlow
And the entities:
Well you would have entity 75d32225-5a59-4115-a0c7-168deec743c8.
And it would ‘contain’ a FuelProvider component. That would be our stand.
And entity 8240d7bd-1ae9-41d7-b617-ebb5c563e4e4.
It would also ‘contain’ a FuelProvider component, but also a Movable component. And a Render component. That would be our Fuel Truck.
So do note that i.e. giving fuel does not know if fuel is being used, it only knows it needs to transfer fuel.
Also note that for refueling it doesn’t matter if you do it with a vehicle, have it built into the stand, or have a person carrying a jerrycan. If you have the right components, it will work.
Another, simplified example:
- A SeatingProvider component
- A SeatingConsumer component
- A SeatingRequest component
- A Seating system
- A passenger that is tired would get the SeatingRequest component.
- The Seating system would find the nearest SeatingProvider and advise the passenger to go there.
- The passenger would (through the pathfinding system) go to the SeatingProvider and sit down.
- The SeatingRequest component is removed, since it is no longer in need of help.
This would mean for modders, that we could add seating simply by adding the SeatingProvider component to something.
That would automatically mean it would be used by passengers.
Wait, but doesn’t Unity already do this?
Well… yes and no. Unity does have it sort of, but the way they implemented it, makes the components too heavy because they put the behavior in the components as well.
There are frameworks that try to mitigate that (https://github.com/sschmid/Entitas-CSharp), but it is a far cry from a true ECS system.
So what are the advantages?
- You ‘only’ have to code the systems. They will take care of the rest.
- Adding behavior to an entity is as simple as adding the component.
- Want to play a sound? just add a Sound Component
- Want to have something shown on screen? Just add a Render and a Position component
- By having all behavior in specific system you can tune it more easily.
- Not every system needs to run every tick
- You can put separate systems on separate threads
So this sounds all fine and dandy, but what are the drawbacks?
The major drawback is that as a developer you need to think differently. Thinking composition instead of inheritance is hard
For the rest it’s all up the the implementation details and remaining vigilant that you don’t make super components / systems that do it all.
- http://gamedevs.org/uploads/data-driven-game-object-system.pdf (The original talk by Scott Bilas, GasPoweredGames)