Ilingis 发表于 2022-10-26 10:14

关于Unity Dots 1.0版本的学习研究(三)之基础概念

一、简介

如果了解了前两篇内容的读者,应该对dots整体已经有了一定的熟悉,且能够利用dots实现一些小功能了。但可能对其中部分概念理解的还是不是很透彻,本文则将更细致的介绍相关的基础概念,帮助大家加深印象。
二、核心概念

2.1 DOTS

全称Data-Oriented Techonology Stack,面向数据的技术栈,由Burst,ECS,JobSystem3大块组成。其中,Burst用来优化代码的编译效果,使运行速度更快,ECS指实体组件系统,JobSystem则提供多线程执行支持(篇幅二已详细介绍)。
2.2 ECS

unity的实体组件系统,由以下3个部分组成:

[*]Entity。实体,ECS中的E,用来标识物体,不带任何方法。Dots模式部分的所有物体运行前都会转化成实体。
[*]ComponentData。组件数据,ECS中的C,仅用来存放组件数据,不存在任何方法。
[*]System。系统,ECS中的S,用来处理组件数据的方法,system代码会在运行游戏后自动执行。
[*]Aspect。横截面,多个组件数据、实体等的组合,也可以包含其他Aspect,适用于需要同时获取多个组件数据的system。
[*]Authoring。创作者,挂在gameobject上,运行时,在对应的脚本转化为实体时会调用,可以用来在转化过程中为目标实体添加新的组件数据。
优势:

[*]通过chunk内存对齐,优化执行效率;
[*]system遍历chunk执行逻辑,数据是连续的,缓存命中率高,执行效率更高;
[*]组件只存在必要的数据,不像之前Transform等存在大量冗余数据,因此内存占用少;
[*]所有数据均为结构体,非托管对象,不存在gc开销。
2.3 Unmanagerd Collection

非托管的集合对象。dots中不能使用托管的内存对象,所以相关集合都需要转化成非托管的对象来使用。
2.4 其他概念


[*]chunk。块,系列二中已介绍过了。包含相同组件的实体存放在一个块中,但块是有大小的(128),当数量很多时,就需要分成多个块。
[*]Archetype。原型,系列二中已介绍过了。包含相同组件类型的实体可能存放在多个chunk中,而原型则存储所有的这些chunk。即原型中包含一个world中的所有具备相同组件的实体数据。
三、Entity详解

3.1 Entity与GameObject的异同


[*]GameObject是托管的对象,而实体仅是一个唯一标识id。
[*]相同类型的组件,一个实体只允许有一个,而gameobject可以有多个。
[*]实体没有parent的概念,可以通过parent组件来实现。
[*]实体可以添加方法,但是不推荐使用。
实体的组件类型可以是IComponentData、ICleanupComponent、IBufferElementData、ISharedComponent等。
3.2 SubScene机制

1.0中为了将ECS和常规开发模式更好的融合,添加了subscene机制(鼠标右键菜单,添加subscene即可创建),需要ECS模式执行的对象可以放在subscene下,运行前,subscene下的所有对象都会转化为实体,其他部分则保持原样不变。
相比于面向对象开发,面向数据开发对大家来说并不是很友好,但很多情况这却是优化性能的关键,所以这个机制好处就在于在平常可以使用面向对象开发,仅把需要ECS优化的逻辑放在subscene下以ECS模式运行,从而很好的实现面向对象和面向数据的共存。



图1 subscene示例

3.3 关于worlds和EntityManagers

world是存放着实体的集合,且实体的唯一id也是仅限于其所在的world,即不同的world之间可能会存在不同的实体,但其实体id相同。
实体的创建和销毁是通过world的EntityManager来管理的。方法简介如下:

[*]CreateEntity。创建一个实体。
[*]Instantiate。根据另一个对象创建一个实体,会复制目标对象上的组件。
[*]DestroyEntity。销毁实体。
[*]AddComponent<T>()。给实体添加组件。
[*]RemoveComponent<T>()。移除组件。
[*]GetComponent<T>()。获得组件。
[*]SetComponent<T>()。更改组件值,因为组件都是结构体,所以赋值需要重新设置值。
3.4 EnityId介绍

