Introduction to ECS in Unity

Are you ready for ECS in Unity? I’m excited about it and couldn’t wait for the final release! Yes, as of now it’s still in preview (preview.33 – 0.0.12 to be exact) so be aware that content of this post might differ in the future.

But this won’t stop us from getting into it! You and I, we are early adopters. ?

What is ECS?

ECS stands for Entity Component System, and it’s a new way of programming in Unity oriented on performance first. It’s very different from the current approach with MonoBehaviours, as objects are split into new pieces.

What are those pieces and how they compare to what we have now?

Entity – which could be compared to the GameObject. However, you won’t see Entities in the Scene Hierarchy.

Component – I would compare them to MonoBehaviours as they are attached to the Entities, but with one catch. Components in ECS hold just data, and there is no logic within them.

System – logic which is missing in Components is extracted to the Systems in ECS. They collect all components of a specific type or types, and then they do operations on the data from Components.

Such an approach to the programming gives Unity a chance to do more optimization only on the software level, but they are also getting an advantage on the hardware level. I don’t want to bore you with the details here, but you can learn more about it on the talk they gave at GDC 2019.

Prepare project for ECS

Because ECS is not a default option for project creation, we have to at it ourselves.

Step 1 – Create an empty project.

Step 2 – Open Window > Package Manager.

Open Package Manager

Step 3 – Show Preview Packages and Dependencies with Advanced options.

Enable Preview packages and Dependencies

Step 4 – Add ECS packages to your project, which are:

  • Entities
  • Hybrid Renderer

Just them?! YES! Package Manager is smart enough and will install all other required packages! ❤️

Great! Now we have everything ready to build our first ECS project!

Example

So what will be our example today? I would say that we need to build something easy to understand the basics of ECS better. So let’s spawn some spinny cubes! ?‍♂️

To start, we need to have something like GameManager to initialize our scene with Entities.

using UnityEngine;

/// <summary>
/// This class contains config for the Entities in this example.
/// </summary>
[System.Serializable]
public class EntitiesConfig
{
    [Header("Rendering")]
    [SerializeField]
    private Mesh mesh;
    public Mesh Mesh => mesh;

    [SerializeField]
    private Material material;
    public Material Material => material;

    [Header("Position")]
    [SerializeField]
    private Vector2 minEntityPosition;
    public Vector2 MinEntityPosition => minEntityPosition;

    [SerializeField]
    private Vector2 maxEntityPosition;
    public Vector2 MaxEntityPosition => maxEntityPosition;

    [Header("Spin")]
    [SerializeField]
    private float minSpin;
    public float MinSpin => minSpin;

    [SerializeField]
    private float maxSpin;
    public float MaxSpin => maxSpin;

    [Header("Scale")]
    [SerializeField]
    private Vector2 minEntityScale;
    public Vector2 MinEntityScale => minEntityScale;

    [SerializeField]
    private Vector2 maxEntityScale;
    public Vector2 MaxEntityScale => maxEntityScale;
}
using UnityEngine;
using Unity.Entities; // ECS
using Unity.Collections; // Native Collections for ECS
using Unity.Transforms; // Transforms - position, rotation, scale for ECS
using Unity.Rendering; // Rendering for ECS
using Unity.Mathematics; // Types for ECS.

/// <summary>
/// This class initialize ECS on the scene with some spinny cubes.
/// </summary>
public class GameManager : MonoBehaviour
{
    // Reference to the Entity Manager, needed for ECS.
    private EntityManager entityManager;

    [Header("Entities Config")]
    // Number of entities to spawn.
    [SerializeField]
    private int numberOfEntitiesToSpawn = 10;

    // Config for entities
    [SerializeField]
    private EntitiesConfig config = new EntitiesConfig();

    /// <summary>
    /// Unity method called on first frame.
    /// </summary>
    private void Start()
    {
        Initialize();
    }

