Questions for implementing ECS and Jobs into game

Alright, so I have been toying around with ECS and Jobs and I decided to start switching current systems that I had for a game I’m working on. So I want to see if I’m indeed understanding how to setup them properly.

My first system is pretty much a look at camera like system. It will make the objects to have the same Y euler rotation as the camera. So here’s how my system is setup right now:

[BurstCompile]
struct PositionUpdateJob : IJobParallelForTransform
{
    [ReadOnly]
    public float cameraRotation;

    public void Execute(int i, TransformAccess transform)
    {
       Quaternion worldRotation = transform.rotation;
        Vector3 eulerRotation = worldRotation.eulerAngles;

        //Ignore if the difference between the rotation is minimum
        if (Compare(eulerRotation.y, cameraRotation))
            return;

        eulerRotation.y = cameraRotation;

        worldRotation.eulerAngles = eulerRotation;
        transform.rotation = worldRotation;
    }
}

public void Update()
{
    //Only execute if the camera is changing rotation
    if (NewBehaviourScript.CamRotationDirection == 0)
        return;

    m_Job = new PositionUpdateJob()
    {
        cameraRotation = Camera.main.transform.parent.eulerAngles.y,
    };

    m_PositionJobHandle = m_Job.Schedule(m_TransformsAccessArray);
}

public void LateUpdate()
{
    m_PositionJobHandle.Complete();
}

private void OnDestroy()
{
    m_TransformsAccessArray.Dispose();
}

Compare function

Here’s my compare function in case that is needed

bool Compare(float a, float b, float allowedDifference = 0.001f)
{
    float absA = math.abs(a);
    float absB = math.abs(b);
    float diff = math.abs(a - b);

    if (a == b)
    {
        return true;
    }
    else if (a == 0 || b == 0 || diff < float.MinValue)
    {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (allowedDifference * float.MinValue);
    }
    else
    {
        return diff / math.min((absA + absB), float.MaxValue) < allowedDifference;
    }
}

m_TransformsAccessArray will contain the array of transforms that I need to rotate. Currently it seems to be working fine and it’s 15 FPS faster than with the old MonoBehaviour system with 5000 objects on scene.

Is this the correct setup I should be following for this kind of system? Right now the GO to rotate can’t be pure ECS since I need to use some Unity systems that are not pure ready.

Well you’re not using ECS, you’re just using jobs but that isn’t an issue; there’s a reason jobs is detached to ECS. Being able to optimize existing code with jobs to get performance is great.

So what you’re doing seems fine and the fact that you can make such a simple change and get noticeable performance benefits is fantastic.

2 Likes

Well, you can certainly make the set up a Hybrid setup. It’s a great segway into using a Pure ECS implementation. It looks like you’re still using MonoBehaviours and scheduling jobs in Update / and completing them in LateUpdate. In terms of correctness, it’s mostly right. Unless that MonoBehaviour is destroyed, you don’t dispose the TransformAccessArray. Scheduling the Job in update and letting it complete in LateUpdate is fine in a MonoBehaviour.
If you want to head in the direction of using a Hybrid approach, you would:

  • Get the data you need from a ComponentGroup / Dependency Injection
  • Create a Job/ComponentSystem
  • Override OnUpdate()
  • Schedule and allow jobs to complete in OnUpdate()
  • Dispose at the end of OnUpdate()

I think taking a look at their TwoStickShooter Hybrid or another project which uses Hybrid provided by Unity can shed some light on how to convert over to a Hybrid approach.
https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/Samples/Assets/TwoStickShooter/Hybrid

1 Like

Alright, following both of your suggestion to use ECS. Here’s my new system:

public class RotationSystem : JobComponentSystem
{
    /// <summary>
    /// Struct used to filter the entities
    /// </summary>
    struct Filter
    {
        public readonly int Length;
        public ComponentArray<PawnComponent> components;
        public TransformAccessArray transformsAccessArray;
    }

    [Inject]
    Filter m_filter;
    float cameraRotation;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        RotationUpdateJob myJob = new RotationUpdateJob()
        {
            cameraRotation = Camera.main.transform.parent.eulerAngles.y,
        };

        return myJob.Schedule(m_filter.transformsAccessArray);
    }

    [BurstCompile]
    struct RotationUpdateJob : IJobParallelForTransform
    {
        [ReadOnly]
        public float cameraRotation;

        public void Execute(int i, TransformAccess transform)
        {
            Quaternion worldRotation = transform.rotation;
            Vector3 eulerRotation = worldRotation.eulerAngles;

            //Ignore if the difference between the rotation is minimum
            if (Compare(eulerRotation.y, cameraRotation))
                return;

            eulerRotation.y = cameraRotation;

            worldRotation.eulerAngles = eulerRotation;
            transform.rotation = worldRotation;
        }
    }
}

Which seems to perform even better, but I’m wondering how I can spread the load into all worker threads?. Right now the job is being executed by only one. This is a screenshot of 50,000 rotating hybrid GameObjects.

