# 前言

主要内容来源于:Metaverse 大衍神君 RoadToDotsTutorials 相关教程,本文为个人笔记
https://github.com/lwwhb/RoadToDotsTutorials.git

注:暂且先从语雀笔记搬过来,还会继续进行整理、修改

# Dots 是什么?

  • Data-Oriented Technology Stack(面向数据的技术堆栈)
  • 由 5 个核心包定义构成:
    • C# Job System
    • Burst Compiler
      • 优化 C# 代码的编译器
    • Unity Mathematics
      • 可以在 JobSystem 使用的数学库,同时针对 burst 编译器特别优化
    • Unity Collections
    • Entities(esc)
  • 其它包
    • Entities.Graphics
    • Netcode
    • Physics
      • Unity Physics (无状态、确定性)
      • Hovak Physics (有状态、不具备确定性,但是更稳定)
    • Animation(wip)
    • Audio(wip)
  • 应用
    • 需要多线程加载、通讯、并行
    • CPU 密集型都可以使用
    • 充分利用现代 CPU 的多核、及缓存效率
  • 学习方法
    • 看、查、学、改、调、写、交流
  • 程序设计方法
    • 指令化编程
    • 函数化编程
    • 过程化编程
    • 面向对象编程 / 设计 (oop/ood)
    • 面向数据设计 (dod)
      • 本质是面向内存、缓存友好的设计
      • 开发人员需要考虑需要什么数据以及如何在内存中如何构造和组织数据,以便 cpu 处理数据时能更有效地访问数据
  • 缓存层级结构
    • L1 Cache:每个指令处理单元独享(分为数据缓存和指令缓存)
    • L2 Cache:核心内多个指令处理单元共享
    • L3 Cache:CPU 多个核心共享
    • fetch+decode+execution 构成一次 cycle
    • 因此当某一级缓存未命中时,向下一级缓存取数据要花费数倍时间
  • 缓存行:一般根据 cpu 结构不同,为 32 或 64 个字节
  • Cache 的 3c 与 3r:
    • compulsory misses:首次读取数据时,必然 miss
    • capacity misses:缓存空间不足时,连续使用期间访问数据过多的话,无法保存所有活动的数据
    • conflict misses:发生访问冲突时,由于数据映射到相同的缓存行,导致缓存抖动
      • 缓存抖动发生比较多,一次 cacheline 没有需要的,实际访问数据不连续
    • rearrange:重新排列(代码、数据),更改布局以增加数据空间的局部性
    • reduce:减少(大小、缓存行读取),更小更智能的格式、压缩(如位运算)
    • reuse:重用,增加数据的时间和空间的局部性(对齐、连续访问、减少发生缓存抖动几率)
  • Cache
    • L1 致力于更快的速度,L2 致力于命中率
    • Cache 如果做大了取一个字的时候时间就会增加,把 L1 做大后显然命中时间和整个延迟开销会增加
    • 缓存分层本质是物理距离决定的(价格与平衡)
  • Cache 层级与排队管理
    • 计算机执行类似排队
    • dots 设计队列结构
    • ecs 组织队列规则
    • jobsystem 队列调度
    • 三级缓存由指令调度需求决定?
  • 面向数据设计需要思考的问题
    • array of structures (AoS) 结构数组 - 面向对象组织
    • structure of arrays (SoA) 数组结构 - 面向数据组织
    • 这分别代表着两种不同的数组组织形式
  • dots 面向数据设计原则
    • 先设计,后编码
    • 为高效使用内存与缓存而设计
    • 为 bittable data 设计
    • 为普通情况设计
    • 拥抱迭代

