总览
在ECS中,Component包含System可读或者可写的数据。
使用 IComponentData 接口,将struct标记为组件类型,该组件类型只能包含非托管数据(非引用类型?),并且可以包含方法,但最好是让它作为纯数据。
一、常见组件类型
组件 | 描述 |
---|---|
Unmanaged components | 最常用的组件类型,只能存储某些类型的字段 |
Managed components | 可以存储任何字段类型的托管组件类型 |
Shared components | 根据Entity的值将Entity分组到块中的组件 |
Cleanup components | 销毁包含清理组件的实体时,Unity会删除所有非清理组件。这对于标记摧毁时需要清理的Entity非常有用。 |
Tag components | 不存储数据,且不占用空间的非托管组件,可以在实体查询中用来筛选Entity |
Buffer components | 充当可调整大小的数组的组件 |
Chunk components | 存储与整个块(而不是单个Entity)关联的值的组件 |
Enableable components | 可以运行时在Entity上启用或者禁用的组件,而无需进行代价高昂的结构更改 |
Singleton components | 在给定环境中只有一个实例的组件 |
1. Unmanaged components (非托管组件)
最常用的存储数据的数据类型。
非托管组件可以存储以下类型的字段:
- BlittableType
- bool
- char
- BlobAssetReference (对Blob数据结构的引用)
- Collections.FixedString (固定大小的字符缓冲区)
- Collections.FixedList
- Fixed array (固定数组,仅在不安全的上下文中允许)
- 符合这些相同限制的其他结构
1.1 Create an unmanaged component 创建非托管组件
要创建非托管组件,只需要继承 IComponentData 即可:
public struct ExampleUnmanagedComponent : IComponentData
{
public int Value;
}
将使用兼容类型的属性添加到结构中,以定义组件的数据。
如果不向组件添加任何属性,它将充当标记组件。(标记组件:标记组件是非托管组件,不存储任何数据,也不占用空间。)
2. Managed components (托管组件)
与非托管组件不同,托管组件可以存储任何类型的属性。但是,它们的存储和访问会占用更多资源,并且具有以下限制:
- 无法在jobs中访问它们
- 不能再Burst编译代码中使用它们
- 它们需要垃圾回收
- 它们必须包含一个无参构造,以便进行序列化
2.1 Managed type properties 托管类型属性
如果托管组件中的属性使用托管类型(引用类型?),则可能需要手动添加clone、compare、serialize该属性的功能。
2.2 Create a managed component 创建托管组件
创建一个类,该类继承 IcomponentData :
public class ExampleManagedComponent : IComponentData
{
public int Value;
}
2.3 Manage the lifecycle of external resources 管理外部资源的生命周期
如果引用外部资源,最佳的做法是实现 ICloneable和IDisposable。
例如,对于存储对 ParticleSystem 的引用的托管组件:
-
如果你复制这个托管组件的实体,默认情况下会创建两个托管组件,他们都引用相同的粒子系统,如果你为托管组件实现ICloneable,就可以直接为第二个托管组件复制粒子系统。
-
如果你销毁了托管组件,默认情况下粒子系统会保留,如果你为托管组件实现IDisposable,就可以在组件被销毁的时候,同时销毁粒子系统。
示例代码如下:
public class ManagedComponentWithExternalResource : IComponentData, IDisposable, ICloneable
{
public ParticleSystem ParticleSystem;
public void Dispose()
{
UnityEngine.Object.Destroy(ParticleSystem);
}
public object Clone()
{
return new ManagedComponentWithExternalResource { ParticleSystem = UnityEngine.Object.Instantiate(ParticleSystem) };
}
}
2.4 Optimize managed components 优化托管组件
与非托管组件不同,Unity 不会将托管组件直接存储在区块中,相反,Unity 将它们存储在一个大数组中。然后,块存储相关托管组件的数组索引。这意味着,当您访问实体的托管组件时,Unity 会处理额外的索引查找。这使得托管组件不如非托管组件优化。
托管组件的性能影响意味着应尽可能改用非托管组件
3. Shared components (共享组件)
共享组件根据其共享组件的值将实体分组到块中,这有助于重复数据消除。
3.1 Shared components 简介
共享组件根据其共享值,将Entity分组到相同区块中存储,这有助于消除重复数据。
Unity将具有相同共享组件值的所有Entity存储到一起,这将会删除Entity之间的重复值。
可以创建托管或非托管的共享组件,他们有着同样的限制与优点。
3.1.1 Shared components 值存储
对于每个World,Shared components将值存储在独立于ECS区块的数组中, 而该world中的原来的ECS区块,会存储句柄,用来查找对应在数组中的值。多个区块可以存储相同的共享组件句柄,也就是说可以使用相同共享组件的实体数量没有限制。
如果要更改Entity的共享组件的值,那么Unity就会将该Entity移动到使用新共享组件值的区块。这意味着修改实体的共享组件值,是一种结构性更改。
3.1.2 Override the default comparison behavior 重载比较行为
如果需要修改ECS比较共享组件实例的方式,可以为共享组件实现 IEquatable,如果执行此操作,ECS 将使用您的实现来检查共享组件的实例是否相等。如果共享组件是非托管的,则可以将 [BurstCompile] 属性添加到共享组件结构、Equals方法和GetHashCode方法中以提高性能
3.2 Create a shared component 创建共享组件
可以创建托管和非托管共享组件。
3.2.1 Create an unmanaged shared component 创建非托管共享组件
要创建非托管共享组件,请创建一个实现标记接口的结构。ISharedComponentData
下面的代码示例演示了一个非托管共享组件:
public struct ExampleUnmanagedSharedComponent : ISharedComponentData
{
public int Value;
}
3.2.2 Create a managed shared component 创建托管共享组件
若要创建托管共享组件,请创建一个实现ISharedComponentData标记接口和 IEquatable<>的结构,并确保实现public override int GetHashCode()。相等方法对于确保在使用默认 Equals 和 GetHashCode 实现时不会由于隐式装箱而不必要地生成托管分配是必需的。
以下代码示例演示了一个托管共享组件:
public struct ExampleManagedSharedComponent : ISharedComponentData, IEquatable<ExampleManagedSharedComponent>
{
public string Value; // A managed field type
public bool Equals(ExampleManagedSharedComponent other)
{
return Value.Equals(other.Value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
}
3.3 Optimize shared components 优化共享组件
3.3.1 Use unmanaged shared components 使用非托管共享组件
如果可能,请使用非托管共享组件,而不是托管共享组件。这是因为 Unity 将非托管共享组件存储在 Burst 编译代码可通过非托管共享组件 API(如 SetUnmanagedSharedComponentData)访问的位置。与托管组件相比,这提供了性能优势。
3.3.2 Avoid frequent updates 避免频繁更新
更新实体的共享组件值是一种结构更改,这意味着 Unity 将实体移动到另一个块。出于性能原因,请尽量避免频繁执行此操作。
3.3.3 Avoid lots of unique shared component values 避免使用大量唯一的共享组件值
块中的所有实体必须共享相同的共享组件值。这意味着如果您为大量实体提供唯一的共享组件值,它会将这些实体分割成许多几乎为空的块。
例如,如果一个原型有500个实体和一个共享组件,每个实体都有一个唯一的共享组件值,Unity将每个实体存储在一个单独的块中。这浪费了每个块中的大部分空间,也意味着要循环遍历原型的所有实体,Unity必须循环遍历所有500个块。这否定了ECS块布局的好处,并降低了性能。为了避免这个问题,尽量少使用唯一的共享组件值。如果500个示例实体只共享10个唯一的共享组件值,Unity可以将它们存储在10个块中。
要小心使用具有多个共享组件类型的原型。原型块中的所有实体必须具有相同的共享组件值组合,因此具有多个共享组件类型的原型容易受到碎片的影响。
4. Cleanup components (清理组件)
清理组件与常规组件类似,但当您销毁包含一个实体的实体时,Unity 会删除所有非清理组件。该实体仍然存在,直到您从中删除所有清理组件。这对于标记销毁时需要清理的实体非常有用。
4.1 使用清理组件
4.1.1 清理组件生命周期
下面的代码示例说明了包含清理组件的实体的生命周期:
// Creates an entity that contains a cleanup component.
// 创建包含清理组件的实体。
Entity e = EntityManager.CreateEntity(
typeof(Translation), typeof(Rotation), typeof(ExampleCleanup));
// Attempts to destroy the entity but, because the entity has a cleanup component, Unity doesn't actually destroy the entity. Instead, Unity just removes the Translation and Rotation components.
// 尝试销毁实体,但是,因为实体有一个清理组件,Unity实际上不会销毁实体。相反,unity只是移除平移和旋转组件。
EntityManager.DestroyEntity(e);
// The entity still exists so this demonstrates that you can still use the entity normally.
// 该实体仍然存在,所以这表明你仍然可以正常使用该实体。
EntityManager.AddComponent<Translation>(e);
// Removes all the components from the entity. This destroys the entity.
// 从实体中移除所有组件。这会破坏实体。
EntityManager.DestroyEntity(e, new ComponentTypes(typeof(ExampleCleanup), typeof(Translation)));
// Demonstrates that the entity no longer exists. entityExists is false.
// 显示实体不再存在。entityExists为false。
bool entityExists = EntityManager.Exists(e);
::: info
注意
清理组件是非托管组件,并且具有与非托管组件相同的所有限制。
:::
4.2 创建清理组件
要创建清理组件,请创建一个继承自ICleanupComponentData的结构。
下面的代码示例显示了一个空的清理组件:
public struct ExampleCleanupComponent : ICleanupComponentData
{