Multithreading OverlapSphere with JOBS

How can I JOB-ify something as simple as this:

[SerializeField] Collider[] hitColliders;
void Update()
{
    hitColliders = Physics.OverlapSphere(transform.position,30f);
}

Now, I know OverlapSphereCommand exists

But I’m clueless on how to properly implement it with Burst. The example code in the link also crashes my editor whenever I have more than 5 objects with the same script, oddly.


I am a little confused with the JOBS coding workflow, I’ve watched a few videos but I can’t implement anything other than basic number operations…

Basically, I just wanna know how can I turn something like the code I showed earlier into multithreaded code. And it should run on multiple objects with the same script.

Please enlighten me, maybe with an example I’ll finally be able to understand!

It would be something like this:

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

public class SphereOverlap : MonoBehaviour
{
    [SerializeField] LayerMask _layerMask = ~0;    
    
    const int k_max_hits_per_command = 3;
    NativeArray<OverlapSphereCommand> _commands;
    NativeArray<ColliderHit> _results;
    JobHandle _dependency;
    
    void Awake ()
    {
        _commands = new ( 1 , Allocator.Persistent );
        _results = new ( _commands.Length*k_max_hits_per_command , Allocator.Persistent );
    }

    void OnDestroy ()
    {
        _dependency.Complete();
        if( _commands.IsCreated ) _commands.Dispose();
        if( _results.IsCreated ) _results.Dispose();
    }

    void FixedUpdate ()
    {
        // complete the jobs that was scheduled last update:
        _dependency.Complete();

        // make productive use of the results:
        foreach( var hit in _results )
        if( hit.collider!=null )
        {
            Debug.Log( hit.collider.name );
        }

        // prepare new jobs:
        _commands[0] = new OverlapSphereCommand(
            point: transform.position ,
            radius: 30f ,
            queryParameters: new QueryParameters( layerMask:_layerMask , hitTriggers:QueryTriggerInteraction.Ignore  )
        );
            
        // schedule new jobs:
        _dependency = OverlapSphereCommand.ScheduleBatch( _commands , _results , minCommandsPerJob:1 , maxHits:k_max_hits_per_command );
    }
}

Hey!

I know it’s been a while, but i got it soved myself.

The first is the master script, it’s attached to a gameobject that’s supposed to be a “processor” for the team:

//what this script does is, it grabs a metric ton of OverlapSphere() commands that are requested by a
//certain amount of gameobjects stored in a list and multi-threads them, then 
//the results are thrown into a big array, where all the objects iterate and get their collision results from.

//it is faster than just calling OverlapSphere() on each individual object since OverlapSphereCommand() (it's JOBS counterpart)
//is multithreaded whereas the normal OverlapSphere() command isn't, thus it clogs the main thread and gives a big CPU overhead,
//which isn't really what anyone wants to happen in their game...

//this looks complex but is actually really simple, you just need to write more lines and have two scripts dedicated to this function.

// - sm777



///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////



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


//the job:
[BurstCompile]
public struct thejob : IJobParallelFor
{
    public NativeArray<OverlapSphereCommand> command;
    public NativeArray<Vector3> position;
    public int layer;
    QueryParameters qp;
    public void Execute(int i)
    {
        qp.layerMask = ~(1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 | 1 << 5 | 1 << layer | 1<<15);
        command[i] = new OverlapSphereCommand(position[i], 25f, qp); //Max range is 25 meters!
    }
}

public class TargetDetectionMaster : MonoBehaviour
{

    public List<GameObject> Objects;
    public GameObject[] colList;
    [SerializeField] int team, maxunits;
    public static int MaxHits = 20,  maxdist = 25;
    /// 
    /// 
    /// 
    /// 

    NativeArray<Vector3> positions;
    NativeArray<OverlapSphereCommand> commands;
    NativeArray<ColliderHit> hitcolliders;

    void Start()
    {
        team = team + 5;

        //the big array in question:
        colList = new GameObject[MaxHits * maxunits];

        InvokeRepeating("CalculateOverlaps", 0.1f, 0.2f);
    }


