当前位置: 首页 > news >正文

Unity官方Dots范例工程学习——Jobs101

  大家好,我是阿赵。
接下来继续学习Unity的DOTS。
首先来学习的是Jobs101项目,之前在Git上面检出的多个项目里面的其中一个:
在这里插入图片描述

  使用Unity6打开该项目,会看到这里主要有4个Step的场景:
在这里插入图片描述

  通过项目命名可以直观的知道,这个Jobs101的范例项目,是打算通过4个步骤,一步步的向我们展示Job System的使用方法。

一、 Step1

  打开Step 1文件夹里面的Step1_NoJobs场景,并且运行:
在这里插入图片描述

  可以看到
在这里插入图片描述

  场景里面有很多红蓝色的方块,然后帧率非常低,非常的卡顿。

1、例子说明

  接下来需要对这个例子进行一定的说明,好让大家知道,Unity想通过这个例子向我们说明什么。因为接下来的步骤里面其实都是同一个例子,只是处理的方式不一样,性能也不一样而已。
在这个例子里面,有2种对象:

1. Seeker

  Seeker是探索者的意思,它在demo里面是蓝色的方块:
在这里插入图片描述

2. Target

  Target是Seeker需要寻找的目标,在demo里面是红色方块
在这里插入图片描述

  然后Seeker和Target上面挂载的Seeker和Target脚本,其实做的事情都是一样的,就是让自身沿着一个方向移动。

    public class Seeker : MonoBehaviour{public Vector3 Direction;public void Update(){transform.localPosition += Direction * Time.deltaTime;}}

  所以这两个脚本可以不用在意。

  这个Demo要做的事情是,先根据设定的数量,生成大量的Seeker探索者和Target目标,然后这些大量的Seeker和Target都是在按照一定的方向和速度在移动的。而Seeker既然成为探索者,那么它们是需要在不断的对自己附近进行探索,找到离自己最近的一个Target的。
切换到Scene视图,就可以看到:
在这里插入图片描述

  每一个蓝色的Seeker,都会通过一条线,找到离自己最近的一个Target。
然后在场景里面,我们可以找到一个Spawner的对象:
在这里插入图片描述

  它上面挂载的Spawner脚本,里面指定了Seeker和Target分别使用的预设对象,还有Seeker和Target分别生成的数量,比如截图里面就是各1000个。最后是生成的范围Bounds是500x500的范围内生成。
接下来的几个Step其实都是这个例子,都是Seeker寻找离自己最近的Target。

2、 Step1例子说明

  回头看看Step1这个例子,由场景的命名NoJobs,我们可以直观的理解到,这个例子,其实是没有使用JobSystem的。
那么使用传统的方法,如果我们需要找到每个Seeker最近的Target,一般有什么办法呢?很明显就是直接遍历然后对比距离的大小了。
所以在每个Seeker对象上面, 都挂载了一个FindNearest的脚本。
在这里插入图片描述

  这个脚本的作用很简单:

namespace Tutorials.Jobs.Step1
{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);}}
}

  就是先从Spawner里面拿到了所有的Target对象,然后逐个对比和自己的距离,找到最近的一个,然后画线。
这一步没什么问题,思路很正常。但由于对象的数量多,假设Seeker有m个,target有n个,那么它的计算次数就是m乘n了。所以,在场景里面各1000个对象的情况下,计算数量是1000x1000等于一百万次的距离计算和比较,就非常的卡顿了。

二、 Step2

  接下来打开Step 2文件夹里面的Step2_SingleThreadedJob场景:
在这里插入图片描述

  从Spawner可以看到,同样是分别创建1000个Seeker和Target,我们运行看看有什么区别:
在这里插入图片描述

  最大的区别在于,帧率飙升了,从刚才step1只有个位数的fps,达到了100fps以上。
是什么操作导致了这么大的提升呢?
先来看看Seeker使用的预设
在这里插入图片描述

  会发现之前的FindNearest脚本没有了,只剩下Seeker脚本。
而FindNearest的脚本变成在Spawner
在这里插入图片描述

  我们从场景的命名上看,SingleThreadedJob,我们大致可以猜到,这个Step2实际上是想告诉我们怎样用一个线程来运行Job System的。
所以我们可以打开Step2里面的FindNearest脚本看看:
在这里插入图片描述

  主要看这个部分,在设置了所有的Seeker和Target的Position之后,脚本创建了一个FindNearestJob,然后通过FindNearestJob又拿到了一个JobHandle。最后这个JobHandle调用了Complete方法。
这个脚本的注释里面已经说明了它所做的事情

1. 创建Job的实例并填充数据

//To schedule a job, we first need to create an instance and populate
its fields.