    /// <summary>
    /// Initialize scene with spinning cubes.
    /// </summary>
    private void Initialize()
    {
        // Assigning reference to EntityManager.
        entityManager = World.Active.EntityManager;

        // Creating an empty array for cube entities.
        NativeArray<Entity> cubeEntities = new NativeArray<Entity>(numberOfEntitiesToSpawn, Allocator.Temp);

        // Creating archetype for cube. It's like a prefab.
        var cubeArchetype = entityManager.CreateArchetype(
                typeof(RenderMesh), // Rendering mesh
                typeof(LocalToWorld), // Needed for rendering
                typeof(Translation), // Transform position
                typeof(Rotation), // Transform rotation
                typeof(NonUniformScale), // Transform scale (version with X, Y and Z)
                typeof(Rotator) // Our custom component
            );


        // Creating entities of provided archetype to fill the list.
        entityManager.CreateEntity(cubeArchetype, cubeEntities);

        // Creating Random Number Generator.
        var rnd = new Unity.Mathematics.Random();
        rnd.InitState((uint)System.DateTime.UtcNow.Ticks);

        // Placing cubes around the scene.
        for (int i = 0; i < cubeEntities.Length; i++)
        {
            var cubeEntity = cubeEntities[i];

            // Setting rendering.
            entityManager.SetSharedComponentData(cubeEntity, new RenderMesh { mesh = config.Mesh, material = config.Material });

            // Setting position.
            entityManager.SetComponentData(cubeEntity, new Translation { Value = rnd.NextFloat3(new float3(config.MinEntityPosition.x, 0, config.MinEntityPosition.y), new float3(config.MaxEntityPosition.x, 0, config.MaxEntityPosition.y)) });
            // Setting scale.
            entityManager.SetComponentData(cubeEntity, new NonUniformScale { Value = rnd.NextFloat3(new float3(config.MinEntityScale.x, config.MinEntityScale.y, config.MinEntityScale.x), new float3(config.MaxEntityScale.x, config.MaxEntityScale.y, config.MaxEntityScale.x)) });

            // Setting our rotator component.
            entityManager.SetComponentData(cubeEntity, new Rotator { Rotation = rnd.NextFloat(0, 360), RotationSpeed = rnd.NextFloat(config.MinSpin, config.MaxSpin) });
        }

        // We have to dispose native collections ourselves!
        cubeEntities.Dispose();
    }
}

Wait a second… Didn’t you tell that we are not going to use MonoBehaviours? That’s true, but there is no other easy way to Initialize entities. Of course, I could dump the config and references to the Resource folder, but it’s easier to do it in Inspector. ?

You may also notice that I’m using Archetype to create Entities. Archetypes are equivalent to creating GameObjects with a set of components through code. You can also add or remove Components from a Entity by using EntityManager.

Next step will be to create custom Component, which in this case is Rotator component. Do you remember that Component only stores data, right?

using Unity.Entities;

/// <summary>
/// Rotator component.
/// Stores rotation data.
/// </summary>
public struct Rotator : IComponentData
{
    public float Rotation;
    public float RotationSpeed;
}

Now, as we have Component it’s time to create a System which will use data from our Rotator!

using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

/// <summary>
/// Rotator system.
/// This system grabs all Rotators and rotate them.
/// </summary>
public class RotatorSystem : ComponentSystem
{
    /// <summary>
    /// System Update, works like regular Update in MonoBehaviours.
    /// </summary>
    protected override void OnUpdate()
    {
        // Grabs all entities with Rotator and Rotation components​ attached to them.
        Entities.WithAll<Rotator>().ForEach((Entity entity, ref Rotator rotator, ref Rotation rotation) =>
        {
            // Updating rotation.
            rotator.Rotation += rotator.RotationSpeed * Time.deltaTime;

            // Assigning new rotation in Rotation Component (equivalent to Transform.rotation)
            rotation.Value = quaternion.RotateY(rotator.Rotation);
        });
    }
}

Great! We are done with the coding part! There is just one last thing left to do. We need to configure our GameManager!

You can do it like I do, or play around with it! ?

Game Manager setup

Now we are ready to click Play Button!

Spinning ECS Cubes!

Congratulations! We’ve made ECS project! Maybe it was a small step for us, but that’s a huge step for gamedevkind! ?

Do you like ECS? Have you done other projects with it? Let me know in the comment section below!

You can also sign up for the Newsletter to never miss upcoming ECS content!

And as always, you can download whole project from my public repository! ?

See you soon!

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x