Entity Component System in Depth

If you are interested to know what the Entity Component System is and how it technically works then this is the article for you.

A couple of days ago I started a poll on Twitter asking you guys what you are most interested in. I knew I was going to post this pretty soon anyways as I had been starting to work on it a while back – I just didn’t get around to write a proper post about it yet.

Introduction:

With the introduction of the ECS, Unity is basically changing a large chunk of their approach on Software Development. This chapter is representing the end of OOP as we knew it. With the Entity Component System around the corner, a lot of the practices we used to follow in working with Unity will have to be modified to accommodate the new system, which will probably need some getting used to for a lot of people but it is going to do a lot for gameĀ performance.

Now, Object-Oriented Programming is a great paradigm – it is very approachable and easy to understand, especially for beginners. The biggest pro of OOP is probably its accessibility, as in how easy it is to create a class with little to no knowledge and maintain the produced code. However, the OOP approach brings some severe downsides to overall performance due to the fact that it can be unbearably hard to avoid duplications which will lead to a certain degree of overhead being produced not to mention that while it is easy, it is also really dependentĀ on references.

Object-Oriented programming is based around the concept of certain objects, whose type is defined by the instance of the class they are, interacting with each other to build the program. Objects can contain data in the form of attributes and methods.

The Entity Component System approach differs in a way where data and behavior/methods are clearly separated, which results in high memory-efficiency and therefore higher performance.

Now, as far as Unity goes, the ECS is still in his baby shoes and has a lot of growing up left to do but you can start using it right now and I think you should.Ā  I’ll be explaining the general ECS approach as well as Hybrid and Pure ECS and I will talk about how to implement it, what the syntax is like and generally where to start.

The ECS Approach:

For the ECS we will speak of Entities rather than GameObjects. Now, an Entity doesn’t seem that different from a GameObject based on its mere description because you could look at it as a Container for your components. However, once you’ll take a closer look you will realize that there is a big difference and that is that an Entity is technically just a handle for a selection of Components.

Are Components the same between OOP and ECS?No, they aren’t. Before the ECS we used to think of a MonoBehaviour attached to one of our game objects as a component. MonoBehaviours contained data and behavior – data from all our variables and behavior from our functions we used to define and call from our game object.

The ECS is different as neither Entities nor Components have any kind of behavioral logic – all they hold is data.

Every logic is contained in Managers/Systems, which will grab a group of entities and execute the requested behavior considering the data each of theĀ grouped Entities is holding. Which means that now, not every Entity will handle their own behavior but rather all of them are handled in one spot.

To completely grasp why the ECS approach is so much faster than the old OOP approach you have to have some knowledge on memory management. So, with the old approach,Ā your data wasn’t organized but rather scattered all over memory, this was due to automatic memory management.

Automatic Memory Management:

In Managed Languages, such as C#, the process of memory allocation and deallocation is automated through the Garbage Collector. Upon start the Mono platform will request a given chunk of memory from the operating system and use it to generate a heap memory space that our code can use. This heap space starts off small but will grow as new more memory is needed by our code. The heap can also decrease in size by releasing formerly claimed memory back to the operating system if it is no longer needed this is what the Garbage Collector does.

For following allocation, if the size fits, the GC will use the remaining “gaps” that formerly heldĀ  data that since has been deallocated.

Moving our data from memory to the cache, therefore, took more effort as references have to be found first.

The ECS is much more optimized when it comes to memory management as data is stored organized by type rather than by the time of allocation. Imagine a store where they put products on the shelves by the order they are coming in versus placing products in a categorized manner.

Another big factor in why the ECS is so much more performant is that, due to the fact that our data is clearly separated, we are caching only relevant data. What do I mean by that? Well, when working with OOP, whenever you are trying to access a GameObject you always cache all of its properties even though you might just need one specific property. And these things have an impact on performance as caching these types does cross the native-managed bridge and, guess what, that results in garbage.

Hybrid ECS:

Now, expecting to program a game that is purely using ECS is unrealistic as of right now, as some of the Unity features aren’t adjusted to it yet – but that isn’t going to hold us back.

Hybrid ECS allows us to incorporate ECS logic into our existing projects and benefit from their impact without having to sacrifice the use of features that aren’t adjusted yet.

