What is an Entity Component System and Why should I care?

In Tutorials, UECS by EforenLeave a Comment

In this first video we just cover the over view of the site sorry no code here!

How ever we do cover everything you need to know about ECS to get started making your first Entity Component System.

Goals

  • Entity Component System Basics

    The basic high level overview of what an ECS really is at its core. Jump To Basics

  • Pitfalls of Object Oriented Approach vs ECS

    Explanation of where ECS fills some of the gaps and pitfalls of a pure Object Oriented approach. Jump To Pitfalls of OO

  • Entity Component System Details

    A detailed look at what an ECS is and how it benefits us. Jump To Details

  • Optional Expansions of the system

    A little about where I want to go with this library that may be a little outside what people normally think of as an ECS. Jump To Expansions

Hopefully the information I include here will provide you with enough to understand why I make the decisions that I do down the road!

Entity Component System Basics

The Entity Component System is primarily made up of Components (Data Only) and Entities (Component Containers). Which makes sense as it's and entity component system after all right? Their are a couple more elements that I will get into more later down the page.

The Entity

In our system the Entity is really almost nothing. Lets look at the Unity3D GameObject it has all sorts of stuff pre-backed into it already.

The Unity GameObject has

  • Behaviors
  • Transforms
  • a Parent
  • etc...

All of these things prebacked in make it a little slow and clunky sure they can be very convenient at times but at the same time this can become unruly and get in the way if you are not doing exactly what they intended you do with it. Thus all of this sorta backs you into a corner and impacts your performance. This bloat also locks you into having your logic and your graphics tied together very closely.

In our system we will not have any of that in our base empty entity. It will really just be a holder for the Component objects we create which will be Data only.

It may be a little more accurate to call the entity a list or something like that because it can only have one of any type of component and the entity ether has a component or it does not. This makes our code to implement the system much simpler.

The Component

As I stated earlier the Component is only data. This means no methods of any kind are allowed in a component!

You might be asking your self well then where is the logic how does the system do anything at all! This is all handled by systems and groups which I will cover later.

Going back to our comparison of how unity does things and how we will be doing them lets compare what a component is in our system vs what a component is in Unity3D.

First thing that sticks out immediately is that Unity3D EntityComponents have a mix of data and methods. In our system that is never the case we only ever have data in our components and all logic is left to systems. As a result of this exclusion our entities can be very simple and straight forward.

Take for example this snippet of sudo-code and see how simple a component can be in our system.

public class Position: Component
{
	public x {get; set;}
	public y {get; set;}
}

Notice that we do not have any methods or anything. It is just the data that is relevant to that component and nothing else!

One last thing about this you will notice that I used getters and setters in the sudo-code. Because we can do this we can make the code inherently thread safe and run the logic in an entirely separate thread from the render code by implementing thread safety in the getters and setters of components that both sides access with a simple lock something like

public class Position: Component
{
	private int _x = 0
	public int x {
		get{ lock(_x){ return _x; } }
		set{ lock(_x){ _x = value; } }
	}

	private int _y = 0
	public int y {
		get{ lock(_y){ return _y; } }
		set{ lock(_y){ _y = value; } }
	}
}

would do the trick and is still very simple to understand.

The Pool

The Pool is really just a box or container that we can dump all of the Entities into. 

For example on a server we might dump all the networking entities into one Pool and all the logic entities into another Pool.

As we will be implementing this system in C# we will be using pointers to the objects. There are some dangers to this if we do not deal with it correctly I will speak about this more when talking about Handles later.

The Systems

Systems basically watch the entity pool. There are several basic types of systems.

Reactive Systems

A Reactive System only runs when an event it is subscribed to happens on an object that its watching for. As the system is watching for a particular set of conditions then all of those events are simplified down to:

  • Entity Added
  • Entity Removed

This is really everything there is to it as that covers any situation. For example if the entity is getting destroyed then it is being removed from the conditions the system is watching. 

An example of how you can use these types of systems with a reactive system you could watch the pool and any time a new entity that has a position and graphic is added it could spin up an object in what ever engine your using that displays the graphic and then you pass it the position component (so it can update its position on the screen if it changes) and then add the ID of that graphical engine object to the entity as a Graphical ID Component.

Later when that entity is disposed of or the component on that entity is removed from the entity another system or the same system could go and use the ID you added before and delete the object in the graphical engine.

Update Systems

An update system is much simpler in a way it like the Reactive system has a set of conditions it is watching for but it does not care if things are added to or removed from its list as it simply runs every logic execution and does what ever its supposed to with its list. 