# ECS

  • 实体组件系统 (ecs) 架构
    • 遵循组合优于继承原则
    • 面向数据设计
    • 弱耦合
    • 常用于游戏开发
  • 本质:组合的是数据的数组,而非对象的数组
  • Entity:虽然叫实体,但并非对象,也不是容器,而是一个对象索引的 id、一个标识符,用来指示某个对象的存在,其中并不包含任何数据和逻辑,可通过组件分配某些属性
  • conponent:数据的容器 (不是对象的容器),其中不包含任何逻辑
  • 注:entity 与 conponent 关系 ——entity 充当数据的标识符或 key 来使用
  • system:真正负责对数据进行操作的部分

  • IJobEntity 被自动生成为 IJobChunk
  • oop:数组的结构(array of structs)
    • 同一个对象放一起
  • esc:结构数组(structs of array)
    • 所有同一类型放一起
  • chunk 大小固定,于是 archytypes 固定,方便动态插入新数据
    • 每个 chunk 数量也应该考虑,否则会出现无效空白内存块
    • 每个 chunk 还有其它额外数据
    • 例如只读的加上 readonly 特性,避免 versionnumber 自增(还有参数只读 in)

  • archetypes (原型):依然是一个标识符,标识所有具有相同 conponent 组合的实体类型
    • 1940844859.jpeg
    • 可以理解为不同的 entity 在内存中的布局
  • archetype chunk:每个 archetype 所标记的内存会被分为固定大小、连续的非托管内存块
    • 2050743949.jpeg
    • 包含共享同一原型的实体组件数组
    • 默认设置下每个 chunk 为 16kb,实体填充不满也会留白
    • 方便做数据并行计算,方便做做缓存的 prefetch
    • 数据对齐同时可以匹配缓存行
    • 查询时利用纵向纬度分块对齐方式做 slice 切片
  • world:一系列 entity 的组合,每个 entity 在一个 world 中唯一,统一受 entitymanager 管理(所以通过 id 查找 entity 并不安全)
  • entitymanager:负责创建、销毁、修改 世界中的实体
  • structural change:所有导致需要重新组织内存块或内容的操作
    • 改变结构
    • 改变 (内存块) 内容
    • 两个都只能在主线程进行,资源密集型操作,效率差
      • 如添加或删除一个 entity 对应的组件导致所属原型发生变化
      • 创建、销毁实体
      • 设置 shared component 值
    • 尽量避免
    • 确定运行时无需修改可在编辑器做 bake 提高效率
  • query:通过切片方式组织查询,让人感觉上是连续的,但并不会造成内存上的移动

# Unity Jobs System

  • 包含 C# Jobs System 和 C++ Jobs System
  • 安全多线程
  • 可与 ECS 结合使用,也可单独使用
  • Blitable:在 C++ 下与 C# 下内存表述一致的类型
    • C# bool 值占用 4 个字节,c++ 1 字节,不一致
    • Jobs 只能使用 Blitable 或 非托管堆数据
    • 可直接复制到本机结构中的类型和非直接复制到本机结构中的类型 - .NET Framework
  • Allocation Types
    • persistent:长生命周期内存
    • tempjob:只在 job 中存在的短生命周期,4 帧以上会收到警告
    • temp:一个函数返回前的短生命周期
  • 调度方式
    • run:主线程立即顺序执行
    • schedule:单个工作线程或主线程,每个 job 顺序执行
    • scheduleparallel:在多个工作线程同时执行,性能最好,但多个工作线程访问同一数据时可能发生冲突
  • job 可以链式依赖,或同时依赖多个 jobhandle,避免 race condition
  • job safety checks:安全检查,在发生 job 访问数据冲突时会抛出异常(仅编辑器生效,可开关,为谨慎性而提供,并不智能)
    • nativedisablecontainersafetyrestriction 特性,关闭对指定数据安全检查
    • readonly 特性标签也可以避免安全检查抛出异常

# Entities

