There are 3 interesting methods on ComponentGroup 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 ComponentGroup. 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 ComponentGroup and you used EntityArchetypeQuery 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!

Look at this example system. I want to schedule a job that take chunks of damaged human or monsters and also chunks of AOE heals. In the job I will sum up total amount of heals first, then apply it to all human and monsters. But the catch is monster do not get heals if they are not just damaged since the last frame this system run. (no reason but just an excuse to ask something from the chunk..)

public class AoeHealingSystem : JobComponentSystem
{
    private struct Human : IComponentData { public int hp; }
    private struct Monster : IComponentData { public int hp; }
    private struct ActiveAoeHealing : IComponentData { public int healAmount; }
    private struct DamagedTag : IComponentData { }

    ComponentGroup HealableGroup;
    ComponentGroup ActiveAoeHealingGroup;
    protected override void OnCreateManager()
    {
        HealableGroup = GetComponentGroup(
            new EntityArchetypeQuery
            {
                All = new ComponentType[]{
                    ComponentType.ReadOnly<DamagedTag>()
                },
                Any = new ComponentType[]{
                    ComponentType.ReadWrite<Human>(),
                    ComponentType.ReadWrite<Monster>()
                },
                None = new ComponentType[]{
                },
            }
        );
        ActiveAoeHealingGroup = GetComponentGroup(ComponentType.ReadOnly<ActiveAoeHealing>());
        
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var humanAndMonsters = HealableGroup.CreateArchetypeChunkArray(Allocator.TempJob, out JobHandle gatherJobHandle1);
        var heals = ActiveAoeHealingGroup.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()
        {
            int totalHeals = 0;
            for (int i = 0; i < activeAoeHealingChunks.Length; i++)
            {
                var heals = activeAoeHealingChunks[i].GetNativeArray(ActiveAoeHealingType);
                for (int j = 0; j < heals.Length; j++)
                {
                    totalHeals += heals[j].healAmount;
                }
            }

            for (int i = 0; i < assortmentOfHumanAndMonsterChunks.Length; i++)
            {
                var ac = assortmentOfHumanAndMonsterChunks[i];
                if (ac.Has(HumanType))
                {
                    var humans = ac.GetNativeArray(HumanType);
                    for (int j = 0; j < humans.Length ; j++)
                    {
                        humans[j] = new Human { hp = humans[j].hp + totalHeals };
                    }
                }
                else if (ac.Has(MonsterType) && ac.DidChange(MonsterType, lastSystemVersion))
                {
                    var monsters = ac.GetNativeArray(MonsterType);
                    for (int j = 0; j < monsters.Length ; j++)
                    {
                        monsters[j] = new Monster { hp = monsters[j].hp + totalHeals };
                    }
                }
            }
        }
    }
}

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

I don't know if it returns runtime or compile error or not, but the point is in the OnUpdate the chunk gather from ComponentGroup 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; }

    ComponentGroup aCg;
    ComponentGroup bCg;
    protected override void OnCreateManager()
    {
        aCg = GetComponentGroup(ComponentType.ReadOnly<A>());
        bCg = GetComponentGroup(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 ComponentGroup you are applying back)

The mini job inside this method automatically knows which dependency to wait on because it is a method of component group, 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.