意思是,如果想计划一个job,我们首先需要创建一个实例并且填充它的字段。
所以

    FindNearestJob findJob = new FindNearestJob{TargetPositions = TargetPositions,SeekerPositions = SeekerPositions,NearestTargetPositions = NearestTargetPositions,};

2. 把Job实例放进job队列

//Schedule() puts the job instance on the job queue.

所以

JobHandle findHandle = findJob.Schedule();

意思是通过Schedule函数的调用,把job的实例放到job的队列里面。

3. 等待Job完成

        // 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.

  意思是,这个Complete函数会一直等待job的处理完成才会返回,在这个过程中,主线程是会一直在这里等待job完成。
所以当调用:

findHandle.Complete();

  这里其实主线程是会等待job执行的。
这个过程我们应该能理解了,它是在执行FindNearestJob。
在这里插入图片描述

  而打开这个Step2文件夹里面的FindNearestJob脚本看看
在这里插入图片描述

  这里实际上也是通过了两层循环去遍历所有的Seeker和Target,来寻找每个Seeker最近的Target。
但这里有些不一样,需要注意的地方:

1. 新数学库的使用

  之前挂载在Seeker上的FindNearest脚本,是使用了Vector3来代表坐标,然后使用Vector3。sqrMagnitude来计算向量平方长度的
这个例子里面,使用了Unity.Mathematics.float3来代替了Vector3,然后使用了Unity.Mathematics.math.distancesq来代替 Vector3.sqrMagnitude.
使用新的Unity.Mathematics.math数学库,是因为旧的数学库诸如Vector3、Quaternion等类型的内存布局非最优,GC压力大。而新的数学库在解决了这些问题之余,还可以利用Burst编译器优化,提升计算的性能。
为了这个目的,所以在生成Job实例之前,需要先通过遍历,把所有的Seeker和Target的坐标,转换成NativeArray数组

NativeArray<float3> TargetPositions;
NativeArray<float3> SeekerPositions;

  需要注意的是,Vector3是可以隐式转换成float3的,所以可以:

TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;

2. 集中遍历

在之前的例子里面,都是单个Seeker对象在Update生命周期里面进行对Target列表的遍历。而这个例子里面,所有的遍历都集中在一起,通过一个线程去统一计算。
做了这么简单的修改之后,可以看到性能就有了突飞猛进的提高了。这让人更其他后面的两个步骤了。

三、 Step3

  接下来打开Step3文件夹下的Step3_ParallelJob场景
在这里插入图片描述

  运行看看:
在这里插入图片描述

  吓了一跳,怎么step2的帧率都有100fps,step3才40多?不要急,看看生成的数量:
在这里插入图片描述

  之前step1和step2都是Seeker和Target各生成1000个,这里变成了各5000个,从遍历的数量级来说,是整整25倍了,从100万次变成了2500万次了。如果改回各生成1000个,fps是有140多的。
在这里插入图片描述

  所以很明显,这次的Step3又在Step2的基础上有了较大的进步。从场景的名称看,ParallelJob,是平行工作的意思,因为Step2是一个线程执行Job,那么我们可以理解成Step3其实是多个线程同时执行Job了。
看看Step2和Step3的主要区别:

1、 Schedule方法的调用

  在Step3的FindNearest脚本里面,可以看到:

JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100);

这里的说明是:

        // 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.

  意思是,Execute方法将会根据SeekerPositions数组长度的每个元素都调用一次,每次调用的index是从0一直到SeekerPositions 数组的长度(不包括数组长度本身)。
然后这些调用会分成每个批次调用100个。
怎么理解呢?
第一个参数很好理解,这个批处理里面我们总共需要处理多少个数据。而第二个参数,其实是指每个线程单次处理的时候,处理多少个数据,比如上面设置了100,那么第一个线程处理的将会是0-99号数据,第二个线程处理的是100-199号数据。
那么问题来了,如果这些数据之间是需要互相访问的,比如我在处理0-99号数据的时候,是需要拿到第110号的数据,怎么办呢?由于线程之间的是同时运行的,如果数据在线程中发生变化,其他线程是不知道的。如果真的需要把所有数据都在过程中修改并且可以互相调用,那么只能把调用批次设置成和数组总长度一样了。

2、 Execute方法的参数

  由于Schedule方法设置了总数量和批次,所以Job的Execute方法也会带有Index参数
在这里插入图片描述

  之前的Step2的Job,里面的Execute方法是没有Index参数的,意味着它每次进程会只调用一次。而Step3的Job,里面的Execute方法是带Index的,意味着根据数组的长度每一个元素会调用一次。
由于这样并行运行了,所以执行的效率就会比Step2更快了。
如果我们改成这样:

JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, SeekerPositions.Length);

  那么其实每次都是一个批次处理完所有元素,那么实际的执行效率将会和Step2一样了。

四、 Step4

  打开Step4文件夹的Step4_ParallelJob_Sorting场景:
