C# Job System Example With MonoBehaviour

Before starting this post, We recommended you to take a look in part 1 of the c# job system. Where we are discussed the brief about how it is useful for parallel processing and memory optimization.

We will give a brief example of IJob, IJobParallelFor, IJobParallelForTransform. So, let’s Start with IJob.

IJob

IJob is the primary interface of c# job system.

public struct SimpleJob: IJob
{
    public float number;

    public NativeArray<float> data;

    public void Execute()
    {
        data[0] += number;
    }
}

Above, we define a simple job which add a number in the 0th position of the data.

Now have a look at how we can use this Simple Job with monobehaviour using c# job system.

public class SimpleJobSystemDemo : MonoBehaviour
{
    // Float to adds in the myData
    private float myNumber = 5;
    // Simple native container of type float.
    private NativeArray<float> myData;
    // Handle use to operate job in main thread.
    private JobHandle simpleJobHandle;
}

In above code we will declare a data in the monobehaviour, which is later used in the simple job.

private void OnEnable()
{
     myData = new NativeArray<float>(1, Allocator.Persistent);
     myData[0] = 2;
}

Developer is responsible for disposing of any memory allocated during the operations. hence, we must disposed the memory of persistent allocator after finishing a job.

private void Start()
{
        // Simple job declaration with the data.
        SimpleJob simpleJob = new SimpleJob
        {
            number = myNumber,
            data = myData
        };

        // Schedule a simple Job (Added in the queue, not running)
        simpleJobHandle = simpleJob.Schedule();
        
        // Run the schedule job.
        JobHandle.ScheduleBatchedJobs();
        
        // Wait for the job to completed.
        simpleJobHandle.Complete();
        
        // Check if completed or not and used data from the job result.
        if (simpleJobHandle.IsCompleted)
        {
            Debug.Log(simpleJob.data[0]);
        }
}

Above we used a simple job to schedule in job queue and then batched to run on any core. After completed this job we can used result data for any other operations. Where does it all goes? let’s see full code.

using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class SimpleJobSystemDemo : MonoBehaviour
{
    private float myNumber = 5;
    private NativeArray<float> myData;

    private JobHandle simpleJobHandle;

    private void OnEnable()
    {
        myData = new NativeArray<float>(1, Allocator.Persistent);
        myData[0] = 2;
    }

    private void OnDisable()
    {
        myData.Dispose();
    }

    private void Start()
    {
        SimpleJob simpleJob = new SimpleJob
        {
            number = myNumber,
            data = myData
        };

        simpleJobHandle = simpleJob.Schedule();

        JobHandle.ScheduleBatchedJobs();

        simpleJobHandle.Complete();

        if (simpleJobHandle.IsCompleted)
        {
            Debug.Log(simpleJob.data[0]);
        }
    }
}

public struct SimpleJob: IJob
{
    public float number;

    public NativeArray<float> data;

    public void Execute()
    {
        data[0] += number;
    }
}

IJobParallelFor

In a game, we want to perform same operations on large data. This is the term parallel comes in the world. In addition, large operations can be performed on parallel job.

using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class ParallelJobSystemDemo : MonoBehaviour
{
    private float myNumber = 0;
    private NativeArray<float> myData;

    private JobHandle parallelJobHandle;

    private void OnEnable()
    {
        myData = new NativeArray<float>(100, Allocator.TempJob);
        
        // Fill the data in the array
        for (int i = 0; i < myData.Length; i++)
        {
            myData[i] = i;
        }
    }

    private void Start()
    {
        ParallelJob parallelJob = new ParallelJob
        {
            number = myNumber,
            data = myData
        };

        //Parameter : length , number of iterations to run in one “batch” on a single core
        parallelJobHandle = parallelJob.Schedule(myData.Length, 32);

        JobHandle.ScheduleBatchedJobs();

        parallelJobHandle.Complete();

        if (parallelJobHandle.IsCompleted)
        {
            for (int i = 0; i < myData.Length; i++)
            {
                Debug.Log(parallelJob.data[i]);
            }
        }

        myData.Dispose();
    }
}

public struct ParallelJob : IJobParallelFor
{
    public float number;

    public NativeArray<float> data;

    // Operations done on large data
    public void Execute(int index)
    {
        data[index] += number;
    }
}

Scheduling a parallel job can take two arguments commonly a length and a number of iterations to run in one “batch” on a single core. Parallel job is runs on more than one core at the same time.

IJobForParallelTransform

This is a another parallel job specially design to operate on Transform. Let’s you elaborate with following example.

using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Jobs;

public class TransformJobSystemDemo : MonoBehaviour
{
    private float myNumber = 0.5f;
    private NativeArray<float> myData;

    private JobHandle transformJobHandle;
    private TransformAccessArray transformAccessArray;

    private void OnEnable()
    {
        myData = new NativeArray<float>(1, Allocator.Persistent);

        for (int i = 0; i < myData.Length; i++)
        {
            myData[i] = i;
        }

        Transform[] myTransforms = { transform };
        transformAccessArray = new TransformAccessArray(myTransforms);
    }

    private void OnDisable()
    {
        myData.Dispose();
        transformAccessArray.Dispose();
    }

