Search Unity

Question Multithreaded Parallelism with Unity Jobs MS these

Discussion in 'C# Job System' started by Patrick_Machado_, Mar 23, 2023.

  1. Patrick_Machado_

    Patrick_Machado_

    Joined:
    Feb 1, 2018
    Posts:
    67
    Hello everyone, I am currently on the final stretch of completing my thesis for a Master's degree. My research focuses on multi-thread performance in Unity Jobs with Burst, and I have some pertinent questions about it. I would love to share a few questions with you all and open this thread for discussions as well.
    1. What is/are the best way(s) to evaluate performance for a multi-threaded script in Unity Jobs? I am currently using the Unity Profiler, which I find difficult to understand. I am unsure if I should evaluate the cost per ms of the job by picking every piece of the job in the thread and taking their average. In some scripts, I have these blue pieces in the threads (referring to the job I am running) split into many places, which does not happen in the samples from Unity. I have seen some articles that measure performance by measuring the FPS of the entire execution. (Picture 1: a screenshot of the Unity Profiler of the Unity sample FindNearest Job, Picture 2: a screenshot of my own job implementation of a flocking algorithm). If FPS is the best way to measure it all, how can I do this properly? What's the best way to properly collect FPS data? Get the average of the highes and lowest pitches? use the in Game tab gizmo? Use the profiler?

    2. Regarding memory allocation cost, do jobs have anything to do with memory allocation? I am seeing a small variation between the parallel and sequential algorithms; let's say the parallel script is 1.15 GB in total, and the sequential one is 1.16 GB. (I am not using ECS.)

    3. This is a highly relevant question: why can't I use a number of batches lower than the number of jobs allocated? In the following code from the sample, it is possible to limit the number of batches to 64, even if the number of Seekers is larger than the number of batches. I cannot reproduce this in any other script I create using Unity Jobs. In the second script, which is mine, I am trying to make the number of batches lower than the number of Boid units (from FlockManager) to better utilize thread spreading allocation in parallel. However, I encounter three errors, as described below. So far, it only works without errors if the number of batches is equal to or greater than the number of job units. What am I doing wrong? (Pitcure 3: Flocking jobs cant use the number of batches lower than the jobs units)


    4. I understand that the development time required to implement Jobs and Burst in an existing script can be more efficient compared to implementing ECS, which is the primary reason I am not using ECS at the moment. I am curious to know if this was also the main reason behind the decision to not use ECS in this Unity sample (FindNearest). Unlike other Unity samples for DOTS, this particular sample does not utilize ECS. Could you kindly provide more information on why this choice was made?

    Below are the code mentioned and the errors I get, also there are pitcures attached.
    Thank you very much, I appreciate it in advance!
    (Didn't know if I should keep this a Question or a Discussion tagged, since im planning on making new answers here if you allow me)

    Best regards!
    Patrick Machado

    Script FindNearest Sample (described on number 3):
    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Jobs;
    3. using Unity.Mathematics;
    4. using UnityEngine;
    5.  
    6. namespace Step3
    7. {
    8.     public class FindNearest : MonoBehaviour
    9.     {
    10.         public NativeArray<float3> TargetPositions;
    11.         public NativeArray<float3> SeekerPositions;
    12.         public NativeArray<float3> NearestTargetPositions;
    13.  
    14.         public void Awake()
    15.         {
    16.             Spawner spawner = Object.FindObjectOfType<Spawner>();
    17.             TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
    18.             SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
    19.             NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
    20.         }
    21.  
    22.         public void OnDestroy()
    23.         {
    24.             TargetPositions.Dispose();
    25.             SeekerPositions.Dispose();
    26.             NearestTargetPositions.Dispose();
    27.         }
    28.  
    29.         public void Update()
    30.         {
    31.             for (int i = 0; i < TargetPositions.Length; i++)
    32.             {
    33.                 TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
    34.             }
    35.  
    36.             for (int i = 0; i < SeekerPositions.Length; i++)
    37.             {
    38.                 SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
    39.             }
    40.  
    41.             FindNearestJob findJob = new FindNearestJob
    42.             {
    43.                 TargetPositions = TargetPositions,
    44.                 SeekerPositions = SeekerPositions,
    45.                 NearestTargetPositions = NearestTargetPositions,
    46.             };
    47.  
    48.             // Execute will be called once for every element of the SeekerPositions array,
    49.             // with every index from 0 up to (but not including) the length of the array.
    50.             // The Execute calls will be split into batches of 64.
    51.             JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 64);
    52.            
    53.             findHandle.Complete();
    54.  
    55.             for (int i = 0; i < SeekerPositions.Length; i++)
    56.             {
    57.                 Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
    58.             }
    59.         }
    60.     }
    61. }
    Script FlockManager (Described on number 3):
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Jobs;
    3. using Unity.Mathematics;
    4. using Unity.Collections;
    5. using System.Collections;
    6. using Unity.Burst;
    7. using Unity.Jobs.LowLevel.Unsafe;
    8. using UnityEngine.Profiling;
    9.  
    10. namespace NewBoid_JobParallelized
    11. {
    12.     public struct Boid
    13.     {
    14.         public float3 position;
    15.         public float3 velocity;
    16.         public float3 acceleration;
    17.     }
    18.  
    19.     public class FlockManager : MonoBehaviour
    20.     {
    21.         public GameObject boidPrefab;
    22.         public int numBoids = 50;
    23.         public float maxSpeed = 5f;
    24.         public float maxForce = 1f;
    25.         public float separationDistance = 2f;
    26.         public float alignmentDistance = 10f;
    27.         public float cohesionDistance = 10f;
    28.         public float separationWeight = 1f;
    29.         public float alignmentWeight = 1f;
    30.         public float cohesionWeight = 1f;
    31.         public float boundsRadius = 50f;
    32.         public Transform target;
    33.  
    34.         private NativeArray<Boid> boids;
    35.         private JobHandle flockJobHandle;
    36.  
    37.         public GameObject[] boidPrefabs;
    38.         public float spawnRadius;
    39.  
    40.         public int BatchSize = 64;
    41.         public int ThreadLimitedTo = -1;
    42.  
    43.         void Start()
    44.         {
    45.            
    46.            
    47.             boids = new NativeArray<Boid>(numBoids, Allocator.Persistent);
    48.             boidPrefabs = new GameObject[numBoids];
    49.  
    50.  
    51.             for (int i = 0; i < numBoids; i++)
    52.             {
    53.                 // Instantiate the boid prefab and store the reference in boidPrefabs
    54.                 Vector3 position = new Vector3(UnityEngine.Random.Range(-spawnRadius, spawnRadius), UnityEngine.Random.Range(-spawnRadius, spawnRadius), UnityEngine.Random.Range(-spawnRadius, spawnRadius));
    55.                 Quaternion rotation = Quaternion.Euler(UnityEngine.Random.Range(-180, 180), UnityEngine.Random.Range(-180, 180), UnityEngine.Random.Range(-180, 180));
    56.                 GameObject _boidPrefab = Instantiate(boidPrefab, position, rotation);
    57.                 boidPrefabs[i] = _boidPrefab;
    58.             }
    59.  
    60.             InitializeBoids();
    61.         }
    62.  
    63.         void InitializeBoids()
    64.         {
    65.             for (int i = 0; i < boids.Length; i++)
    66.             {
    67.                 Boid boid = new Boid
    68.                 {
    69.                     position = boidPrefabs[i].transform.position,
    70.                     velocity = boidPrefabs[i].GetComponent<Rigidbody>().velocity
    71.                 };
    72.                 boids[i] = boid;
    73.             }
    74.         }
    75.  
    76.        
    77.         void Update()
    78.         {
    79.             System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
    80.             // Create the flock job and schedule it
    81.             var flockJob = new FlockJob
    82.             {
    83.                 boids = boids,
    84.                 maxSpeed = maxSpeed,
    85.                 maxForce = maxForce,
    86.                 separationDistance = separationDistance,
    87.                 alignmentDistance = alignmentDistance,
    88.                 cohesionDistance = cohesionDistance,
    89.                 separationWeight = separationWeight,
    90.                 alignmentWeight = alignmentWeight,
    91.                 cohesionWeight = cohesionWeight,
    92.                 boundsRadius = boundsRadius,
    93.                 targetPosition = target.position,
    94.                 deltaTime = Time.deltaTime
    95.             };
    96.  
    97.             // Update the Boid data with the current position and velocity of the prefab
    98.             for (int i = 0; i < boids.Length; i++)
    99.             {
    100.                 GameObject boidPrefab = boidPrefabs[i];
    101.                 boidPrefab.transform.position = boids[i].position;
    102.                 boidPrefab.transform.rotation = Quaternion.LookRotation(boids[i].velocity);
    103.  
    104.                 Boid boid = boids[i];
    105.                 boid.position = boidPrefab.transform.position;
    106.                 boid.velocity = boidPrefab.GetComponent<Rigidbody>().velocity;
    107.                 boids[i] = boid;
    108.             }
    109.  
    110.             //int numBoidsPerJob = (int)Mathf.Ceil((float)numBoids / 64f);
    111.             if (ThreadLimitedTo != -1) JobsUtility.JobWorkerCount = ThreadLimitedTo;
    112.             Debug.Log("Threads: " + JobsUtility.JobWorkerCount);
    113.             flockJobHandle = flockJob.Schedule(boids.Length, BatchSize);
    114.                      
    115.         }
    116.  
    117.         void LateUpdate()
    118.         {
    119.             // Wait for the flock job to complete and update the boid positions
    120.             flockJobHandle.Complete();
    121.             for (int i = 0; i < numBoids; i++)
    122.             {
    123.                 Boid boid = boids[i];
    124.                 boid.velocity += boid.acceleration * Time.deltaTime;
    125.                 boid.velocity = math.clamp(boid.velocity, -maxSpeed, maxSpeed);
    126.  
    127.                 // add target following behavior
    128.                 float3 targetOffset = new float3(target.position.x, target.position.y, target.position.z) - boid.position;
    129.  
    130.                 float targetDistance = math.length(targetOffset);
    131.  
    132.                 if (targetDistance > 0.1f) // if the boid is far from the target
    133.                 {
    134.                     float3 targetVelocity = math.normalize(targetOffset) * maxSpeed;
    135.                     float3 targetAcceleration = (targetVelocity - boid.velocity) * 10f; // use a high multiplier to make the boids follow the target more quickly
    136.                     boid.acceleration += targetAcceleration;
    137.                 }
    138.  
    139.                 boid.position += boid.velocity * Time.deltaTime;
    140.                 boid.acceleration = float3.zero;
    141.                 boidPrefabs[i].transform.position = boid.position;
    142.                 boidPrefabs[i].transform.rotation = Quaternion.LookRotation(boid.velocity);
    143.                 boids[i] = boid;
    144.             }
    145.  
    146.  
    147.         }
    148.  
    149.         void OnDestroy()
    150.         {
    151.             // Dispose of the boids array when the FlockManager is destroyed
    152.             boids.Dispose();
    153.         }
    154.  
    155.         [BurstCompile]
    156.         struct FlockJob : IJobParallelFor
    157.         {
    158.             public NativeArray<Boid> boids;
    159.             public float maxSpeed;
    160.             public float maxForce;
    161.             public float separationDistance;
    162.             public float alignmentDistance;
    163.             public float cohesionDistance;
    164.             public float separationWeight;
    165.             public float alignmentWeight;
    166.             public float cohesionWeight;
    167.             public float boundsRadius;
    168.             public float3 targetPosition;
    169.             public float deltaTime;
    170.  
    171.             public void Execute(int i)
    172.             {
    173.                 Boid boid = boids[i];
    174.                 float3 separation = float3.zero;
    175.                 float3 alignment = float3.zero;
    176.                 float3 cohesion = float3.zero;
    177.                 int numNeighbors = 0;
    178.  
    179.                 for (int j = 0; j < boids.Length; j++)
    180.                 {
    181.                     if (i == j) continue;
    182.                     Boid other = boids[j];
    183.                     float3 offset = other.position - boid.position;
    184.                     float distance = math.length(offset);
    185.                     if (distance < separationDistance)
    186.                     {
    187.                         separation -= math.normalize(offset) / distance;
    188.                     }
    189.                     else if (distance < alignmentDistance)
    190.                     {
    191.                         alignment += other.velocity;
    192.                         numNeighbors++;
    193.                     }
    194.                     else if (distance < cohesionDistance)
    195.                     {
    196.                         cohesion += other.position;
    197.                         numNeighbors++;
    198.                     }
    199.                 }
    200.  
    201.                 if (numNeighbors > 0)
    202.                 {
    203.                     alignment /= numNeighbors;
    204.                     cohesion /= numNeighbors;
    205.                     cohesion = math.normalize(cohesion - boid.position);
    206.                 }
    207.  
    208.                 float3 boundsOffset = float3.zero;
    209.                 if (math.length(boid.position) > boundsRadius)
    210.                 {
    211.                     boundsOffset = -math.normalize(boid.position) * (math.length(boid.position) - boundsRadius);
    212.                 }
    213.  
    214.                 separation = math.normalize(separation) * separationWeight;
    215.                 alignment = math.normalize(alignment) * alignmentWeight;
    216.                 cohesion = math.normalize(cohesion) * cohesionWeight;
    217.                 boundsOffset = math.normalize(boundsOffset);
    218.  
    219.                 boid.acceleration = separation + alignment + cohesion + boundsOffset;
    220.                 boid.acceleration = math.clamp(boid.acceleration, -maxForce, maxForce);
    221.  
    222.                 // add target following behavior
    223.                 float3 targetOffset = targetPosition - boid.position;
    224.                 float targetDistance = math.length(targetOffset);
    225.  
    226.                 if (targetDistance > 0.1f) // if the boid is far from the target
    227.                 {
    228.                     float3 targetVelocity = math.normalize(targetOffset) * maxSpeed;
    229.                     float3 targetAcceleration = (targetVelocity - boid.velocity) * 10f; // use a high multiplier to make the boids follow the target more quickly
    230.                     boid.acceleration += targetAcceleration;
    231.                 }
    232.  
    233.                 boids[i] = boid;
    234.             }
    235.         }
    236.     }
    237. }
    238.  

    Error 1/3 described on number 3:

    IndexOutOfRangeException: Index 64 is out of restricted IJobParallelFor range [0...63] in ReadWriteBuffer.
    ReadWriteBuffers are restricted to only read & write the element at the job index. You can use double buffering strategies to avoid race conditions due to reading & writing in parallel to the same elements from a job.
    Unity.Collections.NativeArray`1[T].FailOutOfRangeError (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].CheckElementReadAccess (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].get_Item (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    NewBoid_JobParallelized.FlockManager+FlockJob.Execute (System.Int32 i) (at Assets/AIs/AI_01_Boids/New_ParallelJobs_Boids/FlockManager.cs:182)
    Unity.Jobs.IJobParallelForExtensions+ParallelForJobStruct`1[T].Execute (T& jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)


    Error 2/3 described on number 3:

    IndexOutOfRangeException: Index 64 is out of restricted IJobParallelFor range [0...63] in ReadWriteBuffer.
    ReadWriteBuffers are restricted to only read & write the element at the job index. You can use double buffering strategies to avoid race conditions due to reading & writing in parallel to the same elements from a job.
    Unity.Collections.NativeArray`1[T].FailOutOfRangeError (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].CheckElementReadAccess (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].get_Item (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    NewBoid_JobParallelized.FlockManager+FlockJob.Execute (System.Int32 i) (at Assets/AIs/AI_01_Boids/New_ParallelJobs_Boids/FlockManager.cs:182)
    Unity.Jobs.IJobParallelForExtensions+ParallelForJobStruct`1[T].Execute (T& jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)



    Error 3/3 described on number 3:

    IndexOutOfRangeException: Index 0 is out of restricted IJobParallelFor range [256...319] in ReadWriteBuffer.
    ReadWriteBuffers are restricted to only read & write the element at the job index. You can use double buffering strategies to avoid race conditions due to reading & writing in parallel to the same elements from a job.
    Unity.Collections.NativeArray`1[T].FailOutOfRangeError (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].CheckElementReadAccess (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    Unity.Collections.NativeArray`1[T].get_Item (System.Int32 index) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)
    NewBoid_JobParallelized.FlockManager+FlockJob.Execute (System.Int32 i) (at Assets/AIs/AI_01_Boids/New_ParallelJobs_Boids/FlockManager.cs:182)
    Unity.Jobs.IJobParallelForExtensions+ParallelForJobStruct`1[T].Execute (T& jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at <44569b2d1c974b5eb5e85c3450cfe46d>:0)

     

    Attached Files:

  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,005
    Use this to measure performance: https://docs.unity3d.com/Packages/com.unity.test-framework.performance@1.0/manual/index.html

    It averages results and you can set number of iterations and warmup. Very nice to remove the odd outliers. Also given that you will write unit tests you have a lot faster iteration time over playmode + profiler and can reproduce results on different machines and compare them just by rerunning tests.

    Jobs are general purpose multithreading tools. Entities are sort of replacements for gameobjects that allow parallel processing. Use entities where you need them, use jobs where a job suffices. For instance, modifying mesh vertices simply doesn‘t need entities, but simulating behaviours of moving objects that should also be rendered to the screen is a use case for entities.
     
    DevDunk likes this.
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,005
    Btw your errors are probably not related to batching. More likely you are trying to index an array with a value other than the Execute(int) value passes in. If you want to do that you can disable some of the native restrictions via attributes but be careful that you are indeed not creating a race condition in those cases.
     
    daniel-holz likes this.
  4. Patrick_Machado_

    Patrick_Machado_

    Joined:
    Feb 1, 2018
    Posts:
    67
    That seems extremely useful for performance evaluation, although I will have to study a little bit more about assembly references in order to implement it properly, It's giving me some errors on other existent scripts, will put some effort on studying it, I appreciate the recomendation!



    Also that helped me a lot, actually to solve the issue I first tried the unsafe solution, but actually race condition was the source of the problem, which I managed to solve creating another "buffer" to store modified Boids structs and then I swap them back, in this way there are no problems with accessing the same NativeArrey Reading and Writting in the same time since now there are two, one for reading and another for Read&Write. I'll let the code below in any case someone might be facing the same issue.

    Other than that I'm very glad for the inputs, thank you very much!

    Flock AI with UnityIJobsParallelFor:
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Jobs;
    3. using Unity.Mathematics;
    4. using Unity.Collections;
    5. using System.Collections;
    6. using Unity.Burst;
    7. using Unity.Jobs.LowLevel.Unsafe;
    8. using UnityEngine.Profiling;
    9.  
    10. namespace NewBoid_JobParallelized
    11. {
    12.     public struct Boid
    13.     {
    14.         public float3 position;
    15.         public float3 velocity;
    16.         public float3 acceleration;
    17.     }
    18.  
    19.     public class FlockManager : MonoBehaviour
    20.     {
    21.         public GameObject boidPrefab;
    22.         public int numBoids = 50;
    23.         public float maxSpeed = 5f;
    24.         public float maxForce = 1f;
    25.         public float separationDistance = 2f;
    26.         public float alignmentDistance = 10f;
    27.         public float cohesionDistance = 10f;
    28.         public float separationWeight = 1f;
    29.         public float alignmentWeight = 1f;
    30.         public float cohesionWeight = 1f;
    31.         public float boundsRadius = 50f;
    32.         public Transform target;
    33.  
    34.         private NativeArray<Boid> boids;
    35.         private NativeArray<Boid> updatedBoids;
    36.         private JobHandle flockJobHandle;
    37.  
    38.         public GameObject[] boidPrefabs;
    39.         public float spawnRadius;
    40.  
    41.         public int BatchSize = 64;
    42.         public int ThreadLimitedTo = -1;
    43.  
    44.         void Start()
    45.         {
    46.             boids = new NativeArray<Boid>(numBoids, Allocator.Persistent);
    47.             updatedBoids = new NativeArray<Boid>(numBoids, Allocator.Persistent);
    48.             boidPrefabs = new GameObject[numBoids];
    49.  
    50.  
    51.             for (int i = 0; i < numBoids; i++)
    52.             {
    53.                 // Instantiate the boid prefab and store the reference in boidPrefabs
    54.                 Vector3 position = new Vector3(UnityEngine.Random.Range(-spawnRadius, spawnRadius), UnityEngine.Random.Range(-spawnRadius, spawnRadius), UnityEngine.Random.Range(-spawnRadius, spawnRadius));
    55.                 Quaternion rotation = Quaternion.Euler(UnityEngine.Random.Range(-180, 180), UnityEngine.Random.Range(-180, 180), UnityEngine.Random.Range(-180, 180));
    56.                 GameObject _boidPrefab = Instantiate(boidPrefab, position, rotation);
    57.                 boidPrefabs[i] = _boidPrefab;
    58.             }
    59.  
    60.             InitializeBoids();
    61.         }
    62.  
    63.         void InitializeBoids()
    64.         {
    65.             for (int i = 0; i < boids.Length; i++)
    66.             {
    67.                 Boid boid = new Boid
    68.                 {
    69.                     position = boidPrefabs[i].transform.position,
    70.                     velocity = boidPrefabs[i].GetComponent<Rigidbody>().velocity
    71.                 };
    72.                 boids[i] = boid;
    73.             }
    74.         }
    75.  
    76.  
    77.         void Update()
    78.         {
    79.             // Create the flock job and schedule it
    80.             var flockJob = new FlockJob
    81.             {
    82.                 boids = boids,
    83.                 updatedBoids = updatedBoids,
    84.                 maxSpeed = maxSpeed,
    85.                 maxForce = maxForce,
    86.                 separationDistance = separationDistance,
    87.                 alignmentDistance = alignmentDistance,
    88.                 cohesionDistance = cohesionDistance,
    89.                 separationWeight = separationWeight,
    90.                 alignmentWeight = alignmentWeight,
    91.                 cohesionWeight = cohesionWeight,
    92.                 boundsRadius = boundsRadius,
    93.                 targetPosition = target.position,
    94.                 deltaTime = Time.deltaTime
    95.             };
    96.  
    97.             // Update the Boid data with the current position and velocity of the prefab
    98.             for (int i = 0; i < boids.Length; i++)
    99.             {
    100.                 GameObject boidPrefab = boidPrefabs[i];
    101.                 boidPrefab.transform.position = boids[i].position;
    102.                 boidPrefab.transform.rotation = Quaternion.LookRotation(boids[i].velocity);
    103.  
    104.                 Boid boid = boids[i];
    105.                 boid.position = boidPrefab.transform.position;
    106.                 boid.velocity = boidPrefab.GetComponent<Rigidbody>().velocity;
    107.                 boids[i] = boid;
    108.             }
    109.  
    110.             if (ThreadLimitedTo != -1) JobsUtility.JobWorkerCount = ThreadLimitedTo;
    111.             Debug.Log("Threads: " + JobsUtility.JobWorkerCount);
    112.  
    113.  
    114.             flockJobHandle = flockJob.Schedule(boids.Length, BatchSize);
    115.         }
    116.  
    117.         void LateUpdate()
    118.         {
    119.             // Wait for the flock job to complete and update the boid positions
    120.             flockJobHandle.Complete();
    121.  
    122.             // Swap the arrays
    123.             var temp = boids;
    124.             boids = updatedBoids;
    125.             updatedBoids = temp;
    126.  
    127.             for (int i = 0; i < numBoids; i++)
    128.             {
    129.                 Boid boid = boids[i];
    130.                 boid.velocity += boid.acceleration * Time.deltaTime;
    131.                 boid.velocity = math.clamp(boid.velocity, -maxSpeed, maxSpeed);
    132.  
    133.                 // add target following behavior
    134.                 float3 targetOffset = new float3(target.position.x, target.position.y, target.position.z) - boid.position;
    135.  
    136.                 float targetDistance = math.length(targetOffset);
    137.  
    138.                 if (targetDistance > 0.1f) // if the boid is far from the target
    139.                 {
    140.                     float3 targetVelocity = math.normalize(targetOffset) * maxSpeed;
    141.                     float3 targetAcceleration = (targetVelocity - boid.velocity) * 10f; // use a high multiplier to make the boids follow the target more quickly
    142.                     boid.acceleration += targetAcceleration;
    143.                 }
    144.  
    145.                 boid.position += boid.velocity * Time.deltaTime;
    146.                 boid.acceleration = float3.zero;
    147.                 boidPrefabs[i].transform.position = boid.position;
    148.                 boidPrefabs[i].transform.rotation = Quaternion.LookRotation(boid.velocity);
    149.                 boids[i] = boid;
    150.             }
    151.  
    152.  
    153.         }
    154.  
    155.         void OnDestroy()
    156.         {
    157.             // Dispose of the boids array when the FlockManager is destroyed
    158.             boids.Dispose();
    159.             updatedBoids.Dispose();
    160.         }
    161.  
    162.         [BurstCompile]
    163.         struct FlockJob : IJobParallelFor
    164.         {
    165.             [ReadOnly]
    166.             public NativeArray<Boid> boids;
    167.             public NativeArray<Boid> updatedBoids;
    168.             public float maxSpeed;
    169.             public float maxForce;
    170.             public float separationDistance;
    171.             public float alignmentDistance;
    172.             public float cohesionDistance;
    173.             public float separationWeight;
    174.             public float alignmentWeight;
    175.             public float cohesionWeight;
    176.             public float boundsRadius;
    177.             public float3 targetPosition;
    178.             public float deltaTime;
    179.  
    180.             public void Execute(int i)
    181.             {
    182.                 Boid boid = boids[i];
    183.                 float3 separation = float3.zero;
    184.                 float3 alignment = float3.zero;
    185.                 float3 cohesion = float3.zero;
    186.                 int numNeighbors = 0;
    187.  
    188.                 for (int j = 0; j < boids.Length; j++)
    189.                 {
    190.                     if (i == j) continue;
    191.                     Boid other = boids[j];
    192.                     float3 offset = other.position - boid.position;
    193.                     float distance = math.length(offset);
    194.                     if (distance < separationDistance)
    195.                     {
    196.                         separation -= math.normalize(offset) / distance;
    197.                     }
    198.                     else if (distance < alignmentDistance)
    199.                     {
    200.                         alignment += other.velocity;
    201.                         numNeighbors++;
    202.                     }
    203.                     else if (distance < cohesionDistance)
    204.                     {
    205.                         cohesion += other.position;
    206.                         numNeighbors++;
    207.                     }
    208.                 }
    209.  
    210.                 if (numNeighbors > 0)
    211.                 {
    212.                     alignment /= numNeighbors;
    213.                     cohesion /= numNeighbors;
    214.                     cohesion = math.normalize(cohesion - boid.position);
    215.                 }
    216.  
    217.                 float3 boundsOffset = float3.zero;
    218.                 if (math.length(boid.position) > boundsRadius)
    219.                 {
    220.                     boundsOffset = -math.normalize(boid.position) * (math.length(boid.position) - boundsRadius);
    221.                 }
    222.  
    223.                 separation = math.normalize(separation) * separationWeight;
    224.                 alignment = math.normalize(alignment) * alignmentWeight;
    225.                 cohesion = math.normalize(cohesion) * cohesionWeight;
    226.                 boundsOffset = math.normalize(boundsOffset);
    227.  
    228.                 boid.acceleration = separation + alignment + cohesion + boundsOffset;
    229.                 boid.acceleration = math.clamp(boid.acceleration, -maxForce, maxForce);
    230.  
    231.                 // add target following behavior
    232.                 float3 targetOffset = targetPosition - boid.position;
    233.                 float targetDistance = math.length(targetOffset);
    234.  
    235.                 if (targetDistance > 0.1f) // if the boid is far from the target
    236.                 {
    237.                     float3 targetVelocity = math.normalize(targetOffset) * maxSpeed;
    238.                     float3 targetAcceleration = (targetVelocity - boid.velocity) * 10f; // use a high multiplier to make the boids follow the target more quickly
    239.                     boid.acceleration += targetAcceleration;
    240.                 }
    241.  
    242.                 updatedBoids[i] = boid;
    243.             }
    244.         }
    245.     }
    246. }