TransformAccess will split based on root transform.

If all your transforms are under the same root parent, it’ll only work on a single thread.

If you split them between 3 root parents, it’ll be split across 3 workers.

1 Like

Alright so for my next system I have been trying to see if it’s worh it to change my Monobehaviours meshes to pure, but I cannot seem to get any better performance than GameObjects with MeshRenderer components. I am not using any custom system since first I wanted to be sure that it would be faster before attempting anything. So here are my tests:

All tests have 5,000 static objects spawned with the same settings. Shadows on, receive shadows and they all are using the same material (standard material with a texture). The mesh is a simple quad.

Images

Does anyone know of any trick to draw meshes that is faster than traditional way? Note that the only code I have is the instantiation of the 5,000 GameObjects and the creation of the 5,000 entities for the pure system.

Did you tick this on your material

3856228--326548--upload_2018-11-5_13-34-30.png

1 Like

That was stupid… I thought I did, but I miss clicked and enabled Double Sided Global Illumination instead of GPU Instancing…

Perfect, now I’m getting better performance. Now is there any way to change a material property of a specific entities? I’m wondering if it’s possible since it uses SharedComponentData.

I want to see if I can change the material offset and tiling, so that I can modify my SpriteSheet animator with pure too.

Not by default at the moment .

But someone hacked together a working material property block system if you want to change colours and other shader properties.

I don’t actually know how tiling is handled and if you could do it within material property blocks.

While I couldn’t find a way to use the property blocks above I managed to make the system, but I’m having an error that I cannot figure out how to fix. It seems like if you change any SharedComponentData the EntityArray gets deallocated so it throws the following error:

InvalidOperationException: The NativeArray has been deallocated, it is not allowed to access it

Here’s my current system:

ComponentGroup group;

protected override void OnCreateManager()
{
    base.OnCreateManager();

    group = EntityManager.CreateComponentGroup(typeof(MeshInstanceRenderer), typeof(SpriteAnimator));
}

protected override void OnUpdate()
{
    EntityArray entitites = group.GetEntityArray();
 
    for (int i = 0; i < entities.Length; i++)
    {
        Entity entity = entities[i];

        MeshInstanceRenderer renderer = EntityManager.GetSharedComponentData<MeshInstanceRenderer>(entity);
  
        //Here I would change something, if it stops throwing the error

        EntityManager.SetSharedComponentData(entity, renderer);
    }
}

As you can see I’m not even making any change, just setting the renderer back and it throws the error. The error points to the line Entity entity = entities*; Am I missing a step here?*
If the SetSharedComponentData line is removed everything works again. Also tried with [Inject] and the same error appears.
EDIT: Error bellow in case that it helps
csharp* *InvalidOperationException: The NativeArray has been deallocated, it is not allowed to access it Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x394ef160 + 0x00052> in <d5141ab31a774986ab28d43b33133e56>:0 Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at C:/buildslave/unity/build/Runtime/Export/AtomicSafetyHandle.bindings.cs:148) Unity.Entities.EntityArray.get_Item (System.Int32 index) (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/Iterators/EntityArray.cs:40) SpriteAnimatorSystem.OnUpdate (Unity.Jobs.JobHandle inputDeps) (at Assets/SpriteAnimatorSystem.cs:67) Unity.Entities.JobComponentSystem.InternalUpdate () (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/ComponentSystem.cs:507) Unity.Entities.ScriptBehaviourManager.Update () (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/ScriptBehaviourManager.cs:77) Unity.Entities.ScriptBehaviourUpdateOrder+DummyDelagateWrapper.TriggerUpdate () (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/ScriptBehaviourUpdateOrder.cs:703)* *

1 Like

Awesome that did it! Many thanks! Here’s how it works with 5,000 fully animated objects at different fps and random colors.
3857386--326698--Capture.PNG
Now to try to move it to a JobComponentSystem. Is there anyway I could either get MeshInstanceRenderer from the Job or pass it from the System? When I tried to pass it, it throws an error about it being marked as WriteOnly and I’m trying to access it.

Here’s my JobSystem:

protected override JobHandle OnUpdate(JobHandle inputDeps)
{
    buffer = m_barrier.CreateCommandBuffer();

    job = new AnimatorJob
    {
        deltaTime = Time.deltaTime,
        renderers = group.GetSharedComponentDataArray<MeshInstanceRenderer>(),
        animators = group.GetComponentDataArray<SpriteAnimator>(),
        entities = group.GetEntityArray(),
        commands = buffer.ToConcurrent()
    };

    handle = job.Schedule(job.animators.Length, 64, inputDeps);
    handle.Complete();

    return handle;
}

struct AnimatorJob : IJobParallelFor
{
    [ReadOnly]
    public float deltaTime;

    public SharedComponentDataArray<MeshInstanceRenderer> renderers;
    [ReadOnly]
    public EntityArray entities;
    [ReadOnly]
    public ComponentDataArray<SpriteAnimator> animators;
    public EntityCommandBuffer.Concurrent commands;

