Unity Dots理论学习-4.ECS有关的模块(3)
Entities(ECS)
Entities包提供了ECS的实现,ECS是一种由实体(Entities)和组件(Components)存储数据,由系统(Systems)执行代码组成的架构模式。
简而言之,一个实体由组件组成,每个组件通常是一个C#结构体。与GameObject类似,实体的组件可以在生命周期内添加和移除。
与GameObject不同,实体的组件通常没有自己的方法。相反,在ECS中,每个系统都有一个Update方法,这个方法通常每帧调用一次,会读取并修改一些实体的组件。例如,一个包含怪物的游戏可能有一个MonsterMoveSystem,它的Update方法会修改每个怪物实体的Transform组件。
原型(Archetypes)
在Unity的ECS中,所有具有相同组件类型集合的实体会被存储在同一个“原型”(Archetype)中。例如,假设你有三种组件类型:A、B和C。每种组件类型的唯一组合就是一个独立的原型,例如:
— 所有具有组件类型A、B和C的实体将存储在一个原型中。
— 所有具有组件类型A和B的实体将存储在第二个原型中。
— 所有具有组件类型A和C的实体将存储在第三个原型中。
向实体添加组件或从实体移除组件会使该实体移动到不同的原型中。
块(Chunks)
在一个原型(Archetype)中,实体及其组件存储在称为块(Chunks)的内存区域中。每个块最多存储128个实体,每种组件类型在块内有自己的数组。例如,在具有组件类型A和B的实体的原型中,每个块将存储三个数组:
— 一个用于存储实体ID的数组
— 一个用于存储A组件的数组
— 另一个用于存储B组件的数组
块中第一个实体的ID和组件存储在这些数组的索引0处,第二个实体存储在索引1,第三个实体存储在索引2,依此类推。
查询(Queries)
原型和块的基于数据布局的一个主要好处是,它能够高效地查询和迭代实体。
要遍历所有具有特定组件类型集合的实体,实体查询首先找到所有匹配该条件的原型,然后它会在这些原型的块中迭代实体:
— 由于块中的组件存储在紧密打包的数组中,因此遍历组件可以大大减少缓存未命中的情况。
— 由于原型的集合在程序的大部分时间内保持稳定,因此通常可以缓存匹配查询的原型集合,从而使查询变得更快。
只要实体的组件类型是非托管的,它们就可以在Burst编译的job中访问。为了访问实体,提供了两种特殊的job类型:IJobChunk和IJobEntity。
为了简化使用,系统可以自动处理job依赖关系和跨系统的job完成。
子场景和烘焙(Baking)
Unity ECS使用子场景(subscenes)来管理应用程序的内容,而不是场景(scenes),这是因为Unity的核心场景系统与ECS不兼容。
虽然实体不能直接包含在Unity的场景中,但一种名为烘焙(baking)的功能允许从场景中加载实体,并将GameObject和MonoBehaviour组件转换为实体和ECS组件。
你可以把子场景看作是嵌套在其他场景中的场景,并且这些子场景会通过烘焙进行处理,每次编辑子场景时都会重新运行烘焙过程。对于子场景中的每个GameObject,烘焙会创建一个实体,这些实体会被序列化为文件,而且当子场景运行时被加载时,这些实体会代替GameObject本身被加载。
哪些组件会添加到烘焙的实体中,由与GameObject组件相关联的“bakers”决定。例如,与标准图形组件(如MeshRenderer)相关联的bakers会将与图形相关的组件添加到实体中。对于你自己的MonoBehaviour类型,你可以定义bakers来控制哪些附加组件会被添加到烘焙的实体中。
一方面,不能直接在场景中添加实体在简单情况下可能会不方便,但另一方面,烘焙过程在更复杂的场景中是非常有用的。
烘焙有效地将editor数据(你在编辑器中编辑的GameObject)与runtime数据(烘焙后的实体)分离,因此你直接编辑的内容和运行时加载的内容不需要一一对应。例如,你可以在烘焙过程中编写代码生成数据,这样可以避免在运行时承担额外的开销。
流式加载(Streaming)
特别是对于大型详细的环境,能够高效且异步地加载和卸载许多元素是非常重要的,尤其是当玩家或相机在环境中移动时。例如,在一个大型开放世界中,一部分元素必须随着视野的变化而加载,一部分元素必须随着视野的消失而卸载。这种技术也被称为流式加载(streaming)。
由于实体消耗的内存和处理开销较少,并且可以更高效地进行序列化和反序列化,所以与GameObject相比,实体更适合流式加载,
评估是否在你的项目中使用DOTS
如果你的代码导致了CPU瓶颈,你可以考虑将其重新实现为Burst编译的job。Burst编译的代码通常比Mono或甚至IL2CPP编译的代码运行更快,而job可以让你将工作负载分配到CPU的所有核心上。
好消息是,Burst编译的job通常可以相对轻松地集成到大多数现有项目中,即使项目本身并未使用DOTS。除了可能需要将数据复制进出非托管集合之外,将现有代码重写为Burst编译的job通常不需要进行重大代码重构。
然而,对于Entities包来说,这种情况不完全成立。虽然有时可以选择性地集成实体来实现特定功能,但ECS架构往往会对整个项目施加自己的代码结构。
以下是四个适合使用Entities构建新项目的理由:
— 项目将包含许多静态元素,例如渲染一个大型、详细的环境。原始的Megacity项目演示了由实体构成的复杂环境。
— 项目将包含许多动态元素,且具有计算密集型的行为。例如,实时战略游戏通常需要为成百上千个单位计算寻路。
— 你更喜欢ECS方式的数据和代码结构,这种方式比常见的面向对象方式更容易理解和维护。至少,ECS通常使得分析和识别瓶颈变得更加容易。
— 项目是一个快节奏的竞争性多人游戏,例如射击游戏,需要授权服务器和客户端预测,以提供良好的玩家体验(如上所述,这些功能在Netcode for Entities中得到了支持,但在Netcode for GameObjects中没有)。
另一方面,许多游戏的瓶颈主要出现在GPU上,在这种情况下,Entities及其他DOTS包和相关技术可能帮助不大,因为DOTS只提高了CPU效率。然而,如果DOTS可以帮助你在更少的CPU时间内完成相同的工作,那么这将为额外功能提供更多的head空间;如果你将来决定将目标设备设置为低功耗设备,额外的head空间也将非常有帮助。