Unity JobSystem动态管理Transform

JobSystem测试性能较低原因:

  • 每帧重新构建整个 TransformAccessArray
  • 每帧重建整个 NativeArray 数据

但我们通常都是动态添加删除Transform,无法做到静态。下面是解决问题的一些思路跟示例。

  1. 仅使用 TransformAccessArray.Allocate 分配 TransformAccessArray 一次。所有修改都使用 Add 或 RemoveAtSwapBack
  2. 维护一个 “object → index in arrays” 字典以便快速查找。此字典还用于检查对象是否已经存在且不需要再次添加,以及在删除之前检查对象是否存在
  3. 我的 “objects to remove” 是一个 SortedSet 的 ints,其中包含使用字典找到的对象索引。这是必需的,因为对象可能会在作业运行时被销毁,因此它们在更新和引发异常之前变为 null。需要进行排序以从最后一个到第一个删除对象,以避免在此删除循环中尝试删除列表中不再存在的索引。
  4. 在按索引删除对象时,我使用了 TransformAccessArray.RemoveAtSwapBack 和自定义的 NativeArray.SwapBack(暂时不需要调整数组的大小)。我有一个包含所有对象的列表,因此我还为它制作了一个 List.RemoveAtSwapBack。这将使每个数组/列表中的所有索引保持同步。
  5. 修复 “object → index” 字典,删除对象并将对象换回原位。好了,现在每个索引都同步了 =P
  6. 现在,是时候将 NativeArray 的大小调整为最终大小 “currentObjects.Count + objectsToAdd.Count” 了。每帧仅一次,并且仅当数组大小发生变化时。我创建了一个 Realloc 方法,该方法创建一个大小合适的新数组,复制重叠数据,然后在大小更改时释放原始数组。请注意,如果没有要添加的对象,调整后的数组将更小并具有正确的大小。
  7. 现在,可以添加新对象了。TransformAccessArray.Add 用于转换,NativeArray[newIndex] = newData 用于新作业数据,List.Add 用于对象列表。在 “object → index” 字典中添加相应的条目。
  8. 清除 “objects to add” 和 “objects to remove” 列表,因为一切都完成了!
  9. 为了在作业运行时使数据可读,我将每帧的 NativeArray 复制到备份帧中。如果不需要,您可以跳过此内容。此克隆非常快,具体取决于数据的大小,因为它会同时发生。
  10. 计划作业
  11. 在下一帧中完成它,重新启动过程

请注意,所有这些数组重建仅在有要添加/删除的内容时发生。
如果没有,则直接跳到步骤 9 并循环“9 → 10 → 11 → 9...”。

示例:

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

namespace SceneTesting
{
    public class ProjectileArcSystem : MonoBehaviour
    {
        public int Capacity = 10000;
        [SerializeField]
        private int projectileCount = 0;
        public ProjectileV2[] ProjectilesArray;
        private List<ProjectileV2> ToAddList = new();
        private List<ProjectileV2> ToRemoveList = new();
        private Stack<int> indexStack = new();

        private NativeArray<ProjectileV2.ProjectileData> projectilesNativeArray;
        private TransformAccessArray projectileTransformAccessArray;
        private TransformAccessArray targetTransformAccessArray;

        private JobHandle jobHandle;


        private void Awake()
        {
            ProjectilesArray = new ProjectileV2[Capacity];
            projectilesNativeArray = new NativeArray<ProjectileV2.ProjectileData>(Capacity, Allocator.Persistent);
            projectileTransformAccessArray = new TransformAccessArray(Capacity);
            targetTransformAccessArray = new TransformAccessArray(Capacity);

            // Fill with null
            for (var i = 0; i < Capacity; i++)
            {
                ProjectilesArray[i] = null;
                projectilesNativeArray[i] = default;
                projectileTransformAccessArray.Add(null);
                targetTransformAccessArray.Add(null);
            }

            for (var i = Capacity - 1; i >= 0; i--)
            {
                indexStack.Push(i);   
            }
        }

        private void OnDestroy()
        {
            jobHandle.Complete();
           
            if (projectilesNativeArray.IsCreated) projectilesNativeArray.Dispose();
            if (projectileTransformAccessArray.isCreated) projectileTransformAccessArray.Dispose();
            if (targetTransformAccessArray.isCreated) targetTransformAccessArray.Dispose();
        }

        public void AddProjectile(ProjectileV2 projectile)
        {
            ToAddList.Add(projectile);
        }

        public void RemoveProjectile(ProjectileV2 projectile)
        {
            ToRemoveList.Add(projectile);
        }

        private void Update()
        {
            // Complete the handle of the previous update loop
            jobHandle.Complete();

            // Write job results back into the data
            Profiler.BeginSample("Process Job Results");
            ProcessJobResults();
            Profiler.EndSample();

            // Process staged projectiles before running a new job
            Profiler.BeginSample("Process staged projectiles");
            ProcessStagedProjectiles();
            Profiler.EndSample();

            // Run the new job
            ExecuteJob();

            // Execute jobs
            JobHandle.ScheduleBatchedJobs();
        }

        private void ProcessJobResults()
        {
            if (projectileCount == 0) return;

            Profiler.BeginSample("Get highest index");
            var count = GetHighestIndex();
            Profiler.EndSample();
           
            Profiler.BeginSample("Copy from Native to Managed");
            for (var i = 0; i < count; i++)
            {
                Profiler.BeginSample("Access native array");
                var projectileData = projectilesNativeArray[i];
                Profiler.EndSample();
               
                if (!projectileData.IsValid) continue;
               
                Profiler.BeginSample("Access Projectile");
                var projectile = ProjectilesArray[i];
                Profiler.EndSample();
               
                Profiler.BeginSample("Set Data");
                projectile.Data = projectileData;
                Profiler.EndSample();
            }
            Profiler.EndSample();
        }