# 基础

  • 少用分支语句,会造成 cache 不友好
  • authoring 模式:编辑器创建
    • 创建 entitysubscene

    • 在 subscene 创建 gameobject 对象

    • 为 gameobject 添加默认 conponent 和 自定义 icomponentdata 数据

    • 自定义 icomponentdata 完成数据编排

    • authoring-baking (隐式)-runtime

    • baker:用来创建自定义数据

    • bakersystem:运行于 baker 之后,处理复杂 baking 情况,可选处理

  • runtime 模式:完全代码创建
    • 创建一个 world
    • 通过 world 中 entitymanager 创建、管理 entity 对象
      • entity:整形 id 和 version 数据组成的结构体
    • 通过 world 中 entitymanager 创建 entity 对应 conponent
      • icomponent:一个 c# 结构体,可以被 entities id 索引,一般无法被托管对象引用,内存紧密排列。
    • 根据创建的 entity 或 entityarchetype 实例化 entity
    • 通过 system 实现对应 entity 逻辑管理
      • systembase:托管类,简单,运行于主线程,不能被 burst 编译
      • isystem:struct,非托管,可以被 burst 编译,快但更复杂

# Components 类型

  • 内存类型划分
    • 非托管(常用)
      • blitable type
      • bool
      • char
      • blobassetreference
      • collections.fixedstring
      • collections.fixedlist
      • Fixed array
      • And so on(不变数据结构、长度、固定大小)
    • 托管
      • 可存储如何数据类型,但无法使用 jobs 访问,也无法使用 burst 编译,且声明 data 对象必须是无参数构造函数
      • 不会存储在 chunk 中,而是所属 world 一个大数组中,chunk 仅存储索引,因此查找 entity 托管类型组件时需要处理额外索引查找工作,更慢
  • 功能类型划分
    • 一般 component
    • shared component
    • tag component
      • 标识 entity 方便查找
      • 不占用存储空间,非常好用于分类功能组件
    • enableable component
      • 运行时启用、禁用
    • cleanup component
      • 错误状态防止销毁 entity
    • singleton component
      • 一个 world 下唯一存在(多个 world 可重复)
  • 数据访问类型划分
    • 按 entity 访问
    • 按 chunk 访问
    • 按 element 访问
  • 按接口类型划分
    • Ienableablecomponent
    • Isystemstatecomponent
    • Isystemstatesharedcomponent
    • Icomponentdata
    • Isharedcomponentdata
    • Ibufferelementdata
  • entities 包中的数据大类
    • componentdata
    • bufferdata
    • isharedcomponentdata
    • entitydata
    • unitycomponentobject

# System

# 类型

  • isystem:提供对非托管内存访问
  • systembase:用于访问存储托管数据
  • 注:任何 system 都能访问任何 world 下的 entity(不推荐)
  • 编辑器下,system 窗口会显示每个操作时间消耗,默认小数点 2 位,可设置更高精度

# 更新

  • 同 systemgroup 下调整 system 更新顺序
    • [updatebefore]
    • [updateafter]
  • 指定 system 到特定的 ComponentSystemGroup 下
    • [updateingroup]
  • 控制 system 的创建与销毁顺序
    • [createbefore]
    • [createafter]
    • 注:被修饰的 system 的销毁顺序与创建顺序相反
  • 禁止 System 被默认创建并添加到默认 ComponentSystemGroup 中
    • [DisableAutoCreation]
    • 注:被修饰的 System 需手动调用 world.getoncreatesystem 创建,及手动调用 xxxsystem.update 更新
  • 空的 systemgroup 与空的 monobehavior 函数一样,都会有时间开销
  • 使用 system 注意点
    • 不要在 system 中定义 public 数据,应将其组织为 component 数据
    • 在 system 更新时,尽量采用 system state 中的 getentityquery、getcomponenttypehandle 等访问 chunk component 数组,而不是通过 entitymanager 方法访问 (有额外开销)

