First steps

Prologue

I’m watching Unity ECS a quite some time, and I’m really excited about it since I first heard of it. When I saw that it has some physics, there was not more excuse, I jumped right into it.

After a couple of days playing around, I must admit, an incredible work was made at Unity. It’s in preview stage (nevertheless there are people out there, who using it for production!), but it’s already have a lot of potential in it right now. And it’s getting better and better.

Well, I stop the bullshitting right now and let’s get started :)

My goals

I wanted a basic game, where I can move my player with physics, I can shoot projectiles, also with physics, and there are a lot of randomly spawning enemies, who come after me. I don’t want to deal with UI for now, they just come, and disappear. If a projectile hit them, they disappear too, and that’s all folks.

According to these, the first big task for me to understand how the ECS works. All I did know that what entities, components and systems are, and the whole thing is about performance by default, leveraging CPU cores. Later about the other ones.

Getting into it

I won’t write about what is DOTS, ECS, how to install packages in Unity, etc. There is a lot of descriptions and tutorials out there! (Also I assume that you are familiar with C#)

I won’t cover some classes that accidentally well documented or a lot of resources out there about them. So if you are confused about something, I leaved that something open, because if you interested in the topic, there is a big chance that you already know what it’s about or you can find the needed information at the first search hit. If not, then contact me, and I will rewrite/complete that section, if needed.

Also if I’m inaccurate about something, don’t hold yourself either!

I’ll move (or at least I’ll try) from little scope, so at the beginning I won’t write about essential stuff like World, etc (honestly I settle with the default one, but who knows). Little necessary pieces at a time.

I’m currently using 2019.2.2f1 and these packages

  • Burst 1.1.2
  • Collections 0.1.1 p
  • Entities 0.1.1 p
  • Hybrid Renderer 0.1.1 p
  • Jobs 0.1.1 p
  • Mathematics 1.1.0
  • Unity Physics 0.2.2 p

I wanted to use ECS for everything as I can, but also didn’t wanted to give up the Unity editor entirely, so mostly I used the hybrid approach, using the latest API as I could.

At the time of the writing of this post it’s a little bit tricky to find valid and/or up-to-date information. Unity’s documentations are great, if there are any. In this case not everything is documented, and anything can be chaged anytime, as same things already did (proxying component data from GameObject, vanishing of the Inject attribute, etc.). Most of the information can be gathered from Unity staff comments on various forums, nothing elsewhere, and some can be only from sample projects (as I writing this I found that they updated it, so I just updated some of my code) and from the packages source code.

Let's start

Performance by default, show thousands of objects at once on the screen with a decent framerate. That’s what they promise. This is exactly what I want, so let’s put on screen a lot of objects. For the first steps your best resource is the Unity’s GitHub repo, EntityComponentSystemSamples. I won’t go through every example, honestly I didn’t even take a look all of them. Usually I give myself a task and I go for it, and when I tired or don’t have a mood for anything else, start to read random articles in the topic or wandering around, maybe found some intresting/useful stuff.

As I wanted to use ECS as pure as it feels comfortable for me, I jumped over the SpawnFromMonoBehaviour, and go for SpawnFromEntity immediately.

The whole thing in a very-very tiny nutshell: ECS build up from three main part. There are entities, what are nothing more, than a unique id basically. As a Unity user, you can look at them as a downgraded GameObject. You can link ComponentDatas to entities, like GameObjects and components. And now we are arrived to the second one, the ComponentDatas, they are the data containers. No logic, no code, only data. Entity is a struct, and ComponentData must be a struct, it is important, because it’s a value type, what’s goes into the stack in the memory (“The stack is a “LIFO” (last in, first out) data structure, that is managed and optimized by the CPU quite closely” [That sentece grabbed from here]). The third one is the Systems. They are where the magic happens. More about them later, step by step.

I paste my code first, then I explan them:

ComponentData (link)


// Assets/Scripts/EnemySpawner/SpawnerData.cs

using Unity.Entities;

public struct SpawnerData : IComponentData
{
  public Entity Prefab;
  public int Count;
  public float Distance;
  public uint RandomSeed;
}

Our and Unity’s Systems are working with ComponentDatas. First we need to create one, what holds the data for a specific task. In this case, my spawner needs to know what to instantiate, how many of it, how far from the origo (maybe I should name it threshold, becasue I use it for a random maximum) and a Seed for the random generator (I could place it many other place, but now I’ll leave it here).

It must be a struct and must be inherited from IComponentData interface, thats why the Unity.Entities namespace is there.

Proxy (Entity creation and initialization)


// Assets/Scripts/EnemySpawner/SpawnerProxy.cs

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

public class SpawnerProxy : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
#pragma warning disable 0649
  [SerializeField] private GameObject _prefab;
  [SerializeField] private int _count;
  [SerializeField] private float _distance;
#pragma warning restore 0649

  public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
  {
    referencedPrefabs.Add(_prefab);
  }

  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    dstManager.AddComponentData(entity, new SpawnerData()
    {
      Prefab = conversionSystem.GetPrimaryEntity(_prefab),
      Count = _count,
      Distance = _distance,
      RandomSeed = (uint)Random.Range(0, int.MaxValue)
    });
  }
}

