Unity Dots学习内容记录


前言

DOTS未来的潜力还是挺多的,目前已经陆续有项目局部投入该技术,追求硬件到软件的极致性能。
主要是记录下学习unity dots技术的过程吧。


学习DOTS的前置

什么是DOTS?

DOTS全称Data Oriented Tech Stack,面向数据的技术堆栈。
是由Unity的下面几个技术栈组成
1.Entities(ECS,Entity-Component Data-System)
2.JobSytem(多线程作业调度)
3.BurstCompiler(IR代码优化编译器)
4.Unity Collections(Native Container,非托管内存集合)
5.Unity Mathematiecs(SIMD,单指令多数据数学库)
6.Unity Physics (Dots版本的物理库)

ECS的相关概念

在进行理解ECS之前,需要理解CPU中的Data Layout。
比方说CPU在执行处理指令时是需要将内存里的数据拷贝到CPU本地的Cahce里面的。结构如下:
在这里插入图片描述
当CPU执行指令要访问数据的时候,首先会在Cache里面寻找这个数据,如果没有找到这个时候就产生了一次Cache Miss。
在这里插入图片描述
接下来它就要到内存里面拷贝一个数据到 CPU 的 Cache 里面,但是这个步骤是非常慢的。当从内存拷贝到 CPU 的 Cache 之后,再从 Cache 里访问这个数据就会非常快。后面再去访问这一条 Cache line 中的数据都会是非常快的。
在这里插入图片描述
在这里插入图片描述
如果后面继续访问数据,发现到了上次拷贝过的数据没有覆盖的另一条 Cache line,就又会发生一次 Cache miss,又会比较慢,需要再去内存拷贝数据。
在这里插入图片描述
在这里插入图片描述
ECS是Enity Component System的缩写,是一种面向数据编程的设计思想,Enity只存放世界场景中的Id(唯一标识),数据放在Component里面,System处理这些数据的逻辑。

值得注意的是,ECS和传统的老GameObject方式不一样。
因为我们在使用传统的GameObject(面向对象)的方式进行编程时候,代码访问内存的地址是随机的,所以产生的内存是零散的,这个时候就会造成CPU大量的Cache Miss,这样会造成大量的性能浪费。
下图为使用GameObject的方式编程时,内存的布局分配。
在这里插入图片描述
所以之后引用了ECS的概念,主要是为了使我们的数据结构对CPU友好,使用ECS的数据结构如下图
在这里插入图片描述
那么这里会有个问题,比方上面的图图片,对应的Enity的下面,不同的方块颜色为不同的组件,上面有4个Enity为7个组件,5个enity有5个组件,那么这样就会产生了不同的Archetype。带入,相同的组件数但是组件不相同也是不同的Archetype。当然不同 Archetype 之间可能会共享一些 Component 数据结构,可以去利用这一点来加速计算。

有了 DOTS 的这种结构之后,实际去执行代码的时候会有一个操作叫做 Query,来 Query 需要的数据对象进行处理。
举个例子,可能有 10 种 Archetype,其中可能有 5 种 Archetype 都有 position 这种 Component。当想要处理所有 position 这些数据计算的时候,首先执行 Query,查询所有有 position Component 的这些 Entity,可以把它查询出来,并且连续放在内存里面。

Query 结束之后,下一步就是执行 System 里面的代码,会顺序处理所有的数据。因为这些数据都是连续存储的,会非常快速地拷贝到 CPU 的 Cache 里面,数据计算就会非常迅速。
在这里插入图片描述
在这里插入图片描述
这里其实有几个注意点。首先,Entity 里面是没有数据的,它和老的 GameObject 是不一样的。GameObject 每一个对象里面存储了自己的数据,有自己的脚本,去处理自己的业务逻辑,但是到了 ECS 之后,Entity 是没有数据的,所有的数据放在 Component 里面。System 里面的代码先做 Query,Query 出来需要的数据之后再对它进行处理。

ECS概念

ECS是Entity – Component Data – System
1.Enity的作用是关联多个组件数据,作为对象的唯一索引
2.CompnentData的作用是保存该组件所需要的数据字段
3.System的作用是遍历,筛选,再进行逻辑处理和数据处理