# 遍历与查询

  • dots 程序三大主要工作
    • 设计与组织数据
    • 遍历与查找数据
    • 修改与更新数据
  • 遍历查询 entity 数据的 5 种方式
    • systemapi.query + foreach
      • 只能在 system 调用
      • 不能存储结果,并在一个或多个 foreach 使用(因为模板类型需要在编译时确定并生成代码)
      • 只能用于主线程
      • 不能过滤同时『包含』的 chunk
      • 有七种重载:iaspect、icomponentdata、dynamicbuff<>、refro<>、refrw<>、enabledrefro<>、enabledtefrw<>
    • Ijobentity
      • 与 Entities.foreach 类似,每个 entity 调用一次
      • 可在多个 system 重用
      • 底层通过 ijobchunk 实现
      • 可通过参数特性修饰查询
      • entityindexinquery 特性获取当前 entity 遍历查询索引
    • Ijobchunk
      • 更底层实现,遍历 archetype chunk
      • 每个 chunk 调用一次
      • 一般用于不需要遍历每个 chunk 中 entity 的情况或对 chunk 内的 entity 执行多次遍历或以不寻常顺序遍历的情况
      • useenabledmask 与 chunkenablemask 辅助过滤 enableable component 未激活的 entity(不会自动过滤)
    • Entities.foreach
      • 只用于 systembase 创建的 system
      • 定义一个 lambda 表达式
      • 使用限制较多,一般不用
    • Manually(手动遍历)
      • entitymanager.getallentities()
      • entitymanager.getallchunks()
      • 限制较小,但性能上不划算

# System 其它特性

  • [requirematchingqueriesforupdate]
    • 每个 entity 结果都是空时则强制跳过 system onupdate 调用
  • state.requireforupdate<>()
    • 只有指定组件存在时,该 system 才运行

# Aspect

  • Aspect 中可包含的数据类型
    • entity
    • refrw<>、refro<>
    • enanledrefrw 和 enabledrefro
    • dynamicbuff<>
    • 其它 aspect 类型
    • 注:tranformaspect 是 unity 内置的唯一一个 aspect,封装各种 local、world 坐标系下的旋转、缩放、位置及其它一些组件数据

# 内置组件

# ComponentLookup随机访问

  • componentlookup<>
    • 本质上是个 nativecontainer
    • 可使用 job 同时在不同的一组实体上迭代
    • [NativeDisableParallelForRestrictionAttribute] 特性可解除对并行写入的限制(如确定每个 job 写入索引不冲突)
    • 注:若希望在 job 之外访问 entity 组件,可考虑使用 entitymanager.getcomponentdata 做随机访问,避免创建 componentlookup 开销
  • entitystorageinfolookup
    • 测试一个 entity 是否存在
    • 通过存在的 entity id 获得 entitystorageinfo

# Entity Command Buffer

可以记录更改实体数据的命令,然后在稍后的主线程回放 ECB 时执行这些命令,基于命令模式设计

  • RenderCommandBufer:同步统一执行渲染状态的改变
  • EntityCommandBuffer:与 RenderCommandBufer 类似,不过 ECB 是同步统一执行 Entity 相关数据的改变
  • Job 中不能直接创建和销毁 Entity
  • Job 中不能添加和删除 Entity 组件
    • 只能使用 Entity Command Buffer 来处理这些操作
    • ECB 具有很多与 EntityManager 相同的方法
  • ECB 的 Playback 方法必须在主线程调用,记录 ECB 命令的 Job 必须在 Playback 方法调用前完成
    • ECB 需要在调用 Playback 后销毁
    • (子线程数据同步至主线程)
    • 奇偶线程只是记录命令,主线程调用 Playback 才是实际生产
    • state.Dependency.Complete();
    • ecb.Playback(state.EntityManager);
    • ecb.Dispose();

