Happy New Year. Here is the second part in the devlog for ‘THRUST//DOLL’.

Last time I described some of the design decisions; this time the focus will be on solving them.

How to make a thing with DOTS

Here’s the general idea of ECS. Everything that DOTS is concerned about is an Entity. Entities are essentially indices into arrays of components. A component is basically just a struct with a small amount of data in it.

In the old object-oriented way, each object would have a bunch of pointers to its behaviour. This is slow and not friendly to the CPU cache. In DOTS, therefore, you have things called ‘systems’, which iterate over all the entities that match some query.

Unity’s ECS has been in development for a long time and hopes to cover a lot of use cases. After quite a bit of research, here is the modern way to do things.

Creating entities and components

Writing an unmanaged Component is easy. (I’m going to brush over the difference between ‘managed’ and ‘unmanaged’ for now—I generally have taken the rule of ‘only ever unmanaged’ but I may find some compelling reason to use a managed one at some point.) You write a struct that implements IComponentData. You can only use certain data types in it, which can be a bit of a pain when e.g. enums get involved.

To make a Unity scene with GameObjects into an Entity, you need to create an Authoring, which is just a normal MonoBehaviour which can have some data, and a Baker with the appropriate type, e.g. if I have UpgradeAuthoring then the baker will be Baker<UpgradeAuthoring>.

Unity gathers up all your Bakers, then there are a series of systems which, for each GameObject with the relevant Authoring, generates an Entity and do whatever you tell it to do in the Bake function—typically add some components to it using AddComponent<SomeComponent>().

For example:

using Unity.Entities;

public class UIToggleAuthoring : UnityEngine.MonoBehaviour
{
}

class UIToggleBaker : Baker<UIToggleAuthoring>
{
    public override void Bake(UIToggleAuthoring authoring)
    {
        AddComponent<UIToggleRingThickness>();
        AddComponent<UIToggleCentreOpacity>();
        AddComponent<UIToggleState>();
    }
}

This will create an entity with the three components listed. That’s the easy part.

Creating multiple entities per GameObject

The documentation makes very explicit that it is not necessary to have a 1:1 mapping between GameObjects and Entities, but it is frustratingly vague on exactly how best to make more entities per GameObject. As best as I have been able to discern, the correct approach is to

  1. add a GameObject field to the Authoring MonoBehaviour, and store a prefab GameObject
  2. use the GetEntity function in the Baker with reference to this GameObject
  3. Unity will generate a prefab Entity, which is just a normal entity with the Prefab component, disabling it from queries.
  4. write a System which uses this prefab entity to generate new entities using the Instantiate function in an Entity Command Buffer.

In my use case, I wanted to create a UI-element Entity corresponding to every existing Entity with an Upgrade tag component. More on that later.

Writing Systems

Once you have some Entities, you need to do stuff with them. This is where things get a bit fiddly, since (this being Unity) there are multiple ways to iterate over Entities according to some query. Fortunately the tank tutorial covered most of them.

The basic way is to use ‘idiomatic for each’, which goes like this:

foreach (var (aspect, component) in SystemAPI.Query<Aspect, Component>())
{
    // do something
}

You create a SystemAPI.Query to get all the entities with a specific Aspect or Component, and then you can further chain e.g. WithAll or WithNone to filter that query. This triggers some clever code generation which ensures your query will be cached, so you don’t need to worry about caching queries yourself.

The second way is Entities.WithAll<Component>.ForEach(/* lambda */) where the lambda function takes a series of components and aspects. This is apparently the old way, mostly deprecated, so I won’t talk too much about it.

The third, fanciest way is to use the job system. This has two parts. You need to create a struct for the job, which can be used in multiple Systems to minimise redundancy. This takes the from of a partial struct implementing IJobEntity. You can give this fields for passing stuff like Entity Command Buffers.

The IJobEntity has an Execute method, and you can declare its arguments with in for a read-only argument, or ref for a read-write argument. For example, from the tank tutorial:

[BurstCompile]
partial struct TurretShoot : IJobEntity
{
    [ReadOnly] public ComponentLookup<LocalToWorldTransform> WorldTransformLookup;
    public EntityCommandBuffer ECB;

    void Execute(in TurretAspect turret)
    {
        var instance = ECB.Instantiate(turret.CannonBallPrefab);
        var spawnLocalToWorld = WorldTransformLookup[turret.CannonBallSpawn];
        var cannonBallTransform = LocalTransform.FromPosition(spawnLocalToWorld.Position);

        cannonBallTransform.Scale = WorldTransformLookup[turret.CannonBallPrefab].Scale;
        ECB.SetComponent(instance, cannonBallTransform);
        ECB.SetComponent(instance, new CannonBall
        {
            Speed = spawnLocalToWorld.Forward() * 20.0f
        });
    }
}

You can filter components using the arguments, and if you want to filter harder, you can put a declarative tag with WithAll etc. e.g.