    //what this does is that it processes all those OverlapSphere calls that were made in the IJobParallelFor.
    //and it throws them into one big array where the gameobjects can access 
    void CalculateOverlaps()
    {

        commands = new NativeArray<OverlapSphereCommand>(Objects.Count, Allocator.TempJob);
        hitcolliders = new NativeArray<ColliderHit>(MaxHits * maxunits, Allocator.TempJob);
        positions = new NativeArray<Vector3>(Objects.Count, Allocator.TempJob);

        thejob ParallelJob = new thejob()
        {
            command = commands,
            position = positions,
            layer = team
        };


        //a "jobless" implementation
        // for (int i = 0; i < commands.Length; i++)
        // {
        //     commands[i] = new OverlapSphereCommand(transform.position, 10, QueryParameters.Default);
        // }

        //grabbing all positions of the objects stored in the list
        for (int i = 0; i < positions.Length; i++)
        {
            positions[i] = Objects[i].transform.position;
        }

        JobHandle handle = ParallelJob.Schedule(Objects.Count, 1);
        handle = OverlapSphereCommand.ScheduleBatch(commands, hitcolliders, 1, MaxHits, handle);
        handle.Complete();

        //throwing all results into the big array
        for (int i = 0; i < colList.Length; i++)
        {
            if (hitcolliders[i].collider != null)
            {
                colList[i] = hitcolliders[i].collider.gameObject;
            }
            else
            {
                colList[i] = null;
            }
        }

        commands.Dispose();
        hitcolliders.Dispose();
        positions.Dispose();

    }






}

And the target detection manager for the individual units:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class TargetDetection : MonoBehaviour
{

    public GameObject[] CollidersInRadius;
    [SerializeField] int ObjectListIndex;
    TargetDetectionMaster OverlapMaster;
    int prevcount;


    // Start is called before the first frame update
    void Start()
    {
        int team = GetComponent<ITeams>().Team;

        OverlapMaster = GameObject.FindGameObjectWithTag("Team " + team + " Processor").GetComponent<TargetDetectionMaster>();
        OverlapMaster.Objects.Add(gameObject);

        //getting the object's index at the start since it's faster than iterating thru the whole list at runtime       
        ObjectListIndex = Utilities.GetIndex(OverlapMaster.Objects, gameObject);

        CollidersInRadius = new GameObject[TargetDetectionMaster.MaxHits];

        StartCoroutine("CheckForIndexAndColliders");
    }

    void OnDestroy()
    {
        OverlapMaster.Objects.Remove(gameObject);
    }

    IEnumerator CheckForIndexAndColliders()
    {
        while (true)
        {
            GetMyColliders();
            yield return new WaitForSeconds(0.1f);
        }
    }

    void GetMyColliders()
    {

        if (prevcount != OverlapMaster.Objects.Count)
        {
            ObjectListIndex = Utilities.GetIndex(OverlapMaster.Objects, gameObject);
            prevcount = OverlapMaster.Objects.Count;
        }


        for (int i = 0; i < CollidersInRadius.Length; i++)
        {
            CollidersInRadius[i] = OverlapMaster.colList[(TargetDetectionMaster.MaxHits * ObjectListIndex) + i];
        }
    }
}

if anyone’s confused about “Utilities.GetIndex()”, it’s a custom function. Apparently it’s faster than the out-of-the-box implementation:


    public static int GetIndex(IList<GameObject> list, GameObject value)
    {
        for (int index = 0; index < list.Count; index++)
        {
            if (list[index] == value)
            {
                return index;
            }
        }
        return -1;
    }

I hope this will be useful for anyone that passes by. If there’s a better way to do this, let me know. This is what i could come up with.

The performance increase is pretty good, i can now have like 300 units without even scratching a dent in the performance. If anything, what actually makes a dent in the performance is Unity’s animator component. Sad that it doesn’t come with some optimizations out of the box, i’ll have to implement those myself. Anyway, i digress.

Hi sm777 , thank you for sharing your solution, good work figuring It out.

May I enquire on how to get an individual set of colliders on a per unit basis?
As I need each unit to know whether an opponent is nearby.

I did it with physics.overlapsphere, but im looking into burst compiling / jobifying it

This is a slight misunderstanding. Burst implementation is decided by a person who writes the particular IJob and this is not such case as OverlapSphereCommand was written by Unity dev team.