传统GameObject和ECS的代码结构比较

在这里插入图片描述

JobSystem和Burst

关于Job System

Job System提供了编写简单且安全的多线程代码,便于使用所有可用的CPU内核来执行代码,这有助于提高程序的性能

Job System线程安全

为了更轻松地编写多线程代码,作业系统有一个安全系统,可以检测所有潜在的竞争条件,并保护您免受它们可能导致的错误的影响。当一个操作的输出取决于另一个进程的时序时,就会发生争用条件,而另一个进程不受其控制。
例如,如果作业系统将对主线程中代码中的数据引用发送到作业,则它无法验证主线程是否在作业写入数据的同时读取数据。此方案将创建争用条件。为了解决这个问题,作业系统向每个作业发送它需要操作的数据副本,而不是对主线程中数据的引用。此副本隔离数据,从而消除争用条件。作业系统复制数据的方式意味着作业只能访问 Blittable数据类型

Blittable数据类型 .Net的定义文档:https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types

IJob接口代码

void Update()
{
    new MyJob().Schedule(); 
}

struct MyJob : IJob
{
     public void Execute()
    {
        Debug.Log($"我运行的线程ID为{Thread.CurrentThread.ManagedThreadId}");
    }
}

由上面代码可以看出来,定义一个结构体MyJob,实现接口方法Execute,方法内部打印所在当前线程的ID,在Mono脚本的Update中执行代码new MyJob(){ }.Execute();看输出可以知道,其一直运行在线程ID为1的线程上面,线程ID为1的线程为Unity的主线程,由此可以看出,Job是可以直接运行在主线程上面的,当然,本身不是多线程就没必要去使用IJob接口了(笑)。把执行的代码改成new MyJob(){ }.Schedule()的时候,就可以打印出来不同的线程ID了,既多线程运行。
在这里插入图片描述

稍微把代码改一下,不用先前那种匿名的写法,拿到对应的Job对象,调用job.Schedule(),通过这个调用返回一个JobHandle,再通过JobHandle对象调用Complete,这个意思是指,主线程必须要等这个Job的任务完成才会执行jobHandle.Complete()的下一条语句。

 void Update()
 {
      MyJob job = new MyJob();
      JobHandle jobHandle = job.Schedule();
      jobHandle.Complete();
 } 
 
struct MyJob : IJobFor
{
    public void Execute(int index)
    {
        Debug.Log($"我运行的线程ID为{Thread.CurrentThread.ManagedThreadId},job索引为{index}");
    }
}

IJobFor接口

可并行运行任务。每个并行运行的工作线程都有一个独占索引,用于安全地访问工作线程之间的共享数据。IJobFor接口可以并行执行,可以单个Job线程执行。

  void Update()
   {
       MyJob job = new MyJob();
       job.Schedule(10,default); 
   } 

struct MyJob : IJobFor
{
    public void Execute(int index)
    {
        Debug.Log($"我运行的线程ID为{Thread.CurrentThread.ManagedThreadId},job索引为{index}");
    }
}

编写代码如下,修改MyJob实现IJobFor接口,执行代码修改为MyJob jon = new MyJob(); job.Schedule(10,default);这里设定这个Job组的长度为10,意味着执行时候会切换执行长度次,第二个参数为一个JobHandle类型,意思是说需要等这个传入的JobHandle的Job任务都完成了才会开始调度这个Job组。IJobFor的对象使用Schedule方法启动调度Job组的时候,它会优先在一个Job线程上执行完这个Job组的所有任务。使用ScheduleParallel方法的时候,可以配置让继承IJobFor的Job组的Job在执行循环中,会被其他Job线程窃取,可以执行为并发。
在这里插入图片描述

IJobFor代码小例子

 struct VelocityJob : IJobFor
    {
        [ReadOnly] public NativeArray<Vector3> velocity;
        public NativeArray<Vector3> position;
        public float deltaTime;

        public void Execute(int i)
        {
            position[i] = position[i] + velocity[i] * deltaTime;
        }
    }