[WithAll(typeof(Shooting))]
[BurstCompile]
partial struct TurretShoot : IJobEntity
{
    //stuff
}

For mysterious reasons this requires you to put a typeof in there.

I’ll talk more about that specific code in a minute. Once you have a IJobEntity, you’d instantiate it with the new keyword. If you want to schedule it in a single thread without blocking the main thread, you just call Schedule(). For a parallel job, you need to put a first argument as [ChunkIndexInQuery] int ChunkIndex and then you can schedule it with ScheduleParallel(). You also need to use parallel writers if you’re adding or removing entities.

In general, it appears that using methods in SystemAPI is preferred to non-SystemAPI code. This is because the SystemAPI is actually a code generator that gets replaced with the boilerplate stuff. Cool but a bit confusing to learn. There’s also some nuances to pay attention to.

Adding and removing Entities

There are certain sync points in every frame when all the systems have to wrap up before the next set of systems fire. This is where EntityCommandBuffers live, reeling off their buffered commands, and this is where you want to do anything which changes the number of entities. You can get an ECB with the following pair of commands, which need to be run in the OnUpdate, taking its ref SystemState argument:

var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

Swap out BeginSimulationEntityCommandBufferSystem (bit of a mouthful) for the relevant command buffer for the step you want. The steps can be seen by opening the Entities -> Systems subwindow inside Unity.

If your job is running in parallel, you need an EntityCommandBuffer.ParallelWriter. This can be received from a normal ECB with the AsParallelWriter() function. This can otherwise be used just like a normal ECB.

Looking up stored entities

Sometimes you will store an entity ID inside another entity. For example, a prefab you intend to use to spawn entities, or a parent or child.

You can look up components by entity using SystemAPI.GetComponent. This has a certain amount of overhead, so it shouldn’t be used in iterations, but not as much overhead as building a ComponentLookup, so it should be used outside of iterations. (Just to keep you on your toes.)

I believe, although I can’t verify, that this is equivalent to getting the EntityManager and running EntityManager.GetComponentData instead.

OK, then, so if you have to iterate, you can use a ComponentLookup that gets passed to the job instead. This basically does all the overhead once before the loop.

A ComponentLookup sticks around, it needs to be initialised in the System, and essentially presents you with an array you can index with a stored reference to an entity. So if you store a reference to an entity prefab on an entity, you need one of these guys.

The old way (non-SystemAPI)

The old way was that you initialise it like this inside a System, and you decide here whether it is read only (true for RO, false for RW):

EntityQuery m_WorldTransformLookup;

[BurstCompile]
public void OnCreate(ref SystemState state)
{
    m_WorldTransformLookup = state.GetComponentLookup<WorldTransform>(true);
}

You also need to update it before doing any iterations over entities:

m_WorldTransformLookup.Update(ref state);

The new way is you just use SystemAPI.GetComponentLookup<Component> when you need a lookup, and the code generation will take care of declaring and updating your lookup.

Once you’ve made a lookup, you can use it like an array with entities as the index, e.g. if turret.CannonBallSpawn holds an entity:

var spawnLocalToWorld = WorldTransformLookup[turret.CannonBallSpawn];

Not entirely sure what this returns if the component doesn’t exist. Maybe you crash? There’s a way to get a ‘safe reference’ using the GetRefRO/GetRefRW methods, which throw exceptions if it doesn’t exist. You can also test this.

Naturally you need to pass m_WorldTransformLookup (or whatever) into your job to be able to use it, using a field.

Aspects

If you find yourself often using the same set of references in queries, you can bundle them together in an Aspect that implements IAspect. You can write custom getters and setters on these aspects to make your code cleaner. This also has a potential performance advantage, in that Unity builds up and caches sets of aspects—but it must rebuild the aspects whenever components change.

Here’s a job that uses an Aspect:

[BurstCompile]
partial struct UIToggleJob : IJobEntity
{
    public float DeltaTime;

    void Execute([ChunkIndexInQuery] int chunkIndex, ref UIToggleAspect uiToggle)
    {
        uiToggle.Opacity = uiToggle.On ? 0.6f : 0f;
        uiToggle.Scale = uiToggle.Hover ? 0.04f : 0.01f;
        uiToggle.Thickness = uiToggle.Hover ? 0.125f : (uiToggle.On ? 0f : 0.5f);
    }
}

Here’s the corresponding System.

[BurstCompile]
partial struct UIToggleSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {

    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var uiToggleJob = new UIToggleJob
        {
            DeltaTime = SystemAPI.Time.DeltaTime
        };
        uiToggleJob.ScheduleParallel();
    }
}

Without an Aspect I’d have to write stuff like opacity.ValueRW.Opacity, where opacity is a component returned by a query. So it’s helpful, especially for entities that act more like traditional GameObjects and don’t change around very much.

