Skip to content

Inheritance to generic struct

What

Type (class) inheritance can be replaced using generics.

Why

C# struct types do not support inheritance and class reference types cannot be Burst compiled. This guide illustrates an alternative way to achieve a similar effect to inheritance using generics. This also improves performance because it removes this virtual table lookup at runtime by replacing it by compile time polymorphism.

How

Instead of providing virtual methods to a type we define an interface specifying those methods and create a type generic over that interface.

Runtime polymorphism:

abstract partial class SomeMovementSystemBase : SystemBase {
    protected abstract void UpdatePosition(RefRW<Position> position);

    protected override void OnUpdate() {
        foreach (RefRW<Position> position in SystemAPI.Query<RefRW<Position>>().WithAll<SomeTag>()) {
            UpdatePosition(position);
        }
    }
}

sealed partial class LinearMovementSystem : SomeMovementSystemBase {
    protected override void UpdatePosition(RefRW<Position> position) {
        position.ValueRW.Value += World.Time.DeltaTime * new float2(1, 0);
    }
}

This can be converted to compile-time polymorphism:

interface IPositionUpdate {
    void UpdatePosition(ref SystemState state, RefRW<Position> position);
}

[BurstCompile]
partial struct MovementSystem<TPositionUpdate> : ISystem 
    where TPositionUpdate : struct, IPositionUpdate
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state) {
        TPositionUpdate updater = new();
        foreach (RefRW<Position> position in SystemAPI.Query<RefRW<Position>>().WithAll<SomeTag>()) {
            updater.UpdatePosition(ref state, position);
        }
    }
}

struct LinearPositionUpdate : IPositionUpdate {
    public void UpdatePosition(ref SystemState state, RefRW<Position> position) {
        position.ValueRW.Value += state.WorldUnmanaged.Time.DeltaTime * new float2(1, 0);
    }
}

// To use this generic system you have to declare it in "AssemblyInfo.cs".
[assembly: RegisterGenericSystemType(typeof(MovementSystem<LinearPositionUpdate>))]

Footnotes

  • Not all polymorphism can be moved to compile-time. Only when the concrete type used is known ahead of runtime this can be done. But a similar effect can be achieved using function pointers.
  • Sometimes we want to modify the state of a base type but the interface does not have access. To achieve this we can store the state in a separate struct and pass that struct by ref to the interface method.
  • We cannot use a generic type parameter with SystemAPI (i.e. specify the above tag as a generic parameter) because Entities code generation does not support this (yet).
  • This will eventually become more usable once SystemAPI is not limited to systems but available in functions that have access to ref SystemState.