Unity DOTS: Creating Generic Systems with Generic Jobs

Summary

Generic systems can update multiple types of components with the same logic.

Since generic parameters are not currently supported in Entities.ForEach(), we must use IJobChunk to implement generic systems. Also, the generic parameters have to be declared with unmanaged and IComponentData type constraints.

To access the data in the components, one way is making the components implementing the same interface, and adding the interface as a type constraint to the generic system.

Environment

  • Unity 2020.1.0f1
  • Entities 0.13.0-preview.24
  • Burst 1.3.2
  • Jobs 0.4.0-preview.18

Tutorial

Assuming we want to create a generic system which can print value of multiple types of components, the first step is to create a system with a generic parameter.

Creating Generic System

public class GenericPrintSystem<T> : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        
    }
}

Then, when we add Entities.ForEach() as usual:

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        JobHandle handle = Entities.ForEach((in T comp) =>
        {

        }).Schedule(inputDeps);

        return handle;
    }

An error message says that Entities.ForEach() is currently not supporting generic parameters:

Assets\Scripts\GenericPrintSystem.cs(16,9): error DC0053: Entities.ForEach cannot be used in system GenericPrintSystem`1 as Entities.ForEach in generic system types are not supported.

Instead, we have to create a generic job implementing IJobChunk.

Creating Generic Job

[BurstCompatible]
public struct GenericPrintJob<T> : IJobChunk
{
    [ReadOnly] public ComponentTypeHandle<T> GenericType;

    public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    {
        NativeArray<T> generics = chunk.GetNativeArray(GenericType);
        for (int i = 0; i < generics.Length; i++)
        {
        
        }
    }
}

And the console will complain:

Assets\Scripts\GenericPrintJob.cs(13,21): error CS0453: The type 'T' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'NativeArray<T>'

In this case, we need struct and IComponentData type constraints to call GetNativeArray().

public struct GenericPrintJob<T> : IJobChunk
    where T : struct, IComponentData
{

However, we don’t know what fields T contains in at compile time, so it’s not possible to read data from component T.

Accessing Data with Interface

Fortunately, apart from knowing exactly what fields the type contains, declaring an interface for the data we need is also a solution.

public interface IData
{
    int GetValue();
}

And add this interface to the type constraints:

public struct GenericPrintJob<T> : IJobChunk
    where T : struct, IComponentData, IData
{

Then we can get the value and print it:

    public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    {
        NativeArray<T> generics = chunk.GetNativeArray(GenericType);
        for (int i = 0; i < generics.Length; i++)
        {
            Debug.Log(generics[i].GetValue());
        }
    }

Creating Component

To create a component which is compatible to our formerly created job, it must implement interface IData.

[GenerateAuthoringComponent]
public struct ValueComponent : IComponentData, IData
{
    public int Value;

    public int GetValue()
    {
        return Value;
    }
}

Finishing Generic System

With the generic job, we can schedule this job in the generic system.

    private EntityQuery GenericQuery;

    protected override void OnCreate()
    {
        GenericQuery = GetEntityQuery(ComponentType.ReadOnly<T>());
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        GenericPrintJob<T> job = new GenericPrintJob<T>
        {
            GenericType = GetComponentTypeHandle<T>(true),
        };
        
        return job.Schedule(GenericQuery, inputDeps);
    }

Make sure the type constraints are added to the system as well.

public class GenericPrintSystem<T> : JobComponentSystem
    where T : unmanaged, IComponentData, IData
{

Inheriting Generic System

The generic system won’t run by itself since it doesn’t know what component it should process. We need to inherit the system with ValueComponent assigned.

public class ValuePrintSystem : GenericPrintSystem<ValueComponent> { }

There is nothing more to implement so it’s all good with this.

Complete Example

This example demonstrates a generic system, GenericPrintSystem, which is capable of printing any component implementing IData interface.

Generic Print System

public class GenericPrintSystem<T> : JobComponentSystem
    where T : struct, IComponentData, IData
{
    private EntityQuery GenericQuery;

    protected override void OnCreate()
    {
        GenericQuery = GetEntityQuery(ComponentType.ReadOnly<T>());
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        GenericPrintJob<T> job = new GenericPrintJob<T>
        {
            GenericType = GetComponentTypeHandle<T>(true),
        };

        return job.Schedule(GenericQuery, inputDeps);
    }
}

Generic Print Job

public struct GenericPrintJob<T> : IJobChunk
    where T : struct, IComponentData, IData
{
    [ReadOnly] public ComponentTypeHandle<T> GenericType;

    public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    {
        NativeArray<T> generics = chunk.GetNativeArray(GenericType);
        for (int i = 0; i < generics.Length; i++)
        {
            Debug.Log(generics[i].GetValue());
        }
    }
}

Value Print System

public class ValuePrintSystem : GenericPrintSystem<ValueComponent> { }

IData

public interface IData
{
    int GetValue();
}

ValueComponent

[GenerateAuthoringComponent]
public struct ValueComponent : IComponentData, IData
{
    public int Value;

    public int GetValue()
    {
        return Value;
    }
}

Restriction

According to the Burst documentation, Burst has limited support for generics. The Burst compiler can only detect generic jobs with fully resolved generic arguments, such as GenericPrintJob<ValueComponent>, while GenericPrintJob<T> won’t be detected.

(Thanks to @BennetJeutter for pointing out this restriction.)

Leave a Comment