上面代码定义了继承了IJobFor接口用来计算物体根据向量运动方向计算坐标的一个Job组,右边是的代码,这里使用了NativeArray容器来保存坐标数据,通过这个容器复制给Job对象来保证线程安全。因为IJobFor下会执行自身的Job组的循环,因为有500个物体则需要运算500次,所以Job组也需要循环500次。

 public void Update()
    {
        var position = new NativeArray<Vector3>(500, Allocator.Persistent);
        var velocity = new NativeArray<Vector3>(500, Allocator.Persistent);
        for (var i = 0; i < velocity.Length; i++)
            velocity[i] = new Vector3(0, 10, 0);

        var job = new VelocityJob()
        {
            deltaTime = Time.deltaTime,
            position = position,
            velocity = velocity
        }; 
        JobHandle sheduleJobDependency = new JobHandle();
        JobHandle sheduleJobHandle = job.Schedule(position.Length, sheduleJobDependency); 
        sheduleJobHandle.Complete();
        Debug.Log(job.position[0]);
        position.Dispose();
        velocity.Dispose();
    }

这里把position,velocity的NativeArray容器复制给对应的Job里面来保证线程安全,这里的数据拷贝其实消耗花费不高,因为NativeArray是一个非托管的在C#代码里面运行的C++对象指针,复制的时候,仅仅是复制了一下指针。有因为NativeArray对象不是GC托管的,所以需要使用计算结束后调用Dispose方法将其释放掉。

IJobParallelFor接口

每个并行运行的工作线程都有一个独占索引,用于安全地访问工作线程之间的共享数据。这个接口使用方法和IJobFor接口使用方法相同,至少其内部执行Job组的循环的时候,必定会发生其他Job线程的窃取。

Unity.Collections

此包提供可用于作业和突发编译代码的非托管数据结构。
此软件包提供的集合分为三类:
1.名称以 开头的集合类型具有安全检查,以确保它们已正确处置并以线程安全的方式使用。 Native-
2.名称以 开头的集合类型没有这些安全检查。 Unsafe-
3.其余的集合类型不会分配,也不包含指针,因此它们的处理和线程安全性从来都不是问题。这些类型仅包含少量数据。
字符串类型 NativeText、 FixedString32Byte是可以在JobSystem+Burst下使用的字符串
文档地址

Burst编译器

Burst 是使用 LLVM 从 IL/.NET 字节码转换为高度优化的本机代码的编译器。
手册地址:https://docs.unity.cn/cn/2020.3/Manual/com.unity.burst.html
知乎文章介绍:https://zhuanlan.zhihu.com/p/623274986

[BurstCompile]
struct CalcJob : IJob{ 
    public void Execute() {
         //todo 计算
    }
} 

在这里插入图片描述

Burst在代码里面的用法仅仅是给对应的代码块加上[BurstCompile]特性,加上这个特性之后,Burst就会对着块代码进行编译通过Unity的菜单栏,Jobs>Burst>Open Inspector。通过这个Inspector窗口就能开到对应的Burst编译器的编译情况了,我们在CalcJob结构体上加上了[BurstCompile]标签所以这个窗口能够看到CalcJob编译的中间代码。

简单编写一个Job程序

首先简单些一段代码,这段代码的作用是每一帧运算 1000 * 10000 次开根号计算。

void Update(){
     for(int i = 0;i<1000;i++){
        for(int j = 0;j < 10000;j++){
           Mainf.Sqrt(2.0f);
        }
     }
}

再unity场景中挂入空物体,然后运行场景。运行场景后打开Profiler工具(Window>Analysis>Profiler)观察CPU的使用情况,如下图:
在这里插入图片描述
我们会发现每帧运行161ms左右。
再稍微修改一下下代码。

