Using IJobChunk jobs
You can implement IJobChunk inside a system to iterate through your data by chunk. When you schedule an IJobChunk job in the OnUpdate()
function of a system, the job invokes your Execute()
function once for each chunk that matches the entity query passed to the job's Schedule()
method. You can then iterate over the data inside each chunk, entity by entity.
Iterating with IJobChunk requires more code setup than does Entities.ForEach, but is also more explicit and represents the most direct access to the data, as it is actually stored.
Another benefit of iterating by chunks is that you can check whether an optional component is present in each chunk with Archetype.Has<T>()
, and then process all of the entities in the chunk accordingly.
To implement an IJobChunk job, use the following steps:
- Create an
EntityQuery
to identify the entities that you want to process. - Define the job struct, and include fields for
ArchetypeChunkComponentType
objects that identify the types of components the job must directly access. Also, specify whether the job reads or writes to those components. - Instantiate the job struct and schedule the job in the system
OnUpdate()
function. - In the
Execute()
function, get theNativeArray
instances for the components the job reads or writes and then iterate over the current chunk to perform the desired work.
For more information, the ECS samples repository contains a simple HelloCube example that demonstrates how to use IJobChunk
.
Query for data with a EntityQuery
An EntityQuery defines the set of component types that an archetype must contain for the system to process its associated chunks and entities. An archetype can have additional components, but it must have at least those that the EntityQuery defines. You can also exclude archetypes that contain specific types of components.
For simple queries, you can use the SystemBase.GetEntityQuery()
function and pass in the component types as follows:
public class RotationSpeedSystem : SystemBase
{
private EntityQuery m_Query;
protected override void OnCreate()
{
m_Query = GetEntityQuery(ComponentType.ReadOnly<Rotation>(),
ComponentType.ReadOnly<RotationSpeed>());
//...
}
For more complex situations, you can use an EntityQueryDesc
. An EntityQueryDesc
provides a flexible query mechanism to specify the component types:
All
: All component types in this array must exist in the archetypeAny
: At least one of the component types in this array must exist in the archetypeNone
: None of the component types in this array can exist in the archetype
For example, the following query includes archetypes that contain the RotationQuaternion
and RotationSpeed
components, but excludes any archetypes that contain the Frozen
component:
protected override void OnCreate()
{
var queryDescription = new EntityQueryDesc()
{
None = new ComponentType[]
{
typeof(Static)
},
All = new ComponentType[]
{
ComponentType.ReadWrite<Rotation>(),
ComponentType.ReadOnly<RotationSpeed>()
}
};
m_Query = GetEntityQuery(queryDescription);
}
The query uses ComponentType.ReadOnly<T>
instead of the simpler typeof
expression to designate that the system does not write to RotationSpeed
.
You can also combine multiple queries. To do this, pass an array of EntityQueryDesc
objects rather than a single instance. ECS uses a logical OR operation to combine each query. The following example selects any archetypes that contain a RotationQuaternion
component or a RotationSpeed
component (or both):
protected override void OnCreate()
{
var queryDescription0 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(Rotation)}
};
var queryDescription1 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationSpeed)}
};
m_Query = GetEntityQuery(new EntityQueryDesc[] {queryDescription0, queryDescription1});
}
Note: Do not include completely optional components in the EntityQueryDesc
. To handle optional components, use the chunk.Has<T>()
method inside IJobChunk.Execute()
to determine whether the current ArchetypeChunk has the optional component or not. Because all entities in the same chunk have the same components, you only need to check whether an optional component exists once per chunk: not once per entity.
For efficiency and to avoid needless creation of garbage-collected reference types, you should create the EntityQueries
for a system in the system’s OnCreate()
function and store the result in an instance variable. (In the above examples, the m_Query
variable is used for this purpose.)
Define the IJobChunk struct
The IJobChunk struct defines fields for the data the job needs when it runs, as well as the job’s Execute()
method.
To access the component arrays inside of the chunks that the system passes to your Execute()
method, you must create an ArchetypeChunkComponentType<T>
object for each type of component that the job reads or writes to. You can use these objects to get instances of the NativeArray
s that provide access to the components of an entity. Include all of the components referenced in the job’s EntityQuery that the Execute()
method reads or writes. You can also provide ArchetypeChunkComponentType
variables for optional component types that you do not include in the EntityQuery.
You must check to make sure that the current chunk has an optional component before you try to access it. For example, the HelloCube IJobChunk example declares a job struct that defines ArchetypeChunkComponentType<T>
variables for two components; RotationQuaternion
and RotationSpeed
:
[BurstCompile]
struct RotationSpeedJob : IJobChunk
{
public float DeltaTime;
public ArchetypeChunkComponentType<Rotation> RotationType;
[ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
// ...
}
}
The system assigns values to these variables in the OnUpdate()
function. ECS uses the variables inside the Execute()
method when it runs the job.
The job also uses the Unity delta time to animate the rotation of a 3D object. The example uses a struct field to pass this value to the Execute()
method.
Writing the Execute method
The signature of the IJobChunk Execute()
method is:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
The chunk
parameter is a handle to the block of memory that contains the entities and components that this iteration of the job has to process. Because a chunk can only contain a single archetype, all of the entities in a chunk have the same set of components.
Use the chunk
parameter to get the NativeArray instances for components:
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
These arrays are aligned so that an entity has the same index in all of them. You can then use a normal for loop to iterate through the component arrays. Use chunk.Count
to get the number of entities stored in the current chunk:
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
for (var i = 0; i < chunk.Count; i++)
{
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
// Rotate something about its up vector at the speed given by RotationSpeed.
chunkRotations[i] = new Rotation
{
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
If you have the Any
filter in your EntityQueryDesc or have completely optional components that don’t appear in the query at all, you can use the ArchetypeChunk.Has<T>()
function to test whether the current chunk contains one of those components before you use it:
if (chunk.Has<OptionalComp>(OptionalCompType))
{//...}
Note: If you use a concurrent entity command buffer, pass the chunkIndex
argument as the jobIndex
parameter to the command buffer functions.
Skipping chunks with unchanged entities
If you only need to update entities when a component value has changed, you can add that component type to the change filter of the EntityQuery that selects the entities and chunks for the job. For example, if you have a system that reads two components and only needs to update a third when one of the first two has changed, you can use a EntityQuery as follows:
private EntityQuery m_Query;
protected override void OnCreate()
{
m_Query = GetEntityQuery(
ComponentType.ReadWrite<Output>(),
ComponentType.ReadOnly<InputA>(),
ComponentType.ReadOnly<InputB>());
m_Query.SetChangedVersionFilter(
new ComponentType[]
{
ComponentType.ReadWrite<InputA>(),
ComponentType.ReadWrite<InputB>()
});
}
The EntityQuery change filter supports up to two components. If you want to check more or you aren't using a EntityQuery, you can make the check manually. To make this check, use the ArchetypeChunk.DidChange()
function to compare the chunk’s change version for the component to the system's LastSystemVersion
. If this function returns false, you can skip the current chunk altogether because none of the components of that type have changed since the last time the system ran.
You must use a struct field to pass the LastSystemVersion
from the system into the job, as follows:
[BurstCompile]
struct UpdateJob : IJobChunk
{
public ArchetypeChunkComponentType<InputA> InputAType;
public ArchetypeChunkComponentType<InputB> InputBType;
[ReadOnly] public ArchetypeChunkComponentType<Output> OutputType;
public uint LastSystemVersion;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var inputAChanged = chunk.DidChange(InputAType, LastSystemVersion);
var inputBChanged = chunk.DidChange(InputBType, LastSystemVersion);
// If neither component changed, skip the current chunk
if (!(inputAChanged || inputBChanged))
return;
var inputAs = chunk.GetNativeArray(InputAType);
var inputBs = chunk.GetNativeArray(InputBType);
var outputs = chunk.GetNativeArray(OutputType);
for (var i = 0; i < outputs.Length; i++)
{
outputs[i] = new Output { Value = inputAs[i].Value + inputBs[i].Value };
}
}
}
As with all the job struct fields, you must assign its value before you schedule the job:
protected override void OnUpdate()
{
var job = new UpdateJob();
job.LastSystemVersion = this.LastSystemVersion;
job.InputAType = GetArchetypeChunkComponentType<InputA>(true);
job.InputBType = GetArchetypeChunkComponentType<InputB>(true);
job.OutputType = GetArchetypeChunkComponentType<Output>(false);
this.Dependency = job.ScheduleParallel(m_Query, this.Dependency);
}
Note: For efficiency, the change version applies to whole chunks not individual entities. If another job which has the ability to write to that type of component accesses a chunk, then ECS increments the change version for that component and the DidChange()
function returns true. ECS increments the change version even if the job that declares write access to a component does not actually change the component value.
Instantiate and schedule the job
To run an IJobChunk job, you must create an instance of your job struct, setting the struct fields, and then schedule the job. When you do this in the OnUpdate()
function of a SystemBase implementation, the system schedules the job to run every frame.
protected override void OnUpdate()
{
var job = new RotationSpeedJob()
{
RotationType = GetArchetypeChunkComponentType<Rotation>(false),
RotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>(true),
DeltaTime = Time.DeltaTime
};
this.Dependency = job.ScheduleParallel(m_Query, this.Dependency);
}
When you call the GetArchetypeChunkComponentType<T>()
function to set your component type variables, make sure that you set the isReadOnly
parameter to true for components that the job reads, but doesn’t write. Setting these parameters correctly can have a significant impact on how efficiently the ECS framework can schedule your jobs. These access mode settings must match their equivalents in both the struct definition, and the EntityQuery.
Do not cache the return value of GetArchetypeChunkComponentType<T>()
in a system class variable. You must call the function every time the system runs, and pass the updated value to the job.