洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在探索DOTS。
之前说了那么多枯燥的原理,咱们来看看Job System的代码如何写吧。
创建Job
创建Job需要定义一个结构体,实现IJob
接口。实现了IJob
接口之后,就可以让这个Job和其他Job并行运行了。
到这呢,就可以给Job一个真正的定义了:Job是一个统称,任何实现了IJob
接口的结构体,都可以成为一个Job。
创建Job的步骤如下:
1、创建一个实现IJob
接口的结构体
2、给结构体添加所需的成员变量,可以使用blittable类型或者NativeContainer类型。
3、在结构体中添加一个Execute方法,具体执行的任务在这个方法里实现。
当执行Job时,Execute方法会在一个内核上执行完毕。
注意:设计job时,记住job操作的是数据的拷贝,除非使用NativeContainer
。所以,在主线程访问job数据的唯一方法就是写入NativeContainer
。
实例代码如下:
// 这个Job的功能:将两个浮点数相加
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
调度Job
创建Job后,如何执行一个Job呢?
这时候需要调度Job,调度Job的步骤如下:
1、实例化Job
2、构造Job的数据
3、调用Schedule
方法。
调用Schedule
方法会将Job放到Job执行队列的适当位置。一旦安排了Job后,就不能再中断job执行了。
注意:只能在主线程中调用Schedule
方法。
// 创建一个长度为1的native array用来存储job执行后的结果
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// 设置job的数据
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
// Schedule
JobHandle handle = jobData.Schedule();
// 等待job执行完毕
handle.Complete();
// 获取result中的数据
float aPlusB = result[0];
// 一定要释放native array申请的内存
result.Dispose();
Job的依赖关系
很多时候,Job并不是独立运行的,需要依赖前一个Job的结果,这时候如何调度呢?
JobHandle
当调用Schedule
方法时,会返回一个JobHandle
。你可以使用这个JobHandle
作为其他job的依赖项。具体方法就是将第一个job的JobHandle
传给第二个job调用Schedule
时的参数,例如:
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
那如果一个job有多个依赖项怎么办呢?这时候可以用JobHandle.CombineDependencies
方法合并他们。具体如下:
// 声明一个JobHandle的NativeArray数组
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// 将多个handles放到数组中
// 将多个handles合并到一起
JobHandle jh = JobHandle.CombineDependencies(handles);
等待Job执行完毕
在主线程中如何等待Job执行完毕呢?可以调用JobHandle
中的Complete
方法强制等待。Complete
方法执行过后,你就可以在主线程中安全地访问job中使用的NativeContainer
了。
注意
当你调用job的Schedule方法后,job并不会立即开始执行。如果你在主线程中等待job执行完毕,并且你需要访问job使用的NativeContainer中的数据时,你可以调用JobHandle.Complete
方法。这个方法会启动job的执行。调用JobHandle的Complete方法后,会将job的NativeContainer所有权还给主线程。所以只有调用过JobHandle上的Complete方法后,主线程才能安全的访问NativeContainer中的数据。同理,也可以调用依赖此job的JobHandle上的Complete方法。例如,你可以调用jobA的Complete方法,也可以调用依赖jobA的JobB的Complete方法。这两种情况下,主线程都可以安全访问jobA使用的NativeContainer。
实例代码
Job代码:
// Job:两个浮点数相加
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
// Job:给一个值加一
public struct AddOneJob : IJob
{
public NativeArray<float> result;
public void Execute()
{
result[0] = result[0] + 1;
}
}
主线程代码:
// 创建存储结果的NativeArray
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// 设置job #1
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
// 调度job1
JobHandle firstHandle = jobData.Schedule();
// 设置job2
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;
// 调度 job2,依赖job1
JobHandle secondHandle = incJobData.Schedule(firstHandle);
// 等待job2执行完毕
secondHandle.Complete();
// 访问结果
float aPlusB = result[0];
// 释放内存
result.Dispose();
总结
这一节是Job System的Hello World,和其他Hello World一样,并不能解决什么实际问题。等咱们基础知识入门完毕后,再一起来看看Job System的威力。
扩展阅读
【扩展学习】在洪流学堂公众号回复
DOTS
可以阅读本系列所有文章,更有视频教程等着你!
呼~ 今天小新絮絮叨叨的真是够够的了。没讲清楚的地方欢迎评论,咱们一起探索。
我是大智(微信:zhz11235),你的技术探路者,下次见!
别走!点赞,收藏哦!
好,你可以走了。