Iterate over component data with SystemAPI.Query
To iterate through a collection of data on the main thread, you can use the SystemAPI.Query<T>
method in both ISystem
and SystemBase
system types. It uses C#’s idiomatic foreach
syntax.
You can overload the method with up to seven type parameters. The supported type parameters are:
IAspect
IComponentData
ISharedComponentData
DynamicBuffer<T>
RefRO<T>
RefRW<T>
EnabledRefRO<T>
where T :IEnableableComponent
,IComponentData
EnabledRefRW<T>
where T :IEnableableComponent
,IComponentData
SystemAPI.Query implementation
Whenever you invoke SystemAPI.Query<T>
, the source generator solution creates an EntityQuery
field on the system itself. It also caches an EntityQuery
that consists of the queried types, with their respective read-write/read-only access modes in this field. During compilation, the source-generation solution replaces the SystemAPI.Query<T>
invocation in a foreach
statement with an enumerator that iterates through the cached query’s data.
Additionally, the source-generation solution caches all the required type handles, and automatically injects TypeHandle.Update(SystemBase system)
or TypeHandle.Update(ref SystemState state)
as necessary before every foreach
. This ensures that the type handles are safe to use.
The source generators also generate code to automatically complete all necessary read and read-write dependencies before each foreach
statement.
Query data
The following is an example that uses SystemAPI.Query
to iterate through every entity that has both LocalTransform
and RotationSpeed
components:
public partial struct MyRotationSpeedSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
}
}
Because the example modifies the LocalTransform
data, it's wrapped inside RefRW<T>
, as a read-write reference. However, because it only reads the RotationSpeed
data, it uses RefRO<T>
. RefRO<T>
usage is entirely optional and you can use the following instead as valid code:
float deltaTime = SystemAPI.Time.DeltaTime;
foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RotationSpeed>())
transform.ValueRW = transform.ValueRO.RotateY(speed.RadiansPerSecond * deltaTime);
RefRW<T>.ValueRW
, RefRW<T>.ValueRO
, and RefRO<T>.ValueRO
all return a reference to the component. When called, ValueRW
conducts a safety check for read-write access, and ValueRO
does the same for read-only access.
Accessing entities in the foreach statement
Unity.Entities.Entity
isn't a supported type parameter. Every query is already an implicit filter of all existing entities. To get access to the entity, use WithEntityAccess
. For example:
foreach (var (transform, speed, entity) in SystemAPI.Query<RefRW<LocalToWorld>, RefRO<RotationSpeed>>().WithEntityAccess())
{
// Do stuff;
}
Note that the Entity
argument comes last in the returned tuple.
Known limitations
SystemAPI.Query
has the following limitations, which are outlined below.
Dynamic buffer read-only limitation
DynamicBuffer<T>
type parameters in SystemAPI.Query<T>
are read-write access by default. However, if you want read-only access, you have to create your own implementation, similar to the following:
var bufferHandle = state.GetBufferTypeHandle<MyBufferElement>(isReadOnly: true);
var myBufferElementQuery = SystemAPI.QueryBuilder().WithAll<MyBufferElement>().Build();
var chunks = myBufferElementQuery.ToArchetypeChunkArray(Allocator.Temp);
foreach (var chunk in chunks)
{
var numEntities = chunk.Count;
var bufferAccessor = chunk.GetBufferAccessor(ref bufferHandle);
for (int j = 0; j < numEntities; j++)
{
var dynamicBuffer = bufferAccessor[j];
// Read from dynamicBuffer and perform various operations
}
}
Reusing SystemAPI.Query
You can't store SystemAPI.Query<T>
in a variable and then use it in multiple foreach
statements: there isn't a way to reuse SystemAPI.Query
. This is because the implementation of the API relies on knowing what the query types are at compile time. The source-generation solution doesn't know at compile-time what EntityQuery
to generate and cache, which type handles to call Update
on, nor which dependencies to complete.