Skip to content

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

From Unity docs.

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();
    }
}