Search code examples
c#unity-game-enginejobsentity-component-system

How to pass Entities to a job to add Components?


(EDIT: See my Edit below)

In order to reduce boilerplate while using ECS, I turned this code into that:

Before:

public class MySystem : JobComponentSystem
{
    public struct Group
    {
       // A long list of different ComponentDataArray
       // Eg.
       public ComponentDataArray<Position> Positions;

       public int Length;
    }

    [Inject] private Group _Group;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        MyJob job = new Job()
        {
           DeltaTime = Time.deltaTime,
           // That same long list of ComponentDataArray
        };
        return job.Schedule(_Group.Positions.Length, 64, inputDeps);
    }

    [BurstCompile]
    struct MyJob : IJobParallelFor
    {
        public float DeltaTime;
        // That same long list of ComponentDataArray AGAIN.

        public void Execute(int i)
        {
             // MyJob code
        }
    }
}

After:

public class MySystem : JobComponentSystem
{
    public struct Group
    {
       // A long list of different ComponentDataArray
       // Eg.
       public ComponentDataArray<Position> Positions;

       public int Length;
    }

    [Inject] private Group _Group;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        MyJob job = new Job()
        {
           DeltaTime = Time.deltaTime,
           Group = _Group
        };
        return job.Schedule(_Group.Positions.Length, 64, inputDeps);
    }

    [BurstCompile]
    struct MyJob : IJobParallelFor
    {
        public float DeltaTime;
        public Group Group;

        public void Execute(int i)
        {
             // MyJob code
        }
    }
}

Until then, everything worked fine. Until I tried to add a component to each entities once they were initialized.

public class MySystem: JobComponentSystem
{
    public struct Group
    {
        // A long list of different ComponentDataArray
        // Eg.
        public ComponentDataArray<Position> Positions;

        public SubtractiveComponent<Initialized> Initialized;

        public int Length;
        public EntityArray Entities;
    }

    [Inject] private Group _Group;

    public class EntityBarrier : BarrierSystem { }

    [Inject] private EntityBarrier _EntityBarrier;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        MyJob job = new MyJob()
        {
            DeltaTime = Time.deltaTime,
            Group = _Group,
            CommandBuffer = _EntityBarrier.CreateCommandBuffer()
        };

        return job.Schedule(_Group.Positions.Length, 64, inputDeps);
    }

    [BurstCompile]
    struct MyJob : IJobParallelFor
    {
        public float DeltaTime;
        public Group Group;
        [ReadOnly] public EntityCommandBuffer CommandBuffer;

        public void Execute(int i)
        {
            CommandBuffer.AddComponent(Group.Entities[i], new Initialized());

            // MyJob code
        }
    }
}

By which point it raised:

InvalidOperationException: The NativeArray MyJob.Group.Entities must be marked [ReadOnly] in the job MySystem:MyJob, because the container itself is marked read only.

But if I do change it to [ReadOnly], it complains I edit it when reaching the line CommandBuffer.AddComponent.

Also which container is it mentioning being marked ReadOnly?

Is there another way to feed the EntityArray (or add a component to the entity of index i) to the job without requiring all the boilerplate code seen in example 1?

EDIT: Still complains if I feed it manually like this:

public class MySystem : JobComponentSystem
{
    public struct Group
    {
       // A long list of different ComponentDataArray
       // Eg.
       public ComponentDataArray<Position> Positions;

       public SubtractiveComponent<Initialized> Initialized;

       public int Length;
       public EntityArray Entities;
    }

    [Inject] private Group _Group;

    public class EntityBarrier : BarrierSystem { }

    [Inject] private EntityBarrier _EntityBarrier;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        MyJob job = new Job()
        {
           DeltaTime = Time.deltaTime,
           Entities = _Group.Entities,
           CommandBuffer = _EntityBarrier.CreateCommandBuffer(),
           // That same long list of ComponentDataArray
        };
        return job.Schedule(_Group.Positions.Length, 64, inputDeps);
    }

    [BurstCompile]
    struct MyJob : IJobParallelFor
    {
        public float DeltaTime;
        public EntityArray Entities;
        // That same long list of ComponentDataArray AGAIN.

        public void Execute(int i)
        {
             CommandBuffer.AddComponent(Entities[i], new Initialized());

             // MyJob code
        }
    }
}

Isn't it how we're supposed to do it? It's done here: https://forum.unity.com/threads/some-beginner-questions-about-pure-ecs.524700/#post-3449277

EDIT2: The only differences I spotted in his code are

[ReadOnly]public EntityArray Entities;
public EntityCommandBuffer CommandBuffer;

And using EndFrameBarrier. I upgraded my code but receive:

InvalidOperationException: MyJob.CommandBuffer is not declared [ReadOnly] in a IJobParallelFor job. The container does not support parallel writing. Please use a more suitable container type.

Hence why I put it as ReadOnly in the first place.


Solution

  • I found a way but I'm not sure it's safe or if it's the way to do it but:

     // In your job
     [NativeDisableParallelForRestriction] public EntityCommandBuffer CommandBuffer;
    

    Found here: https://forum.unity.com/threads/to-make-it-clear-do-i-have-a-start-function-on-ecs.523943/#post-3441718

    Regardless if you're using EndFrameBarrier or inheriting it yourself. (I don't really know the difference)