Skip to content

7. Parallel Systems

Knowlife4 edited this page Mar 10, 2023 · 3 revisions

WARNING: NOT RECOMMENDED FOR BEGINNERS

Multithreaded code can be very important when dealing with large quantities of entities.

ECT tackles this with a feature called Parallel Systems.

Parallel Systems allow you to run many of the same systems across multiple entities using multithreading.

If you are wondering why this isn't used on all systems by default, that's because it requires a change in syntax and complexity.

Creating a Parallel System

Creating a parallel system is identical to creating a standard one except you inherit from System<>.Parallel<> instead.

Ex. Instead of System<PlayerMovement> derive from System<PlayerMovement>.Parallel<>.

The first of these generics is your component, and the second is our next topic:

Parallel Data Structures

Parallel Systems actually introduce a new type to the ECT hierarchy, Parallel Data Structures.

When working with Parallel Systems, the Parallel Data Structure is now responsible for modifying data, and your system is now responsible for populating the Parallel Data Structure and extracting the results.

Parallel Data Structures should be defined within the corresponding component of the system.

Start by creating a public struct that inherits from IParallelData<>.

The generic type should be the Parallel Data Structure itself.

Ex. public struct ParallelData : IParallelData<ParallelData>

In here is where you should define your fields necessary for execution.

Note: If you are looking for native performance, you will need to keep all of these as data types. (Use ECTParallelTransform for transforms).

Parallel Data Structures have a single method, public ParallelData Execute(NativeArray<ParallelData> data).

This is where all of your logic should be contained.

Passing Data

To pass data into your Parallel Data Structure, you need to override the public UpdateData(ref data) method like so:

public override void PopulateData(ref ParallelData data)
{

}

Now just assign all of the data within the Parallel Data Structure.

public override void PopulateData(ref ParallelData data)
{
    data.Transform = transform;
    data.Target = target.transform;
    data.Speed = Component.speed;
    data.DeltaTime = Time.deltaTime;
}

Scheduling Data

To schedule your Parallel Data Structure, override the public method Schedule(NativeArray<ParallelData> dataArray) with the following API call like so:

public override void Schedule(NativeArray<ParallelData> dataArray) => ParallelConfigAPI.Create(dataArray).Run();

Note: If you are aiming for Native performance (Burst), make sure to use the burst parameter as true in the Run() method.

Retrieving Data

To extract data from the Parallel Data Structure after it executes, override the ExtractData(data) method like so:

public override void OnComplete(ParallelData data)
{

}

Now use the data from the Parallel Data Structure:

public override void OnComplete(ParallelData data)
{
    transform.rotation = data.Transform.rotation;
}

Ta-Da!

That's it! You have written a functional Parallel System.

Here's an example of all of this done correctly:

public class ParallelSystem : System<PlayerMovementRotate>.Parallel<Data>
{
    Transform transform;

    SceneReference reference;

    protected override IValidation[] Validations => new[]
    {
        QueryReference(out reference),
        ValidateReference(Root.transform, out transform),
    };

    protected override void PopulateData(ref Data data)
    {
        data.Transform = transform;
        data.Target = reference.Target;
        data.Speed = Component.Speed;
        data.DeltaTime = Time.deltaTime;
    }

    protected override void ExtractData(Data data) => transform.rotation = data.Transform.rotation;

    protected override void Schedule(NativeArray<Data> dataArray) => ParallelConfigAPI.Create(dataArray).Run();
}

public struct Data : IParallelData<Data>
{
    public ECTParallelTransform Transform;
    public ECTParallelTransform Target;

    public float Speed;
    public float DeltaTime;

    public Data Execute(NativeArray<Data> dataArray)
    {
        float3 direction = Target.position - Transform.position;

        float rotationSpeed = Speed * DeltaTime;
        float3 up = new(0f, 1f, 0f);

        Transform.rotation = math.slerp(Transform.rotation, quaternion.LookRotationSafe(direction, up), rotationSpeed);

        return this;
    }
}

System