在这里插入图片描述

  运行之后
在这里插入图片描述

  看到帧率只有不到30,不过还是不用急,看看生成的数量。这次生成的是Seeker和Target各一万个,所以实际的遍历数量是1亿次了。
如果是这个数量级,在Step3里面运行:
在这里插入图片描述

  帧率就降到只有10几了。
从场景命名看,ParallelJob_Sorting,比Step3多了一个Sorting,意味着多了一个排序,那么具体是做了什么呢?
主要的区别是:

SortJob<float3, AxisXComparer> sortJob = TargetPositions.SortJob(new AxisXComparer { });FindNearestJob findJob = new FindNearestJob
{TargetPositions = TargetPositions,SeekerPositions = SeekerPositions,NearestTargetPositions = NearestTargetPositions,
};JobHandle sortHandle = sortJob.Schedule();
JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100, sortHandle);

在这里创建了一个sortJob,这个是一个排序的Job,具体的方法是AxisXComparer :

    public struct AxisXComparer : IComparer<float3>{public int Compare(float3 a, float3 b){return a.x.CompareTo(b.x);}}

  这个排序是对TargetPositions里面的所有元素的x轴进行从小到大的排序的。
然后在FindNearestJob里面,再次利用AxisXComparer 方法,找到离当前Index的Seeker的x方向距离最短的Target,作为一个备选答案,求出当前Seeker和这个Target的实际距离的平方,然后再从这个Target的序号分别往前和往后遍历,每个Target和当前的备选Target做对比,看有没有比备选答案距离更短的。
我个人觉得,这只是一个优化方式的举例,各位不用太较真这个排序算法本身。这里是做了一个小算法,由于已经知道了目标和当前Seeker和备选Target的实际距离的平方,而实际距离的平方是x偏移x偏移+y偏移y偏移,所以只要计算下一个Target的x偏移*x偏移大于当前的距离平方,就可以不用考虑这个Target了:
在这里插入图片描述

  而由于之前已经通过排序方法对TargetPositions进行排序了,所以应该来说从x最近的Target开始,从往左或者往右的其中一边,应该有连续的一大段都不需要参加计算。
不要在意这个算法本身,需要留意的是,我们可以在执行Schedule函数的时候,指定多一个handle,用于多线程处理。不过由于这个例子本身的计算还是比较简单,所以感受不出很大的变化。

五、 总结

  看完了4个例子,已经基本掌握了Unity的Job System的使用了,这个官方例子告诉了我们这些内容:

1、 Job System是可以单独使用的

  在这几个例子里面,并没有使用到ECS,只是普通的常用代码方式编写。所以并不是说它是DOTS中的一员,就一定要和ECS一起使用的,也可以单独使用。

2、需要使用Job System,需要的步骤是:

1.创建Job实例

  通过new方法创建出想要的Job实例,并且填充里面的字段数据。创建出来的实例并不会立刻执行

2.调用计划

  改过Job实例调用Schedule方法,可以传入总长度和批次,也可以不传。

3、等待完成

Schedule返回的handle调用Complete方法,等待Job完成

http://www.dtcms.com/a/394820.html

相关文章:

  • 如何在SQLite中实现事务处理?
  • 广东省省考备考(第一百零四天9.22)——判断推理(强化训练)
  • k8s 常用命令
  • windows远程桌面服务安全加固的配置指南
  • datawhale玩转通义四大新模型 202509 第4次作业
  • MySQL 表约束实战指南:从概念到落地,守护数据完整性
  • 64位整型变量错误使用int类型对应的格式化符%d导致软件崩溃问题的排查与分析(借助deepseek辅助分析)
  • 【Linux操作系统】简学深悟启示录:Ext系列文件系统
  • 第8节-PostgreSQL数据类型-UUID
  • S2多维可视分析表格解析
  • 面经分享--百度开发一面
  • 第15讲 机器学习的数学
  • NestJS-身份验证JWT的使用以及登录注册
  • ChatGPT “影子泄露” 漏洞:黑客可隐秘窃取电子邮件数据
  • Coze Stdio模型配置
  • DSC 参数ARCH_HANG_FLAG对集群的影响
  • Android Jetpack Compose 从入门到精通
  • 【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧
  • C#练习题——LinkedList 的进阶应用与测试
  • 手机CPU型号
  • jdbc相关知识
  • yolov12 导出onnx
  • Linux 环境变量与程序地址空间
  • LeetCode:48.路径总和Ⅲ
  • 计算机网络的性能
  • 深度学习笔试选择题:题组1
  • 统一配置管理根据不同域名展现不同信息或相近信息 Vue3类单例模式封装
  • 人工智能深度学习——循环神经网络(RNN)
  • 单例模式指南:全局资源的安全访问
  • 容器化 Tomcat 应用程序