There are 3 interesting methods on EntityQuery that has an overload with out JobHandle :

public NativeArray<ArchetypeChunk> CreateArchetypeChunkArray(Allocator allocator);
public NativeArray<ArchetypeChunk> CreateArchetypeChunkArray(Allocator allocator, out JobHandle jobhandle);

public NativeArray<T> ToComponentDataArray<T>(Allocator allocator) where T : struct, IComponentData;
public NativeArray<T> ToComponentDataArray<T>(Allocator allocator, out JobHandle jobhandle) where T : struct, IComponentData;

public void CopyFromComponentDataArray<T>(NativeArray<T> componentDataArray, out JobHandle jobhandle) where T : struct, IComponentData;
public void CopyFromComponentDataArray<T>(NativeArray<T> componentDataArray) where T : struct, IComponentData;

What's the deal? Have you been using only the normal version?

Unity schedules and complete a job just to do those work for you

The existence of out JobHandle means that inside these method Unity schedules mini jobs to "gather" and make you the desired NativeArray. It complete the job immediately because the call is synchronous. You get the NativeArray right away. (Don't forget to dispose it)

Remember that these methods are called from the main thread since it is a method of EntityQuery. You get worker thread utilization even when working with main thread things. It is really considerate!

Also this is why you could not use Allocator.Temp, since it use the newly allocated NativeArray in that mini job. Requiring at least Allocator.TempJob.

Unlocking the main thread

Where you kick off job stuff you are probably working in the system's OnUpdate. It is in the main thread for just a while. The part that I say "complete the job immediately" is unfortunately blocking the main thread.

But you could make it return an incomplete NativeArray by using the out JobHandle overload. If you call .Complete on that JobHandle it would be equivalent to normal overload and block the main thread.

But the point is you don't call complete, but use that JobHandle as a dependency for the next job likely in the same system, directly below.

You can instantly use the (incomplete) NativeArray as an input for that job. But not to worry, because you are going to schedule that job with not just your usual inputDeps but instead JobHandle.CombineDependencies(inputDeps, thatGatherJh). Ensuring the best possible scenario :

  • The OnUpdate code is blazing fast, it does not yet gather NativeArray but already give you what would be the product of that gather. Squeezing you that precious main thread ms you are trying to get less than 16.66ms.
  • That broken NativeArray can be given to the job right now. Thanks to dependency chaining with that JobHandle, you can ensure the NativeArray is completed by the time that job runs.

Power moves for each methods

CreateArchetypeChunkArray

This method looks like the most lightweight operation out of all, but still Unity schedule a job to gather you chunks. (how considerate?)

Bringing NativeArray<ArchetypeChunk> to the job is of course for when you want maximum tailor made job possible.

  • You want that raw, hardcore, memory area to iterate/read/write (depending if your ArchetypeChunkComponentType allows you to write or not).
  • And maybe you want to ask the chunk something along the way. (Has? Chunk component? DidChange?)
  • Or maybe you are not satisfied with one archetype based EntityQuery and you used EntityQueryDesc to get you assortment of chunks based on the uniqe  Any criteria. ( All and None is doable without query)

You cannot get anymore custom than this in the job. But don't just stop there if you had come this far. Before the job you can still optimize the chunk gathering as an another job!

public class AoeHealingSystem : JobComponentSystem
{
...
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var humanAndMonsters = HealableQuery.CreateArchetypeChunkArray(Allocator.TempJob, out JobHandle gatherJobHandle1);
        var heals = ActiveAoeHealingQuery.CreateArchetypeChunkArray(Allocator.TempJob, out JobHandle gatherJobHandle2);

        JobHandle finalJobHandle = new Job
        {
            ActiveAoeHealingType = GetArchetypeChunkComponentType<ActiveAoeHealing>(isReadOnly: true),
            DamagedTagType = GetArchetypeChunkComponentType<DamagedTag>(isReadOnly: true),
            HumanType = GetArchetypeChunkComponentType<Human>(isReadOnly: false),
            MonsterType = GetArchetypeChunkComponentType<Monster>(isReadOnly: false),

            //These NativeArray are not finished gathering yet at this point, but you can start the job now.
            assortmentOfHumanAndMonsterChunks =  humanAndMonsters,
            activeAoeHealingChunks = heals,

            lastSystemVersion = LastSystemVersion,
        }.Schedule(JobHandle.CombineDependencies(inputDeps, gatherJobHandle1, gatherJobHandle1));

        return finalJobHandle;
    }

    private struct Job : IJob
    {
        [ReadOnly] public ArchetypeChunkComponentType<ActiveAoeHealing> ActiveAoeHealingType;
        [ReadOnly] public ArchetypeChunkComponentType<DamagedTag> DamagedTagType;
        public ArchetypeChunkComponentType<Human> HumanType;
        public ArchetypeChunkComponentType<Monster> MonsterType;

        [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<ArchetypeChunk> assortmentOfHumanAndMonsterChunks;
        [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<ArchetypeChunk> activeAoeHealingChunks;

        public uint lastSystemVersion;

        public void Execute()
        {
            ...
        }
    }
}

(If you are thinking why not parallelize with IJobParallelFor or IJobChunk , chill out, it is just an example...)

Some parts are omitted, but the point is in the OnUpdate the chunk gather from EntityQuery is not blocking the main thread anymore and I combined the dependencies. You have optimized something outside of your main job by chaining more granular jobs!

ToComponentDataArray

"To" methods means you get a new NativeArray that is not linked with ECS database anymore to do as you please. (But dispose it too) If you change something inside it, nothing get updated. So it means you get a copy of data. You can feel that this might going to be a bit expensive. But you could alleviate it with out JobHandle overload. And maybe stick that to run before an another job that use it. Probably you may want to add [DeallocateOnJobCompletion].

(So the product from this method is not the same as that NativeArray from archetypeChunk.GetNativeArray. That's the real deal, portal to ECS database. If you change it, things changes *if your ACCT allows)

Also a neat little trick with C#7's out var syntax : the out variable is considered declared before the usage, so you could throw that into .Schedule without syntax error. It looked quite elegant.

public class Sys : JobComponentSystem
{
    private struct A : IComponentData { int x; }
    private struct B : IComponentData { float y; }

    EntityQuery aCg;
    EntityQuery bCg;
    protected override void OnCreateManager()
    {
        aCg = GetEntityQuery(ComponentType.ReadOnly<A>());
        bCg = GetEntityQuery(ComponentType.ReadOnly<B>());
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        JobHandle finalHandle = new Job
        {
            naA = aCg.ToComponentDataArray<A>(Allocator.TempJob, out var jhA),
            naB = aCg.ToComponentDataArray<B>(Allocator.TempJob, out var jhB),
        }.Schedule(JobHandle.CombineDependencies(inputDeps, jhA, jhB)); // <--- here
        return finalHandle;
    }

    private struct Job : IJob
    {
        [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<A> naA;
        [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<B> naB;
        public void Execute()
        {
            //...
        }
    }
}

CopyFromComponentDataArray

This one is to combo with ToComponentDataArray. If you did change the content in in that and want to apply back you can delay the apply with out JobHandle overload. (Make sure the length of that NativeArray still matches total entities from the EntityQuery you are applying back)

The mini job inside this method automatically knows which dependency to wait on because it is a method of entity query, the thing full of types. Complete immediately overload or not. Then if you use the out JobHandle overload you can avoid completing immediately those dependency that it automatically knows.