Search Unity

Question [C# Job System] How / when to manage TransformAccessArray?

Discussion in 'C# Job System' started by MaskedMouse, Jun 11, 2022.

  1. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    I've been messing around with the C# job system with GameObjects.
    I was looking whether I could make the projectile movement multi-threaded using the C# job system.

    In a sense it is possible but looking at the performance, the TransformAccessArray management takes in a lot of time.

    Example Scenario:
    Tower has a target and launches a projectile.
    Projectile System instantiates or gets object from
    ObjectPool<T>

    On Get the projectile gets added to a "To add list".
    The projectile arc system first finishes the job handle and writes the struct data from native list back into the MonoBehaviour component.
    It processes all the "to add" and "to remove" projectiles.
    Schedules the new job
    Calls
    JobHandle.ScheduleBatchedJobs();
    manually to make them run.
    When the projectile hits the target, it'll return to the ObjectPool and add it to the "to remove" list. That list is processed after the current job has been done to prevent changing the NativeArray whilst it is doing a Job and to prevent calling
    .Complete()
    early.

    The problem
    Though when processing the 'to remove projectiles' the
    RemoveAtSwapBack
    takes quite some time. Requiring to do this 4 times. For the
    List<T>
    ,
    NativeList<T>
    and 2
    TransformAccessArrays
    .

    The actual jobs run very quickly. But just the management of the
    NativeList<T>
    and
    TransformAccessArray
    is slow.

    What I've tried
    Things I've tried is to allocate the
    NativeList<T>
    with
    AllocatorHandle.tempJob
    and
    TransformAccessArray
    with 1
    desiredJobCount
    .
    Dispose of them after the results have been processed.
    Though then disposing of the list and array is expensive.
    Despite the manual saying:
    Having temporary lists is more expensive on disposal every frame.

    I tried setting
    TransformAccessArray[index] = ...
    directly but then I'm getting an index out of range because despite setting its capacity it does not create the elements.
    I'd still have to call
    .Add(projectile.Transform);

    Filling it up with nulls ain't a good idea either.

    All examples that I've found are using pre-serialized lists that don't change at runtime.

    Questions
    So how do people manage their TransformAccessArrays?
    Especially with dynamic objects coming in and going out through an ObjectPool.
    Is there a better way of handling the data from and to?

    Script
    Sorry for the naming and calculations. That's a work in progress. I've constantly been switching it up to try other ways of handling.
    I want to get to know the C# job system and how to leverage it properly before I dive into the rest.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using UnityEngine;
    7. using UnityEngine.Jobs;
    8. using UnityEngine.Profiling;
    9.  
    10. namespace SceneTesting
    11. {
    12.     public class ProjectileArcSystem : MonoBehaviour
    13.     {
    14.         public List<ProjectileV2> ProjectilesList = new();
    15.         private readonly List<ProjectileV2> ToAddList = new();
    16.         private readonly List<ProjectileV2> ToRemoveList = new();
    17.  
    18.         private NativeList<ProjectileV2.ProjectileData> projectilesNativeList;
    19.         private TransformAccessArray projectileTransformAccessArray;
    20.         private TransformAccessArray targetTransformAccessArray;
    21.  
    22.         private JobHandle jobHandle;
    23.  
    24.         private int projectileCount = 0;
    25.  
    26.         private void Awake()
    27.         {
    28.             projectilesNativeList = new NativeList<ProjectileV2.ProjectileData>(10000, Allocator.Persistent);
    29.             projectileTransformAccessArray = new TransformAccessArray(10000);
    30.             targetTransformAccessArray = new TransformAccessArray(10000);
    31.         }
    32.  
    33.         private void OnDestroy()
    34.         {
    35.             ProjectilesList.Clear();
    36.             if (projectilesNativeList.IsCreated) projectilesNativeList.Dispose();
    37.             if (projectileTransformAccessArray.isCreated) projectileTransformAccessArray.Dispose();
    38.             if (targetTransformAccessArray.isCreated) targetTransformAccessArray.Dispose();
    39.         }
    40.  
    41.         public void AddProjectile(ProjectileV2 projectile)
    42.         {
    43.             ToAddList.Add(projectile);
    44.         }
    45.  
    46.         public void RemoveProjectile(ProjectileV2 projectile)
    47.         {
    48.             ToRemoveList.Add(projectile);
    49.         }
    50.  
    51.         private void Update()
    52.         {
    53.             // Complete the handle of the previous update loop
    54.             jobHandle.Complete();
    55.  
    56.             // Write job results back into the data
    57.             Profiler.BeginSample("Process Job Results");
    58.             ProcessJobResults();
    59.             Profiler.EndSample();
    60.  
    61.             // Process staged projectiles before running a new job
    62.             Profiler.BeginSample("Process staged projectiles");
    63.             ProcessStagedProjectiles();
    64.             Profiler.EndSample();
    65.  
    66.             // Run the new job
    67.             ExecuteJob();
    68.  
    69.             // Execute jobs
    70.             JobHandle.ScheduleBatchedJobs();
    71.         }
    72.  
    73.         private void ProcessJobResults()
    74.         {
    75.             if (projectileCount == 0) return;
    76.  
    77.             Profiler.BeginSample("Copy from Native to Managed");
    78.             for (var i = 0; i < projectilesNativeList.Length; i++)
    79.             {
    80.                 var projectile = ProjectilesList[i];
    81.                 // Write the data back
    82.                 projectile.Data = projectilesNativeList[i];
    83.             }
    84.             Profiler.EndSample();
    85.         }
    86.  
    87.         private void ProcessStagedProjectiles()
    88.         {
    89.             Profiler.BeginSample("Removing projectiles");
    90.             foreach (var projectile in ToRemoveList)
    91.             {
    92.                 Profiler.BeginSample("Finding the index");
    93.                 var index = ProjectilesList.IndexOf(projectile);
    94.                 Profiler.EndSample();
    95.              
    96.                 Profiler.BeginSample("Remove at swapback");
    97.                 ProjectilesList.RemoveAtSwapBack(index);
    98.                 projectilesNativeList.RemoveAtSwapBack(index);
    99.                 projectileTransformAccessArray.RemoveAtSwapBack(index);
    100.                 targetTransformAccessArray.RemoveAtSwapBack(index);
    101.                 Profiler.EndSample();
    102.              
    103.                 projectileCount--;
    104.             }
    105.             Profiler.EndSample();
    106.  
    107.             Profiler.BeginSample("Adding projectiles");
    108.             foreach (var projectile in ToAddList)
    109.             {
    110.                 ProjectilesList.Add(projectile);
    111.              
    112.                 projectilesNativeList.Add(projectile.Data);
    113.                 projectileTransformAccessArray.Add(projectile.Transform);
    114.                 targetTransformAccessArray.Add(projectile.Target);
    115.              
    116.                 projectileCount++;
    117.             }
    118.             Profiler.EndSample();
    119.  
    120.             ToRemoveList.Clear();
    121.             ToAddList.Clear();
    122.         }
    123.  
    124.         private void ExecuteJob()
    125.         {
    126.             if (projectileCount == 0) return;
    127.  
    128.             var getPositionsJob = new GetEnemyPositionsJob
    129.             {
    130.                 Output = projectilesNativeList
    131.             };
    132.  
    133.             var calculationJob = new ArcProjectileCalculationJob
    134.             {
    135.                 ProjectileData = projectilesNativeList,
    136.                 DeltaTime = Time.deltaTime
    137.             };
    138.  
    139.             var movementJob = new ArcProjectileMovementJob
    140.             {
    141.                 ProjectileData = projectilesNativeList
    142.             };
    143.  
    144.             var readTargetPositionJob = getPositionsJob.Schedule(targetTransformAccessArray);
    145.             var calculationJobHandle = calculationJob.Schedule(projectileCount, 64, readTargetPositionJob);
    146.  
    147.             jobHandle = movementJob.Schedule(projectileTransformAccessArray, calculationJobHandle);
    148.         }
    149.  
    150.         [BurstCompile]
    151.         public struct GetEnemyPositionsJob : IJobParallelForTransform
    152.         {
    153.             public NativeArray<ProjectileV2.ProjectileData> Output;
    154.  
    155.             public void Execute(int index, TransformAccess transform)
    156.             {
    157.                 var data = Output[index];
    158.                 data.TargetPosition = transform.position;
    159.                 Output[index] = data;
    160.             }
    161.         }
    162.  
    163.         [BurstCompile]
    164.         public struct ArcProjectileMovementJob : IJobParallelForTransform
    165.         {
    166.             [ReadOnly]
    167.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    168.  
    169.             public void Execute(int index, TransformAccess transform)
    170.             {
    171.                 // Move ourselves towards the target at every frame.
    172.                 var projectileData = ProjectileData[index];
    173.  
    174.                 // Write position
    175.                 transform.position = projectileData.CalculatedPosition;
    176.             }
    177.         }
    178.  
    179.         [BurstCompile]
    180.         public struct ArcProjectileCalculationJob : IJobParallelFor
    181.         {
    182.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    183.  
    184.             [ReadOnly]
    185.             public float DeltaTime;
    186.  
    187.             public void Execute(int index)
    188.             {
    189.                 // Read data from array
    190.                 var projectileData = ProjectileData[index];
    191.                 var targetPosition = projectileData.TargetPosition;
    192.                 var currentPosition = projectileData.CurrentPosition;
    193.                 var speed = projectileData.Speed;
    194.                 var arcFactor = projectileData.ArcFactor;
    195.  
    196.                 var direction = targetPosition - currentPosition;
    197.                 var deltaTimeDistance = speed * DeltaTime;
    198.                 var step = currentPosition + math.normalize(direction) * speed * DeltaTime;
    199.  
    200.                 // Arc Height
    201.                 var distanceTravelled = projectileData.DistanceTravelled + deltaTimeDistance;
    202.                 var totalDistance = math.distance(projectileData.Origin, targetPosition);
    203.                 var sin = math.sin(distanceTravelled * math.PI / totalDistance);
    204.                 var heightOffset = arcFactor * totalDistance * sin;
    205.                 var calculatedPosition = step + new float3(0f, heightOffset, 0f);
    206.  
    207.                 // Write data
    208.                 projectileData.CurrentPosition = step;
    209.                 projectileData.CalculatedPosition = calculatedPosition;
    210.                 projectileData.DistanceTravelled = distanceTravelled;
    211.                 ProjectileData[index] = projectileData;
    212.             }
    213.         }
    214.     }
    215. }
     
    Last edited: Jun 13, 2022
  2. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    I guess not many people are using the C# Job system with game objects the way I am trying to use it?

    One of the solutions I thought of was to pass in the amount of transforms to process in the
    Schedule
    extension method. Just like the calculation job requires a length. But apparently that is not a overload. Nor am I sure whether it is possible.
    But I could then just fill the access array with nulls. Since those wouldn't get processed either way due to the count not being higher than the amount of projectiles going.
    I wouldn't need to use
    RemoveAtSwapBack
    the projectiles from the array then either. Just set the entry to null and re-synchronize transforms.

    Despite its name TransformAccessArray, it feels more like a List. Since you have to
    Add
    items before the index is accessible.
     
  3. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,497
    It might be that posting on the DOTS forum would get you some feedback from those that hang out there. :) I can, of course, move your post if you so wish.
     
    MaskedMouse likes this.
  4. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Perhaps, though from what I can tell most of the DOTS section is more about ppl using ECS. Which in my case I am using GameObjects. Either way it is a scripting question.

    There doesn't seem to be a DOTS for GameObjects section
     
  5. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,497
    It was only a suggestion to help you. DOTS is not about NOT using GameObjects but includes lots of technology including the Job System. TransformAccessArray was created for DOTS/Jobs access.

    I'll leave your post here then.
     
    MaskedMouse likes this.
  6. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Can always move it later though. I appreciate the thought
     
    MelvMay likes this.
  7. tassarho

    tassarho

    Joined:
    Aug 25, 2019
    Posts:
    75
    i encounter the same issue and yes it is painful...

    - in the job i always check: if (!transform.IsValid) return; i use to have issue with gameobject being destroyed during the job resulting in null reference.

    - I also tried TransformAccessArray[index] = ... but it seems to be readonly (if i remember correctly i had either an error or try check if the value changed with a log).

    - as you said allocating a new list each frame is too expensive and i also ended up caching my containers

    -I learned, when using IJobParallelForTransform you don't need to call Job.Complete() (i asked about that : https://forum.unity.com/threads/ijo...xige-jobhandle-complete.1223433/#post-7805433 )

    -One thing i'm not certain is if TransformAccessArray allow Duplicates, since your list don't check if the projectile to add is already in, there might be a risk, but i can't find anything on the documentation.

    Otherwise your way of handling the containers is not different from mine, one issue i had with the remove at swapback is when tracking element by index....
    so each remove i have to make this check :
    Code (CSharp):
    1. if (!arrivedUnits.Contains(destinations.Count)) return;
    2.             int indexToChange = arrivedUnits.IndexOf(destinations.Count);
    3.             arrivedUnits[indexToChange] = unitIndex;
    i basically change the index corresponding to the old length of the list and replace it by the index where the swapback occure.

    But unfortunatelly i don't have a better solution than the one you currently have. Maybe reduce the number of data in your ProjectileData to the strict minimum (if it's not already the case). but otherwise i don't see any possible improvement.

    just one last thing since i see 10'000 element you may want to add
    [NativeDisableParallelForRestriction] to your native array (fields in job) if it's expected to reach that number.

    Code (CSharp):
    1. [BurstCompile(CompileSynchronously = true)]
    2.     public struct JMoveUnits : IJobParallelForTransform
    3.     {
    4.         [ReadOnly] private readonly float Speed;
    5.         [ReadOnly] private readonly float DeltaTime;
    6.  
    7.         [NativeDisableParallelForRestriction]
    8.         [ReadOnly] private NativeArray<Quaternion> GoalsRotation;
    9.        
    10.         [NativeDisableParallelForRestriction]
    11.         [ReadOnly] private NativeArray<Vector3> GoalsPosition;
    12.  
    13.          ..//
    14.    }
    15.  
     
  8. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Without completing the job you can't change the array. You'd get errors afaik. In examples, they complete the job handle before performing a new one. Mostly in LateUpdate. To have as much time in between jobs I just complete it the next frame.
    I also the schedule batched jobs call is to wake up the workers so they start doing their job. In the end I refactor this to have all systems create their jobs and only at the end schedule it for execution.

    This is still something that I could do. Perhaps maybe use a
    Transform[]
    to then call
    SetTranforms
    on the
    TransformAccessArray
    . To sync it that way.
    Then use the IsValid property and return early.

    I'll change it up a bit again and see what it brings up.

    Well I don't know how many projectiles to expect. It depends on how many GameObjects spawn their projectiles. Which would be tied to the attack speed. Which is variable, not a constant.
    My goal is to be able to handle as many as possible and still have 60 frames per second. To handle more than what is possible with just a regular Update loop.
    At this point doing it with a regular Update loop, fps drops using around 5000 projectiles on an i7 10700k. So on a lower end system it will be less.
    Perhaps the goal is too ambitious. But it is still part of my research whether it is possible or not possible to get more out of it. If it is, it's nice. If its not I'll just have to face the limitations. Either way it is a learning experience.
     
  9. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Well I tried filling them up with null and synchronizing before scheduling the job.
    Though as more projectiles enter the list the read for the Target transform goes up drastically.
    Even though this is already a cached version... it still takes a lot of time getting the reference from the projectile.
    This iteration might even have some mistakes. I'm quickly iterating to test performance.

    upload_2022-6-14_22-14-8.png

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using UnityEngine;
    7. using UnityEngine.Jobs;
    8. using UnityEngine.Profiling;
    9.  
    10. namespace SceneTesting
    11. {
    12.     public class ProjectileArcSystem : MonoBehaviour
    13.     {
    14.         public List<ProjectileV2> ProjectilesList = new();
    15.         private readonly List<ProjectileV2> ToAddList = new();
    16.         private readonly List<ProjectileV2> ToRemoveList = new();
    17.  
    18.         private NativeArray<ProjectileV2.ProjectileData> projectilesNativeArray;
    19.         private TransformAccessArray projectileTransformAccessArray;
    20.         private TransformAccessArray targetTransformAccessArray;
    21.  
    22.         private JobHandle jobHandle;
    23.  
    24.         private int projectileCount = 0;
    25.  
    26.         private void Awake()
    27.         {
    28.             projectilesNativeArray = new NativeArray<ProjectileV2.ProjectileData>(10000, Allocator.Persistent);
    29.             projectileTransformAccessArray = new TransformAccessArray(10000);
    30.             targetTransformAccessArray = new TransformAccessArray(10000);
    31.  
    32.             // Fill with null
    33.             for (var i = 0; i < 10000; i++)
    34.             {
    35.                 projectileTransformAccessArray.Add(null);
    36.                 targetTransformAccessArray.Add(null);
    37.             }
    38.         }
    39.  
    40.         private void OnDestroy()
    41.         {
    42.             ProjectilesList.Clear();
    43.             if (projectilesNativeArray.IsCreated) projectilesNativeList.Dispose();
    44.             if (projectileTransformAccessArray.isCreated) projectileTransformAccessArray.Dispose();
    45.             if (targetTransformAccessArray.isCreated) targetTransformAccessArray.Dispose();
    46.         }
    47.  
    48.         public void AddProjectile(ProjectileV2 projectile)
    49.         {
    50.             ToAddList.Add(projectile);
    51.         }
    52.  
    53.         public void RemoveProjectile(ProjectileV2 projectile)
    54.         {
    55.             ToRemoveList.Add(projectile);
    56.         }
    57.  
    58.         private void Update()
    59.         {
    60.             // Complete the handle of the previous update loop
    61.             jobHandle.Complete();
    62.  
    63.             // Write job results back into the data
    64.             Profiler.BeginSample("Process Job Results");
    65.             ProcessJobResults();
    66.             Profiler.EndSample();
    67.  
    68.             // Process staged projectiles before running a new job
    69.             Profiler.BeginSample("Process staged projectiles");
    70.             ProcessStagedProjectiles();
    71.             Profiler.EndSample();
    72.  
    73.             Profiler.BeginSample("Prepare job");
    74.             PrepareJob();
    75.             Profiler.EndSample();
    76.  
    77.             // Run the new job
    78.             ExecuteJob();
    79.  
    80.             // Execute jobs
    81.             JobHandle.ScheduleBatchedJobs();
    82.         }
    83.  
    84.         private void PrepareJob()
    85.         {
    86.             if (projectileCount == 0) return;
    87.        
    88.             // Synchronize managed and native arrays
    89.             Profiler.BeginSample("Synchronize Managed with Native");
    90.             for (var i = 0; i < ProjectilesList.Count; i++)
    91.             {
    92.                 var projectile = ProjectilesList[i];
    93.                 Profiler.BeginSample("Set native Data");
    94.                 projectilesNativeArray[i] = projectile.Data;
    95.                 Profiler.EndSample();
    96.                 Profiler.BeginSample("Set Projectile Transform");
    97.                 projectileTransformAccessArray[i] = projectile.Transform;
    98.                 Profiler.EndSample();
    99.                 Profiler.BeginSample("Set Target Transform");
    100.                 targetTransformAccessArray[i] = projectile.Target;
    101.                 Profiler.EndSample();
    102.             }
    103.             Profiler.EndSample();
    104.  
    105.             Profiler.BeginSample("Fill remainder with null");
    106.             for (var i = ProjectilesList.Count; i < 10000; i++)
    107.             {
    108.                 projectileTransformAccessArray[i] = null;
    109.                 targetTransformAccessArray[i] = null;
    110.             }
    111.             Profiler.EndSample();
    112.         }
    113.  
    114.         private void ProcessJobResults()
    115.         {
    116.             if (projectileCount == 0) return;
    117.  
    118.             Profiler.BeginSample("Copy from Native to Managed");
    119.             for (var i = 0; i < ProjectilesList.Count; i++)
    120.             {
    121.                 var projectile = ProjectilesList[i];
    122.                 projectile.Data = projectilesNativeArray[i];
    123.             }
    124.             Profiler.EndSample();
    125.         }
    126.  
    127.         private void ProcessStagedProjectiles()
    128.         {
    129.             Profiler.BeginSample("Removing projectiles");
    130.             foreach (var projectile in ToRemoveList)
    131.             {
    132.                 ProjectilesList.Remove(projectile);
    133.                 projectileCount--;
    134.             }
    135.             Profiler.EndSample();
    136.  
    137.             Profiler.BeginSample("Adding projectiles");
    138.             foreach (var projectile in ToAddList)
    139.             {
    140.                 ProjectilesList.Add(projectile);
    141.                 projectileCount++;
    142.             }
    143.             Profiler.EndSample();
    144.  
    145.             ToRemoveList.Clear();
    146.             ToAddList.Clear();
    147.         }
    148.  
    149.         private void ExecuteJob()
    150.         {
    151.             if (projectileCount == 0) return;
    152.  
    153.             var getPositionsJob = new GetEnemyPositionsJob
    154.             {
    155.                 Output = projectilesNativeArray
    156.             };
    157.  
    158.             var calculationJob = new ArcProjectileCalculationJob
    159.             {
    160.                 ProjectileData = projectilesNativeArray,
    161.                 DeltaTime = Time.deltaTime
    162.             };
    163.  
    164.             var movementJob = new ArcProjectileMovementJob
    165.             {
    166.                 ProjectileData = projectilesNativeArray
    167.             };
    168.  
    169.             var readTargetPositionJob = getPositionsJob.Schedule(targetTransformAccessArray);
    170.             var calculationJobHandle = calculationJob.Schedule(projectileCount, 64, readTargetPositionJob);
    171.  
    172.             jobHandle = movementJob.Schedule(projectileTransformAccessArray, calculationJobHandle);
    173.         }
    174.  
    175.         [BurstCompile]
    176.         public struct GetEnemyPositionsJob : IJobParallelForTransform
    177.         {
    178.             public NativeArray<ProjectileV2.ProjectileData> Output;
    179.  
    180.             public void Execute(int index, TransformAccess transform)
    181.             {
    182.                 var data = Output[index];
    183.                 data.TargetPosition = transform.position;
    184.                 Output[index] = data;
    185.             }
    186.         }
    187.  
    188.         [BurstCompile]
    189.         public struct ArcProjectileMovementJob : IJobParallelForTransform
    190.         {
    191.             [ReadOnly]
    192.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    193.  
    194.             public void Execute(int index, TransformAccess transform)
    195.             {
    196.                 // Move ourselves towards the target at every frame.
    197.                 var projectileData = ProjectileData[index];
    198.  
    199.                 // Write position
    200.                 transform.position = projectileData.CalculatedPosition;
    201.             }
    202.         }
    203.  
    204.         [BurstCompile]
    205.         public struct ArcProjectileCalculationJob : IJobParallelFor
    206.         {
    207.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    208.  
    209.             [ReadOnly]
    210.             public float DeltaTime;
    211.  
    212.             public void Execute(int index)
    213.             {
    214.                 // Read data from array
    215.                 var projectileData = ProjectileData[index];
    216.                 var targetPosition = projectileData.TargetPosition;
    217.                 var currentPosition = projectileData.CurrentPosition;
    218.                 var speed = projectileData.Speed;
    219.                 var arcFactor = projectileData.ArcFactor;
    220.  
    221.                 var direction = targetPosition - currentPosition;
    222.                 var deltaTimeDistance = speed * DeltaTime;
    223.                 var step = currentPosition + math.normalize(direction) * speed * DeltaTime;
    224.  
    225.                 // Arc Height
    226.                 var distanceTravelled = projectileData.DistanceTravelled + deltaTimeDistance;
    227.                 var totalDistance = math.distance(projectileData.Origin, targetPosition);
    228.                 var sin = math.sin(distanceTravelled * math.PI / totalDistance);
    229.                 var heightOffset = arcFactor * totalDistance * sin;
    230.                 var calculatedPosition = step + new float3(0f, heightOffset, 0f);
    231.  
    232.                 // Write data
    233.                 projectileData.CurrentPosition = step;
    234.                 projectileData.CalculatedPosition = calculatedPosition;
    235.                 projectileData.DistanceTravelled = distanceTravelled;
    236.                 ProjectileData[index] = projectileData;
    237.             }
    238.         }
    239.     }
    240. }
    So what I've tried also is synching it up with just a regular Transform[] array.
    Though using the
    SetTransforms
    takes in a lot of time.
     
    Last edited: Jun 14, 2022
  10. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    So in my adventure of improvement. I've got a system now that does seem to start performant. But once it starts to return objects, it'll go bad again. Profiled it and it is actually a weird one. Setting the value in the
    TransformAccessArray
    at a given index to null costs a lot of time.
    Whilst the other
    TransformAccessArray
    which does the same thing, has no issue at all, only 0.02ms.

    upload_2022-6-19_16-35-25.png

    Though that's in the editor. In a development build it looks like this:
    upload_2022-6-19_16-41-31.png

    Which shows the same issue where nullifying the value at the given index is a performance hit. For only a few items it is taking 1.18 ms.

    So what about in-editor deep profile?
    upload_2022-6-19_16-44-17.png

    It seems to be that the time goes to
    TransformAccessArray.SetTransform()
    deeper than that it doesn't go.

    Perhaps it is a bug? I'm not sure. But it does seem odd that both access arrays, one is less performant than the other. both calling the same method. Just different transforms.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.Burst;
    4. using Unity.Collections;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using UnityEngine;
    8. using UnityEngine.Jobs;
    9. using UnityEngine.Profiling;
    10.  
    11. namespace SceneTesting
    12. {
    13.     public class ProjectileArcSystem : MonoBehaviour
    14.     {
    15.         public int Capacity = 10000;
    16.         [SerializeField]
    17.         private int projectileCount = 0;
    18.         public ProjectileV2[] ProjectilesArray;
    19.         private List<ProjectileV2> ToAddList = new();
    20.         private List<ProjectileV2> ToRemoveList = new();
    21.         private Stack<int> indexStack = new();
    22.  
    23.         private NativeArray<ProjectileV2.ProjectileData> projectilesNativeArray;
    24.         private TransformAccessArray projectileTransformAccessArray;
    25.         private TransformAccessArray targetTransformAccessArray;
    26.  
    27.         private JobHandle jobHandle;
    28.  
    29.  
    30.         private void Awake()
    31.         {
    32.             ProjectilesArray = new ProjectileV2[Capacity];
    33.             projectilesNativeArray = new NativeArray<ProjectileV2.ProjectileData>(Capacity, Allocator.Persistent);
    34.             projectileTransformAccessArray = new TransformAccessArray(Capacity);
    35.             targetTransformAccessArray = new TransformAccessArray(Capacity);
    36.  
    37.             // Fill with null
    38.             for (var i = 0; i < Capacity; i++)
    39.             {
    40.                 ProjectilesArray[i] = null;
    41.                 projectilesNativeArray[i] = default;
    42.                 projectileTransformAccessArray.Add(null);
    43.                 targetTransformAccessArray.Add(null);
    44.             }
    45.  
    46.             for (var i = Capacity - 1; i >= 0; i--)
    47.             {
    48.                 indexStack.Push(i);  
    49.             }
    50.         }
    51.  
    52.         private void OnDestroy()
    53.         {
    54.             jobHandle.Complete();
    55.            
    56.             if (projectilesNativeArray.IsCreated) projectilesNativeArray.Dispose();
    57.             if (projectileTransformAccessArray.isCreated) projectileTransformAccessArray.Dispose();
    58.             if (targetTransformAccessArray.isCreated) targetTransformAccessArray.Dispose();
    59.         }
    60.  
    61.         public void AddProjectile(ProjectileV2 projectile)
    62.         {
    63.             ToAddList.Add(projectile);
    64.         }
    65.  
    66.         public void RemoveProjectile(ProjectileV2 projectile)
    67.         {
    68.             ToRemoveList.Add(projectile);
    69.         }
    70.  
    71.         private void Update()
    72.         {
    73.             // Complete the handle of the previous update loop
    74.             jobHandle.Complete();
    75.  
    76.             // Write job results back into the data
    77.             Profiler.BeginSample("Process Job Results");
    78.             ProcessJobResults();
    79.             Profiler.EndSample();
    80.  
    81.             // Process staged projectiles before running a new job
    82.             Profiler.BeginSample("Process staged projectiles");
    83.             ProcessStagedProjectiles();
    84.             Profiler.EndSample();
    85.  
    86.             // Run the new job
    87.             ExecuteJob();
    88.  
    89.             // Execute jobs
    90.             JobHandle.ScheduleBatchedJobs();
    91.         }
    92.  
    93.         private void ProcessJobResults()
    94.         {
    95.             if (projectileCount == 0) return;
    96.  
    97.             Profiler.BeginSample("Get highest index");
    98.             var count = GetHighestIndex();
    99.             Profiler.EndSample();
    100.            
    101.             Profiler.BeginSample("Copy from Native to Managed");
    102.             for (var i = 0; i < count; i++)
    103.             {
    104.                 Profiler.BeginSample("Access native array");
    105.                 var projectileData = projectilesNativeArray[i];
    106.                 Profiler.EndSample();
    107.                
    108.                 if (!projectileData.IsValid) continue;
    109.                
    110.                 Profiler.BeginSample("Access Projectile");
    111.                 var projectile = ProjectilesArray[i];
    112.                 Profiler.EndSample();
    113.                
    114.                 Profiler.BeginSample("Set Data");
    115.                 projectile.Data = projectileData;
    116.                 Profiler.EndSample();
    117.             }
    118.             Profiler.EndSample();
    119.         }
    120.  
    121.         private int GetHighestIndex()
    122.         {
    123.             for (var i = ProjectilesArray.Length - 1; i >= 0; i--)
    124.             {
    125.                 if (ProjectilesArray[i] != null) return i;
    126.             }
    127.  
    128.             return 0;
    129.         }
    130.  
    131.         private void ProcessStagedProjectiles()
    132.         {
    133.             Profiler.BeginSample("Removing projectiles");
    134.             for (var i = 0; i < ToRemoveList.Count; i++)
    135.             {
    136.                 Profiler.BeginSample("Access Removal List");
    137.                 var projectile = ToRemoveList[i];
    138.                 Profiler.EndSample();
    139.                
    140.                 Profiler.BeginSample("Access Index");
    141.                 var index = projectile.Index;
    142.                 Profiler.EndSample();
    143.                
    144.                 Profiler.BeginSample("Nulling Array Data");
    145.                 ProjectilesArray[index] = null;
    146.                 Profiler.EndSample();
    147.                
    148.                 Profiler.BeginSample("Nulling projectile transform");
    149.                 projectileTransformAccessArray[index] = null;
    150.                 Profiler.EndSample();
    151.                
    152.                 Profiler.BeginSample("Nulling target transform");
    153.                 targetTransformAccessArray[index] = null;
    154.                 Profiler.EndSample();
    155.                
    156.                 Profiler.BeginSample("Set default");
    157.                 projectilesNativeArray[index] = default;
    158.                 Profiler.EndSample();
    159.  
    160.                 Profiler.BeginSample("Set IsValid");
    161.                 var projectileData = projectile.Data;
    162.                 projectileData.IsValid = false;
    163.                 projectile.Data = projectileData;
    164.                 Profiler.EndSample();
    165.                
    166.                 indexStack.Push(index);
    167.                
    168.                 projectileCount--;
    169.             }
    170.  
    171.             Profiler.EndSample();
    172.  
    173.             Profiler.BeginSample("Adding projectiles");
    174.             for (var i = 0; i < ToAddList.Count; i++)
    175.             {
    176.                 var projectile = ToAddList[i];
    177.                 var nullIndex = indexStack.Pop();
    178.                 projectile.Index = nullIndex;
    179.                 var projectileData = projectile.Data;
    180.                 projectileData.IsValid = true;
    181.                 projectile.Data = projectileData;
    182.                 ProjectilesArray[nullIndex] = projectile;
    183.                 projectilesNativeArray[nullIndex] = projectileData;
    184.                 projectileTransformAccessArray[nullIndex] = projectile.Transform;
    185.                 targetTransformAccessArray[nullIndex] = projectile.Target;
    186.                 projectileCount++;
    187.             }
    188.             Profiler.EndSample();
    189.  
    190.             ToRemoveList.Clear();
    191.             ToAddList.Clear();
    192.         }
    193.  
    194.         private void ExecuteJob()
    195.         {
    196.             if (projectileCount == 0) return;
    197.  
    198.             var getPositionsJob = new GetEnemyPositionsJob
    199.             {
    200.                 Output = projectilesNativeArray
    201.             };
    202.  
    203.             var calculationJob = new ArcProjectileCalculationJob
    204.             {
    205.                 ProjectileData = projectilesNativeArray,
    206.                 DeltaTime = Time.deltaTime
    207.             };
    208.  
    209.             var movementJob = new ArcProjectileMovementJob
    210.             {
    211.                 ProjectileData = projectilesNativeArray
    212.             };
    213.  
    214.             var readTargetPositionJob = getPositionsJob.Schedule(targetTransformAccessArray);
    215.             var calculationJobHandle = calculationJob.Schedule(Capacity, 64, readTargetPositionJob);
    216.  
    217.             jobHandle = movementJob.Schedule(projectileTransformAccessArray, calculationJobHandle);
    218.         }
    219.  
    220.         [BurstCompile]
    221.         public struct GetEnemyPositionsJob : IJobParallelForTransform
    222.         {
    223.             public NativeArray<ProjectileV2.ProjectileData> Output;
    224.  
    225.             public void Execute(int index, TransformAccess transform)
    226.             {
    227.                 if (!transform.isValid) return;
    228.                
    229.                 var data = Output[index];
    230.                 data.TargetPosition = transform.position;
    231.                 Output[index] = data;
    232.             }
    233.         }
    234.  
    235.         [BurstCompile]
    236.         public struct ArcProjectileCalculationJob : IJobParallelFor
    237.         {
    238.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    239.  
    240.             [ReadOnly]
    241.             public float DeltaTime;
    242.  
    243.             public void Execute(int index)
    244.             {
    245.                 // Read projectile data from array
    246.                 var projectileData = ProjectileData[index];
    247.                 // If the data is invalid, return
    248.                 if (!projectileData.IsValid) return;
    249.                
    250.                 var targetPosition = projectileData.TargetPosition;
    251.                 var currentPosition = projectileData.CurrentPosition;
    252.                 var speed = projectileData.Speed;
    253.                 var arcFactor = projectileData.ArcFactor;
    254.  
    255.                 var direction = targetPosition - currentPosition;
    256.                 var deltaTimeDistance = speed * DeltaTime;
    257.                 var step = currentPosition + math.normalize(direction) * speed * DeltaTime;
    258.  
    259.                 // Arc Height
    260.                 var totalDistance = math.distance(projectileData.Origin, targetPosition);
    261.                 var distanceTravelled = projectileData.DistanceTravelled + deltaTimeDistance;
    262.                 // Clamp the value between 0 and total distance so it doesn't overshoot.
    263.                 distanceTravelled = math.clamp(distanceTravelled, 0, totalDistance);
    264.                
    265.                 var sin = math.sin(distanceTravelled * math.PI / totalDistance);
    266.                 var heightOffset = arcFactor * totalDistance * sin;
    267.                 var calculatedPosition = step + new float3(0f, heightOffset, 0f);
    268.  
    269.                 // Write data
    270.                 projectileData.CurrentPosition = step;
    271.                 projectileData.CalculatedPosition = calculatedPosition;
    272.                 projectileData.DistanceTravelled = distanceTravelled;
    273.                 ProjectileData[index] = projectileData;
    274.             }
    275.         }
    276.        
    277.         /// <summary>
    278.         /// Job to apply the transforms new position
    279.         /// </summary>
    280.         [BurstCompile]
    281.         public struct ArcProjectileMovementJob : IJobParallelForTransform
    282.         {
    283.             [ReadOnly]
    284.             public NativeArray<ProjectileV2.ProjectileData> ProjectileData;
    285.  
    286.             public void Execute(int index, TransformAccess transform)
    287.             {
    288.                 if (!transform.isValid) return;
    289.                
    290.                 // Move ourselves towards the target at every frame.
    291.                 var projectileData = ProjectileData[index];
    292.  
    293.                 // Write position
    294.                 transform.position = projectileData.CalculatedPosition;
    295.             }
    296.         }
    297.     }
    298. }
     
  11. VirtusH

    VirtusH

    Joined:
    Aug 18, 2015
    Posts:
    95
    Did you resolve this in any way?
    I'm contemplating just using my own transform structs and setting transform values on the main thread to avoid this whole headache with TransformAccessArrays.
     
  12. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    I haven't done much after that. It was a hobby project.

    There always seemed to be some form of inefficiency.
    - Reading / writing the property Transform.position
    - SetTransform of the TransformAccessArray
    - Disposing a NativeArray

    I did have an idea but never implemented it. Instead of managing the transform access array, just only add to it. But don't remove anything. So it will be a large collection of all the transforms whether they're active or not.
    Have the accompanied struct data contain whether it is active or not. If not, skip.
    Iterating over all the inactives isn't performance heavy in comparison to removing / adding it into the transform access array.
     
    VirtusH likes this.
  13. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    Hi there!
    So, as I posted just now, I just made a nice UpdateTransformJobManager class that manages read-write transform jobs in a centralized manager object. Feel free to use it, give feedback and stuff =D

    To make it run quickly, I tested lots of things and ended up with the current implementation, which runs very quickly in regards to adding/removing objects, even on my mid-end Android device.
    The main ideia of storing a list of "objects to add" and "objects to remove" and only updating in the "complete job, refresh, start new job" cycle is about the same.

    In summary, what made things slow in my tests:
    - Rebuilding the whole TransformAccessArray every frame
    - Rebuilding the whole NativeArray of data every frame

    How did I overcame this?
    1. Only allocate the TransformAccessArray once with TransformAccessArray.Allocate. All modifications use Add or RemoveAtSwapBack
    2. Maintain a "object -> index in arrays" Dictionary for fast lookup. This dictionary is also used to check if an object is already present and doesn't need to be added again as well as checking if the object exists before removing
    3. My "objects to remove" is a SortedSet of ints with the object indices found using the dictionary. This was needed because objects may be destroyed while the job runs, so they became null before the update and exceptions were being thrown. The sorting was needed to remove objects from last to first, to avoid trying to remove an index that doesn't exist in the list anymore in this remove loop.
    4. When removing objects by index, I used TransformAccessArray.RemoveAtSwapBack and a custom made NativeArray.SwapBack (no need to resize the array yet). I have a list of all objects, so I made a List.RemoveAtSwapBack for it as well. This keeps all indices in every array/list synced.
    5. Fix the "object -> index" dictionary with the object removed and the object swapped back in its place. Ok, now every index is synced =P
    6. Now it's time to resize the NativeArray to its final size "currentObjects.Count + objectsToAdd.Count". Only once per frame and only if the array size changed. I made a Realloc method that creates a new array with the right size, copies the overlapping data, then disposes the original array if the size changed. Notice that if there are no objects to add, the resized array will be smaller and have the right size.
    7. Now it's time to add the new objects. TransformAccessArray.Add for the transforms, NativeArray[newIndex] = newData for the new job data, List.Add for the object list. Add the corresponding entries in the "object -> index" dictionary.
    8. Clear the "objects to add" and "objects to remove" lists, since it's all done!
    9. To make the data readable while the job is running, I copy the NativeArray every frame to a backup one. You may skip this if not needed. This clone is very quick depending on the size of the data, since it happens all at once.
    10. Schedule the job
    11. Complete it in the next frame, restart process

    Notice that all of this array rebuild happens only if there is something to add/remove.
    If there is not, you skip directly to step 9 and cycle "9 -> 10 -> 11 -> 9...".

    So, that's about it!
     
    Last edited: Feb 14, 2023
  14. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    Just to point out, I was refactoring my code and found a bug: when removing objects, I was accessing the list to find the object to remove from the dictionary and occasionally got an ArgumentOutOfRangeException.
    This was happening when more than one object was being removed in a single frame, since one of the last indices in the remove list might not exist anymore at the time the loop reaches it.
    Using a SortedSet instead of List for the "objects to remove" to remove objects from last to first did the trick.

    I've edited my previous post with this information.
     
    Last edited: Feb 14, 2023
  15. JuTekPixel

    JuTekPixel

    Joined:
    Sep 17, 2018
    Posts:
    7
    Hello, I am very fresh into the JOBS system, experimenting for a while now and I found this thread very interesting.
    I know I might ask for to much but will it be able for you to present how to use your "Update Manager" in some kind of YouTube tutorial? So that it will be easier for a inexperience coder like me to try it out and understand it?
    Some simple examples how to use to move, rotate or scale TransformAccessArray will be very interesting to see.
    For the moment I am creating new TransformAccessArray each frame and was looking for some solutions around it.
    Thank you in advance.
     
  16. JuTekPixel

    JuTekPixel

    Joined:
    Sep 17, 2018
    Posts:
    7
    Hello just to show how am I currently managing IJobParellelForTransform, I will paste entire Class responsible for pooling projectiles and moving them with JOBS. Currently this is creating every frame a new TransformAccessArray and disposing it on LateUpdate or OnDisable.
    It works but if i believe this can be improved somehow with proper TransfromAccessArray management without creating a new one every frame. I just don't know how to do it :/
    I would be very thankful to gilzoide if you could present how it is done with a 11 steps method described above.


    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using Sirenix.OdinInspector;
    4. using UnityEngine.Pool;
    5. using UnityEngine.Jobs;
    6. using Unity.Jobs;
    7. using Unity.Burst;
    8. using System.Linq;
    9.  
    10. public class YellowTurret_ProjectilesPool : MonoBehaviour
    11. {
    12.     public bool using_JOB_System = false;
    13.     [SerializeField] GlobalGameSpeed_SO globalGameSpeed;
    14.     [SerializeField][BoxGroup("Pool data")] private bool _usePool = true;
    15.     [SerializeField][BoxGroup("Pool data")] private int _targetPool = 80;
    16.     [SerializeField][BoxGroup("Pool data")] private int _maxPool = 90;
    17.     [SerializeField] private GameObject _projectilePrefab;
    18.     private ProjectileData_SO _projectileData;
    19.     private ObjectPool<GameObject> _pool;
    20.     private Vector2 _projectilePosition;
    21.     private Quaternion _projectileRotation;
    22.     private RegularProjectilePoolData _projectilePoolData;
    23.  
    24.     //------------JOB System---------------
    25.     private List<Transform> _myBulletsTomove = new List<Transform>();
    26.     TransformAccessArray _bulletsMovementTransform;
    27.     JobHandle _bulletsJobHandle;
    28.     MoveTurretProjectile _bulletsMoveJob;
    29.     private bool _newProjetilesList = true;
    30.  
    31.     private void Awake()
    32.     {
    33.         _pool = new ObjectPool<GameObject>(CreateProjectile, TakeProjectileFromPool, ReturnProjectileToThePool, DestroyProjectile, false, _targetPool, _maxPool);
    34.         _bulletsMoveJob = new MoveTurretProjectile
    35.         {
    36.             deltaTime = Time.deltaTime,
    37.         };
    38.     }
    39.  
    40.     private void Update()
    41.     {
    42.         if (using_JOB_System && _myBulletsTomove.Count != 0 && _projectileData!=null)
    43.         {
    44.             _bulletsMoveJob.deltaTime = Time.deltaTime;
    45.             _bulletsMoveJob.speed = _projectileData.BulletSpeed * globalGameSpeed.generalGameSpeed.value;          
    46.             _bulletsMovementTransform = new TransformAccessArray(_myBulletsTomove.ToArray());
    47.             _bulletsJobHandle = _bulletsMoveJob.Schedule(_bulletsMovementTransform);
    48.         }
    49.     }
    50.  
    51.     private void LateUpdate()
    52.     {
    53.         _bulletsJobHandle.Complete();
    54.         if (_bulletsMovementTransform.isCreated) _bulletsMovementTransform.Dispose();
    55.     }
    56.  
    57.     private void OnDisable()
    58.     {
    59.         if (_bulletsMovementTransform.isCreated) _bulletsMovementTransform.Dispose();
    60.     }
    61.  
    62.     public void _RequestForProjectile(object projectile)
    63.     {
    64.         _projectilePoolData = projectile as RegularProjectilePoolData;
    65.         if (_projectilePoolData != null)
    66.         {
    67.             ShootProjectile(_projectilePoolData.position, _projectilePoolData.rotation, _projectilePoolData.data);
    68.         }
    69.     }
    70.  
    71.     public void ShootProjectile(Vector2 position, Quaternion rotation, ProjectileData_SO data)
    72.     {
    73.         _projectileData = data;
    74.         _projectilePosition = position;
    75.         _projectileRotation = rotation;      
    76.         var myGO = _usePool ? _pool.Get() : Instantiate(_projectilePrefab, _projectilePosition, _projectileRotation);
    77.     }
    78.  
    79.     //----------------- All Data For Projectile Pooling ---------------------------
    80.     private GameObject CreateProjectile()
    81.     {
    82.         GameObject projectile = Instantiate(_projectilePrefab, _projectilePosition, _projectileRotation);
    83.         return projectile;
    84.     }
    85.  
    86.     private void TakeProjectileFromPool(GameObject myProjectile)
    87.     {
    88.         myProjectile.gameObject.SetActive(true);
    89.         CheckifBulletIsMovingOnStart(myProjectile.transform);
    90.         myProjectile.transform.position = _projectilePosition;
    91.         myProjectile.transform.rotation = _projectileRotation;
    92.         myProjectile.GetComponent<Collider2D>().enabled = true;
    93.         myProjectile.GetComponent<Player_Projectile>().ProjectileData = _projectileData;
    94.         myProjectile.GetComponent<Player_BasicProjectile>().Init(Return_ProjectileSignal);
    95.         myProjectile.GetComponent<Custom_SpriteAnimator>().BulletInit();
    96.         MoveBulletWithJobsSystem(myProjectile);
    97.     }
    98.  
    99.     private void ReturnProjectileToThePool(GameObject myProjectile)
    100.     {
    101.         RemoveBulletFromMovementList(myProjectile.transform);
    102.         myProjectile.SetActive(false);
    103.     }
    104.  
    105.     private void DestroyProjectile(GameObject myProjectile)
    106.     {
    107.         Destroy(myProjectile);
    108.     }
    109.  
    110.     private void Return_ProjectileSignal(GameObject GO)
    111.     {
    112.         if (_usePool) _pool.Release(GO);
    113.         else Destroy(GO);
    114.     }
    115.  
    116.     private void CheckifBulletIsMovingOnStart(Transform myTransform)
    117.     {
    118.         if (_myBulletsTomove.Contains(myTransform))
    119.         {
    120.             RemoveBulletFromMovementList(myTransform);
    121.         }
    122.     }
    123.     private void MoveBulletWithJobsSystem(GameObject myGO)
    124.     {
    125.      
    126.         AddBulletToMovementList(myGO.transform);          
    127.      
    128.     }
    129.  
    130.     private void AddBulletToMovementList(Transform myTransform)
    131.     {
    132.         _myBulletsTomove.Add(myTransform);      
    133.     }
    134.  
    135.     private void RemoveBulletFromMovementList(Transform myTransform)
    136.     {
    137.         _myBulletsTomove.Remove(myTransform);
    138.     }
    139. }
    140.  
    141. public class RegularProjectilePoolData
    142. {
    143.     public Vector2 position;
    144.     public Quaternion rotation;
    145.     public ProjectileData_SO data;
    146.  
    147.     public RegularProjectilePoolData(Vector2 position, Quaternion rotation, ProjectileData_SO data)
    148.     {
    149.         this.position = position;
    150.         this.rotation = rotation;
    151.         this.data = data;
    152.     }
    153. }
    154.  
    155. //----------- Unity JOBS ------------------
    156. [BurstCompile]
    157. public struct MoveTurretProjectile : IJobParallelForTransform
    158. {
    159.     public float deltaTime;
    160.     public float speed;
    161.     private Quaternion rotation;
    162.     private Vector3 direction;
    163.  
    164.     public void Execute(int index, TransformAccess transform)
    165.     {
    166.         rotation = transform.rotation;
    167.         direction = rotation * Vector3.right;
    168.         transform.position += direction * speed * deltaTime;
    169.     }
    170. }
     
    Last edited: Feb 19, 2023
  17. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    Hey @Antosyk ! Have you profiled your builds to see if your system actually needs improving?

    Now that I'm thinking of it, it is quite likely that the slow downs I faced when rebuilding my TransformAccessArrays was because I was fetching lots of transforms using the GameObjects' transform property. GameObject.transform is known to not be negligible in terms of performance if called every frame on lots of objects.
    In your case, since you maintain a list of the Transforms, it is possible that you won't have that much of a penalty at all.

    One easy optimization you could do is only dispose/recreate the TransformAccessArray if the list of transforms changed. Add a boolean field and mark it as true in both "AddBulletToMovementList" and "RemoveBulletFromMovementList", then check for it before recreating the array in "Update". If the list didn't change, there is no need to rebuild the TransformAccessArray.

    My convoluted "never rebuild the TransformAccessArray from scratch" has a quite more complex implementation and may not make that much of a difference in your case. Of course, we'd need to profile both solutions to actually find what difference it would make to change between them.

    With that said, though, in case you're interested, I can point you to the code I used. It's all open source at GitHub.
     
  18. JuTekPixel

    JuTekPixel

    Joined:
    Sep 17, 2018
    Posts:
    7
    Thanks, I have been profiling my build and for most parts my game works ok. There are some bottlenecks but I've discovered that those are related to a oversized polygon colliders rather than number of elements of screen.

    However I always look for more "tidy" and performance oriented solutions and approach you ave described seems to be one of those :) Since I am not from IT world it is just simpler for me to swallow YT tutorial rather than dig in a code.
    In regards to boolean I think just this might create an error if for example I disable one object from a list and JOBS will try to do something with it.
    I think maintaining two separate, additional lists like MaskedMouse did with ToAdd & ToRemove Lists might be necessary so that it can be synchronised in the same frame but after the job is done.
    I will give it a try. Cheers.
     
    gilzoide likes this.
  19. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    I really don't think it would give any errors. Your List<Transform> is completely independent to the TransformAccessArray.
    You need to recreate/edit the TransformAccessArray to actually change what transforms will be used in jobs, and it will only affect the next job scheduled.
    From my tests, disabling an object in the same frame where a job uses its TransformAccess doesn't give any errors. I haven't tested destroying the objects, though. Either way, if it were to give errors, your current solution would probably already have these errors as well, assuming you disable the objects while returning them to the pool and removing them from the Transform list.

    As for synchronizing stuff, your List<Transform> is already responsible for this. Maintaining "ToAdd" and "ToRemove" lists is mostly useful if you need to sync additional data from native collections like NativeArray, because you cannot mess with them while the job is running.
    You actually cannot mess with the TransformAccessArray while the job is running as well, but your list of Transforms covers this already :)
     
  20. JuTekPixel

    JuTekPixel

    Joined:
    Sep 17, 2018
    Posts:
    7
    Hello again, so I tried out to put bool in Update to avoid rebuilding whole Array but it has shown an error.

    This it what I did: (note I have // disposal of the TransformAccessArray in LateUpdate)
    Code (CSharp):
    1.  
    2.  private void Update()
    3.     {
    4.         if (using_JOB_System && _myBulletsTomove.Count != 0 && _projectileData!=null)
    5.         {
    6.             _bulletsMoveJob.deltaTime = Time.deltaTime;
    7.             _bulletsMoveJob.speed = _projectileData.BulletSpeed * globalGameSpeed.generalGameSpeed.value;
    8.             if (_listChanged)
    9.             {
    10.                 _bulletsMovementTransform = new TransformAccessArray(_myBulletsTomove.ToArray());                
    11.                 _listChanged = false;
    12.             }
    13.             _bulletsJobHandle = _bulletsMoveJob.Schedule(_bulletsMovementTransform);
    14.         }
    15.     }
    16.  
    17.     private void LateUpdate()
    18.     {
    19.         _bulletsJobHandle.Complete();
    20.         //if (_bulletsMovementTransform.isCreated) _bulletsMovementTransform.Dispose();
    21.     }
    22.  
    It works for a few second without problem and after this is an error I've got .

    upload_2023-2-22_9-51-15.png


    For the moment I guess I will have to stay with Disposing the TransformArray and recreating it in each frame :(
     

    Attached Files:

  21. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    Hey @Antosyk , you're on the right path there.
    So you still need to dispose of the TransformAccessArray when you're done with it, or memory will leak. Unity warns about that, which is very nice, because we can fix these problems before shipping the game. Since you are reusing the array between frames, you cannot dispose of it every frame, so commenting the disposal at LateUpdate seems right.

    So when is the right time to call Dispose? When is the TransformAccessArray not needed anymore? Well, there are two points in your case for this:
    1. At either OnDisable or OnDestroy, as you have already done in your previous post with the full code.
    2. Right before recreating a new TransformAccessArray. You should dispose of the previously allocated array before creating a brand new one, or the only reference you had to it will be lost and memory will leak.

    So the snippet of Update and LateUpdate would be something like this (added lines 9 and 10):
    Code (CSharp):
    1.     private void Update()
    2.     {
    3.         if (using_JOB_System && _myBulletsTomove.Count != 0 && _projectileData!=null)
    4.         {
    5.             _bulletsMoveJob.deltaTime = Time.deltaTime;
    6.             _bulletsMoveJob.speed = _projectileData.BulletSpeed * globalGameSpeed.generalGameSpeed.value;
    7.             if (_listChanged)
    8.             {
    9.                 // ↓↓↓ Before recreating the array, dispose of the previous one to avoid memory leaks! ↓↓↓
    10.                 (_bulletsMovementTransform.isCreated) _bulletsMovementTransform.Dispose();
    11.                 _bulletsMovementTransform = new TransformAccessArray(_myBulletsTomove.ToArray());              
    12.                 _listChanged = false;
    13.             }
    14.             _bulletsJobHandle = _bulletsMoveJob.Schedule(_bulletsMovementTransform);
    15.         }
    16.     }
    17.     private void LateUpdate()
    18.     {
    19.         _bulletsJobHandle.Complete();
    20.         //if (_bulletsMovementTransform.isCreated) _bulletsMovementTransform.Dispose();
    21.     }
     
    JuTekPixel likes this.
  22. JuTekPixel

    JuTekPixel

    Joined:
    Sep 17, 2018
    Posts:
    7
    It works like a charm :D thanks.

    I thought while creating a new Array the old one will be automatically overwrite but is seems it has to be done manually each time.
     
  23. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    No problem! =D

    In this case of Unity's Native Collections, where the memory is unmanaged (that is, not managed by the C# runtime) you need to manually dispose of them. This is true to NativeArray<>, TransformAccessArray and other collections from the Unity Collections package.
    Managed data, like List<> and C# arrays like Transform[], on the other hand, is managed by the C# runtime and their memory gets reclaimed automatically by the Garbage-Collector when appropriate, so you don't need to worry about those.

    As a best practice, though, if a class/struct implements the IDisposable interface and/or has a void Dispose() method, you should always dispose of it when appropriate.

    If you used _bulletsMovementTransform.SetTransforms(...), on the other hand, I think it would reuse the native memory if possible or otherwise dispose and recreate the native memory for you. I haven't used SetTransforms, so I'm not sure. You'd still need to dispose of it at OnDisable/OnDestroy, though.
     
  24. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Wow I have the exact question for my projectile system on managing bullet transforms. These solutions are so complicated, I might just consider a full dots migration in the future
     
  25. gilzoide

    gilzoide

    Joined:
    Oct 7, 2015
    Posts:
    30
    Yep, complicated is a very good word for it.

    But that's why I implemented the Unity C# Job System-enabled UpdateJobManager in my Update Manager package: https://github.com/gilzoide/unity-update-manager
    No need to worry about these nitty-gritty details of how data is structured, just let the manager run jobs every frame for you!
     
    Last edited: May 9, 2023
  26. Rein_Iwakura

    Rein_Iwakura

    Joined:
    Jul 4, 2023
    Posts:
    12
    Hi @gilzoide , thanks for your comprehensive tips.
    However, I noticed that when I modify my TransformAccessArray with
    Add()
    or
    RemoveAtSwapBack()
    , there will be a
    TransformAccessArray.Sort
    in main thread, causing a large overhead of
    IJobParallelForTransform.Schedule()
    . Is there any way to fix this?

    Edit: I realized that
    TransformAccessArray.Sort
    only added negligible overheads.

    Sometimes
    IJobParallelForTransform.Schedule()
    are blocked for half a milisecond even without
    TransformAccessArray.Sort
    . It looks like
    Schedule()
    is waiting for Jobs to finish.

    I've created a thread: Question - Large overhead of IJobParallelForTransform.Schedule - Unity Forum
     
    Last edited: Aug 2, 2023