    public void Execute(int index)
    {
        //This throws an error
        MeshInstanceRenderer renderer = renderers[index];
    }
}

It throws the following error that points to MeshInstanceRenderer renderer = renderers[index];

InvalidOperationException: The native container has been declared as [WriteOnly] in the job, but you are reading from it.
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x3950fa30 + 0x00052> in <d5141ab31a774986ab28d43b33133e56>:0
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at C:/buildslave/unity/build/Runtime/Export/AtomicSafetyHandle.bindings.cs:148)
Unity.Entities.SharedComponentDataArray`1[T].get_Item (System.Int32 index) (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/Iterators/SharedComponentDataArray.cs:40)
SpriteAnimatorSystem+AnimatorJob.Execute (System.Int32 index) (at Assets/SpriteAnimatorSystem.cs:88)
Unity.Jobs.IJobParallelForExtensions+ParallelForJobStruct`1[T].Execute (T& jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at C:/buildslave/unity/build/Runtime/Jobs/Managed/IJobParallelFor.cs:43)

First - remove ReadOnly from Concurrent ECB

Fixed, that was a bad copy/paste. I’m not using ReadOnly on it.

Second - mark field “renderers” in job [ReadOnly]

Still gives the same error

Are you shure? It’s all code?

Yes, here’s the full system class:

public class SpriteAnimatorSystem : JobComponentSystem
{
    public class SpriteAnimatorBarrier : BarrierSystem { }

    [Inject]
    private SpriteAnimatorBarrier m_barrier;

    float deltaTime;
    EntityCommandBuffer buffer;
    ComponentGroup group;
    JobHandle handle;
    AnimatorJob job;

    protected override void OnCreateManager()
    {
        base.OnCreateManager();

        group = EntityManager.CreateComponentGroup(typeof(MeshInstanceRenderer), typeof(SpriteAnimator));
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        buffer = m_barrier.CreateCommandBuffer();

        job = new AnimatorJob
        {
            deltaTime = Time.deltaTime,
            renderers = group.GetSharedComponentDataArray<MeshInstanceRenderer>(),
            animators = group.GetComponentDataArray<SpriteAnimator>(),
            entities = group.GetEntityArray(),
            commands = buffer.ToConcurrent()
        };

        handle = job.Schedule(job.animators.Length, 64, inputDeps);
        handle.Complete();

        return handle;
    }

    struct AnimatorJob : IJobParallelFor
    {
        [ReadOnly]
        public float deltaTime;

        [ReadOnly]
        public SharedComponentDataArray<MeshInstanceRenderer> renderers;
        [ReadOnly]
        public EntityArray entities;
        [ReadOnly]
        public ComponentDataArray<SpriteAnimator> animators;
        public EntityCommandBuffer.Concurrent commands;

        public void Execute(int index)
        {
            //This throws an error
            MeshInstanceRenderer renderer = renderers[index];
        }
    }
}

And the error: (Pointing to the same line MeshInstanceRenderer renderer = renderers[index];)

InvalidOperationException: The native container has been declared as [WriteOnly] in the job, but you are reading from it.
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x19a0fa30 + 0x00052> in <d5141ab31a774986ab28d43b33133e56>:0
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckReadAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at C:/buildslave/unity/build/Runtime/Export/AtomicSafetyHandle.bindings.cs:148)
Unity.Entities.SharedComponentDataArray`1[T].get_Item (System.Int32 index) (at Library/PackageCache/com.unity.entities@0.0.12-preview.19/Unity.Entities/Iterators/SharedComponentDataArray.cs:40)
SpriteAnimatorSystem+AnimatorJob.Execute (System.Int32 index) (at Assets/SpriteAnimatorSystem.cs:74)
Unity.Jobs.IJobParallelForExtensions+ParallelForJobStruct`1[T].Execute (T& jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at C:/buildslave/unity/build/Runtime/Jobs/Managed/IJobParallelFor.cs:43)

FYI. You must call GetComponentGroup. EntityManager.CreateComponentGroup is Unity rudiment and it’s not for usage. More precisely, it is an internal method.
3857782--326755--upload_2018-11-5_17-40-53.png

And all works fine when I try your code

    struct AnimatorJob : IJobParallelFor
    {
        [ReadOnly]
        public float deltaTime;

        [ReadOnly]
        public SharedComponentDataArray<MeshInstanceRenderer> renderers;
        [ReadOnly]
        public EntityArray entities;
        [ReadOnly]
        public ComponentDataArray<SpriteAnimator> animators;
        public EntityCommandBuffer.Concurrent commands;

        public void Execute(int index)
        {
            MeshInstanceRenderer renderer = renderers[index];
            Debug.Log("All fine");
        }
    }

3857782--326758--upload_2018-11-5_17-41-31.png

I think problem in other place, this error not point to some code row it’s just info. You must check other systems.

I must be missing something else then, because I still got the error. How are you creating the system? What version are you running right now? My tests are with Unity 2018.3.0b8 and:
3857845--326782--Capture.PNG