来源:https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html
我会对官方文档内容略作整理,有需要可以查看官方文档
2.组件——Component
组件中储存着与实体相关的数据(通过实体来索引组件及其数据)
ECS中的组件是具有以下接口的结构:
- IComponentData
- ISharedComponentData
- ISystemStateComponentData
- ISharedSystemStateComponentData
EntityManager会组织所有实体中的组件的组合并构成Archetype(原型),它将所有具有相同原型的的实体的组件储存在一块内存中,称之为chunk(区块),一个区块中的所有组件具有相同的原型。
SharedComponent(共享组件)是一种特殊的组件,可以使用它其中的一个特殊值来细分实体(用于区别其他实体),当为一个实体添加一个共享组件,EntityManager就会将所有特殊值相同的实体放置在相同的区块,共享组件允许系统一起处理相似的实体。
注意: 过度使用共享组件可能会导致较差的块利用率,因为它涉及基于原型和每个共享组件字段的每个唯一值组合扩展所需的内存块数量,所以需要避免向共享组件添加不必要的字段,可以通过Entity Debugger来查看当前的块利用率。
如果对实体添加或删除组件、更改SharedComponent的值,EntityManager会将实体移动到其他Chunk,在必要时会创建新Chunk。
系统状态组件的行为类似于普通组件或共享组件,但在销毁实体时,EntityManager不会删除任何系统状态组件,也不会在删除实体ID之前回收它们,这种差异允许系统在销毁实体时清理其内部状态或释放资源。
2.1、组件介绍
2.1.1、ComponentData
Unity中的ComponentData(也是标准ECS系统中的一个Component)对一个实体而言,它是只包括了数据的结构,他不能包含方法;对比旧的Unity系统而言,他有点像旧的Component系统,但是是一个只包含数据的Component。
UnityECS提供了一个IComponentData接口供使用。
- 传统的Unity组件(包括MonoBehaviour)是一个面向对象的类,包含了数据和方法
- IComponentData是一个ECS组件,因此它不包含任何方法,只包含数据
- IComponentData是一个结构体而不是一个类,这意味着默认情况下它是通过值而不是通过引用复制的,需要使用以下模式来修改数据:
var transform = group.transform[index]; // Read
transform.heading = playerInput.move; // Modify
transform.position += deltaTime * playerInput.move * settings.playerMoveSpeed;
group.transform[index] = transform; // Write
2.1.2、SharedComponentData
IComponentData适用于实体之间不同的数据,例如存储所在的World,而ISharedComponentData在许多实体具有共同点时使用,例如在Boid演示中,需要实例化来自同一Prefab的RenderMesh的许多Boid实体,这些实体完全相同。
[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
最棒的是使用ISharedComponentData时,每个实体的内存成本几乎为零,我们使用ISharedComponentData时会将具有相同InstanceRenderer数据的实体分组在一起,然后有效地提取所有矩阵进行渲染。生成的代码简单有效,因为要访问的数据的布局是完全相同的。
使用SharedComponentData的注意点:
- 具有相同SharedComponentData的实体被相同的Chunk给组织起来,而这些SharedComponentData的索引是通过Chunk进行储存的,而不是每个实体,因此SharedComponentData对每个实体的内存开销而言是零
- 使用EntityQuery可以迭代所有具有相同类型的实体
- 此外,可以使用EntityQuery.SetFilter()迭代具有特定SharedComponentData值的实体,由于数据布局,此迭代具有较低的开销
- 使用EntityManager.GetAllUniqueSharedComponents可以取回所有添加在现有实体上的唯一SharedComponentData
- SharedComponentData会自动引用计数
- SharedComponentData应该很少改变,如果需要改变,就需要使用memcpy将那个实体所有的ComponentData拷贝到一个不同的Chunk上去
2.1.3、SystemStateComponents
SystemStateComponentData的目的是允许你跟踪系统内部的资源,并且有机会适当的创建和销毁所需的资源而不需要任何的回调
SystemStateComponentData和SystemStateSharedComponentData类似ComponentData和SharedComponentData
除了一个重要的方面:
- SystemStateComponentData不会在销毁实体时被删除
DestroyEntity的作用是:
- 根据特定的实体ID找到其所有组件的引用
- 删除这些组件
- 回收实体ID以便重用
但是,如果SystemStateComponentData存在,则不会被移除。这使系统有机会清除与实体ID相关联的任何资源或状态,只有当实体中所有的SystemStateComponentData被删除后,才会重用实体ID 。
动机
- 系统有时需要保持ComponentData的内部状态,例如,分配资源时
- 系统需要能够管理该状态,因为值和状态更改是由其他系统进行的,例如,当组件中的值更改时,或添加或删除相关组件时
- “无回调”是ECS设计规则的重要元素
概念
所期望的SystemStateComponentData的一般用途是镜像用户组件,提供内部状态
例如,给定:
- FooComponent(ComponentData,用户分配)
- FooStateComponent(SystemComponentData,系统分配)
检测组件添加
当用户添加FooComponent时,FooStateComponent不存在
FooSystem会在没有FooStateComponent的状态下更新查询FooComponent组件
推断他们已经被添加了
此时,FooSystem将添加FooStateComponent和任何所需的内部状态
检测组件删除
当用户删除FooComponent时,FooStateComponent依然存在
FooSystem会在没有FooComponent的状态下更新查询FooStateComponent组件
推断他们已经被删除了
此时,FooSystem将删除FooStateComponent并修复任何所需的内部状态
检测销毁实体
DestroyEntity的作用是:
- 根据特定的实体ID找到其所有组件的引用
- 删除这些组件
- 回收实体ID以便重用
但是,SystemStateComponentData如果不删除,则在DestroyEntity删除最后一个组件之前不会回收实体ID。这使系统有机会以与移除组件完全相同的方式清理内部状态,我们在创建SystemStateComponentData的时候会给予一个ReadOnly的修饰符。
SystemStateComponent
SystemStateComponentData类似于ComponentData的用法
struct FooStateComponent : ISystemStateComponentData
{
}
SystemStateComponentData的修饰符的使用方式和一个普通组件的使用方式相同(using private, public, internal),然而,作为例外,一般而言,
SystemStateSharedComponent
SystemStateSharedComponentData类似于SharedComponentData的用法
struct FooStateSharedComponent : ISystemStateSharedComponentData
{
public int Value;
}
2.1.4、Dynamic Buffers
DynamicBuffer是一种与实体相关联的,可变大小的弹性的组件,它表现为一种可以承受一定数量元素的组件,但是当它的内部容量耗尽就会分配一块堆内存。使用该方法时,内存管理是自动的,内存由DynamicBuffers联系,而DynamicBuffers由EntityManager管理,所以当删除DynamicBuffer组件时,也会自动释放任何关联的堆内存。
DynamicBuffers取代了已经被删除的fixed array
声明Buffer的元素类型
声明一个Buffer可以使用要放入的元素类型声明它
// This describes the number of buffer elements that should be reserved
// in chunk data for each instance of a buffer. In this case, 8 integers
// will be reserved (32 bytes) along with the size of the buffer header
// (currently 16 bytes on 64-bit targets)
[InternalBufferCapacity(8)]
public struct MyBufferElement : IBufferElementData
{
// These implicit conversions are optional, but can help reduce typing.
public static implicit operator int(MyBufferElement e) { return e.Value; }
public static implicit operator MyBufferElement(int e) { return new MyBufferElement { Value = e }; }
// Actual value each buffer element will store.
public int Value;
}
虽然这看上去很奇怪,但是这种描述方式由两个好处:
- 它支持多个float3类型的DynamicBuffer或任何其他常见的值类型,可以添加任意数量的利用相同值类型的数据到Buffers中去,只要元素被独一无二的包装在顶级结构体中
- 我们可以在EntityArchetypes中包含Buffer元素类型,它通常会表现为一个组件
向实体添加Buffer类型
使用向组件类型添加Buffer类型组件的常规方法:
- 使用AddBuffer()
entityManager.AddBuffer<MyBufferElement>(entity);
- 使用原型
Entity e = entityManager.CreateEntity(typeof(MyBufferElement));
访问Buffer类型
有几种方法可以访问DynamicBuffers,这是对常规组件数据的并行访问方法
- 仅主线程直接访问
DynamicBuffer<MyBufferElement> buffer = entityManager.GetBuffer<MyBufferElement>(entity);
基于实体的访问
- 通过JobComponentSystem对每一个实体组件查询Buffers
var lookup = GetBufferFromEntity<EcsIntElement>();
var buffer = lookup[myEntity];
buffer.Append(17);
buffer.RemoveAt(0);