# Entity Command Buffer System

  • 使用 ecb 更简便
    • world.getexistingSystemManaged<>().CreateCommandBuffer()
    • 不需要手动调用 playback 及释放
  • 创建自定义 ECB System 继承自 EntityCommandBufferSystem(通常不需要)
  • Unity 提供了默认一些 ECB System(单例),可直接获取使用
  • SystemAPI.GetSingleton<T.Singleton>():
    • BeginInitializationEntityCommandBufferSystem:代表在下一帧开始时创建
    • EndInitializationEntityCommandBufferSystem
    • BeginSimulationEntityCommandBufferSystem
    • EndSimulationEntityCommandBufferSystem
    • BeginFixedStepSimulationEntityCommandBufferSystem
    • BeginPresentationEntityCommandBufferSystem

# DynamicBufferComponent

  • 非托管内存下一个可以调整大小的数组
  • 继承自 IBufferElementData 接口
  • capacity:容量,默认 128 字节以内元素,共 8 个元素,每个元素 16bytes
    • 可使用 [InternalBufferCapacity] 特性修改
  • internal ptr:指向缓冲区数据的具体位置
  • buffer 长度超过内部容量后,会销毁原本 chunk 数组并移到外部更大 chunk 中,即使缩小也回不去了
  • 获取:entitymanager.getbuff<>
  • structure change 会对 dbc 检索产生影响
    • jobs 读写访问也有影响(只读无所谓)
  • 可使用 ecb 修改 dbc
    • ecb.addbuffer、ecb.setbuffer、ecb.appendbuff
  • 若两个 dbc 大小相同,可以用一个解释另一个(类似于联合)

# EnableableComponent

  • 运行时禁用、启用 entity 对象上某个组件,用于处理频繁修改组件状态的情况
  • 避免直接增删组件导致的 structural change
  • 还可以替换一组零大小的 tagcomponent 来表示 entity 状态的情况,以减少 entity archetype 数量,得到更好的 chunk 利用率
  • 只能用于 icomponentdata、ibufferelementdata 类型并同时继承实现 ienableablecomonent 的接口
  • 使用 EnableableComponent 不会改变 entity,也不会造成任何数据移动(意味着可以直接在 job 中开启或禁用组件)
  • 使用具有对 enableable 组件写访问权限的 job 可能会导致线程操作阻塞,即使 job 没有在任何 entity 上启用或禁用组件
  • 操作
    • entitymanager
    • componentlookup
    • entitycommandbuffer
    • archetypechunk
    • 的 iscomponentenabled<>、setcomponentenabled<> 启用或禁用
  • 默认情况下通过 createentity 创建的新 entity 会启用所有可用组件,从 prefabs 实例化的 entity 则继承之前组件状态
  • 查询时,entityquery 会不匹配禁用组件,不过可以手动忽略
    • ignorefilter
    • entityqueryoption.ignorecomponentenabledstate 查询

# ShareComponent

  • 共享组件数据存储在与 ecs chunk 分离的数组中
  • world 中的 chunk 则存储句柄,用来定位共享组件在分离数组中的值
  • 同一个 chunk 中实体共享相同的 sharecomponent 的值
  • -2144605345.jpeg
  • 若更改了 entity 的共享组件值,该 entity 会被移动到新的共享组件的 chunk 中
    • 若共享组件值在数据组中存在相等的值,则 entity 被移动至存储现在有值索引的 chunk 中
    • 否则,添加新值到共享组件值中,并将 entity 移动到存储新值索引的新的 chunk 中
    • -1348735719.jpeg
    • 意味着更改共享组件值一定会带来 structual change
    • 应尽量避免频繁更新
    • 避免大量有独特值的共享组件 (会导致不同共享值的实体被分割,出现空 chunk 块)
    • 避免实体具有多个共享组件类型组合,避免 archetype 碎片化 (可以想象成 shader 的关键字,与其增长速度是一样的,也会造成内存碎片化)
  • 分为托管与非托管类型的 sharedcomponent
    • 根据组件中存储的数据的 blitable type 类型划分
    • 托管与非托管类型的共享组件分开存储
    • 非托管:继承 isharedcomponentdata(可选继承 iequatable)
    • 托管:除继承 isharedcomponentdata ,还必须实现 iequatable,并实现 equals 及 gethashcode
  • 因为共享同一 ShareComponent 的 entity 会被放入一个 chunk,因此也可以利用这一点进行分组(当然需要注意内存碎片化问题)
  • 进阶用法
    • 不同 world 使用共享组件共享同一个托管对象
    • 共享组件内包含对象引用类型或指针的情况