[Serialize]
public bool UseJobSystem;
void Update()
 {
        if (UseJobSystem)
        {
            NativeArray<JobHandle> jobHandles = new NativeArray<JobHandle>(1000, Allocator.TempJob);
            for (int i = 0;i < 1000;i++)
            {
                CalcJob calcJob = new CalcJob();
                JobHandle jobHandle = calcJob.Schedule();
                jobHandles[i] = jobHandle;
            }
            
            JobHandle.CompleteAll(jobHandles);
            jobHandles.Dispose();
        }
        else
        { 
            int result = 0;
            for (int i = 0;i<1000;i++)
            {
                for (int j = 0;j < 10000;j++) {
                    Mathf.Sqrt(2.0f);
                } 
            } 
        } 
struct CalcJob : IJob
{
   // public NativeArray<int> result;
    public void Execute()
    { 
        for (int i = 0;i<10000;i++)
        {
            Mathf.Sqrt(2.0f);
        } 
    }
}

这里使用一个结构体来继承IJob接口,并且使用NativeArray来管理JobHandle。
打开UseJobSystem变量。观察Profile面板
在这里插入图片描述
发现每帧的耗时降到了30ms。再详细看一下线程使用情况
在这里插入图片描述
会看到主线程做了Job线程的作业调度,然后等待所有Job线程的调度完成。所以说使用Job的情况下,因为使用了多线成分担了这些计算量,所以速度也提升了,因为Job线程有四个,所以速度大概提升了四倍左右。

我们再在结构体CalcJob上面添加一个[BurstCompile]特性。如下

 [BurstCompile]
struct CalcJob : IJob
{
   // public NativeArray<int> result;
    public void Execute()
    {
        int a = 0;
        for (int i = 0;i<10000;i++)
        {
            a = (int)Mathf.Sqrt(2.0f);
        }
 
    }
}

耗时可以降到1.11ms
在这里插入图片描述
BurstCompile特性是使用一种优化过指令的编译方式了生成中间代码,低层使用了SIMD(Single Instruction Multiple Data),单指令多数据流,一个指令多多个数据进行处理。

Package包的使用

Unity的环境搭建

IDE和编辑器的版本支持
Unity版本为2022.3.0f1或以上
Visual Studio 2022或以上
Rider 2021.3.3或以上

安装Package
打开Unity,选中Unity的菜单Window->Packages Manager。点击左上角+号。点击Add Package by name。
把com.unity.entities.graphics拷贝上去并且点击Add,等待添加Package下载安装完成。如下图
在这里插入图片描述

Unity的Entities包

Entities 包是 Unity 面向数据的技术堆栈 (DOTS) 的一部分,它提供了实体组件系统 (ECS) 架构的面向数据的实现。

文档地址:https://docs.unity.cn/Packages/com.unity.entities@1.0/manual/index.html

使用流程

1.场景内创建子场景

因为ECS的场景是需要和Game Object的场景分开的,所以我们需要在Unity的场景中建立一个Sub Scene小场景。在Hierarchy面板里选择鼠标空白处,选择New Sub Scene-》Empty Scene
在这里插入图片描述

2.创建一个System

创建一个脚本文件,名字叫CubeMoveSystem.cs,里面进行编写如下。
在这里插入图片描述
文件内编写了一个CubeMoveSystem的结构体,这个结构继承ISystem接口必须要使用partical进行修饰,因为Unity会生成这个结构体的其他部分代码。这个时候启动就能看到Console面板的打印。
在这里插入图片描述

场景中是需要将Authoring经过烘培转换运行时的实体的。
在这里插入图片描述
在对应的Inspector面板可以查看到对应的Authoring和Running Time选择Authoring模式就是查看Mono脚本下的状态,选择Mixed就是可以查看两种的状态,选择Runtime可以查看这个Authoring转换成的Entity的情况。
在这里插入图片描述

3.对场景的东西进行烘焙(Game Object转换Entity)

子场景中创建一个空节点,并且命名为GameObjectManager。
在这里插入图片描述
再创建两个脚本,文件名GameObjectManagerAuthoring和CubeAuthoring.CS。编写以下代码。编写完成后,脚本挂载到GameObjectManager节点下。
在这里插入图片描述
两个都是可以挂载带Unity节点上的脚本类型,我们把GameObjectManagerAuthoring脚本挂到子场景的Game Object Authoring字点下,创建一个3D物体Cube,把CubeAuthoring脚本挂载到Cube上面并把Cube做成预制体。把预制体挂到GameObjectManagerAuthoring的CubePrefba字段上面去。
在这里插入图片描述
添加两个组件的声明一个是CubeData的结构体,另外一个是GameObjectData的结构体,并且都继承IComponentData接口,都是用来保存上面两个Authoring对象数据的组件。
在这里插入图片描述
再进行烘焙脚本的编写,声明一个GameObjectManagerBaker类,基类为Baker,这里的泛型模板为Baker,因为是要把GameObjectManagerAuthoring烘焙成Entity类型,注意这里烘焙实际上只会进行一次,当I System接口的代码场景一个新的Entity的时候是进行自我拷贝的。编写脚本如下:
在这里插入图片描述
Authoring类就是转换成Entity前的GameObject对象,通过烘焙的方式生成Entity,把Authoring原本的数据拷贝进一些组件上面,比如坐标、缩放等会拷贝到LocalTransform组件,这里的GameObjectData组件主要是用来保存Cube Authoring生成Entity的原型的,所以只保存一个Entity类型的字段。

再就是进行CubeBaker类的编写。
在这里插入图片描述
参数为CubeAuthoring类型,我们需要使用GetEntity方法来创建出一个Entity实例。GetEntity方法第一个参数就是要烘焙的预制体。第二个参数是一个TransformUsageFlags类型,值为Dynamic,为动态的,意思为给这个实体关联上LocalTransform、LocalToWorld等组件,因为Unity已经为用户处理了这些组件,无需用户去维护。下一句代码AddComponent(entity,new CubeData(){ Move Speed = authoring.Move Speed});。这里就是把烘焙出来的实体关联上Cube Data组件。
烘焙相关的代码已经编写完成了,在ECS中所有逻辑都是在System部分完成的,所有我们需要回头处理一下CubeMove System脚本。

修改一下Cube Move System脚本,如下。
在这里插入图片描述
这里代码是,给System做一个技术用的Shared Static,保证在Update方法里面保证只创建一个Entity。编写好我们通过SystemAPI类里面提供的查询方法来查询拥有对应组件。System本身就是用来查询遍历用的。通过上面的查询之后,再通过RefRW对坐标的重写写入,从而做到一个物体一直往(1,0,0)方向移动。而使用RefRW的时候,是支持对应组件的数据写入的,如果是RefRO则是只能进行读的操作。

外观封装

在这里插入图片描述
我们查询的代码是通过SystemAPI.Query提供进行查询的,如果我们查询的组件数量很多,那么代码就不适合这样编写。这里使用一个继承IAspect,再通过封装好的外观进行查询,这样能够使得代码的可读性更高,看起来更舒适。
在这里插入图片描述

世界概念

世界是实体的集合。实体的 ID 号仅在其自己的世界中是唯一的。世界有一个 EntityManager 结构,您可以使用它来创建、销毁和修改世界中的实体。一个世界拥有一组系统,这些系统通常只访问同一世界中的实体。此外,世界中具有相同组件类型的一组实体一起存储在一个原型中,该原型决定了程序中的组件在内存中的组织方式。

默认情况下,当您进入播放模式时,Unity 会创建一个实例,并将每个系统添加到此默认世界中。可以支持多个世界。

系统概念

系统提供将组件数据从当前状态转换为下一个状态的逻辑。例如,系统可能会更新所有移动实体的位置,方法是其速度乘以自上次更新以来的时间间隔。
系统每帧在主线程上运行一次。系统被组织成系统组的层次结构,您可以使用这些层次结构来组织系统应更新的顺序。
可以在实体中创建非托管系统或托管系统。若要定义受管系统,请创建一个继承自 SystemBase 的类。若要定义非托管系统,请创建继承自 I System 的结构。更多信息,请参见系统概述。
两者都有三种方法可以重写:、 和 。系统的方法每帧执行一次。Isystem SystemBase OnUpdate OnCreate OnDestroy OnUpdate
一个系统只能处理一个世界中的实体,因此系统与特定世界相关联。可以使用 World 属性返回系统附加到的世界。

原型概念

原型是世界中具有相同唯一组件类型组合的所有实体的唯一标识符。例如,世界中具有组件类型 A 和 B 的所有实体共享一个原型。具有组件类型 A、B 和 C 的所有实体共享不同的原型,并且具有组件类型 A 和 Z 的所有实体共享另一个原型。
在实体中添加或删除组件类型时,世界的 EntityManager 会将实体移动到相应的原型。例如,如果某个实体具有组件类型 A、B 和 C,并且您删除了其 B 组件,则 会将该实体移动到具有组件类型 A 和 C 的原型。如果不存在这样的原型,则创建它。
基于原型的实体组织意味着按实体的组件类型查询实体非常有效。例如,如果要查找具有组件类型 A 和 B 的所有实体,则可以查找具有这些组件类型的所有原型,这比扫描所有单个实体的性能更高。世界中现有的原型集往往会在程序生命周期的早期稳定下来,因此您可以缓存查询以获得更快的性能。
只有当原型的世界被摧毁时,原型才会被摧毁。

原型块

具有相同原型的所有实体和组件都存储在称为块的统一内存块中。每个块由 16KiB 组成,它们可以存储的实体数量取决于块原型中组件的数量和大小。EntityManager 根据需要创建和销毁区块。
块包含每个组件类型的一个数组,以及一个用于存储实体 ID 的附加数组。例如,在具有组件类型 A 和 B 的原型中,每个块都有三个数组:一个数组用于 A 组件值,一个数组用于 B 组件值,一个数组用于实体 ID。
块的数组是紧密打包的:块的第一个实体存储在这些数组的索引 0 中,块的第二个实体存储在索引 1 中,后续实体存储在连续索引中。将新实体添加到区块时,该实体将存储在第一个可用索引中。当一个实体从块中删除时(因为它被销毁或被移动到另一个原型),块的最后一个实体被移动以填补空白。
将实体添加到原型时,如果原型的现有块全部已满,则创建一个新块。当从块中删除最后一个实体时,将销毁该块。

结构变化概念

导致 Unity 重新组织内存块或内存块内容的操作称为结构更改。重要的是要了解哪些操作是结构性更改,因为它们可能是资源密集型的,并且您只能在主线程上执行它们;不是来自工作。
以下操作被视为结构更改:
1.创建或销毁实体。
2.添加或删除组件。
3.设置共享组件值。

标签组件

标记组件是非托管组件,不存储任何数据,也不占用空间。
从概念上讲,标签组件的用途与游戏对象标签类似,它们在查询中很有用,因为您可以按实体是否具有标签组件来筛选实体。例如,您可以将它们与清理组件和筛选实体一起使用以执行清理。
给对应的对象继承,IEnableableComponent可以控制组件的开关。如果继承了IEnableableComponent接口,在通过这句代码把这个组件给关闭掉。
在这里插入图片描述

共享组件

共享组件根据其共享组件的值将实体分组到块中,这有助于消除重复数据。组件继承ISharedComponentData接口。共享组件的修改和设置会引起结构变化

IJob Entity接口

这个的使用方法和IJobFor接口的使用方法类似,IJobEntity根据其“Execute”方法的参数生成组件数据查询。继承这个接口的结构体可以配合Burst编译器一起使用,大大提高程序性能。
在这里插入图片描述
把向前的CubeMoveSystem的OnUpdate方法移动物体的代码移动到声明的CubeMoveJob方法里面就能在多线程下移动物体坐标。CubeMoveJob的Execute参数由”ref CubeAspect queryRes”填写成”ref LocalTransform localTransform”就会变成查询所有的LocalTransform的组件的实体调用进来。
因为前面的CubeDataz组件的代码继承了IEnableComponent,可以这样编写来查询出来这个组件是否被激活还是开启,RW结尾就是可读可写,RO结尾就是仅仅可读
在这里插入图片描述

IJob Entity依赖

在System的Update使用Job Entity的调度的时候,可以使用当前这个世界的上下文调用CompleteDependency方法,调用这个方法后,是必须要等所有的CubeMoveJob的调度执行完了才会调用往后的代码。不如有两个Job,第二个Job需要等第一个Job执行玩了才往下执行。
在这里插入图片描述

Entity Command Buffer

EntityCommandBuffer简称ecb,主要保存对Entity操作,因为在操作Entity各种组件时,大多数情况下都是并发的情况,那么我们就需要把这些操作结束后产生的结果收集起来,在某一个指定的时刻按顺序修改到有修改的实体的组件上面去,这主要是解决了多线程安全物体。下面直接贴一段代码。

[BurstCompile] 
public void OnUpdate(ref SystemState state) { 
         EntityCommandBuffer.ParallelWriter ecb = GetEntityCommandBuffer(ref state);
         new ProcessSpawnerJob { ElapsedTime = SystemAPI.Time.ElapsedTime, Ecb = ecb }.ScheduleParallel();
 }

 private EntityCommandBuffer.ParallelWriter GetEntityCommandBuffer(ref SystemState state) { 
          var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>(); 
          var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); 
          return ecb.AsParallelWriter(); 
}

