Gravity Sim Job System - Video 04 - GravitySystem

Video on YouTube

In this video, we discuss the GravitySystem class and how it completes our mini-ECS implementation and its responsibility in the overall program. The system manages the job code, the data pipeline, the memory and JobHandles needed to make this work. We should note that this Mini-ECS only solves for one component and one system and the entities are fixed at start-up. A little more work is required to allow entities to be added or removed and a fair bit more is needed to support multiple component types and systems.

That said, it achieves what was intended, demonstrate the use of Unity.Mathematics, albeit in a very focused way, and how entites, components and systems could work with the C# Job System as the foundation.

Concepts

We compare this to the GravityController which is a MonoBehaviour class. The GravityController handles integration with Unity for Start(), Update() and OnDestroy() lifecycle events. It also create the GravitySystem instance.

The GravitySystem sets the jobs up with a daisy-chain of dependencies to form a data pipeline. The jobs in the pipeline were discussed in Video 2 and Video 3 but it’s the GravitySystem that makes them work this way and, in the process, makes the computation and movement of the objects work completely outside the Unity main thread.

The public methods of the GravitySystem are used by the GravityController to manage the lifecycle of the system with the lifecycle of the game environment.

Code

Here is the code for the GravitySystem class. Note one thing - you will see a constant declaring the number of threads for the TransformAccessArray - best to think of this as a desired level of concurrency. The Unity engine will apply this based on the number of worker threads it creates and that can differ by CPU.

GravitySystem.cs

using UnityEngine;
using UnityEngine.Jobs;

using Unity.Mathematics;
using Unity.Jobs;
using Unity.Collections;

public class GravitySystem : System.IDisposable
{
    private NativeArray<GravityComponent> _Components;
    private NativeArray<float3> _Forces;
    private TransformAccessArray _Transforms;

    const int NUMBER_THREADS = 10;

    private JobHandle
        _ForceJob,
        _PhysicsJob,
        _TranslationJob;

    public GravitySystem(GravityBehaviour[] gravityObjects)
    {
        _Components = new NativeArray<GravityComponent>(
            GetGravityComponents(gravityObjects),
            Allocator.Persistent
        );

        _Transforms = new TransformAccessArray(
            GetTransforms(gravityObjects), NUMBER_THREADS
        );

        _Forces = new NativeArray<float3>(gravityObjects.Length, Allocator.Persistent);
    }

    public void UpdateSystem(float repulsionRadius, float gravityStrength)
    {
        if ( IsPreviousPipelineRunComplete() )
        {
            UnlockAllArrays();
            UpdateForcesJob(repulsionRadius, gravityStrength);
            UpdatePhysicsJob();
            UpdateTranslationsJob();
        }
    }

    public void Dispose()
    {
        JobHandle.ScheduleBatchedJobs(); /// Force pending jobs to begin
        UnlockAllArrays();

        _Forces.Dispose();
        _Components.Dispose();
        _Transforms.Dispose();
    }

    private bool IsPreviousPipelineRunComplete()
        =>  _ForceJob.IsCompleted
            && _PhysicsJob.IsCompleted
            && _TranslationJob.IsCompleted;

    private void UnlockAllArrays()
        => JobHandle.CompleteAll(ref _ForceJob, ref _PhysicsJob, ref _TranslationJob);

    private void UpdateForcesJob(float repulsionRadius, float gravityStrength)
        =>  _ForceJob = GravityForceJob.Begin(
                _Components,
                _Forces,
                repulsionRadius,
                gravityStrength
            );

    private void UpdatePhysicsJob()
        =>  _PhysicsJob = GravityAccelerationVelocityAndPositionJob.Begin(
                _Forces, _Components, _ForceJob
            );

    private void UpdateTranslationsJob()
        =>  _TranslationJob = TranslationJob.Begin(
                _Components, _Transforms, _PhysicsJob
            );

    private T[] GetDataFromBehaviour<T>(
        GravityBehaviour[] behavioursArray,
        System.Func<GravityBehaviour, T> query
    )
    {
        T[] data = new T[behavioursArray.Length];
        for (int index = 0; index < behavioursArray.Length; index++)
        {
            data[index] = query( behavioursArray[index] );
        }
        return data;
    }

    private GravityComponent[] GetGravityComponents(GravityBehaviour[] behavioursArray)
        =>  GetDataFromBehaviour<GravityComponent>(
                behavioursArray, (behaviour) => behaviour.GravitySettings
            );

    private Transform[] GetTransforms(GravityBehaviour[] behavioursArray)
        =>  GetDataFromBehaviour<Transform>(
                behavioursArray, (behaviour) => behaviour.GetComponent<Transform>()
            );
}