# BlobAsset

  • Blob (binary large object)::是 unity 中存储数据的一种格式,为流式传输优化的二进制数据片,在内存中是存储连续字节块中的不可变的非托管二进制数据
  • 只读,创建时设置后不可变
  • 一次性分配,可以用 memcpy 重新定位,不能包含虚类对象数据
  • unity 的 blobification 框架可以更紧密打包数据,借助 reduce copy 减少重复数据,并使 blob asset 可以 relocatable 重新定位从磁盘直接读取
  • dots 中的 Blob asset
    • 同样可以有效优化数据存储与内存使用
    • dots 下不能直接使用,而是通过 blobAssetRef
    • 当不需要时,需通过调用 blobAssetReference dispose 释放
    • bake entity 引用的 blob asset 会与场景一起序列化加载
    • 注意与 sharecomponent 差异
    • -683181080.jpeg
    • dota 中 blob 数据类型
      • 普通 blitable type 数据类型
      • blobString
      • blobArray
      • blobPtr

# CleanupComponent

  • 当销毁一个包含该组件的 entity 时,unity 会删除所有非该组件的其它组件,实际上这个 entity 还存在
  • 该组件不会随 entity 复制到另一个 world 中
  • 主要用于在创建实体后帮助初始化或销毁实体后帮助清理
  • 根据继承不同,分为不同类型
    • 例如分为托管和非托管类型、dynamic buffer 类型、shared component 类型
  • 在 dots 创建 entity
    • 4 种方法:
      1. 通过 ibaker 接口将 prefab bake 成原型,然后运行时通过 entitymanager 根据原型实例化新的 entity 对象
      • 使用简单,好理解,不过在 world 中会有额外一个原型存在
      1. entitymanager.CreateEntity()
      • 然后添加 requestentityprefabloaded 即 prefab 异步加载并转化为 entity 的组件
      • 组件加载转化完成后,会在 entity 上自动添加 prefabloadedresult 组件,可以从该组件上的 prefabroot 字段拿到 prefab 转化后的 entity
      1. 混合方式
      • 直接将 prefab 数组引用在 baker 中保存至 componentdata 中
      • 通过继承 systembase 的 system 直接使用 instantiate 实例化
      • 该方法本质上还是 gameobject 处理,无法利用 dots 的数据结构优势(不推荐)
      1. 完全动态创建(entity graphics 核心包)

# ChunkComponent

  • 按 chunk 而不是按 entity 存储的组件
  • 同样继承 icomponentdata
  • 使用一组不同的 api 进行操作
  • 如果只是想只读,使用 query 查询时传入 chunkcomponentreadonly
  • 在 entity 上添加或删除 chunkcomponent 会导致 structual change
  • 在执行 job 时由于其按 chunk 存储,一般用 ijobchunk 而不是 ijobentity
  • 1201698463.jpeg
  • 具有相同 archetype 的 entity,指向在 chunk 中同一个 chunkcomponent,否则指向同一 chunk 不同的 chunkcomponent
  • -854237890.jpeg

# Entities Graphics 包

  • 充当 dots 与 unity 现有渲染架构的桥梁
  • 使用 ecs entity 而不是 gameobject 来改善大型场景中运行时内存布局与性能
  • 保持 unity 现有工作流的兼容与易用性