Lets tie our example of what you might use an update system for into the other examples we have used.

Say you have a system that runs on anything that has a position and a velocity. Every logic tick it can loop through everything in its list and take the velocity and multiply it by the delta time and then set the position.

Because the reactive system made the graphical engine use the position component as its location we don't have to do any complicated logic to update the position nor even call out to the render system at all. Our Velocity System does not care even if the entity has a graphical rendering or not all it cares about is if an entity has a position and a velocity. This lets our code be simpler and not have tons of bloat.

The Tags

While not all ECS explicitly implement tags as a part of the system we will be doing that in our system as that will allow us to avoid extra object collection or extra memory usage that is unnecessary.

Often times if an ECS does not implement Tags itself then you will see things like this in someones code.

public class Enemy: Component
{
}

It works and that's grate and all but every time you want to flag an object like that you have to create a new Enemy Component that gets assigned to the Entity this is not good it means you have to eather cache all of those somewhere so they don't get Garbage Collected and them manage all of them or you just throw them away and yes that could totally work and its not that much data but everything adds up particularly if your doing VR If your working with VR then you barely have time for doing anything at all!

Pitfalls of Object Oriented Approach vs ECS

To compare Object Oriented Design and the ECS pattern we need to break it down some more and give some examples of how one would create objects in pure Object Oriented Design

The OO Design Pattern

So lets use this OO Pattern we all know so well and make some game objects starting with a moster a merchent and a quest Giver.

First we would want a Game Object class that handles all the common stuff which might have some position stuff in it and some other things like a common render code. 

public abstract class GameObject
{
	public x {get; set;}
	public y {get; set;}
	public void render(){
		...
	}
}

This base class might be used for a door or a ball or anything else but we will be only using NPCs for this example so lets continue and make a base class for tall the NPCs.

public abstract class NPC: GameObject
{
	public string Name {get; set;}
	public int HealthCurrent {get; set;}
	public int HealthMax {get; set;}
	...

	public abstract void runAI(){
		...
	}
	public abstract void die(){
		...
	}
}

Now we have an NPC class that extends the GameObject class we made and we added a Name because all our NPCs will have a name. We also added a method runAI that we can overload and will run the AI for each NPC when its time for them to think.

The Monster
The main thing that is different about a monster is that it can attack things.
public class Monster: NPC
{
	...

	public override void runAI(){
		...
	}
	public override void die(){
		...
	}
	public void attack(NPC target){
		target.HealthCurrent--;
	}
}

Now we have a Monster class that extends the NPC class we made and can attack other NPCs.

The Merchant

Now we add a Merchant 

public class Merchent: NPC
{
	...

	public override void runAI(){
		...
	}
	public override void die(){
		...
	}
	public void sellItem(...){
		...
	}
}

Now we have a Merchent class that extends the NPC class we made and can sell things.

The Quest Giver

Now we add a Quest Giver

public class QuestGiver: NPC
{
	...

	public override void runAI(){
		...
	}
	public override void die(){
		...
	}
	public void questOffer(...){
		...
	}
	public void questReward(...){
		...
	}
}

Now we have a QuestGiver class that extends the NPC class we made and can sell things.

The Problem

This is all great to this point we have a nice little game starting.

The inheritance tree currently looks something like this.

  • GameObject {Position}
    • NPC {Health & Name & Have AI}
      • Moster {Attack any NPC}
      • Merchant {Can Sell Items}
      • QuestGiver {Can Offer Quests & Can Give Rewards}

The first problem we run into is wait a second we don't have a player where do we work that in and right now the monster attacks only NPC's because that is where the health is. So we need to move the health and stuff into GameObject or we need to make a new inheritence and make the NPC inherit from that changing our structure to something like this.

  • GameObject {Position}
    • LivingObject {Health & Name}
      • NPC {Have AI}
        • Moster {Attack any LivingObject}
        • Merchant {Can Sell Items}
        • QuestGiver {Can Offer Quests & Can Give Rewards}
      • Player {Get Direct Control from User}

That works but then say we want to add a Merchant who also can give quests where do we extend that from do we put it under Monster or under Merchant. As C# can't extend from 2 classes at the same time we have to duplicate the functionality or push the functions up the tree so lets do that.

  • GameObject {Position}
    • LivingObject {Health & Name}
      • NPC {Have AI & Can Sell Items & Can Offer Quests & Can Give Rewards}
        • Moster {Attack any LivingObject}
        • Merchant {Flag: Has Items for sale}
        • QuestGiver {Flag: Has Quests}
        • MerchantQuestGiver {Flags: Has Items for sale & Has Quests}
      • Player {Get Direct Control from User}

