此篇记录Dots使用的步骤。
Demo逻辑是在场景内创建2000个物体。1000个是搜索,1000是被搜索的目标。
每个物体都随机朝某个方向移动。
在Update里。1000个搜索者会对1000被搜索的目标进行遍历查找到哪个目标离他最近,并且画出连线。
逻辑如下:
//搜索者 public class Seeker : MonoBehaviour{public Vector3 Direction;public void Update(){transform.localPosition += Direction * Time.deltaTime;}}//目标者public class Target : MonoBehaviour{public Vector3 Direction;public void Update(){transform.localPosition += Direction * Time.deltaTime;}}//查找方法public class FindNearest : MonoBehaviour{public void Update(){// Find nearest Target.// When comparing distances, it's cheaper to compare// the squares of the distances because doing so// avoids computing square roots.Vector3 nearestTargetPosition = default;float nearestDistSq = float.MaxValue;foreach (var targetTransform in Spawner.TargetTransforms){Vector3 offset = targetTransform.localPosition - transform.localPosition;float distSq = offset.sqrMagnitude;if (distSq < nearestDistSq){nearestDistSq = distSq;nearestTargetPosition = targetTransform.localPosition;}}Debug.DrawLine(transform.localPosition, nearestTargetPosition);}}//场景初始驱动public class Spawner : MonoBehaviour{// The set of targets is fixed, so rather than // retrieve the targets every frame, we'll cache // their transforms in this field.public static Transform[] TargetTransforms;public GameObject SeekerPrefab;public GameObject TargetPrefab;public int NumSeekers;public int NumTargets;public Vector2 Bounds;public void Start(){Random.InitState(123);for (int i = 0; i < NumSeekers; i++){GameObject go = GameObject.Instantiate(SeekerPrefab);Seeker seeker = go.GetComponent<Seeker>();Vector2 dir = Random.insideUnitCircle;seeker.Direction = new Vector3(dir.x, 0, dir.y);go.transform.localPosition = new Vector3(Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));}TargetTransforms = new Transform[NumTargets];for (int i = 0; i < NumTargets; i++){GameObject go = GameObject.Instantiate(TargetPrefab);Target target = go.GetComponent<Target>();Vector2 dir = Random.insideUnitCircle;target.Direction = new Vector3(dir.x, 0, dir.y);TargetTransforms[i] = go.transform;go.transform.localPosition = new Vector3(Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));}}}
此段代码没有介入Dots任何优化,在我的电脑里跑帧率每秒4.3帧
对上面的进行改造介入job
//查找功能用Job来实现
[BurstCompile]public struct FindNearestJob : IJob{// All of the data which a job will access should // be included in its fields. In this case, the job needs// three arrays of float3.// Array and collection fields that are only read in// the job should be marked with the ReadOnly attribute.// Although not strictly necessary in this case, marking data // as ReadOnly may allow the job scheduler to safely run // more jobs concurrently with each other.// (See the "Intro to jobs" for more detail.)[ReadOnly] public NativeArray<float3> TargetPositions;[ReadOnly] public NativeArray<float3> SeekerPositions;// For SeekerPositions[i], we will assign the nearest // target position to NearestTargetPositions[i].public NativeArray<float3> NearestTargetPositions;// 'Execute' is the only method of the IJob interface.// When a worker thread executes the job, it calls this method.public void Execute(){// Compute the square distance from each seeker to every target.for (int i = 0; i < SeekerPositions.Length; i++){float3 seekerPos = SeekerPositions[i];float nearestDistSq = float.MaxValue;for (int j = 0; j < TargetPositions.Length; j++){float3 targetPos = TargetPositions[j];float distSq = math.distancesq(seekerPos, targetPos);if (distSq < nearestDistSq){nearestDistSq = distSq;NearestTargetPositions[i] = targetPos;}}}}}//数据都用NativeArray来替代。在Update里调用上面写的Jobpublic class FindNearest : MonoBehaviour{// The size of our arrays does not need to vary, so rather than create// new arrays every field, we'll create the arrays in Awake() and store them// in these fields.NativeArray<float3> TargetPositions;NativeArray<float3> SeekerPositions;NativeArray<float3> NearestTargetPositions;public void Start(){Spawner spawner = Object.FindFirstObjectByType<Spawner>();// We use the Persistent allocator because these arrays must// exist for the run of the program.TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);}// We are responsible for disposing of our allocations// when we no longer need them.public void OnDestroy(){TargetPositions.Dispose();SeekerPositions.Dispose();NearestTargetPositions.Dispose();}public void Update(){// Copy every target transform to a NativeArray.for (int i = 0; i < TargetPositions.Length; i++){// Vector3 is implicitly converted to float3TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;}// Copy every seeker transform to a NativeArray.for (int i = 0; i < SeekerPositions.Length; i++){// Vector3 is implicitly converted to float3SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;}// To schedule a job, we first need to create an instance and populate its fields.FindNearestJob findJob = new FindNearestJob{TargetPositions = TargetPositions,SeekerPositions = SeekerPositions,NearestTargetPositions = NearestTargetPositions,};// Schedule() puts the job instance on the job queue.JobHandle findHandle = findJob.Schedule();// The Complete method will not return until the job represented by// the handle finishes execution. Effectively, the main thread waits// here until the job is done.findHandle.Complete();// Draw a debug line from each seeker to its nearest target.for (int i = 0; i < SeekerPositions.Length; i++){// float3 is implicitly converted to Vector3Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);}}}
运行效率提升很明显:
并且在Profiler能看到该job的运行情况
对此提升的解释为:
1.Update从原来的每个Seeker里调用改成到只在Spawner里调。次数从1000次变为1次
2.这点提升最多,数据的获取是从3级缓存里提取非堆内存提取。因为数据是连续非离散的,这提高了访问效率。
既然这个能单线程跑,那么一定也能多线程跑起来从而进一步提升效率
对Job进行改造:
//job逻辑是一样的,但是接入接口不一样,此为IJobParallelFor[BurstCompile]public struct FindNearestJob : IJobParallelFor{[ReadOnly] public NativeArray<float3> TargetPositions;[ReadOnly] public NativeArray<float3> SeekerPositions;public NativeArray<float3> NearestTargetPositions;public void Execute(int index){float3 seekerPos = SeekerPositions[index];float nearestDistSq = float.MaxValue;for (int i = 0; i < TargetPositions.Length; i++){float3 targetPos = TargetPositions[i];float distSq = math.distancesq(seekerPos, targetPos);if (distSq < nearestDistSq){nearestDistSq = distSq;NearestTargetPositions[index] = targetPos;}}}}//find这块,执行并发public class FindNearest : MonoBehaviour{NativeArray<float3> TargetPositions;NativeArray<float3> SeekerPositions;NativeArray<float3> NearestTargetPositions;public void Start(){Spawner spawner = GetComponent<Spawner>();TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);}public void OnDestroy(){TargetPositions.Dispose();SeekerPositions.Dispose();NearestTargetPositions.Dispose();}public void Update(){for (int i = 0; i < TargetPositions.Length; i++){TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;}for (int i = 0; i < SeekerPositions.Length; i++){SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;}FindNearestJob findJob = new FindNearestJob{TargetPositions = TargetPositions,SeekerPositions = SeekerPositions,NearestTargetPositions = NearestTargetPositions,};// Execute will be called once for every element of the SeekerPositions array,// with every index from 0 up to (but not including) the length of the array.// The Execute calls will be split into batches of 100.//把任务拆成100个小任务,可能分到不同的cpu执行JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100);findHandle.Complete();for (int i = 0; i < SeekerPositions.Length; i++){Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);}}}
帧率又进行了提升:
在Profiler里能看到已经分配到别的cpu执行调用了
最后是对排序那块进行部分优化。使用二分法,不过看起来性能没多少提升。甚至下降
[BurstCompile]public struct FindNearestJob : IJobParallelFor{[ReadOnly] public NativeArray<float3> TargetPositions;[ReadOnly] public NativeArray<float3> SeekerPositions;public NativeArray<float3> NearestTargetPositions;public void Execute(int index){float3 seekerPos = SeekerPositions[index];// Find the target with the closest X coord.int startIdx = TargetPositions.BinarySearch(seekerPos, new AxisXComparer { });// When no precise match is found, BinarySearch returns the bitwise negation of the last-searched offset.// So when startIdx is negative, we flip the bits again, but we then must ensure the index is within bounds.if (startIdx < 0) startIdx = ~startIdx;if (startIdx >= TargetPositions.Length) startIdx = TargetPositions.Length - 1;// The position of the target with the closest X coord.float3 nearestTargetPos = TargetPositions[startIdx];float nearestDistSq = math.distancesq(seekerPos, nearestTargetPos);// Searching upwards through the array for a closer target.Search(seekerPos, startIdx + 1, TargetPositions.Length, +1, ref nearestTargetPos, ref nearestDistSq);// Search downwards through the array for a closer target.Search(seekerPos, startIdx - 1, -1, -1, ref nearestTargetPos, ref nearestDistSq);NearestTargetPositions[index] = nearestTargetPos;}void Search(float3 seekerPos, int startIdx, int endIdx, int step,ref float3 nearestTargetPos, ref float nearestDistSq){for (int i = startIdx; i != endIdx; i += step){float3 targetPos = TargetPositions[i];float xdiff = seekerPos.x - targetPos.x;// If the square of the x distance is greater than the current nearest, we can stop searching.if ((xdiff * xdiff) > nearestDistSq) break;float distSq = math.distancesq(targetPos, seekerPos);if (distSq < nearestDistSq){nearestDistSq = distSq;nearestTargetPos = targetPos;}}}}public struct AxisXComparer : IComparer<float3>{public int Compare(float3 a, float3 b){return a.x.CompareTo(b.x);}}