# GameObject 转化系统

  • 将 gameobject 转化为等效 dots entity 的系统,支持编辑器与运行时转化换
  • meshrender meshfilter->rendermesh
  • lodgroup->meshlodgroupcomponent
  • transform->localtoworld
  • 最好的运行时实例化 entity 方式
    • prefabs
    • rendermeshutility.addcomponent api
  • 不要手动添加渲染组件,效率低,有可能与未来 entity graphics 包不兼容
  • 不推荐 monobehavior 创建 entity
  • 运行时创建 entity
    • entity graphics 渲染需要包含最小组件集
    • 推荐 rendermeshutility.addcomponent api—— 不过这是一个主线程 api,不适合创建大量实体,最好通过原型实例化克隆现有实体

# Material 覆写

两种方法:

  • 使用 material override asset
  • 使用 c# burst code(推荐)
    • 定义的 componentdata 类用 materialproperty 特性修饰
  • 禁用渲染
    • disablerender tag 组件
    • 仅仅是不渲染,实际还在更新
    • 会导致 structural change

# BatchRenderGroup API

  • GPUInstancing
    • DrawMeshInstanced:有 1024 个数量限制
    • DrawMeshInstancedIndirect:有兼容性和剔除方面限制
    • BatchRenderGroup
  • (BRG) 是一个用于执行可自定义的高性能实例对象渲染接口,可以提前设置批次,使用描述如何加载网格和材质的共享元数据来设置实例组
  • 当 Unity 渲染 BatchRenderGroup 时,会调用 OnPerformCulling 回调执行可见性剔除,并可声明一个可变大小绘制命令列表,描述如何渲染 BatchRenderGroup 可见部分
  • 每个绘制命令都指示 Unity 使用特定的网格和材质渲染给定批次中的一组实例
  • Entities Graphics 就是构建在 BatchRenderGroup API 之上的,此 API 将 Entities Graphics 连接到 Unity 引擎后端渲染
    • 注:其实 BatchRenderGroup API 可以脱离 Dots 直接脚本调用

# 用途

  • 渲染 DOTS 实体
  • 渲染大量使用单个 GameObject 的资源密集型环境对象 (如程序化放置的植物或岩石)
  • 渲染自定义地形,可使用不同的网格或材质显示不同级别的详细信息

# 使用限制

  • 不支持 BuildIn 管线,需要 SRPBatcher 选项开启
  • 不能剔除 BRG 的变体
    • BatchRenderGroup Variants 需要设置为 KeepAll
    • URP 最好在设置中直接关闭 StripUnusedVariants 避免被剔除变体
  • 需要开启 unsafe 选项(允许不安全代码)
  • 主流平台基本都支持 BRG 接口,不过 Android 上需要 Vulkan 支持
    • OpenGL 下可以使用 const buffer 处理一些不兼容情况,但是渲染提升并不明显
      • opengle 和 opengles 上只能使用 ConstantBuffer,其他的都是使用 RawBuffer
    • 所以最好选用 Vulkan 图形 API