Depending how quickly your components change, you may want to decide between tag components or enableable components in order to prevent Unity constantly rebuilding the list of Aspects. That’s a deep hole I’m going to avoid for now though.

Bring it all together: writing some Systems

Here are two systems for the configurator portion of THRUST//DOLL. Here’s a very basic System which takes a buffer of clicks and deletes entities that you click on, using DOTS Physics raycasting…

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(PhysicsSimulationGroup))]
[BurstCompile]
partial struct ClickSystem : ISystem
{   
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        PhysicsWorldSingleton physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>();

        //ECB boilerplate
        var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
        var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

        foreach(var inputBuffer in SystemAPI.Query<DynamicBuffer<UIClick>>())
        {
            foreach(var uiClick in inputBuffer)
            {
                if(physicsWorld.CastRay(uiClick.Value,out var hit))
                {
                    ecb.DestroyEntity(hit.Entity);
                }
            }
            inputBuffer.Clear();
        }
    }
}

Unity has been throwing a warning with this, which seems to be that it can’t find the PhysicsSimulationGroup. I haven’t solved this yet, but it doesn’t seem to prevent the program running.

Here’s a slightly more complicated System which creates an Entity with a UIToggle parented to every entity with an Upgrade, and stores a link between them. (Is this the most optimal way to do this? Not sure yet, but let’s get something working first.)

using System;
using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;

[BurstCompile]
partial struct UIToggleSetupSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<UIToggleSpawner>();
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        //ECB boilerplate
        var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
        var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

        //get the UIToggle prefab
        var prefab = SystemAPI.GetSingleton<UIToggleSpawner>().UITogglePrefab;

        foreach (var (upgradeRef, parentEntity) in SystemAPI.Query<RefRW<Upgrade>>().WithEntityAccess())
        {
            var instance = ecb.Instantiate(prefab);
            ecb.SetComponent(instance,LocalTransform.Identity);

            //define the coordinates to be relative to the parent; ParentSystem will take care of the rest.
            ecb.AddComponent(instance,new Parent{ Value = parentEntity });

            //store a reference to its UIToggle on the component.
            //IT IS VERY IMPORTANT THAT YOU USE THE ECB FOR THIS OR YOU'LL HAVE A NEGATIVE TEMPORARY ENTITY
            ecb.SetComponent<Upgrade>(parentEntity, new Upgrade { UIToggle = instance });
        }

        //This system should only run once.
        state.Enabled = false;
    }
}

Here are some things I learned the painful way while writing this:

Now we can change our ClickSystem system to update the state of the corresponding UI toggle when it’s clicked. There is a potential issue in that if somehow a click was registered before the UIToggleSetupSystem, we’d follow null entities that are supposed to point to UIToggles. Unlikely in practice but lets put in [UpdateInGroup(typeof(InitializationSystemGroup))] to make sure. The ECB commands will then run on the first frame before any ClickSystem, buffer their commands before the SimulationSystemGroup, and everything will be done before the user gets a chance to click. (We can worry about the issue of loading and unloading scenes later.)

Now let’s make a real ClickSystem.

Since all we want to do now is change data on a component, we no longer need an ECB. For now I will write it the simplest possible way with GetComponent and SetComponent. Although we are in an iteration here, this is a special case unlikely to be an issue, since I doubt we will be handling more than one or maybe two clicks per frame.

foreach(var inputBuffer in SystemAPI.Query<DynamicBuffer<UIClick>>())
{
    foreach(var uiClick in inputBuffer)
    {
        if(physicsWorld.CastRay(uiClick.Value,out var hit))
        {
            var uiToggleEntity = SystemAPI.GetComponent<Upgrade>(hit.Entity).UIToggle;

            UIToggleState oldState = SystemAPI.GetComponent<UIToggleState>(uiToggleEntity);

            oldState.On = !oldState.On;
            
            SystemAPI.SetComponent<UIToggleState>(uiToggleEntity, oldState);
        }
    }
    inputBuffer.Clear();
}

In combination with the two systems seen above, this works! You can click on stuff and it toggles the corresponding UI element. My fan goes kind of crazy, which is weird because it’s not doing very much, but the framerate is uncapped and it’s rendering at around 200FPS, so that’s probably the reason. I imagine at this point way more CPU time is spent on DOTS overhead than actual game-related calculations.

The next steps

Next we need to handle mouse hover. The easiest way to do that will be to do a second raycast that just uses the mouse position. It may also be sensible to eliminate the GameObject bridge, since right now the full power of the UI System seems unnecessary. But that’s premature optimisation. Later when the interface is more developed, I’d like to be able to use controller bindings and at that point it will be useful!

I am going to add some animation to the UIToggleSystem, which currently instantly changes UI element state, but with a small amount of work could create nice little damped-oscillator animations instead. That will look cute, and it will be a nice demo of what’s possible in this approach.

The non-DOTS parts of the interface will need to be built, but that’s a job for next week.

Comments

Add a comment
[?]