EntityManager为了能够通过id来查找实体,其内部维护了一个实体元数据的数组,如图2所示,每个实体Id对应此数组的一个槽,当要获取指定id的实体数据时,仅需把id当成此数组的index,获取对应槽的值,而槽的值为指向实体在chunk中位置的指针,如果指向的数据为空,则说明,实体不存在,反之则是要查找的目标实体。因此查找效率还是挺高的。
另外实体是可以销毁的,销毁后,对应槽的指针将指向null,同时这个索引将可以继续使用,此时假如为此槽分配了新的实体,那么之前访问实体的地方,再次根据id查找实体时将获得新的实体,导致出错,因此实体数据中除了id外,还会储存一个VersionNumber,标识实体的版本,槽中的实体每销毁一次,其存储的versionNumber就会自动+1,这样,之前访问实体的地方获取实体时,发现版本不对,就会提示不存在,从而解决了旧的引用获取新实体的问题。



图2

3.5 EnityQuery

system往往需要处理包含特定组件的实体,而这样的组件的数据的收集,则是通过EnityQuery来完成的。由于原型存放着包含相同组件类型的实体。因此当要查找包含某些组件或不包含某些组件的实体时,仅需比较原型就可以很快的拿到要查找的目标实体的chunks,而不用每个实体一一比对,这也是其高效查找的原因。
关于查询实体的方法有很多种,大家可以看demo了解下,后续有需要笔者也会总结在这里。
四、ComponentData

组件数据为结构体类型,且需继承自IComponentData。它是ECS中数据的载体,开发中需要根据不同的功能开发不同的组件,有的时候它可以没有变量,仅用来标识具备某个功能。
4.1 如何为monobehaviour转化的实体添加组件

创建一个组件数据,把脚本中的变量在组件数据中声明一下,再为其创建对应的backer,在baker中为其添加对应的组件即可。除此之外,还可以通过EntityManager等方法操作组件,这部分案例很多,就不过多介绍了。



图3

4.2 DynamicBuffer

可变长的数组,需要实现IBufferElementData接口,默认的初始数组长度为8,如果想要改变默认长度,可以通过
给类添加InternalBufferCapacity特性来调整。注意,它是存储在chunk外的。
当dynamicbuffer结构改变后,后续使用将会被安全检测机制检测到并抛出异常,因此每次结构改变都要创建一个新的dynamicbuffer。
4.3 Enableable components

实现了IBufferElementData或IComponentData的结构体,还可以同时实现IEnableableComponent接口,只有实现了此接口,组件的使能状态才能被控制,不使能的组件将无法查询到。官方文档中提供的存在控制组件使能状态API的途径:

[*]EntityManager。API: SetComponentEnable<T>(Entity)。
[*]ComponentLookup<T>。 API:SetComponentEnabled(Entity,bool)
[*]BufferLookup<T>(控制dynamic buffer使能状态)。 API:SetComponentEnabled(Entity,bool)
[*]EnabledRefRW<T> (used in a SystemAPI.Query or an IJobEntity)
[*]ArchetypeChunk。API:SetComponentEnabled()。
五、System

5.1 system实现

system类写好后,无需调用,就会自动运行,通常是每帧调一次,且只能在主线程运行。system的实现有2种方式,一种是实现ISystem接口,一种是继承SystemBase。demo中有案例,新版本推荐通过ISystem接口实现,它可以将类及函数都加上burst特性,运行效率更高,且代码更简洁。
5.2 System和SystemGroup

一个系统组可以包含多个系统及其他系统组,而系统组中的update方法可以重写,从而控制其组内的system的调用顺序,除此之外,系统也可以添加特性UpdateBefore和UpdateAfter来控制执行在组内的执行顺序。默认的系统组按执行顺序分别为InitializationSystemGroup, SimulationSystemGroup, 和PresentationSystemGroup。我们自己创建的系统和组将默认放在SimulationSystemGroup,不过可以通过UpdateInGroup来调整所在组。
创建系统组案例如图4所示。



图4

设置一个系统所在系统组的案例如图5所示。



图5

UpdateBefore和UpdateAfter特性使用举例:
5.3 SystemState

它是ISystem接口中方法的参数,是系统的实例,同时包含以下重要属性及方法:

[*]world,system所在的world。
[*]EntityManager,所在world的实体管理器,操作实体必备。
[*]GetEntityQuery(),查询目标实体的方法,可以被系统追踪,从而更好的检测多线程不安全的使用。
[*]GetComponentTypeHandle<T>(),组件类型句柄,chunk可以通过它判断、获取组件,可以通过其可以被系统追踪,从而更好的检测多线程不安全的使用
[*]GetComponentLookup<T>(),获得world中的所有指定类型的组件,并返回一个包含这些组件的类似于字典的容器。可以被系统追踪,从而更好的检测多线程不安全的使用。
5.4 关于SystemState.Dependency