Now we got past that problem by moving the logic code out of the Merchant and QuestGiver classes into the NPC class and made a way to have sub objects flag themselfs as having items for sale or having quests. But it came at a cost we did have to add code to the NPC class that bloats it up and is only used in a couple of its sub objects. But we will continue and not worry about that right now its not that much more bloat after all right?

Oh shiz... the designer wants us to ad a monster that can also sell things?! ok... well we know how to solve this problem as we just did that we can take the code out of the monster and put it into the NPC also...

  • GameObject {Position}
    • LivingObject {Health & Name}
      • NPC {Have AI & Can Sell Items & Can Offer Quests & Can Give Rewards & Attack any LivingObject}
        • Moster {Can Attack}
        • Merchant {Flag: Has Items for sale}
        • QuestGiver {Flag: Has Quests}
        • Merchant Quest Giver {Flags: Has Items for sale & Has Quests}
        • MosterMerchant {Flags: Can Attack & Can sell stuff}
      • Player {Get Direct Control from User}

Are you noticing a pattern here? As we keep adding more to the game the things we add just keep floating down the tree into the NPC object this anti-pattern is know as the God object anti-pattern. The problem is its really really easy to introduce huge far reaching bugs when your working on the NPC object and holding in your mind all the places that a particular method is used because unreasonable and this is just a small game with 5 sub-types imaging if it was a full on game with 20 or 30 or 100 sub-types and different branches. This pattern can get out of hand fast and results in the lower level objects getting stale and locked in as no one wants to touch them for fear of breaking the entire game.

Fixing the problem using ECS

Lets start by building an NPC object with ECS because everything is just an NPC irreverent of what its purpose is we can just have our NPC be created by a function or method. So lets look at what that method might look like.

public Entity NPC(Entity entity = new Entity()) //Default to a new blank entity when we call the function
{
	// Now we need to think about what an NPC really is at its core
	entity.add<Position>(); // We need a position so we know where it is
	entity.add<Velocity>(); // We need to be able to move it around
	entity.add<Direction>(); // It needs to face some direction
	entity.add<RenderModel>(); // We need to be able to see it so we will add a model to it
	entity.add<Health>(); // because we need to be able to kill it
	return entity;
}

Now we have a function that we can call when ever we want to make an NPC. We can call it on its own to make a new entity and hand it over to it or if we have an entity object and we want it to become an NPC we can call the function against that entity to make it become and NPC. 

Because we would have systems in-place that handle each of those types of component we do not need to worry about their logic here and its much easier to keep a mental model of everything that's going on.

Lets look at what a Quest Giver might look like using this method.

You can see how easy it was for us to just tack the quest stuff onto the NPC stuff we already made before.

Lets look at what a Merchant might look like using this method.

// We do the same thing as before
public Entity Merchant(Entity entity = new Entity())
{
	NPC(entity); // This adds everything from NPC to the entity we are working on
	entity.add<Inventory>(); // The merchant needs to have the ability to hold things to sell
	entity.add<Trade>(); // Allow the trade system to trade things in the Inventory at default markup
	return entity;
}

public Entity QuestGiver(Entity entity = new Entity()) //Default to a new blank entity when we call the function
{
	// Now because of the way we are inplementing this we can just add the functionality from the NPC similar to how we would extend it in normal OO
	NPC(entity); // This adds everything from NPC to the entity we are working on
	entity.add<QuestOffer>(); // We want this NPC to offer a quest (this leaves out what the quest actually is but this is just an example to explain)
	entity.add<QuestReward>(); // We want the entity to be able to turn in the quest
	return entity;
}

Lets look at what a Monster might look like using this method.

// We do the same thing as before again
public Entity Monster(Entity entity = new Entity())
{
	NPC(entity); // This adds everything from NPC to the entity we are working on
	entity.add<Attack>(); // Give it the ability to attack
	entity.add<Inventory>(); // Give it an inventory
	entity.add<LootTable>(); // Use a Loot Table to fill inventory
	entity.addTag<Lootable>(); // When flags it for the loot system
	return entity;
}

The monster gets a LootTable Component that would somehow indicate what table to use in a real implementation. There would be a system setup that just looks for any game object with an Inventory and a LootTable if it has both then it would read the LootTable Component and generate an inventory stuff it into the inventory component on that entity and lastly remove the LootTable Component. BAM!!! Loot tables done simply and efficiently no  methods embedded in object no craziness just implemented across everything and not caring about any other data.