It works with helper classes, like Game Object Entity, that convert our GameObjects into Entities and their attached MonoBehaviours into components.

We are writing regular C# scripts that derive from MonoBehaviour that only contain data but no behavior, we then attach these MonoBehaviours to our GameObject that also has the Game Object Entity attached to it.

Our behavior has to be placed in a Manager/System class – this class has to derive from ComponentSystem. We do not need to attach this class to any kind of game object in our scene as Unity will detect it’s existence and execute it automatically.

In our System class, we are going to define our Entity as a struct by simply defining the attached components within. Instead of an Update function, we now have an OnUpdate in which we are getting all of our entities and we iterate through them executing the behavior.

So let’s see what that looks like in Code!

ECS Solar System (Hybrid):

Now, that you have learned what the entity component system is, it’s time to look at a practical example. As mentioned before switching to ECS will be hard for some people as you have to change your approach to building a program. I hope this segment will make it easier for you.

As you might have seen on Twitter we are going to program a little Universe, we are going to use Hybrid ECS as this is what is “really” accessible to you right now and I hope after this you’ll see that ECS isn’t scary and you’ll feel motivated to go and give it a try.

Preparing your project for ECS:

I am currently on Unity 2018.2.0.2 with this (just as an FYI) – first things first you’ll need the entities package from the package manager. Navigate to WIndow >> PackageManager. Now you’ll see a bunch of options – we are looking for Entities.
After you’ve installed this, your Console will look like something went terribly wrong, don’t panic – this is normal! We also have to change the scripting runtime version from .Net 3.5 to .Net 4.x.
To do that you’ll have to go to your Build Settings >> Player Settings.
That’s everything as far as preparation goes, so we are ready to get into it!

Prefabs:

As I mentioned before, Hybrid ECS has helper classes that can help us turn regular MonoBehaviours in Components, let’s take a closer look.Ā 
I am starting out by creating a bunch of Prefabs for my Planets, Stars, Ellipses, and Moons.

As you can see in this screenshot, this looks like a regular Prefab, not much interesting is happening – We have a Transform, Mesh Filter, Mesh Renderer and Mesh Collider.

Now the next Component you see attached to this Prefab is the Game Object Entity. This isn’t a custom script I wrote but rather the aforementioned helper class that is responsible for turning our regular GameObject into an Entity,

You’ll also see the PlanetComonent, this is probably the most complicated Script I have ever written – I hope you are prepared to see this:

using UnityEngine;

public class PlanetComponent : MonoBehaviour
{

    public float rotationSpeed;
    public float orbitDuration;
    public OrbitalEllipse orbit;
}

 

