ECS 概念
概念
Entities package使用实体组件系统(ECS)架构来组织代码和数据。实体是唯一标识符,就像游戏对象的轻量级的非托管替代品。实体相当与一个ID,用来关联包含实体数据的各个组件。与游戏对象不同,实体不包含代码:它们是由您创建的系统来处理的数据单元。
标题 | 描述 |
---|---|
Entity | 实体是不包含代码的游戏对象的轻量级替代品。 |
Component | 组件包含有关单个实体的数据。 |
System | 向系统添加代码以处理实体和组件。 |
World | 世界将实体组织成孤立的群体。 |
Archetype | 原型是一个或多个实体可能具有的组件的独特组合。 |
Structural changes | 结构更改是会影响应用程序性能的资源密集型操作。 |
实体
实体表示程序中具有自己数据集的离散事物,例如角色、视觉效果、UI 元素,甚至是网络事务等抽象事物。实体类似于非托管(unmanaged)的轻量级游戏对象,代表程序的特定元素。但是,实体仅充当将各个唯一组件关联在一起的 ID,而不包含任何代码或充当其关联组件的容器。
实体的集合存储在一个世界中,世界的 EntityManager
管理世界中的所有实体。 EntityManager
包含可用于创建、销毁和修改该世界中的实体的方法。其中包括以下常用方法:
方法 | 描述 |
---|---|
CreateEntity | 创建一个新实体。 |
Instantiate | 复制现有实体并从该副本创建新实体。 |
DestroyEntity | 销毁现有实体。 |
AddComponent | 将组件添加到现有实体。 |
RemoveComponent | 从现有实体中删除组件。 |
GetComponent | 获取实体组件的值。 |
SetComponent | 覆盖实体组件的值。 |
当您创建或销毁实体时,这是一种结构更改,会影响应用程序的性能。有关详细信息,请参阅有关结构更改的文档
实体没有类型,但您可以根据与其关联的组件类型对实体进行分类。 EntityManager
跟踪现有全部实体上组件的唯一组合。这些独特的组合称为原型(Archetype)。有关原型如何工作的更多信息,请参阅有关原型概念的文档。
编辑器中的实体
在编辑器中,以下图标代表实体: 。当您使用特定的实体窗口和检查器时,您会看到这一点。
组件
在实体组件系统 (ECS) 体系结构中,组件包含系统可以读取或写入的实体数据。
使用没有方法的IComponentData
接口将一个结构体标记为组件类型。此组件类型只能包含非托管数据,并且它们可以包含方法,但最佳实践是它们只有纯数据。如果你想创建一个托管组件,你将它定义为一个类。有关详细信息,请参阅托管组件。
对于不同的目的,有不同类型的组件。根据您希望如何管理项目中的数据,某些组件允许对应用程序的性能进行更精细的控制。有关详细信息,请参阅组件类型。
一组独特的实体的组件称为原型。 ECS 架构按原型将组件数据存储在称为块的 16KiB 内存块中。有关 ECS 如何存储组件数据的更多信息,请参阅原型概念文档。
系统
系统提供将组件数据从当前状态转换为下一个状态的逻辑。例如,系统可能会通过速度乘以自上次更新以来的时间间隔来更新所有移动实体的位置。
系统每帧在主线程上运行一次。系统被组织成被称为系统组的层次结构,您可以使用这些系统组来组织系统更新的顺序。
您可以创建非托管或托管系统。要定义托管系统,请创建一个继承自SystemBase
的类。要定义非托管系统,请创建一个继承自ISystem
的结构。
ISystem
和SystemBase
都有三种方法,您可以覆盖OnUpdate
、OnCreate
和OnDestroy
。系统的OnUpdate
方法每帧执行一次。
一个系统只能处理一个世界中的实体,所以一个系统与一个特定的世界相关联。您可以使用World
属性返回系统附加到的世界。
默认情况下,一个自动引导过程会为每个系统和系统组创建一个实例。这个引导将创建一个默认世界,其中包含三个系统组:InitializationSystemGroup
、SimulationSystemGroup
和PresentationSystemGroup
。默认情况下系统的一个实例将被添加到SimulationSystemGroup
。您可以使用[UpdateInGroup]
属性来覆盖此行为。
要禁用自动引导过程,请使用脚本定义#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
。
系统类型
您可以使用多种类型的系统:
SystemBase
: 为托管系统提供基类。ISystem
: 为非托管系统提供接口。EntityCommandBufferSystem
: 为其他系统提供实体命令缓冲区实例。这允许您将结构更改组合在一起以提高应用程序的性能。ComponentSystemGroup
: 为系统提供嵌套组织和更新顺序。
系统组
一个系统组可以有系统和其他系统组作为它的子系统。系统组有一个可以覆盖的更新方法,基本的更新方法将按排序顺序更新组的子级。
每次将组添加到系统组时,它都会重新排序系统更新顺序。要控制系统组的更新顺序,请将UpdateBefore
或UpdateAfter
属性添加到系统以指定它应该在之前或之后更新哪些系统。这些属性仅适用于同一系统组的子系统。例如:
要创建系统组,请创建一个继承自ComponentSystemGroup
的类。因为系统属于一个世界,所以必须使用World.GetOrCreateSystem
创建一个系统。要将系统添加到组,请使用group.AddSystemToUpdateList
。您可以将其他系统组添加到现有系统组。
有关详细信息,请参阅有关系统更新顺序的文档。
检查系统
您可以使用“Systems window”窗口检查每个世界中系统的更新顺序,并查看系统组的完整层次结构。有关详细信息,请参阅有关系统窗口参考的文档。
编辑器中的系统
在编辑器中,以下图标代表不同类型的系统。当您使用特定的实体窗口和检查器时,您会看到这一点。
图标 | 含义 |
---|---|
一个系统组 | |
一个系统 | |
一个带有OrderFirst 参数的,设置为在开头执行的实体命令缓冲区系统 。 |
|
一个带有OrderLast 参数的,设置为在末尾执行的实体命令缓冲区系统 。 |
世界
世界是实体的集合。实体的 ID 号仅在其自己的世界中是唯一的。一个世界有一个EntityManager
结构体,你可以用它来创建、销毁和修改世界中的实体。
一个世界拥有一组系统,这些系统通常只访问同一个世界中的实体。此外,世界中具有相同组件类型集的一组实体一起存储在原型中,原型决定了你程序中的组件在内存中的组织方式。
初始化
默认情况下,当您进入播放模式时,Unity 会创建一个世界实例并将每个系统添加到这个默认世界。
如果您更喜欢手动将系统添加到默认世界,请创建一个实现ICustomBootstrap
接口的类。
如果你想完全手动控制引导,使用这些定义来禁用默认的世界创建:
-#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD
:禁用默认运行时世界的生成。
-#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_EDITOR_WORLD
:禁用默认编辑器世界的生成。
-#UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
:禁用两个默认世界的生成。
然后,您的代码负责创建您的世界和系统,并将您的世界的更新插入到 Unity 可编写脚本的 PlayerLoop 中。
Unity 使用WorldFlags
在编辑器中创建专门的世界。
时间考量
世界控制其中系统的Time
属性的值。系统的Time
属性是当前世界时间的别名。
默认情况下,Unity 为每个世界创建一个TimeData
实体,由UpdateWorldTimeSystem
实例更新。这反映了自上一帧以来经过的时间。
FixedStepSimulationSystemGroup
中的系统处理时间的方式与其他系统组不同。固定步长模拟组中的系统以固定间隔更新,而不是在当前增量时间更新一次,并且如果固定间隔是帧时间的足够小的一部分,则每帧可能更新不止一次。
如果您需要对世界中的时间进行更多控制,可以使用World.SetTime
直接指定一个时间值。您还可以使用PushTime
临时更改世界时间,使用PopTime
返回到之前的时间(在时间栈中)。
原型
原型是世界中具有相同的唯一组件类型组合的所有实体的唯一标识符。例如,一个世界中具有组件类型 A 和 B 的所有实体共享一个原型。所有具有组件类型 A、B 和 C 的实体共享一个不同的原型,所有具有组件类型 A 和 Z 的实体共享另一个原型。
当您从实体中添加或删除组件类型时,世界的EntityManager
会将实体移动到适当的原型。例如,如果实体具有组件类型 A、B 和 C,并且您删除了它的 B组件,EntityManager
会将实体移动到具有组件类型 A 和 C 的原型。如果不存在这样的原型,则EntityManager
会创建它。
频繁移动实体会占用大量资源并降低应用程序的性能。有关详细信息,请参阅有关结构变更概念的文档。
基于原型的实体架构意味着通过组件类型查询实体是高效的。例如,如果您想要查找具有组件类型 A 和 B 的所有实体,您可以找到具有这些组件类型的所有原型,这比扫描所有单个实体的性能更高。世界中的现有原型集往往会在程序生命周期的早期稳定下来,因此您可以缓存查询以获得更快的性能。
原型只有在它的世界被销毁时才会被销毁。
原型块 Archetype chunks
所有具有相同原型的实体和组件都存储在称为块的统一内存块中。每个块由 16KiB 组成,它们可以存储的实体数量取决于块原型中组件的数量和大小。EntityManager
根据需要创建和销毁块。
块包含每个组件类型的数组,以及用于存储实体 ID 的附加数组。例如,在具有组件类型 A 和 B 的原型中,每个块都具有三个数组:一个数组用于 A 组件值,一个数组用于 B 组件值,一个数组用于实体 ID。
块的数组是紧密打包的:块的第一个实体存储在这些数组的索引 0 处,块的第二个实体存储在索引 1 处,后续实体存储在连续的索引中。当一个新的实体被添加到块中时,它被存储在第一个可用的索引中。当一个实体从块中移除时(因为它被破坏或被移动到另一个原型),块的最后一个实体被移动以填补空白。
将实体添加到原型时,如果原型的现有块已满,则EntityManager
会创建一个新块。当最后一个实体从块中移除时,EntityManager
会销毁该块。
编辑器中的原型
原型窗口列出了项目中所有世界的原型,并显示了每个原型的已分配和未使用内存量。
在编辑器中,以下图标代表原型:。
结构更改
导致 Unity 重新组织内存块或内存中块的内容的操作称为结构更改。了解哪些操作是结构更改很重要,因为它们可能是资源密集型的,您只能在主线程上执行它们;不是来自jobs。
以下操作被视为结构更改:
- 创建或销毁实体。
- 添加或删除组件。
- 设置一个共享组件值。
创建一个实体
当您创建一个实体时,Unity 要么将实体添加到现有块中,要么如果没有块可用于实体的原型,则创建一个新块并将实体添加到其中。
销毁一个实体
当您销毁一个实体时,Unity 会将该实体从其块中移除。如果移除实体在块中留下空隙,Unity 会移动块中的最后一个实体来填补空隙。如果删除实体使块为空,Unity 会释放块。
添加或删除组件
在实体中添加或删除组件时,您会更改实体的原型。 Unity 将每个实体存储在与实体原型匹配的块中。这意味着如果您更改实体的原型,Unity 必须将该实体移动到另一个块。如果不存在合适的块,Unity 会创建一个新块。如果移动使前一个块有间隙或留空,Unity 将移动块中的最后一个实体以分别填充间隙或释放块。
设置一个共享组件值
当您设置实体的共享组件的值时,Unity 会将实体移动到与新共享组件值匹配的块中。如果不存在合适的块,Unity 会创建一个新块。如果移动使前一个块有间隙或为空,Unity 将移动块中的最后一个实体以填充间隙或分别释放块。
设置常规组件值不是结构更改,因为它不需要 Unity 移动实体。
同步点
您不能直接在作业中进行结构更改,因为它可能会使其他已安排的作业无效,并创建一个同步点。
一个同步点(sync point)是程序执行中的一个点,它等待到目前为止已经调度的所有作业完成。同步点会限制您在一段时间内使用作业系统中可用的所有工作线程的能力。因此,您的目标应该是避免同步点。 ECS 中数据的结构变化是产生同步点的主要原因。
结构更改不仅要求同步点,而且还会使对任何组件数据的所有直接引用无效。这包括DynamicBuffer
的实例和提供对组件(例如ComponentSystemBase.GetComponentDataFromEntity
)的直接访问的方法的结果。
避免同步点
您可以使用实体命令缓冲区来排队缓冲结构更改,而不是立即执行它们。您可以在帧的稍后时间执行存储在实体命令缓冲区中的命令。这将跨帧分布的多个同步点减少为单个同步点。
每个标准ComponentSystemGroup
实例都提供EntityCommandBufferSystem
作为组中更新的第一个和最后一个系统。如果您从这些标准系统之一获取实体命令缓冲区对象,则所有结构更改都发生在帧中的同一点,从而产生一个同步点。您还可以使用实体命令缓冲区来记录作业中的结构更改,而不是仅在主线程上进行结构更改。
如果您不能为任务使用实体命令缓冲区,请将所有进行结构更改的系统按系统执行顺序分组在一起。两个都进行结构更改的系统如果按顺序更新,则只会创建一个同步点。