简单介绍一下需要了解的几个关键点
Unity官方文档: https://github.com/Unity-Technologies/EntityComponentSystemSamples
详情: https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Docs/jobs.md
实体是游戏对象的轻量级、非托管替代品。实体在很多方面类似于 GameObjects 并且可以发挥相似的作用,但它们有关键的区别:
component
通常是struct value
。Foo
的组件。(一个Entity
上只能挂载一个名为Foo
的组件)component
中不建议设置除了GET/SET
之外的Function
)Parent
组件包含对另一个实体的引用,允许形成实体转换层次结构。(Entity
的父子结构为引用)实体组件类型是通过实现这些接口来定义的:
组件种类 | 描述 |
---|---|
IComponentData | 定义最常见、最基本的组件类型。 |
IBufferElementData | 定义动态缓冲区(可增长数组)组件类型。 |
ISharedComponent | 定义一个共享组件类型,其值可以被多个实体共享。 |
ICleanupComponent | 定义清理组件类型,这有助于正确设置和拆卸资源。 |
ISharedComponent | 定义一个共享组件类型,其值可以被多个实体共享。 |
使用IComponentData
定义最常见、最基本的组件类型。
eg
///
/// 空组件称为“标记组件”
///
struct TankTag : IComponentData
{
}
using Unity.Entities;
///
/// 同样的方法对于炮弹,我们正在创建一个组件来识别实体。
/// 但这次它不是一个标记组件(空的),因为它包含数据: Speed字段。
/// 它不会立即被使用,但当我们实施议案时会变得有意义。
///
struct CannonBallComponent : IComponentData
{
public float Speed;
}
World
是实体的集合。一个实体的 ID 号只在它自己的世界中是唯一的,即在一个世界中具有特定 ID 的实体与在不同世界中具有相同 ID 的实体完全无关。
一个World
也拥有一组系统,它们是在主线程上运行的代码单元,通常每帧一次。一个世界的实体通常只能被该世界系统访问(以及这些系统安排的工作),但这不是强制限制。
世界中的实体通过世界的EntityManager
.
EntityManager
方法包括:
方法 | 描述 |
---|---|
CreateEntity() |
创建一个新实体。 |
Instantiate() |
使用现有实体的所有组件的副本创建一个新实体。 |
DestroyEntity() |
销毁现有实体。 |
AddComponent |
将类型 T 的组件添加到现有实体。 |
RemoveComponent |
从现有实体中移除类型 T 的组件。 |
HasComponent |
如果实体当前具有 T 类型的组件,则返回 true。 |
GetComponentData |
检索类型 T 的实体组件的值。 |
SetComponentData(T) |
覆盖类型 T 的实体组件的值。 |
Tip : 以上所有方法,除了
GetComponentData
和SetComponentData
,都是结构变化操作。
eg
///
/// 空组件称为“标记组件”
///
struct TankTag : IComponentData
{
}
using Unity.Burst;
using Unity.Entities;
[BurstCompile]
partial struct TurretShootingSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//拿到当前世界的EntityManager
EntityManager entityManager = state.EntityManager;
//创建一个新实体
Entity entity = entityManager.CreateEntity();
//使用现有实体的所有组件的副本创建一个新实体
Entity newEntity = entityManager.Instantiate(entity);
//销毁现有实体
entityManager.DestroyEntity(newEntity);
//将类型 T 的组件添加到现有实体
entityManager.AddComponent<TankTag>(entity);
//从现有实体中移除类型 T 的组件
entityManager.RemoveComponent<TankTag>(entity);
//如果实体当前具有 T 类型的组件,则返回 true
bool hasCom = entityManager.HasComponent<TankTag>(entity);
//检索类型 T 的实体组件的值
TankTag tankTag = entityManager.GetComponentData<TankTag>(entity);
//覆盖类型 T 的实体组件的值
entityManager.SetComponentData(entity,tankTag);
}
}
原型代表世界中组件类型的特定组合:世界中具有特定组件类型集的所有实体都存储在同一原型中。
EntityManager
实际将实体及其组件从其旧原型移动到新原型。Tip : 过于频繁地在原型之间移动太多实体会增加大量成本。
原型是在EntityManager
您创建和修改实体时创建的,因此您不必担心显式创建原型。即使所有实体都从原型中移除,原型也只有在其世界被摧毁时才会被摧毁。
原型的实体存储在属于称为chunks的原型的 16KiB 内存块中。每个块最多存储 128 个实体(精确数量取决于原型组件类型的数量和大小)。
每种类型的实体 ID 和组件都存储在块内它们自己单独的数组中。
块的创建和销毁由EntityManager
处理:
EntityManager
一个实体被添加到一个已经存在的块都已满的原型时,才会创建一个新块。EntityManager
只有当块的最后一个实体被移除时,才会销毁该块。任何块在EntityManager
内添加、删除或移动实体的操作都称为结构更改。此类更改通常只应在主线程上进行,而不应在作业中进行。(可以使用 EntityCommandBuffer
来解决此限制。)
一个EntityQuery有效地找到具有一组指定的组件类型的所有实体。例如,如果查询查找具有组件类型A和B的所有实体,则该查询将收集具有这两种组件类型的所有原型的块,而不管这些原型可能具有的任何其他组件类型。因此,这样的查询将匹配具有组件类型A和B的实体,但该查询也会匹配具有组件类型 A、B和C的实体。
与查询匹配的原型将被缓存,直到下一次将新原型添加到世界中。因为世界中现有的原型集往往会在程序生命周期的早期稳定下来,所以这种缓存往往会提高性能。
查询还可以指定要从匹配原型中排除的组件类型。例如,如果查询查找具有组件类型A和B但不具有组件类型C的所有实体,则该查询将匹配具有组件类型A和B的实体,但查询不会匹配具有组件类型A、B和C.
eg
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
///
/// 基于Isystem的非托管系统可以是突发编译,但这还不是默认值。
/// 因此,我们必须使用[BurstCompile]属性显式选择Burst编译。
/// 它必须被添加到结构和OnCreate/0nDestroy/0nUpdate函数上才能生效。
///
[BurstCompile]
partial struct TurretRotationSystem : ISystem
{
//ISystem定义的每一个函数都必须实现,即使为空
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//在2秒内绕Y旋转360度所需的旋转量。
var rotation =quaternion.RotateY(SystemAPI.Time.DeltaTime*math.PI);
//经典的C#foreach就是我们常说的方面提供了比直接访问组件数据更高级别的接口。“通用foreach”(IFE)。
//将IFE与方面一起使用是编写主线程代码的一种强大而富有表现力的方式
foreach (var transform in SystemAPI.Query<TransformAspect>().WithAll<TurretComponent>())
{
transform.RotateWorld(rotation);
}
}
}
最常见的基本类型的组件类型定义为结构实现IComponentData
。
eg
public struct EnergyShield : IComponentData
{
public int HitPoints;
public int MaxHitPoints;
public float RechargeDelay;
public float RechargeRate;
}
结构应该是非托管的IComponentData
,因此它不能包含任何托管字段类型。具体来说,允许的字段类型是:
bool
char
BlobAssetReference
, 对 Blob 数据结构的引用Collections.FixedString
, 一个固定大小的字符缓冲区Collections.FixedList
没有字段的IComponentData
结构称为标记组件。尽管标签组件不存储数据,但它们仍然可以像任何其他组件类型一样从实体中添加和删除,并且它们可用于标记实体以供查询。例如,如果我们表示怪物的所有实体都有一个Monster
标签组件,则对Monster
组件类型的查询将匹配所有怪物实体。
eg
public struct OnFire : IComponentData
{
//空组件称为“标记组件”
//标记组件不占用存储空间,但可以像任何其他组件一样对其//进行查询、添加和删除
}
DynamicBuffer
是一个组件类型,它是一个可调整大小的数组。要定义DynamicBuffer
组件类型,请创建一个实现该IBufferElementData
接口的结构。
示例
Aspects
是实体组件子集上的类对象包装器。方面可用于简化查询和与组件相关的代码。TransformAspect
例如,将标准变换组件(、LocalTransform
和ParentTransform
)组合在一起WorldTransform
。
示例
在查询中包含方面与包含由方面包装的组件相同,例如,包含TransformAspect
标准变换矩阵组件的查询。
一个方面被定义为只读的部分结构实现IAspect
。该结构可以包含以下类型的字段:
字段类型 | 描述 |
---|---|
Entity | 包装实体的实体 ID。 |
RefRW 或者RefRO |
对包装实体的 T 组件的引用。 |
EnabledRefRW 和EnabledRefRO |
对包装实体的 T 组件的启用状态的引用。 |
EnabledRefRW 和EnabledRefRO |
$1 |
DynamicBuffer |
包装实体的动态缓冲区 T 组件。 |
另一个Aspects 类型 |
包含方面将包含 “embedded”Aspects 的所有字段。 |
eg
using Unity.Entities;
///
/// 而不是直接访问炮塔组件,我们正在创建一个方面。Aspects允许您为访问组件提供定制的API。
///
readonly partial struct TurretAspect : IAspect
{
readonly RefRO<TurretComponent> m_Turret;
//此引用提供了对炮塔组件的只读访问。
//试图在只读引用上使用ValueRw (而不是ValueRo)是一个错误。
public Entity CannonBallSpawn=>m_Turret.ValueRO.CannonBallSpawn;
//注意ValueRO在下列属性中的使用。
public Entity CannonBallPrefab=>m_Turret.ValueRO.CannonBallPrefab;
}
这些 EntityManager 方法创建方面的实例:
方法 | 描述 |
---|---|
GetAspect |
返回包装实体的类型 T 的一个Aspect。 |
GetAspectRO |
返回包装实体的类型 T 的只读Aspect。如果您使用任何试图修改底层组件的方法或属性,只读Aspect将引发异常。 |
方面实例也可以通过SystemAPI.GetAspectRW
检索SystemAPI.GetAspectRO
或在IJobEntityorSystemAPI.Query
循环中访问。
TIP : 在系统中,您应该通过
SystemAPI.GetAspectRW
而SystemAPI.GetAspectRO
不是通过EntityManager
方法获取方面实例。与EntityManager
方法不同,方法向SystemAPI
系统注册方面的底层组件类型,这是系统调度具有每个所需依赖性的作业所必需的。
系统是属于世界并在主线程上运行的代码单元(通常每帧一次)。通常,一个系统只会访问它自己世界的实体,但这不是强制性的限制。
示例
系统被定义为实现ISystem接口的结构,它具有三个关键方法:
ISystemState 方法 |
描述 |
---|---|
OnUpdate() |
通常每帧调用一次,但这取决于SystemGroup 系统所属的系统。 |
OnCreate() |
在第一次调用之前OnUpdate 以及系统恢复运行时调用。 |
OnDestroy() |
当系统被销毁时调用。 |
系统可以另外实现ISystemStartStop
,它具有以下方法:
ISystemStartStop 方法 |
描述 |
---|---|
OnStartRunning() |
在第一次调用 之前和系统属性从 更改为之后OnUpdate的任何时间调用。Enabled false true |
OnStopRunning() |
在系统属性从 更改为之前OnDestroy和之后调用。Enabled true false |
一个世界的系统被组织成系统组。每个系统组都有一个有序的系统列表和其他系统组作为其子项,因此系统组形成一个层次结构,它决定了更新顺序。系统组定义为继承自的类ComponentSystemGroup
。
更新系统组时,该组通常会按其排序顺序更新其子项,但可以通过覆盖组的更新方法来覆盖此默认行为。
每次从组中添加或删除子项时,组的子项都会重新排序。
属性[UpdateBefore]
和[UpdateAfter]
用于确定组中子项之间的相对排序顺序。例如,如果 FooSystem
具有属性UpdateBefore(typeof(BarSystem))]
,则将按排序顺序FooSystem
放在前面的某个位置。BarSystem
但是,如果FooSystem
和BarSystem
不属于同一组,则该属性将被忽略。如果排序属性与另一个属性相矛盾,则会抛出异常。
eg
//示例系统。
//该系统将添加到称为MySystemGroup的系统组中。
//通过标记将ISystem方法通过标记使“入口点”爆发
//它们和结构本身具有burstCompile属性。
[BurstCompile]
[UpdateInGroup(typeof(MySystemGroup))]
public partial struct MySystem : ISystem
{
// Called once when the system is created.
[BurstCompile]
public void OnCreate(ref SystemState state) { }
[BurstCompile]
public void OnDestroy(ref SystemState state) { }
[BurstCompile]
public void OnUpdate(ref SystemState state) { }
}
// 一个示例系统组。
public class MySystemGroup : ComponentSystemGroup
{
//除非您需要,否则系统组是空的
//覆盖update,ongreate或ondestroy。
}
默认情况下,自动引导过程会创建一个包含三个系统组的默认世界:
InitializationSystemGroupInitialization
,它在Unity 播放器循环阶段结束时更新。SimulationSystemGroupUpdate
,它在Unity 播放器循环阶段结束时更新。PresentationSystemGroupPreLateUpdate
,它在Unity 播放器循环阶段结束时更新。系统和系统组通常会添加到 SimulationSystemGroup
中,但这可以通过使用[UpdateInGroup]
属性标记它们来覆盖。例如,如果 FooSystem
具有属性UpdateInGroup(typeof(InitializationSystemGroup))]
,FooSystem
将被添加到InitializationSystemGroup
而不是SimulationSystemGroup
。[DisableAutoCreation]
自动引导不会实例化具有该属性的系统或系统组。
SystemState
系统的OnUpdate()
, OnCreate()
, 和methods
的参数OnDestroy()
表示系统实例的状态,具有重要的方法和属性,包括:
方法或属性 | 描述 |
---|---|
World | 系统的世界。 |
EntityManager | 系统世界的EntityManager 。 |
Dependency | 用于JobHandle 在系统之间传递作业依赖关系。 |
GetEntityQuery() |
返回一个EntityQuery |
GetComponentTypeHandle |
返回一个ComponentTypeHandle |
GetComponentLookup |
返回一个ComponentLookup |
TIP : 尽管可以直接从 获取实体查询、组件类型句柄和组件查找
EntityManager
,但系统通常只从 获取这些东西是合适的SystemState
。通过SystemState,访问的组件类型将被系统跟踪,这对于Dependency
属性在系统之间正确传递作业依赖性至关重要。查看有关访问实体的作业的更多信息。
eg
using Unity.Burst;
using Unity.Entities;
[BurstCompile]
partial struct TurretShootingSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state){}
[BurstCompile]
public void OnDestroy(ref SystemState state){}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//拿到当前世界的EntityManager
EntityManager entityManager = state.EntityManager;
}
}
该类有许多静态便捷方法,涵盖了World
与EntityManager
、SystemState
和SystemAPI
相同的大部分功能。
这些SystemAPI
方法依赖于源生成器,因此它们只能在System
和IJobEntity
(但不是IJobChunk)中工作。SystemAPI
这些方法在两种上下文中产生相同的结果,因此SystemAPI通常更容易在这两种上下文之间复制粘贴。
TIP : 如果您对在哪里寻找关键实体功能感到困惑,一般规则是先检查
SystemAPI
。如果SystemAPI
没有您要查找的内容,请在System
中查找SystemState
,如果您要查找的内容不存在,请在EntityManager
和World
中查找。
SystemAPI
提供了一种特殊的Query()
方法,通过源代码生成,帮助方便地创建一个foreach
循环遍历匹配查询的实体和组件。
eg
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
///
/// 基于Isystem的非托管系统可以是突发编译,但这还不是默认值。
/// 因此,我们必须使用[BurstCompile]属性显式选择Burst编译。
/// 它必须被添加到结构和OnCreate/0nDestroy/0nUpdate函数上才能生效。
///
[BurstCompile]
partial struct TurretRotationSystem : ISystem
{
//ISystem定义的每一个函数都必须实现,即使为空
[BurstCompile]
public void OnCreate(ref SystemState state){}
[BurstCompile]
public void OnDestroy(ref SystemState state){}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//在2秒内绕Y旋转360度所需的旋转量。
var rotation =quaternion.RotateY(SystemAPI.Time.DeltaTime*math.PI);
//经典的C#foreach就是我们常说的方面提供了比直接访问组件数据更高级别的接口。“通用foreach”(IFE)。
//将IFE与方面一起使用是编写主线程代码的一种强大而富有表现力的方式
foreach (var transform in SystemAPI.Query<TransformAspect>().WithAll<TurretComponent>())
{
transform.RotateWorld(rotation);
}
}
}
您可以使用C# Job System将实体数据的处理卸载到工作线程。Entities 包有两个接口用于定义访问实体的作业:
IJobChunk
, 其Execute()
方法为匹配查询的每个单独块调用一次。IJobEntity
,它的Execute()
方法为匹配查询的每个实体实体调用一次。虽然IJobEntity
通常更方便编写和使用,但IJobChunk
提供更精确的控制。在大多数情况下,它们的性能对于同等工作是相同的。
这个可以与下面一章一起看
上面解释比较繁琐,看下面的讲的比较清晰
ECS的简单入门(五):Entity Command Buffer
LocalTransform组件表示实体的转换
TIP : 虽然您可以安全地读取实体的Child缓冲区组件,但您不应该直接修改它。仅通过设置实体的Parent组件来修改转换层次结构。
每一帧,LocalToWorldSystem
计算每个实体的世界空间变换(从LocalTransform
实体的组件及其祖先)并将其分配给实体的LocalToWorld
组件。
系统Entity.Graphics
读取LocalToWorld
组件但不读取任何其他转换组件,因此LocalToWorld是实体需要呈现的唯一转换组件。
TransformAspect
为使用实体的转换组件提供了一个方便的抽象。
eg
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
///
/// 基于Isystem的非托管系统可以是突发编译,但这还不是默认值。
/// 因此,我们必须使用[BurstCompile]属性显式选择Burst编译。
/// 它必须被添加到结构和OnCreate/0nDestroy/0nUpdate函数上才能生效。
///
[BurstCompile]
partial struct TurretRotationSystem : ISystem
{
//ISystem定义的每一个函数都必须实现,即使为空
[BurstCompile]
public void OnCreate(ref SystemState state){ }
[BurstCompile]
public void OnDestroy(ref SystemState state){ }
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//在2秒内绕Y旋转360度所需的旋转量。
var rotation =quaternion.RotateY(SystemAPI.Time.DeltaTime*math.PI);
//经典的C#foreach就是我们常说的方面提供了比直接访问组件数据更高级别的接口。“通用foreach”(IFE)。
//将IFE与方面一起使用是编写主线程代码的一种强大而富有表现力的方式
foreach (var transform in SystemAPI.Query<TransformAspect>().WithAll<TurretComponent>())
{
transform.RotateWorld(rotation);
}
}
}
烘焙是一个构建时过程,它使用面包师和烘焙系统将子场景转换为实体场景:
Baker
是一个扩展类Baker
,其中 T
是 MonoBehaviour
。带有 Baker
的 MonoBehaviour
称为创作组件。[WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)]
属性的普通系统。(烘焙系统是完全可选的,通常只有高级用例才需要。)烘焙子场景通过几个主要步骤完成:
简单来说就是在Sub Scene
中的物体会自动转为Entity
,并触发Break脚本
eg
using Unity.Entities;
using UnityEngine;
public class CannonBallAuthoring : MonoBehaviour
{
}
class CannonBallBaker : Baker<CannonBallAuthoring>
{
public override void Bake(CannonBallAuthoring authoring)
{
//默认情况下,组件是零初始化的。
//所以在本例中,CannonBallComponent中的Speed字段将是float3.zero。
AddComponent<CannonBallComponent>();
}
}
增量烘焙需要 Baker’s 跟踪他们读取的所有数据。Baker 创作组件的字段会自动跟踪,但 Baker 读取的其他数据必须通过 Baker 方法添加到其依赖项列表中:
Baker 方法 | 描述 |
---|---|
GetComponent |
访问子场景中任何游戏对象的任何组件。 |
DependsOn() |
跟踪此 Baker 的资产。 |
GetEntity() |
返回在子场景中烘焙或从预制件烘焙的实体的 ID。(实体尚未完全烘焙,因此您不应尝试读取或修改实体的组件。) |
出于流式传输的目的,场景的实体被分成由索引号标识的部分。实体属于哪个部分由其SceneSection
共享组件指定。默认情况下,实体属于第 0 部分,但这可以通过SceneSection
在烘焙过程中进行设置来更改。
TIP : 在烘焙过程中,子场景中的实体只能引用相同部分或部分 0 的其他实体(这是一种特殊情况,因为部分 0 总是在其他部分之前加载,并且只有在场景本身被卸载时才被卸载)。
当一个场景被加载时,它由一个实体表示,该实体具有关于场景的元数据,并且它的每个部分也由一个实体表示。通过操纵其实体的RequestSceneLoaded
组件来加载和卸载单个部分:当该组件更改时, 会做出响应SceneSectionStreamingSystem
。
SceneSystemGroup
包含用于加载和卸载实体场景的SceneSystem静态方法:
SceneSystem 方法 | 描述 |
---|---|
LoadSceneAsync() |
开始加载场景。返回表示加载场景的实体。 |
LoadPrefabAsync() |
开始加载预制件。返回引用加载预制件的实体。 |
UnloadScene() |
销毁已加载场景的所有实体。 |
IsSceneLoaded() |
如果加载了场景,则返回 true。 |
IsSectionLoaded() |
如果加载了一个部分,则返回 true。 |
GetSceneGUID() |
返回表示场景资产的 GUID(由其文件路径指定)。 |
GetScenePath() |
返回场景资产的路径(由其 GUID 指定)。 |
GetSceneEntity() |
返回表示场景的实体(由其 GUID 指定)。 |
TIP :
Entity scene
和section loading
始终是异步的,无法保证请求后多久加载完数据。在大多数情况下,代码应该检查是否存在从场景加载的特定数据,而不是检查场景本身的加载状态。这种方法避免了将代码绑定到特定场景:如果数据被移动到不同的场景、从网络下载或程序生成,代码仍然可以不加修改地工作。
慢慢补充