SystemBase
to ISystem
¶
What¶
Systems can be written in two ways, either as managed class deriving from SystemBase
or as unmanaged struct implementing ISystem
. To achieve best performance systems should use ISystem
where possible.
Why¶
Systems using ISystem
provide better performance over SystemBase
since they are Bust compatible and because dependency handling and job scheduling is slower (Link). It's also beneficial for the clear separation of the unmanaged DOTS layer since ISystem
does not support managed memory. Additionally compilation time of ISystem
is less due to decreased complexity of the source generation logic.
How¶
Migrating a system to ISystem
is only possible when it does not require managed memory access. Therefore check whether managed components can be converted to unmanaged e.g. replacing managed array data with dynamic buffer or asset blobs for immutable data. Use of allocating unmanaged containers is inadvisable since they are not automatically disposed when the component is removed and manual handling of disposal like cleanup components or ISystem.OnDestroy
is necessary.
The use of managed containers in a system can be replaced by unmanaged containers from "Unity.Collections".
Additional patterns that provide help migrating can be found here.
Comparison¶
Since ISystem
is an interface implementing systems have to access the system state directly which is passed as a ref
parameter to the event callback methods:
internal sealed partial class ExampleSystem : SystemBase {
protected override void OnCreate() {
RequireForUpdate<PositionComponent>();
}
protected override void OnDestroy() { }
protected override void OnUpdate() { }
}
[BurstCompile]
internal partial struct ExampleSystem : ISystem {
[BurstCompile]
public void OnCreate(ref SystemState state) {
state.RequireForUpdate<PositionComponent>();
}
[BurstCompile]
public void OnDestroy(ref SystemState state) { }
[BurstCompile]
public void OnUpdate(ref SystemState state) { }
}
Differences between systems¶
Feature | ISystem | SystemBase |
---|---|---|
Burst compile OnCreate , OnUpdate , and OnDestroy |
Yes | No |
Unmanaged memory allocated | Yes | No |
GC allocated | No | Yes |
Can store managed data directly in system type | No | Yes |
Idiomatic foreach | Yes | Yes |
Entities.ForEach | No | Yes |
Job.WithCode | No | Yes |
IJobEntity | Yes | Yes |
Supports inheritance | No | Yes |
Create entity queries¶
ISystem
does not provide the Entities
property so entity queries should be created using EntityQueryBuilder
or SystemAPI.QueryBuilder
. Beware that the latter generates an inaccessible backing field through code generation and it should only be used when a query is not referenced by multiple callback methods i.e in OnCreate
and OnUpdate
.
internal sealed partial class ExampleSystem : SystemBase {
private EntityQuery _query;
protected override void OnCreate() {
_query = Entities.WithAll<PositionComponent>().ToQuery().
}
}
internal partial struct ExampleSystem : ISystem {
private EntityQuery _query;
public void OnCreate(ref SystemState state) {
_query = new EntityQueryBuilder(Allocator.Temp)
.WithAll<PositionComponent>()
.Build(ref state);
}
}
To illustrate what SystemAPI.QueryBuilder
does let's look at the generated code:
internal partial struct ExampleSystem : ISystem {
public void OnUpdate(ref SystemState state) {
EntityQuery query = SystemAPI.QueryBuilder().WithAll<PositionComponent>().Build();
}
}
Excerpt of generated code - notice that the query is created once and cached for use in OnUpdate
:
internal struct ExampleSystem : SystemBase {
Unity.Entities.EntityQuery __query_1369318364_1;
void __AssignQueries(ref global::Unity.Entities.SystemState state) {
var entityQueryBuilder =
new global::Unity.Entities.EntityQueryBuilder(global::Unity.Collections.Allocator.Temp);
__query_1369318364_1 = entityQueryBuilder
.WithAll<global::Stratkit.Army.EntityGraphics.ArmyVisualPosition>()
.Build(ref state);
entityQueryBuilder.Reset();
entityQueryBuilder.Dispose();
}
protected override void OnCreateForCompiler() {
base.OnCreateForCompiler();
__AssignQueries(ref this.CheckedStateRef);
}
void __OnUpdate_1817F1CB() {
EntityQuery query = __query_1369318364_1;
}
}
Iterate component data / Convert Entities.ForEach
¶
Unmanaged systems don't support Entities.ForEach
to iterate over component data. There are multiple options to choose from, the most straight forward ways are SystemAPI.Query
with idiomatic foreach
to iterate data on the main thread or IJobEntity
to use the jobs system executing in the thread worker pool, optionally in parallel.
Synchronous iteration SystemBase
¶
internal sealed partial class ExampleSystem : SystemBase {
protected override void OnUpdate() {
Entities
.WithAll<ArmyTag>()
.ForEach((ref Position position, in Velocity velocity) => {
position.Value += World.Time.DeltaTime * velocity.Value;
})
.Run();
}
}
Synchronous iteration ISystem
¶
internal partial struct ExampleSystem : ISystem {
public void OnCreate(ref SystemState state) {
foreach ((RefRW<Position> position, RefRO<Velocity> velocity) in SystemAPI
.Query<RefRW<Position>, RefRO<Velocity>>()
.WithAll<ArmyTag>()
) {
position.ValueRW.Value += SystemAPI.Time.DeltaTime * velocity.ValueRO.Value;
}
}
}
Asynchronous and parallel iteration SystemBase
¶
internal sealed partial class ExampleSystem : SystemBase {
protected override void OnUpdate() {
Entities
.WithAll<ArmyTag>()
.ForEach((ref Position position, in Velocity velocity) => {
position.Value += World.Time.DeltaTime * velocity.Value;
})
.ScheduleParallel();
}
}
Asynchronous and parallel iteration ISystem
¶
[WithAll(typeof(ArmyTag))]
internal partial struct UpdatePositionJob : IJobEntity {
private void Execute(ref Position position, in Velocity velocity) {
position.Value += World.Time.DeltaTime * velocity.Value;
}
}
internal partial struct ExampleSystem : ISystem {
public void OnUpdate(ref SystemState state) {
new UpdatePositionJob().ScheduleParallel();
}
}