I didn’t wanted to leave behind the Unity editor’s comfort, so I didn’t go with pure ECS, so this proxy is responsible for making the entity from itself, and attaching the components. With this way, we can achieve this in the Editor like the “old” way. It’s a MonoBehaviour, so we can place it on a GameObject in the Hierarchy, and tweeking it’s serialized fields.

To make it work, we need some work beforehand. First of all, we need to attach the ConvertToEntity (it’s in the Entities package) script to the GameObject. It’s making the conversion of the GameObject to entity (with this way, it’s adding various components to the output Entity from it’s Transform what are not necessary, but in this case we will destroy the entity after it’s done, so be it).

After (or before) that, we need to attach the proxy class, what translates our data from the Editor to the entity. So we have that two interface, and two method associated with them. The DeclareReferencedPrefabs needed for the conversion system to know about the referenced prefabs ahead of time (AOT), and the Convert where we get the reference for the output entity, so we can attach to it anything we want (and can).

With the help of EntityManager we can add components to the newborn entity. Due to DeclareReferencedPrefabs the backend knows about our prefab (it’s converted to entity as well), so the conversionSystem can get the created entity from the referenced prefab (I didn’t find description about GetPrimaryEntity, but I think it’s returns the uppermost parent if there’s a hierarchy. The question is what if the referenced GameObject is a child of another. Don’t know, don’t get a thought when could a situation appear when it could be a case, so right now, it doesn’t matter).

Now we have our SpawnerData filled from our proxy class and attached to the entity, ready for usage.

Systems and SystemGroups

It’s time to talk about Systems. We organize our instruction sets with entities and their ComponentDatas in them. In the Default World which we use, there are a bunch of predefined Systems organized by SystemGroups. There are three main SystemGroup (if you start the game and take a look at the Window/Analysis/EntityDebugger, you can see them on the left side. Running order from top to bottom), what are running in the PlayerLoop, the InitializationSystemGroup for initalizing your data (runs on the very beginning of the PlayerLoop), the SimulationSystemGroup for most of the gamelogic (in the Default World here are calculated the entities’ positions, physics) and the PresentationSystemGroup for rendering and late update calculations.

Every default SystemGroup have two EntityCommandBufferSystems, these are sync points where the main thread waits for the worker threads to complete, so the Begin prefixed Systems will run before and the End prefixed runs after everything else in the SystemGroup. Creating, destroying entities, removing and adding ComponentDatas on entities only can be done within these to keep data consistency and preventing race conditions.


By default, if you don’t determine your System where to go, then it will be updated in the SimulationSystemGroup.

There is two System class, the ComponentSystem which is running on the main thread, and the JobComponentSystem which is running in a worker thread (this is what we are aiming for whenever we can. In the future Unity try to jobify everything what they can). More on that later.


// Assets/Scripts/EnemySpawner/SpawnerSystem.cs

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