SystemState.Dependency存储了当前系统以来的jobhandler,最好等待其完成在执行本系统的内容,同时如果本系统执行完毕,返回之前,如果有希望后面的系统以来的jobhandler,则可以赋值给SystemState.Dependency,这样系统就能够有一个安全可靠的依赖关系,顺序执行,当然这不是必须的,需要使用者自行判断。
六、Dots中的集合

6.1 分类


[*]Unity.Collections命名空间下,命名以Native-开头的的集合。具备安全检测机制,在没有正确销毁或者多线程不安全的情况使用时会抛出异常。
[*]Unity.Collections.LowLevel.Unsafe命名空间下,命名以UnSafe-开头的集合。没有Native-系列的安全监测机制。
[*]其他的类型使用比较少,且不分配指针,所以不需要关心dispose和线程安全。
6.2 Allocator

分配非托管内存集合时一个参数,用来控制其生存周期。

[*]Allocator.Temp。临时数据,只存在于当前帧,帧结束后会自动销毁,分配效率最高。
[*]Allocator.TempJob。可持续最多4帧的临时数据,分配效率比Allocator.Temp略慢,需要使用者通过dispose销毁,如果4帧内没正确销毁,将抛出异常。
[*]Allocator.Persistant。永久的数据,需要手动dispose(),分配速度最慢。
6.3 集合简介

dots中的大多数集合都拥有一个GetEnumerator方法,迭代遍历方便高效,很多集合类型还会有一个嵌入的类型ParallelWriter,用以在并行任务中安全的使用数组。如NativeList<T>对应的NativeList<T>.ParallelWriter类型。
关于支持的集合类型,笔者将文档中的简介贴了出来,还是比较多的,就不一一单独介绍了,想了解使用细节,可以在官网查看API介绍!不过,之后在使用过程中,如果笔者发现哪些使用起来不是很好理解,或者有些坑要踩,笔者会回来完善此块,避免大家踩坑。
6.4 支持的数组型集合




图6

6.5支持的Map类型集合




图7

6.6 位相关集合




图8

6.7字符串类型集合




图9

6.8 其他类型集合




图10

七、其他重要基础概念

对于此部分笔者理解的还不是很透彻,知其然,不知其所以然,不过在后续的实践中笔者随着理解的深入,会反过来完善这部分。
7.1 ComponentLookUp<T>介绍

在job中不推荐通过EnityManager获取操作实体的组件,提倡使用此接口来获取。具体原因,笔者尚未完全理解,待之后更新。需要注意的是通过id来查找实体,可能会代码缓存丢失命中,所以要慎用。
7.2 SystemAPI

具备丰富的api,但只能在IjobEntity和ISystem中使用,关于查询实体官方推荐优先使用SystemAPI里的接口,其次是SystemState,最后是EntityManager和world。
7.3 EntityCommandBuffer和EntityCommandBuffer.ParallelWriter

功能如其名,把对实体相关的操作,用一个命令缓存存储起来,之后待主线程执行playback()后统一执行。而ParallelWriter模式则用在并行的job中,它可以对命令按一定规则排序,从而保证更安全的在多线程中使用它。
额外注意的点:官方建议每个job单独使用一个EntityCommandBuffer,共用一个常常会出现出错。
7.4 Temporary Entities

当在EntityCommandBuffer创建实体时,在playback()之前,实体实际是还未真实创建的状态,因此有了此临时实体id临时标识这个实体,待退出EntityCommandBuffer后,实体将分配真正的id,它也将变得没有意义。
7.5 EntityCommandBufferSystem

用来处理延迟执行的EntityCommandBuffer的中的命令,即专门用来执行EntityCommandBuffer的playback()方法的系统。这个一般情况下是不需要使用者自己创建,默认的world本身提供5种此类system,按执行顺序依次是BeginInitializationEntityCommandBufferSystem、EndInitializationEntityCommandBufferSystem、BeginSimulationEntityCommandBufferSystem、EndSimulationEntityCommandBufferSystem、BeginPresentationEntityCommandBufferSystem。和之前提到的3大systemgourp相对应。
八、结语

本文是笔者根据官方文档总结而来,存理论型的,权且了解下dots的基本内容,后续的实践中,笔者还会把遇到的重要的知识点在补充进来,同时也会修改理解的不到位的地方!
至此,关于dots1.0的介绍已经大体告一段落,后续笔者会通过些有意思的的案例,来加深理解和使用!
页: [1]
查看完整版本: 关于Unity Dots 1.0版本的学习研究(三)之基础概念