    private void Update()
    {
        TransformJob transformJob = new TransformJob
        {
            number = myNumber,
            data = myData,
            deltaTime = Time.deltaTime
        };

        transformJobHandle = transformJob.Schedule(transformAccessArray);

        JobHandle.ScheduleBatchedJobs();

        transformJobHandle.Complete();

        if (transformJobHandle.IsCompleted)
        {
            Debug.Log("Transform Job Completed!");
        }
    }
}

public struct TransformJob: IJobParallelForTransform
{
    public float number;

    public float deltaTime;

    public NativeArray<float> data;

    public void Execute(int index, TransformAccess transform)
    {
        transform.localPosition += Vector3.one * (data[index] + number) * deltaTime;
    }
}

This job can change the position of the gameobject by the given value. These all three example are a basic and independent jobs. What about an dependent job? check out below.

Simple dependency Jobs

Simple dependency is when result of one job dependent on another job. Following example shown a simple dependency like a->b->c.

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using Unity.Jobs;
using UnityEngine.Jobs;

public class DependencyDemo : MonoBehaviour
{
    private float myNumber = 5;
    private NativeArray<float> myData;

    private JobHandle simpleJobHandle;
    private JobHandle parallelJobHandle;
    private JobHandle transformJobHandle;

    private TransformAccessArray transformAccessArray;

    private void OnEnable()
    {
        myData = new NativeArray<float>(100, Allocator.Persistent);

        for (int i = 0; i < myData.Length; i++)
        {
            myData[i] = i;
        }

        Transform[] myTransforms = { transform };
        transformAccessArray = new TransformAccessArray(myTransforms);
    }

    private void Start()
    {
        SimpleJob simpleJob = new SimpleJob
        {
            number = myNumber,
            data = myData
        };

        ParallelJob parallelJob = new ParallelJob
        {
            number = myNumber,
            data = myData
        };

        TransformJob transformJob = new TransformJob
        {
            number = myNumber,
            data = myData,
            deltaTime = Time.deltaTime
        };

        simpleJobHandle = simpleJob.Schedule();
        parallelJobHandle = parallelJob.Schedule(myData.Length, 32, simpleJobHandle);
        transformJobHandle = transformJob.Schedule(transformAccessArray, parallelJobHandle);

        JobHandle.ScheduleBatchedJobs();

        simpleJobHandle.Complete();
        parallelJobHandle.Complete();
        transformJobHandle.Complete();

        if (simpleJobHandle.IsCompleted)
        {
            Debug.Log("Simple Job Result: " + simpleJob.data[0]);
        }

        if (parallelJobHandle.IsCompleted)
        {
            for (int i = 0; i < myData.Length; i++)
            {
                Debug.Log("Parallel Job Result: " + parallelJob.data[i]);
            }
        }

        myData.Dispose();
        transformAccessArray.Dispose();
    }
}

Combine dependency Jobs

It is possible that two or more job can be dependent on another job.for example ab->c.

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Jobs;

public class CombineDependancyDemo : MonoBehaviour
{
    private float myNumber = 5;
    private NativeArray<float> mySimpleData;
    private NativeArray<float> myParallelData;
    private NativeArray<float> myTransfromData;

    private JobHandle simpleJobHandle;
    private JobHandle parallelJobHandle;
    private JobHandle transformJobHandle;

    private TransformAccessArray transformAccessArray;

    private void OnEnable()
    {
        mySimpleData = new NativeArray<float>(1, Allocator.Persistent);
        mySimpleData[0] = 5f;

        myParallelData = new NativeArray<float>(100, Allocator.Persistent);

        for (int i = 0; i < myParallelData.Length; i++)
        {
            myParallelData[i] = i;
        }

        myTransfromData = new NativeArray<float>(50, Allocator.Persistent);

        for (int i = 0; i < myTransfromData.Length; i++)
        {
            myTransfromData[i] = i;
        }

        Transform[] myTransforms = { transform };
        transformAccessArray = new TransformAccessArray(myTransforms);
    }

    private void Start()
    {
        SimpleJob simpleJob = new SimpleJob
        {
            number = myNumber,
            data = mySimpleData
        };

        ParallelJob parallelJob = new ParallelJob
        {
            number = myNumber,
            data = myParallelData
        };

        TransformJob transformJob = new TransformJob
        {
            number = myNumber,
            data = myTransfromData,
            deltaTime = 0.5f
        };

        simpleJobHandle = simpleJob.Schedule();
        parallelJobHandle = parallelJob.Schedule(myParallelData.Length, 32);

        JobHandle combineHandle = JobHandle.CombineDependencies(simpleJobHandle, parallelJobHandle);

        transformJobHandle = transformJob.Schedule(transformAccessArray, combineHandle);

        JobHandle.ScheduleBatchedJobs();

        simpleJobHandle.Complete();
        parallelJobHandle.Complete();
        combineHandle.Complete();
        transformJobHandle.Complete();

        if (simpleJobHandle.IsCompleted)
        {
            Debug.Log("Simple Job Result: " + simpleJob.data[0]);
        }

        if (parallelJobHandle.IsCompleted)
        {
            for (int i = 0; i < myParallelData.Length; i++)
            {
                Debug.Log("Parallel Job Result: " + parallelJob.data[i]);
            }
        }

        mySimpleData.Dispose();
        myParallelData.Dispose();
        myTransfromData.Dispose();
        transformAccessArray.Dispose();
    }
}

You can schedule another job like in the above examples. Before deep dive into the c# job system example take a look at part 1 of this series.

Leave a Reply