[BurstCompile] 
public partial struct ProcessSpawnerJob : IJobEntity { 
            public EntityCommandBuffer.ParallelWriter Ecb; 
            public double ElapsedTime;   
            private void Execute([ChunkIndexInQuery] int chunkIndex, ref Spawner spawner) {   
                     if (spawner.NextSpawnTime < ElapsedTime) {  
                          Entity newEntity = Ecb.Instantiate(chunkIndex, spawner.Prefab); 
                          Ecb.SetComponent(chunkIndex, newEntity, LocalTransform.FromPosition(spawner.SpawnPosition));                                                                   w                       spawner.NextSpawnTime = (float)ElapsedTime + spawner.SpawnRate; 
                      }
            } 
}

关于Job System + Burst 于Game Object的相互通讯

Burst 对访问静态只读数据具有基本支持。但是,如果要共享静态可变数据,使用结构如下。

public static class MutableStaticTest { 
        public static readonly SharedStatic<int> IntField = SharedStatic<int>.GetOrCreate<MutableStaticTest, IntFieldKey>();  
         // Define a Key type to identify IntField 
         private class IntFieldKey {} 
}

在IntField声明的字段里面的数据,Burst和Game Object部分都可以进行访问。

相关工程地址

Demo的主要作用是为了比较使用DOTS方案实现多怪物场景和Game Object方案实现性能差异。
所以Demo有两个场景一个是Dots实现的一个是使用GameObject方式实现的。
工程链接:https://github.com/kof123w/DotsProject