        private int GetHighestIndex()
        {
            for (var i = ProjectilesArray.Length - 1; i >= 0; i--)
            {
                if (ProjectilesArray[i] != null) return i;
            }

            return 0;
        }

        private void ProcessStagedProjectiles()
        {
            Profiler.BeginSample("Removing projectiles");
            for (var i = 0; i < ToRemoveList.Count; i++)
            {
                Profiler.BeginSample("Access Removal List");
                var projectile = ToRemoveList[i];
                Profiler.EndSample();
               
                Profiler.BeginSample("Access Index");
                var index = projectile.Index;
                Profiler.EndSample();
               
                Profiler.BeginSample("Nulling Array Data");
                ProjectilesArray[index] = null;
                Profiler.EndSample();
               
                Profiler.BeginSample("Nulling projectile transform");
                projectileTransformAccessArray[index] = null;
                Profiler.EndSample();
               
                Profiler.BeginSample("Nulling target transform");
                targetTransformAccessArray[index] = null;
                Profiler.EndSample();
               
                Profiler.BeginSample("Set default");
                projectilesNativeArray[index] = default;
                Profiler.EndSample();

                Profiler.BeginSample("Set IsValid");
                var projectileData = projectile.Data;
                projectileData.IsValid = false;
                projectile.Data = projectileData;
                Profiler.EndSample();
               
                indexStack.Push(index);
               
                projectileCount--;
            }

            Profiler.EndSample();

            Profiler.BeginSample("Adding projectiles");
            for (var i = 0; i < ToAddList.Count; i++)
            {
                var projectile = ToAddList[i];
                var nullIndex = indexStack.Pop();
                projectile.Index = nullIndex;
                var projectileData = projectile.Data;
                projectileData.IsValid = true;
                projectile.Data = projectileData;
                ProjectilesArray[nullIndex] = projectile;
                projectilesNativeArray[nullIndex] = projectileData;
                projectileTransformAccessArray[nullIndex] = projectile.Transform;
                targetTransformAccessArray[nullIndex] = projectile.Target;
                projectileCount++;
            }
            Profiler.EndSample();

            ToRemoveList.Clear();
            ToAddList.Clear();
        }

        private void ExecuteJob()
        {
            if (projectileCount == 0) return;

            var getPositionsJob = new GetEnemyPositionsJob
            {
                Output = projectilesNativeArray
            };

            var calculationJob = new ArcProjectileCalculationJob
            {
                ProjectileData = projectilesNativeArray,
                DeltaTime = Time.deltaTime
            };

            var movementJob = new ArcProjectileMovementJob
            {
                ProjectileData = projectilesNativeArray
            };

            var readTargetPositionJob = getPositionsJob.Schedule(targetTransformAccessArray);
            var calculationJobHandle = calculationJob.Schedule(Capacity, 64, readTargetPositionJob);

            jobHandle = movementJob.Schedule(projectileTransformAccessArray, calculationJobHandle);
        }

        [BurstCompile]
        public struct GetEnemyPositionsJob : IJobParallelForTransform
        {
            public NativeArray<ProjectileV2.ProjectileData> Output;

            public void Execute(int index, TransformAccess transform)
            {
                if (!transform.isValid) return;
               
                var data = Output[index];
                data.TargetPosition = transform.position;
                Output[index] = data;
            }
        }

        [BurstCompile]
        public struct ArcProjectileCalculationJob : IJobParallelFor
        {
            public NativeArray<ProjectileV2.ProjectileData> ProjectileData;

            [ReadOnly]
            public float DeltaTime;

            public void Execute(int index)
            {
                // Read projectile data from array
                var projectileData = ProjectileData[index];
                // If the data is invalid, return
                if (!projectileData.IsValid) return;
               
                var targetPosition = projectileData.TargetPosition;
                var currentPosition = projectileData.CurrentPosition;
                var speed = projectileData.Speed;
                var arcFactor = projectileData.ArcFactor;

                var direction = targetPosition - currentPosition;
                var deltaTimeDistance = speed * DeltaTime;
                var step = currentPosition + math.normalize(direction) * speed * DeltaTime;

                // Arc Height
                var totalDistance = math.distance(projectileData.Origin, targetPosition);
                var distanceTravelled = projectileData.DistanceTravelled + deltaTimeDistance;
                // Clamp the value between 0 and total distance so it doesn't overshoot.
                distanceTravelled = math.clamp(distanceTravelled, 0, totalDistance);
               
                var sin = math.sin(distanceTravelled * math.PI / totalDistance);
                var heightOffset = arcFactor * totalDistance * sin;
                var calculatedPosition = step + new float3(0f, heightOffset, 0f);

                // Write data
                projectileData.CurrentPosition = step;
                projectileData.CalculatedPosition = calculatedPosition;
                projectileData.DistanceTravelled = distanceTravelled;
                ProjectileData[index] = projectileData;
            }
        }
       
        /// <summary>
        /// Job to apply the transforms new position
        /// </summary>
        [BurstCompile]
        public struct ArcProjectileMovementJob : IJobParallelForTransform
        {
            [ReadOnly]
            public NativeArray<ProjectileV2.ProjectileData> ProjectileData;

            public void Execute(int index, TransformAccess transform)
            {
                if (!transform.isValid) return;
               
                // Move ourselves towards the target at every frame.
                var projectileData = ProjectileData[index];

                // Write position
                transform.position = projectileData.CalculatedPosition;
            }
        }
    }
}

 

posted @ 2024-09-03 14:29  Flamesky  阅读(67)  评论(0编辑  收藏  举报