As I mentioned in before, a Components Job is to hold data, so there is nothing else needed in this function. So let me post all of the Components we are going to need for this project here as there isn’t much explaining to do with these.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoonComponent : MonoBehaviour {

    public float movementSpeed;
    public GameObject parentPlanet;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OrbitalElipseComponent : MonoBehaviour {

    public float xExtent;
    public float yExtent;
    public GameObject parent;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StarComponent : MonoBehaviour {

    [Range(0f, 100f)] public float twinkleFrequency;
}

 

That’s all our Components and Prefabs done. Easy, isn’t it?

HybridECSSolarSystem:

Before we took care of all of our data by creating Components, if you’ve read the explanation above closely you’ll know that we now need to tackle the behavior. To do so we are going to add a new C# Script and call that HybridECSSolarSystem – we don’t attach this Script to anything, in fact, it’s not even a MonoBehaviour, it rather has to derive from ComponentSystem.

The Unity Engine will detect this script internally and execute it. Remeber to place Unity.Entities in your using statements though. I am defining a bunch of structs for Stars, Planets and Moons at the beginning of this class, as I mentioned an Entity is a handle of a group of Components – so to put this first struct in simple words, a Entity of type Stars is every entity that has the Component StarComponent and a MeshRenderer attached to it.

You might also notice that instead of the Update() function we used to know, we now have a protected override void OnUpdate() – every class that derives from ComponentSystem needs to have it and this is also where our entities behavior will be executed.

If you take a look at the first foreach loop in OnUpdate(),Ā  you’ll see that I am getting all entities of the type Stars that I defined earlier and I then loop through each of them. I am getting randomness by checking for a Random.RangeĀ that is dependent on the entities starComponent.twinkleFrequency.

using UnityEngine;
using Unity.Entities;

public class HybridECSSolarSystem : ComponentSystem
{
    struct Stars
    {
        public StarComponent starComponent;
        public MeshRenderer renderer;
    }

    struct Planets
    {
        public PlanetComponent planetComponent;
        public Transform transform;
    }

    struct Moons
    {
        public Transform transform;
        public MoonComponent moonComponent;
    }

    protected override void OnUpdate()
    {
        foreach (var starEntity in GetEntities<Stars>())
        {
            int timeAsInt = (int)Time.time;
            if(Random.Range(1f, 100f) < starEntity.starComponent.twinkleFrequency)
            {
                starEntity.renderer.enabled = timeAsInt % 2 == 0;
            }
        }


        foreach (var planetEntity in GetEntities<Planets>())
        {
            
            planetEntity.transform.Rotate(Vector3.up * Time.deltaTime * planetEntity.planetComponent.rotationSpeed, Space.Self);
            
            planetEntity.transform.position = planetEntity.planetComponent.orbit.Evaluate(Time.time / planetEntity.planetComponent.orbitDuration);
        }

        foreach (var moonEntity in GetEntities<Moons>())
        {
            Vector3 parentPos = moonEntity.moonComponent.parentPlanet.transform.position;

            Vector3 desiredPos = (moonEntity.transform.position - parentPos).normalized * 5f + parentPos;

            moonEntity.transform.position = Vector3.MoveTowards(moonEntity.transform.position, desiredPos, moonEntity.moonComponent.movementSpeed);
            moonEntity.transform.RotateAround(moonEntity.moonComponent.parentPlanet.transform.position, Vector3.up, moonEntity.moonComponent.movementSpeed);

        }

    }
    
}

 

Instead of having the Update function we used to have we now execute an Update in our System class and this is pretty much everything that is different when using Hybrid ECS. By now you’ll hopefully understand why I can only highly suggest giving it a try.

Even though this is everything we needed to do to turn our GameObjects into Entities, there is still a class we need to Instantiate our Galaxy, so lets just quickly do that.

HybridECSInstantiator:

This class is really straightforward and shouldn’t need much explanation. We are basically setting up a bunch of variables to create a GameObject in our Scene that is going to Instantiate our entire solar system. We are imagining our Universe as a Sphere by using onUnitSphere and our UniverseRadius for the object placement.

Our Ellipses are created by simply calculating an Ellipse and then drawing it using a LineRenderer Component.
We use the Ellipse.Evaluate() function for the planet movement in our HybridECSSolarSystem.cs class.
(Thanks to Sebastian Lague for making that part much nicer <3)

In general, for each of our objects – let it be a Star or a Planet we are simply instantiating them, setting their matching Component and placing them.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HybridECSInstatiator : MonoBehaviour
{
    [Header("General Settings:")]
    [SerializeField] float universeRadius;

    [Header("Sun:")]
    [SerializeField] GameObject sunPrefab;
    [SerializeField] Vector3 sunPosition;

    [Header("Moon:")]
    [SerializeField] GameObject moonPrefab;
    [SerializeField] float minMoonMovementSpeed;
    [SerializeField] float maxMoonMovementSpeed;

    [Header("Stars:")]
    [SerializeField] GameObject starPrefab;
    [SerializeField] float minStarsize;
    [SerializeField] float maxStarsize;
    [SerializeField] int starsAmount;
    [SerializeField] [Range(0, 100)] float minTwinkleFrequency;
    [SerializeField] [Range(0, 100)] float maxTwinkleFrequency;

    [Header("Orbital Elipses:")]
    [SerializeField] int elipseSegments;
    [SerializeField] float elipseWidth;
    [SerializeField] GameObject orbitalElipsePrefab;

    [Header("Planets:")]
    [SerializeField] List<Planet> planets = new List<Planet>();

    static HybridECSInstatiator instance;
    public static HybridECSInstatiator Instance { get { return instance; } }

    GameObject sun;

    void Awake()
    {
        instance = this;
        PlaceSun();
        PlaceStars();
        PlacePlanets();
    }

    #region Sun
    void PlaceSun()
    {
        sun = Instantiate(sunPrefab, sunPosition, Quaternion.identity);
        GameObject sunParent = new GameObject();

        sunParent.name = "Sun";

        sun.transform.parent = sunParent.transform;
    }
    #endregion

    #region Stars
    void PlaceStars()
    {
        GameObject starParent = new GameObject();
        starParent.name = "Stars";

        for (int i = 0; i < starsAmount; i++)
        {
            GameObject currentStar = Instantiate(starPrefab);
            currentStar.transform.parent = starParent.transform;

            currentStar.GetComponent<StarComponent>().twinkleFrequency = Random.Range(minTwinkleFrequency, maxTwinkleFrequency);

            float randomStarScale = Random.Range(minStarsize, maxStarsize);
            currentStar.transform.localScale = new Vector3(randomStarScale, randomStarScale, randomStarScale);
            currentStar.transform.position = Random.onUnitSphere * universeRadius;
            currentStar.SetActive(true);
        }
    }
    #endregion

    #region OrbitalElipses
    void DrawOrbitalElipse(LineRenderer line, OrbitalEllipse ellipse)
    {
        Vector3[] drawPoints = new Vector3[elipseSegments + 1];

        for (int i = 0; i < elipseSegments; i++)
        {
            drawPoints[i] = ellipse.Evaluate(i / (elipseSegments - 1f));
        }
        drawPoints[elipseSegments] = drawPoints[0];

        line.useWorldSpace = false;
        line.positionCount = elipseSegments + 1;
        line.startWidth = elipseWidth;
        line.SetPositions(drawPoints);
    }

    #endregion

    #region Planets
    void PlacePlanets()
    {
        GameObject planetParent = new GameObject();
        planetParent.name = "Planets";

        for (int i = 0; i < planets.Count; i++)
        {
            GameObject currentPlanet = Instantiate(planets[i].planetPrefab);
            currentPlanet.transform.parent = planetParent.transform;

            currentPlanet.GetComponent<PlanetComponent>().rotationSpeed = planets[i].rotationSpeed;
            currentPlanet.GetComponent<PlanetComponent>().orbitDuration = planets[i].orbitDuration;
            currentPlanet.GetComponent<PlanetComponent>().orbit = planets[i].orbit;

            GameObject currentElipse = Instantiate(orbitalElipsePrefab, sunPosition, Quaternion.identity);
            currentElipse.transform.parent = sun.transform;
            DrawOrbitalElipse(currentElipse.GetComponent<LineRenderer>(), planets[i].orbit);

            if(planets[i].hasMoon)
            {
                GenerateMoon(currentPlanet);
            }
        }
    }
    #endregion

    #region Moons
    void GenerateMoon(GameObject planet)
    {
        GameObject moonParent = new GameObject();
        moonParent.name = "Moons";

        GameObject currentMoon = Instantiate(moonPrefab);
        currentMoon.transform.parent = moonParent.transform;

        currentMoon.GetComponent<MoonComponent>().movementSpeed = Random.Range(minMoonMovementSpeed, maxMoonMovementSpeed);
        currentMoon.GetComponent<MoonComponent>().parentPlanet = planet;
    }
    #endregion
}

[System.Serializable]
public class OrbitalEllipse
{
    public float xExtent;
    public float yExtent;
    public float tilt;

    public Vector3 Evaluate(float _t)
    {
        Vector3 up = new Vector3(0, Mathf.Cos(tilt * Mathf.Deg2Rad), -Mathf.Sin(tilt * Mathf.Deg2Rad));

        float angle = Mathf.Deg2Rad * 360f * _t;

        float x = Mathf.Sin(angle) * xExtent;
        float y = Mathf.Cos(angle) * yExtent;

        return up * y + Vector3.right * x;
    }
}
[System.Serializable]
public class Planet
{
    public GameObject planetPrefab;
    public OrbitalEllipse orbit;

    public bool hasMoon;

    [Header("Movement Settings:")]
    public float rotationSpeed;
    public float orbitDuration;
}

 

Get the Repository here!

Liked it? Take a second to support Kristin on Patreon!

2 comments

Leave a Reply

Your email address will not be published. Required fields are marked *