Unity’s ECS API is still in preview, and thus it’s in flux. There are different ways to query data. Some of them might be deprecated even before the more stable API is released. Querying data by injection is the most used method as it is easy to understand, and requires less code. However, it’s not very flexible for complex component queries. The Unity devs in ECS forum have expressed that injection would be deprecated together with structures like ComponentDataArray. To prepare for this eventuality, I refactored my ECS code to not use injection and ComponentDataArray.

The baseline API for querying ECS data is called Chunk Iteration. All other APIs are using this one internally, including data injection. It’s main con is it’s very verbose. But if you’re tinkering with ECS and you want your code to be more future proof, this is the API to use. This post will show you the basics of chunk iteration.

Inject Version

Let’s say we’re writing a system that moves a position by some velocity. Unity already has a Position component, so we only need a Velocity component:

public struct Velocity : IComponentData { public float3 value; }

This is the system using injection:

public class MoveByVelocitySystem : ComponentSystem { // Data private struct Data { public readonly int Length; public ComponentDataArray<Position> Positions; public ComponentDataArray<Velocity> Velocities; } [Inject] private Data data; protected override void OnUpdate() { for (int i = 0; i < this.data.Length; ++i) { this.data.Positions[i] = new Position() { Value = this.data.Positions[i].Value + this.data.Velocities[i].value * Time.deltaTime }; } } }v

This is very straightforward. We're getting the entities with Position and Velocity components. We update the Position by the Velocity value multiplied by delta time.

Chunk Iteration Version

Let’s rewrite MoveByVelocitySystem using chunk iteration. Here are the main steps:

Prepare a ComponentGroup

On each update: Get an ArchetypeChunkComponentType for each component that you want to iterate Get the array of ArchetypeChunk and process each chunk Dispose the array of ArchetypeChunk



Here’s how it looks like:

public class MoveByVelocitySystem : ComponentSystem { private ComponentGroup group; private ArchetypeChunkComponentType<Position> positionType; private ArchetypeChunkComponentType<Velocity> velocityType; protected override void OnCreateManager() { // Prepare group this.group = GetComponentGroup(typeof(Position), typeof(Velocity)); } protected override void OnUpdate() { // Get each ArchetypeChunkComponentType this.positionType = GetArchetypeChunkComponentType(); this.velocityType = GetArchetypeChunkComponentType(); // Get the chunks and process each NativeArray<ArchetypeChunk> chunks = this.group.CreateArchetypeChunkArray(Allocator.TempJob); for (int i = 0; i < chunks.Length; ++i) { Process(chunks[i]); } // Don't forget to dispose the chunks chunks.Dispose(); } private void Process(ArchetypeChunk chunk) { // Use the ArchetypeChunkComponentType to get an array of each component NativeArray<Position> positions = chunk.GetNativeArray(this.positionType); NativeArray<Velocity> velocities = chunk.GetNativeArray(this.velocityType); // Update the components here for (int i = 0; i < chunk.Count; ++i) { positions[i] = new Position() { Value = positions[i].Value + velocities[i].value * Time.deltaTime }; } } }

As you can see, there's a lot more code here. You can imagine that this is what's happening under the hood when you're using injection and other data query APIs.

Chunk Iteration In Jobs

Chunk iteration wouldn’t be useful if it can’t be used inside jobs. Here’s MoveByVelocitySystem but using an IJobParallelFor:

public class MoveByVelocitySystem : JobComponentSystem { private ComponentGroup group; protected override void OnCreateManager() { this.group = GetComponentGroup(typeof(Position), typeof(Velocity)); } protected override JobHandle OnUpdate(JobHandle inputDeps) { NativeArray<ArchetypeChunk> chunks = this.group.CreateArchetypeChunkArray(Allocator.TempJob); Job job = new Job() { deltaTime = Time.deltaTime, positionType = GetArchetypeChunkComponentType(), velocityType = GetArchetypeChunkComponentType(), chunks = chunks }; return job.Schedule(chunks.Length, 64, inputDeps); } private struct Job : IJobParallelFor { public float deltaTime; public ArchetypeChunkComponentType<Position> positionType; public ArchetypeChunkComponentType<Velocity> velocityType; [ReadOnly] [DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> chunks; public void Execute(int index) { Process(this.chunks[index]); } private void Process(ArchetypeChunk chunk) { NativeArray<Position> positions = chunk.GetNativeArray(this.positionType); NativeArray<Velocity> velocities = chunk.GetNativeArray(this.velocityType); for (int i = 0; i < chunk.Count; ++i) { positions[i] = new Position() { Value = positions[i].Value + velocities[i].value * this.deltaTime }; } } } }

The attribute [DeallocateOnJobCompletion] is needed for the array of chunks since you won't have a chance to dispose it when you schedule the job. You don't dispose the array right after scheduling because the array will already be lost when the job is executed.

That's it for now. Happy coding!