# 创建流程

  1. 创建 BatchRenderGroup 实例,使用 OnPerformCulling 的实现进行初始化 (主要入口)
    • Visibility Culling:基于 BatchCullingContext 参数确定哪些渲染实例对象可见,输出实际绘制命令以渲染这些实例
    • OnPerformCulling 逻辑可在回调实现,也可以在 BurstJob 完成 (更高效)
  2. 向 BRG 中注册网格和材质
    • Mesh 与 Material 是 Unity 托管 C# 对象,不能在 Burst C# 使用,必须预先注册 (与 EntitiesGraphicsSystem 一致)
    • 通过 BatchRenderGroup.RegisterMesh/BatchRenderGroup.RegisterMaterial 注册,其返回 BatchMeshID 与 BatchMaterialID (是包含 Burst 兼容句柄的普通数据结构体)
    • 即使用 BRG 前必须先注册对象 Mesh 与 Material,且材质必须支持 DOTS Instancing
  3. 创建批次 Batch
    • 绘制命令之前需要为每个实例提供数据,如变化矩阵、光照探针系数、光照贴图、纹理坐标等
    • BRG 使用 Batch 的概念,每个 Batch 都有一组元数据和一个 GraphicsBuffer 的概念,批处理中的每个实例都共享这些数据
    • 通过 BatchRenderGroup.AddBatch 接受元数据值的数组以及 GraphicsBuffer 的句柄,Unity 会将 Batch 中的数据传递给 Shader (未传入默认为 0),并将 GraphicsBuffer 绑定为 unity_DOTSInstanceData
    • 注 1:创建 Batch 元数据后不能修改,若想修改必须创建新的替换 (可通过 SetBatchBuffer 随时修改 Batch 的 GraphicsBuffer)—— 即数据的 Layout 不能修改,但数据内容可以修改
    • 注 2:创建 Batch 时不需要指定大小,相反、必须确保 Shader 能够正确处理传递给它的实例索引:意味着 SRP Shader 在传递的索引处的缓冲区必须有有效的实例数据
  4. 创建绘制命令 DrawCommands
    • 创建绘制命令需要使用 BatchRenderGroup.OnPerformCulling 回调和回调参数
      • BatchCullingOutput 参数包含一个带单个元素的 NativArray,可以直接修改其内容,不需要复制数据
      • NativeArray 中的元素是个 BatchCullingOutputDrawCommands 结构,它包含实际绘制命令
      • OnPerformCulling 回调中生成任意数量的绘制命令,可以是只使用一个网格和材质的简单命令,也可以是使用成千上万个网格和材质的复杂命令
      • 为提供最大灵活性,Unity 不会预先分配 BatchCullingOutputDrawCommands 输出结构中的数组而是将其存储为原始的指针
        • 这样就可以更轻松低从 Burst Job 中分配和使用(需要使用 UnsafeUtility.Malloc 和 Allocator.TempJob 分配器来分配数组)
        • 另外在 OnPerformCulling 回调中不会释放这些数据内存,而是在 Unity 使用绘制命令完成渲染后释放

# Hybrid Entities

  • 允许将 MonoBehavior 组件附加到 DOTS Entity,而不需要将其转化为 ComponentData
    • 转换系统将调用 AddBybridComponent 将托管组件附加到 DOTS Entity 上
  • 默认情况会禁用 Camera 组件转换,因为场景主 Camera 不能是 HybridEntity
    • 需要启用,可使用宏 HYBRID_ENTITY_CAMERA_CONVERSION
  • 当 Unity 更新 DOTS LocalToWorld 时,都会更新 HybridEntity 转换

# Unity Physics

  • 是一个完整的确定性的刚体物理系统
  • 功能上仅是 Physics 或 Havok 的子集
  • 完全使用 HPC# 编写,遵循 DOTS 整体设计理念
  • 遵循最小依赖性和完全可控制性

特性

  • 无状态:该帧信息不会被缓存到下一帧
  • 模块化
  • 高性能
  • 互通性

# 物理模拟顺序

  • 获取实体上组件当前状态
  • 检测激活实体边界,进行快速碰撞检测
  • 根据碰撞器形状计算确切接触点
  • 基于碰撞点、质量、摩擦系数、弹性等参数做出碰撞反应
  • 处理碰撞结果,并根据 Joint 等限制结算出刚体新的速度
  • 根据动力学物理的线速度和角速度按时间步长移动物理体
  • 将新的变换 Transform 应用到该物体实体上

# 设置方式

  • PhysicsSharp 组件 + PhysicsBody 组件
  • PhysicsSharp 组件 + RigidBody 组件
    • 底层实际通过 Authoring 将 RigidBody 转换为 PhysicsBody 组件
  • 其设置不受 ProjectSetting 中 Physics 全局设置影响了
    • 可以在场景添加 PhysicsStep 用作场景的全局物理设置