工程的运行环境:
Unity版本 2022.3.14及以上 前置依赖包 Entities.Graphics,Unity.Physics,Unity.Burst,Univerisal RP
渲染管线 Universal Renderer Pipeline

参考文档

ECS官方文档

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity DOTS(DOTS: Data-Oriented Technology Stack)是Unity的一种数据导向技术堆栈,它旨在提高游戏性能和可扩展性。它基于实体组件系统(Entity-Component-System,ECS)的概念,其中游戏对象被拆分为实体(Entity)和组件(Component),并通过系统(System)进行处理。 在Unity DOTS中,可以使用实体组件系统来管理和处理游戏对象。通过将游戏对象转换为实体和组件的形式,可以实现高效的数据处理和并行计算。例如,在创建实体时,可以使用GameObjectConversionUtility.ConvertGameObjectHierarchy函数将GameObject转换为Entity,并使用IConvertGameObjectToEntity接口进行自定义转换。然后,可以使用系统(System)对实体和组件进行处理,例如旋转方块事件。 Unity DOTS的优势包括更高的性能,更好的可扩展性和更方便的并行计算。通过采用数据导向的设计,可以减少内存访问和数据处理的开销,从而提高游戏的帧率和响应性。 总而言之,Unity DOTS是一种数据导向的技术堆栈,通过实体组件系统和并行计算来提高游戏性能和可扩展性。它可以通过转换游戏对象为实体和组件的形式,并使用系统进行处理来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [UnityDots技术入门](https://blog.csdn.net/m0_37920739/article/details/108181541)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Unity DOTS简明教程](https://blog.csdn.net/mango9126/article/details/105219215)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值