The same thing happens with the Lootable Tag there would be a system that watches for any entity that has an Position Component, Inventory Component and both the Dead and Lootable Tags. When an object has all of those then it would do something like spew the items in the inventory on the ground at the Position. Because it will only be watching for entities to be added to that group then the system will only trigger for that entity when it becomes in a state where it has all 4, for example when it had Position, Inventory, and Lootable but had Dead added.

The Problem Cases

Lets look at one of our problem cases from the Strict OO Pattern the Merchant with Quests what would that look like in our ECS and how much trouble would it cause?

public Entity MerchantQuestGiver(Entity entity = new Entity())
{
	// Making a merchant with quests is actually extreamly easy now.
	Merchant(entity); // This adds everything from Merchent to the entity we are working on
	QuestGiver(entity); // This adds everything from QuestGiver to the entity we are working on
	return entity;
}

It really is that simple. Because our system only allows one of each object we don't get any duplicate components or anything. All of the components that define a Merchant and a QuestGiver are on the entity together and thus the systems that deal with each will do their thing and make it be both.

Its the same with the Monster Merchant!

public Entity MonsterMerchant(Entity entity = new Entity())
{
	// Making a merchant with quests is actually extreamly easy now.
	Merchant(entity); // This adds everything from Merchent to the entity we are working on
	Monster(entity); // This adds everything from Monster to the entity we are working on
	return entity;
}

Now say we decide we want to make a monster who also gives a quest? How much trouble would we be making for our selfs?

Not much actually because we could do it just like the others!

public Entity MonsterQuestGiver(Entity entity = new Entity())
{
	// Making a merchant with quests is actually extreamly easy now.
	Monster(entity); // This adds everything from Monster to the entity we are working on
	QuestGiver(entity); // This adds everything from QuestGiver to the entity we are working on
	return entity;
}

As you can see its fairly trivial to add that new functionality we had not thought of before without having to refactor tons and tons of things.

Unity3D does allow you to achieve some of these things but not all of them. Also because of the way Unity handles things its a little bloated and slow no mater what. On the other hand if our logic code is designed from the ground up with this in mind we can keep down the bloat.

That covers some of the pitfalls of the Object Oriented Approach that the ECS we will be making will solve.

Entity Component System Details

The next thing I want to cover with you is exactly how we will be implementing our ECS system. Its time to jump into the deep end of the pool and take a look at the details of the system up close.

The Components

The most important thing is that we follow these rules.

  • Pure Data
  • No Methods
  • One Logical Concern
Pure Data with One Logical Concern

We want to not have anything except data in our components. 

But more then that we want to keep the data concerns pure. That is to say that we don't mix concerns for example say we want to deal with velocity we do not do something like this.

public sealed class BadVelocity: IComponent
{
	public int PosX {get; set;}
	public int PosY {get; set;}
	public int PosZ {get; set;}
	public int SpeedX {get; set;}
	public int SpeedY {get; set;}
	public int SpeedZ {get; set;}
}

This example component mixes the concerns. You can see that it is not just storing a speed but also a cached position.

If you see your self doing this break it down into separate concerns and make multiple components like this.

public sealed class Position: IComponent
{
	public int X {get; set;}
	public int Y {get; set;}
	public int Z {get; set;}
}

public sealed class Velocity: IComponent
{
	public int X {get; set;}
	public int Y {get; set;}
	public int Z {get; set;}
}
No Methods

This is one of the most important not following this rule is one of the quickest ways to degrade the performance of your program. It will also complicate things.

Take for example if we had a move method in this component. 

public sealed class Position: IComponent
{
	public int X {get; set;}
	public int Y {get; set;}
	public int Z {get; set;}
	public void move(int x, int y, int z){
		this.X += x;
		this.Y += y;
		this.Z += z;
	}
}

This confuses the concern and mutates the data in a weird way. 

For example where would we call move vs using the velocity component.

One Logical Consern

If we don't maintain one logical concern then this can lead to confusing the systems. For example if we have components that contain more then one concern it will be hard to reuse them without causing side effects.

If we have a player component that has position and inventory and a bunch of other stuff in there we have to remember every thing in it, unlike if we have them separate if we want to for example apply a velocity to the entity (player) then we can't just say any entity with a position and velocity does xyz every frame we have to worry about all sorts of other things.

Leave a Comment