[UpdateInGroup(typeof(InitializationSystemGroup))]
public class SpawnerSystem : JobComponentSystem
{
  private EndInitializationEntityCommandBufferSystem _entityCommandBufferSystem;

  protected override void OnCreate()
  {
    _entityCommandBufferSystem = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
  }

  private struct SpawnJob : IJobForEachWithEntity<SpawnerData>
  {
    public EntityCommandBuffer.Concurrent CommandBuffer;

    public void Execute(Entity entity, int index, [ReadOnly] ref SpawnerData spawner)
    {
      Random rand = new Random(spawner.RandomSeed);

      for (int i = 0; i < spawner.Count; i++)
      {
        Entity instance = CommandBuffer.Instantiate(index, spawner.Prefab);
        float3 position = math.transform(float4x4.identity, new float3(rand.NextFloat(-spawner.Distance, spawner.Distance), .0f, rand.NextFloat(-spawner.Distance, spawner.Distance)));

        CommandBuffer.SetComponent(index, instance, new Translation { Value = position });
      }

      CommandBuffer.DestroyEntity(index, entity);
    }
  }

  protected override JobHandle OnUpdate(JobHandle inputDeps)
  {
    JobHandle spawnJob = new SpawnJob()
    {
      CommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
    }.ScheduleSingle(this, inputDeps);

    _entityCommandBufferSystem.AddJobHandleForProducer(spawnJob);
    return spawnJob;
  }
}

When we inherit from the abstract JobComponentSystem (or ComponentSystem) we got some MonoBehaviour-like methods to override. The most important one is the OnUpdate, which will be called from the main thread every frame (by default, but it can be changed, explained here).

But first, we move this System to the InitializationSystemGroup with the [UpdateInGroup(typeof(InitializationSystemGroup))], so it runs before anything else, and created a field for the EndInitializationEntityCommandBufferSystem.  EntityCommandBuffersneeded for caching operations against entities, because they can’t be done in worker threads. When the PlayerLoop reach the end of the InitializationSystemGroup, the EndInitializationEntityCommandBufferSystem executes these changes. With Unity’s words (maybe for someone it’s give a better understanding, taken from here): “Command buffers allow you to perform any, potentially costly, calculations on a worker thread, while queuing up the actual insertions and deletions for later”.

We can get the CommandBufferSystem (or any other System) from the Default World with the World.GetOrCreateSystem method. Right now I won’t get deeper in this topic. We use the Defaul World along the way.

The SpawnJob struct will be executed on the worker thread. It’s inherited from the IJobForEachWithEntity generic interface. In the backend, all entities are gathered which has the ComponentData(s) listed in the interface declaration. In our case here, there are only one entity will exists. As I using the IJobForEach**WithEntity**, the Execute method's first two parameter will be the actual Entity's reference and the job's index, followed by the manually added types in the interface declaration. If we don't need a reference to the entity, the the IJobForEach is enough.

We then can write our entity creation code. We enqueue the instantiations, and at the end, destroy the EnemySpawner entity. This whole stuff will be performed in the EndInitializationEntityCommandBufferSystem.

You may already spotted that [ReadOnly] attribute before the SpawnerData parameter. This is optional, and also recommended. If you only read or write (yeah, there’s also a [WriteOnly] ) a parameter, then add either one of these. It helps the backend to perform better.

After we have our magic, it needs to be invoked. In the OnUpdate we create a JobHandle by creating it and call Schedule on it. In this case we use the ScheduleSingle, because we want it to run only once. If we using an EntityCommandBuffer, we have to call the AddJobHandleForProducer on it with passing the job, indicationg that it must await that jobs completion before enqueue the orders.

The result

Now we done something with Unity DOTS. If we create a GameObject, attach a ConvertToEntity and the SpawnerProxy script to it, fill up the serialized fields (add values to Prefab, Count and Distance. A simple cube, 50.000 and 1.000 for example) and we are ready to watch the result of the first steps on performance by default land. To the prefab we don’t need to attach the ConvertToEntity script, because the conversion system do it for us (remember our SpawnerProxy script).

If you still reading, then thank you for your time, I hope it helped.