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