实体组件系统(ECS)是Unity面向数据的技术堆栈的核心。顾名思义,ECS包含三个主要部分:
一个World
拥有EntityManager和一组ComponentSystems。您可以创建任意数量的World
对象。通常,您将创建模拟World
,渲染或演示World
。
public static World Active { get; set; }
public static ReadOnlyCollection AllWorlds { get; }
//实体管理
public EntityManager EntityManager { get; }
public bool IsCreated { get; }
public string Name { get; }
public bool QuitUpdate { get; set; }
public ulong SequenceNumber { get; }
//系统
public IEnumerable Systems { get; }
public int Version { get; }
//每次系统更新后都会增加一个计数器,表示版本
public uint GlobalSystemVersion { get; }
//提供调试信息和操作的对象。
public EntityManager.EntityManagerDebug Debug { get; }
//内部实体数组的容量。
public int EntityCapacity { get; }
//排他实体事务的Job依赖关系。
public JobHandle ExclusiveEntityTransactionDependency { get; set; }
//报告EntityManager是否已初始化。
public bool IsCreated { get; }
//匹配所有组件的EntityQuery实例。
public EntityQuery UniversalQuery { get; }
//最新的实体的版本。
public int Version { get; }
//当前的世界,一个世界有一个EntityManager,而EntityManager管理一个世界的实体。
public World World { get; }
EntityManager提供了一个用于创建,读取,更新和销毁实体的API。
典型的系统在一组具有特定组件的实体上运行。系统使用EntityQuery(JobComponentSystem)或 EntityQueryBuilder(ComponentSystem)识别感兴趣的组件。然后,系统找到与查询匹配的实体并对其进行迭代,读取和写入数据,并根据需要执行其他实体操作。
//控制此系统在调用其OnUpdate函数时是否执行。
public bool Enabled { get; set; }
public EntityManager EntityManager { get; }
//该系统缓存的查询对象。
public EntityQuery[] EntityQueries { get; }
//此World中当前的更改版本号。
public uint GlobalSystemVersion { get; }
//系统当前版本
public uint LastSystemVersion { get; }
public World World { get; }
//选择并迭代所有的entity
protected EntityQueryBuilder Entities { get; }
//系统更新功能完成后,要播放的与实体相关的命令队列。
public EntityCommandBuffer PostUpdateCommands { get; }
当遍历具有Entities的实体的集合时,系统禁止会导致该集合无效的结构更改。此类更改包括创建和销毁实体,添加或删除组件以及更改共享组件的价值。而是将结构更改命令添加到此PostUpdateCommands命令缓冲区。在此系统的OnUpdate()函数返回之后,系统按顺序执行添加到该命令缓冲区的命令。
//实体内部列表的索引。
public int index;
//当前实体的版本
public int version;
尽管实体没有类型,但是可以按与实体相关联的数据组件的类型对实体组进行分类。创建实体并向其中添加组件时,EntityManager会跟踪现有实体上组件的唯一组合。这种独特的组合称为原型。将组件添加到实体时,EntityManager会创建一个EntityArchetype结构。您可以使用现有的EntityArchetypes创建符合该原型的新实体。您也可以预先创建EntityArchetype并使用它来创建实体。
{Entity两个int字段,A组件:一个int字段,B:两个int,C:三个double},如果有六个Entity甲乙丙丁戊己,且都有上面假设的A,B,C组件的话,其Chunk布局如图(顺序为从左到右再由上到下):
引用自:https://blog.csdn.net/yudianxia/article/details/80498015
传统的Unity组件(包括MonoBehaviour
)是面向对象的类,其中包含行为的数据和方法。IComponentData
是纯ECS样式的组件,这意味着它没有定义任何行为,仅定义了数据。IComponentData
是一个结构而不是一个类,这意味着默认情况下它是通过值而不是通过引用复制的。
您会看到每个结构都继承自 IComponentData。这将数据标记为实体组件系统要使用和跟踪的类型,并允许在后台以智能方式分配和打包数据,同时您可以完全专注于您的游戏代码。ComponentDataWrapper 类允许您将这些数据公开到其附加的预制件的检视窗。
IComponentData
结构可能不包含对托管对象的引用。因为所有人都ComponentData
生活在简单的非垃圾收集的跟踪大块内存中。
using System;
using Unity.Entities;
namespace Shooter.ECS
{
[Serializable]
public struct MoveSpeed : IComponentData
{
public float Value;
}
public class MoveSpeedComponent : ComponentDataWrapper { }
}
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
数据将所有实体分组在一起,然后有效地提取所有矩阵以进行渲染。生成的代码简单而高效,因为数据的布局与访问时的布局完全相同。
的目的SystemStateComponentData
是允许您跟踪系统内部的资源,并有机会根据需要适当地创建和销毁这些资源,而不必依赖各个回调。
SystemStateComponentData
和SystemStateSharedComponentData
分别与ComponentData
和类似SharedComponentData
,但在一个重要方面:
SystemStateComponentData
实体被销毁时不会删除。DestroyEntity
的简单流程:
但是,如果SystemStateComponentData
存在,则不会将其删除。这使系统有机会清除与实体ID相关联的任何资源或状态。实体ID仅在全部SystemStateComponentData
删除后才能重新使用。
A DynamicBuffer
是一种组件数据,它允许将可变大小的“可伸缩”缓冲区与实体相关联。它的行为类似于承载一定数量元素内部容量的组件类型,但是如果内部容量已用尽,则可以分配堆内存块。
使用这种方法时,内存管理是全自动的。与关联的内存 DynamicBuffers
由进行管理,EntityManager
以便在DynamicBuffer
删除组件时,也会自动释放任何关联的堆内存。
DynamicBuffers
取代已删除的固定阵列支持。
A DynamicBuffer
是一种组件数据,它允许将可变大小的“可伸缩”缓冲区与实体相关联。它的行为类似于承载一定数量元素内部容量的组件类型,但是如果内部容量已用尽,则可以分配堆内存块。
使用这种方法时,内存管理是全自动的。与关联的内存 DynamicBuffers
由进行管理,EntityManager
以便在DynamicBuffer
删除组件时,也会自动释放任何关联的堆内存。DynamicBuffers
取代已删除的固定阵列支持。要声明一个 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;
}
尽管描述元素类型而不是描述元素Buffer
本身似乎很奇怪,但这种设计在ECS中具有两个主要优点:
它支持具有多个DynamicBuffer
type float3
或任何其他公共值类型。您可以添加任意数量的Buffers
利用相同值类型的内容,只要元素被唯一地包装在顶层结构中即可。
我们可以在中包含Buffer
元素类型EntityArchetypes
,它的行为通常类似于具有组件。
管理依赖关系很困难。这就是为什么JobComponentSystem
我们会为您自动进行。规则很简单:来自不同系统的作业可以IComponentData
并行读取相同类型的内容。如果作业之一正在写入数据,那么它们将不能并行运行,并且将根据作业之间的依赖关系进行调度。
public class RotationSpeedSystem : JobComponentSystem
{
[BurstCompile]
struct RotationSpeedRotation : IJobForEach
{
public float dt;
public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
{
rotation.value = math.mul(math.normalize(rotation.value), quaternion.axisAngle(math.up(), speed.speed * dt));
}
}
// Any previously scheduled jobs reading/writing from Rotation or writing to RotationSpeed
// will automatically be included in the inputDeps dependency.
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new RotationSpeedRotation() { dt = Time.deltaTime };
return job.Schedule(this, inputDeps);
}
}
所有作业以及系统都声明其读取或写入的ComponentType。结果,当JobComponentSystem返回JobHandle时,它将自动向EntityManager
和所有类型注册,包括有关其正在读取或写入的信息。
因此,如果一个系统写入component A
,然后另一个系统随后从component读取A
,则将JobComponentSystem
浏览它正在从中读取的类型的列表,从而将对第一个系统的作业的依赖性传递给您。
JobComponentSystem
只需在需要的地方将作业链接为依赖项,从而不会在主线程上造成停顿。但是,如果非作业ComponentSystem
访问相同的数据会怎样?由于声明了所有访问权限,因此ComponentSystem
在调用之前,会自动完成针对系统使用的组件类型运行的所有作业OnUpdate
。
该EntityCommandBuffer
课程解决了两个重要问题:
EntityManager
。EntityManager
(例如,创建实体)时,会使所有注入的数组和EntityQuery
对象无效。通过EntityCommandBuffer
抽象,您可以将更改排队(从作业或从主线程进行),以使更改可以稍后在主线程上生效。有两种使用方法EntityCommandBuffer
:
ComponentSystem
在主线程上更新的子类有一个可用的自动调用PostUpdateCommands
。要使用它,只需引用该属性并使更改排队。从系统Update
功能返回后,它们会立即自动应用于世界。
这是一个例子:
PostUpdateCommands.CreateEntity(TwoStickBootstrap.BasicEnemyArchetype);
PostUpdateCommands.SetComponent(new Position2D { Value = spawnPosition });
PostUpdateCommands.SetComponent(new Heading2D { Value = new float2(0.0f, -1.0f) });
PostUpdateCommands.SetComponent(default(Enemy));
PostUpdateCommands.SetComponent(new Health { Value = TwoStickBootstrap.Settings.enemyInitialHealth });
PostUpdateCommands.SetComponent(new EnemyShootState { Cooldown = 0.5f });
PostUpdateCommands.SetComponent(new MoveSpeed { speed = TwoStickBootstrap.Settings.enemySpeed });
PostUpdateCommands.AddSharedComponent(TwoStickBootstrap.EnemyLook);
如您所见,该API与该EntityManager
API 非常相似。在这种模式下,将自动模式EntityCommandBuffer
视为方便的工具很有用,它使您可以在仍然对环境进行更改的同时防止系统内部的数组失效。
对于作业,必须EntityCommandBuffer
在主线程上从实体命令缓冲区系统请求,然后将其传递给作业。当EntityCommandBufferSystem
更新时,命令缓冲区将在顺序播放在主线程创建它们。需要执行此额外步骤,以便可以集中进行内存管理,并可以确保所生成实体和组件的确定性。
再次让我们看一下两杆射手示例,以了解其实际工作原理。
使用组件系统组来指定系统的更新顺序。您可以使用系统类声明中的[UpdateInGroup]属性将系统放在一个组中。然后,您可以使用[UpdateBefore]和[UpdateAfter]属性在组中指定更新顺序。
ECS框架会创建一组默认系统组,可用于在框架的正确阶段更新系统。您可以将一个组嵌套在另一个组中,以便组中的所有系统都在正确的阶段进行更新,然后根据其组内的顺序进行更新。
ComponentSystemGroup类表示应按特定顺序一起更新的相关组件系统的列表。ComponentSystemGroup是从ComponentSystemBase派生的,因此在所有重要的方面它都像组件系统一样工作-可以相对于其他系统进行排序,具有OnUpdate()方法等。最相关的是,这意味着可以将组件系统组嵌套在其中其他组件系统组,形成一个层次结构。
默认情况下,当Update()
调用ComponentSystemGroup的方法时,它将在其成员系统的排序列表中的每个系统上调用Update()。如果任何成员系统本身就是系统组,则它们将递归更新自己的成员。生成的系统顺序遵循树的深度优先遍历。
您可以在JobComponentSystem中实现IJobChunk以逐块迭代数据。JobComponentSystem为包含您要系统处理的实体的每个块调用一次Execute()函数。然后,您可以逐个实体地处理每个块中的数据。
与IJobForEach相比,使用IJobChunk进行迭代需要更多的代码设置,但是也更明确,并且代表对数据的最直接访问,因为它实际上是存储的。
使用按块迭代的另一个好处是您可以检查每个块中是否存在可选组件(使用Archetype。)并相应地处理块中的所有实体。
实施IJobChunk作业涉及的步骤包括:
该ECS样本库包含了演示如何使用IJobChunk一个简单的例子HelloCube。
EntityQuery定义原型必须包含的一组组件类型,系统才能处理其关联的块和实体。原型也可以具有其他组件,但是它必须至少具有EntityQuery定义的组件。您也可以排除包含特定类型组件的原型。
对于简单查询,可以使用JobComponentSystem.GetEntityQuery()函数,传入组件类型:
public class RotationSpeedSystem : JobComponentSystem
{
private EntityQuery m_Group;
protected override void OnCreate()
{
m_Group = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly());
}
//…
}
对于更复杂的情况,可以使用EntityQueryDesc。EntityQueryDesc提供了一种灵活的查询机制来指定组件类型:
All
=此数组中的所有组件类型必须存在于原型中Any
=原型中必须至少存在此数组中的一种组件类型None
=原型中不能存在此数组中的任何组件类型例如,以下查询包括包含RotationQuaternion和RotationSpeed组件的原型,但不包括任何包含Frozen组件的原型:
protected override void OnCreate()
{
var query = new EntityQueryDesc
{
None = new ComponentType[]{ typeof(Frozen) },
All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly() }
}
};
m_Group = GetEntityQuery(query);
}
该查询使用ComponentType.ReadOnly
而不是更简单的typeof
表达式来表示系统未写入RotationSpeed。
您还可以通过传递EntityQueryDesc对象的数组而不是单个实例来组合多个查询。每个查询都使用逻辑或运算进行组合。以下示例选择包含RotationQuaternion组件或RotationSpeed组件(或两者)的原型:
protected override void OnCreate()
{
var query0 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationQuaternion)}
};
var query1 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationSpeed)}
};
m_Group = GetEntityQuery(new EntityQueryDesc[] {query0, query1});
}
IJobChunk Execute方法的签名为:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
该chunk
参数是内存块的句柄,该内存块包含在Job的此迭代中要处理的实体和组件。由于块只能包含一个原型,因此块中的所有实体都具有相同的组件集。
使用chunk
参数获取组件的NativeArray实例:
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
对齐这些数组,以使实体在所有数组中具有相同的索引。然后,您可以使用常规的for循环遍历组件数组。使用chunk.Count
得到存储在当前块的实体的数量:
for (var i = 0; i < chunk.Count; i++)
{
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
// Rotate something about its up vector at the speed given by RotationSpeed.
chunkRotations[i] = new RotationQuaternion
{
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
如果您Any
在EntityQueryDesc中使用过滤器,或者具有完全不包含在查询中的完全可选组件,则可以在使用之前使用该ArchetypeChunk.Has
函数测试当前块是否包含这些组件之一:
if (chunk.Has(OptionalCompType) )
{//...}
注意:如果使用并发实体命令缓冲区,则将chunkIndex参数作为jobIndex
参数传递给命令缓冲区函数。
public ComponentType.AccessMode AccessModeType
public int TypeIndex
public bool HasEntityReferences { get; }
public bool IgnoreDuplicateAdd { get; }
public bool IsBuffer { get; }
public bool IsChunkComponent { get; }
public bool IsSharedComponent { get; }
public bool IsSystemStateComponent { get; }
public bool IsSystemStateSharedComponent { get; }
public bool IsZeroSized { get; }
您可以使用ComponentSystem来处理数据。ComponentSystem方法在主线程上运行,因此无法利用多个CPU内核。在以下情况下使用ComponentSystems:
重要提示:进行结构更改会强制完成所有作业。此事件称为同步点,可能会导致性能下降,因为系统在等待同步点时无法利用所有可用的CPU内核。在ComponentSystem中,应该使用更新后命令缓冲区。同步点仍会发生,但是所有结构性更改都是成批发生的,因此影响较小。为了获得最大效率,请使用JobComponentSystem和实体命令缓冲区。当创建大量实体时,您还可以使用单独的世界创建实体,然后将这些实体转移到主游戏世界。
public class RotationSpeedSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach( (ref RotationSpeed rotationSpeed, ref RotationQuaternion rotation) =>
{
var deltaTime = Time.deltaTime;
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
});
}
Entities.ForEach( (Entity entity, ref RotationSpeed rotationSpeed, ref RotationQuaternion rotation) =>
{
var __deltaTime __= Time.deltaTime;
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * __deltaTime__));
if(math.abs(rotationSpeed.RadiansPerSecond) <= float.Epsilon) //Speed effectively zero
PostUpdateCommands.RemoveComponent(entity, typeof(RotationSpeed));
});
OnUpdate()函数完成后,系统将在更新后缓冲区中执行命令。
Entities.WithAll().ForEach( (Entity e) =>
{
// do stuff
});
Entities.WithAll().ForEach( (Entity e, ref Rotation r) =>
{
// do stuff
});
Entities.WithAll().WithAny().ForEach( (Entity e) =>
{
// do stuff
});
Entities.WithNone().ForEach( (Entity e) =>
{
// do stuff
});
Entities.WithNone().With(EntityQueryOptions.IncludeDisabled).ForEach( (Entity e) =>
{
// do stuff
});
您还可以在NativeArray中显式请求所有块,并使用Job等处理它们IJobParallelFor
。如果您需要以某种方式管理块,而这种方式不适用于简单地迭代EntityQuery中所有块的简化模型,则建议使用此方法。如:
public class RotationSpeedSystem : JobComponentSystem
{
[BurstCompile]
struct RotationSpeedJob : IJobParallelFor
{
[DeallocateOnJobCompletion] public NativeArray Chunks;
public ArchetypeChunkComponentType RotationType;
[ReadOnly] public ArchetypeChunkComponentType RotationSpeedType;
public float DeltaTime;
public void Execute(int chunkIndex)
{
var chunk = Chunks[chunkIndex];
var chunkRotation = chunk.GetNativeArray(RotationType);
var chunkSpeed = chunk.GetNativeArray(RotationSpeedType);
var __instanceCount __= chunk.Count;
for (int i = 0; i < instanceCount; i++)
{
var rotation = chunkRotation[i];
var speed = chunkSpeed[i];
rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * DeltaTime));
chunkRotation[i] = rotation;
}
}
}
EntityQuery m_group;
protected override void OnCreate()
{
var query = new EntityQueryDesc
{
All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly() }
};
m_group = GetEntityQuery(query);
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var rotationType = GetArchetypeChunkComponentType();
var rotationSpeedType = GetArchetypeChunkComponentType(true);
var chunks = m_group.CreateArchetypeChunkArray(Allocator.__TempJob__);
var rotationsSpeedJob = new RotationSpeedJob
{
Chunks = chunks,
RotationType = rotationType,
RotationSpeedType = rotationSpeedType,
DeltaTime = Time.deltaTime
};
return rotationsSpeedJob.Schedule(chunks.Length,32,inputDeps);
}
}
//以下代码段循环访问活动世界中的所有实体:
var entityManager = World.Active.EntityManager;
var allEntities = entityManager.GetAllEntities();
foreach (var entity in allEntities)
{
//...
}
allEntities.Dispose();
//尽管此代码段遍历了活跃世界中的所有块:
var entityManager = World.Active.EntityManager;
var allChunks = entityManager.GetAllChunks();
foreach (var chunk in allChunks)
{
//...
}
allChunks.Dispose();
读取或写入数据的第一步是找到该数据。ECS框架中的数据存储在组件中,这些组件根据它们所属实体的原型在内存中分组在一起。要定义仅包含给定算法或流程所需的特定数据的ECS数据视图,可以构造EntityQuery。
创建EntityQuery之后,您可以
EntityQuery m_Group = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly());
该查询使用ComponentType.ReadOnly
而不是更简单的typeof
表达式来表示系统未写入RotationSpeed。尽可能始终指定只读,因为对数据的读取访问限制较少,这可以帮助作业计划程序更有效地执行作业。
对于更复杂的查询,可以使用EntityQueryDesc来创建EntityQuery。EntityQueryDesc提供了一种灵活的查询机制,可以根据以下几组组件指定要选择的原型:
All
=此数组中的所有组件类型必须存在于原型中Any
=原型中必须至少存在此数组中的一种组件类型None
=原型中不能存在此数组中的任何组件类型例如,以下查询包括包含RotationQuaternion和RotationSpeed组件的原型,但不包括任何包含Frozen组件的原型:
var query = new EntityQueryDesc
{
None = new ComponentType[]{ typeof(Frozen) },
All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly() }
}
EntityQuery m_Group = GetEntityQuery(query);
创建EntityQueryDesc时,可以设置其Options
变量。这些选项允许进行专门的查询(通常不需要设置它们):
设置FilterWriteGroup选项时,视图中将仅包含具有明确包含在查询中的写入组中那些组件的实体。具有来自同一WriteGroup的任何其他组件的实体将被排除。
例如,假设C2和C3是基于C1的同一写入组中的组件,并且您使用需要C1和C3的FilterWriteGroup选项创建了查询:
public struct C1: IComponentData{}
[WriteGroup(C1)]
public struct C2: IComponentData{}
[WriteGroup(C1)]
public struct C3: IComponentData{}
// ... In a system:
var query = new EntityQueryDesc{
All = new ComponentType{typeof(C1), ComponentType.ReadOnly()},
Options = EntityQueryDescOptions.FilterWriteGroup
};
var m_group = GetEntityQuery(query);
您可以通过传递EntityQueryDesc对象的数组而不是单个实例来组合多个查询。每个查询都使用逻辑或运算进行组合。以下示例选择包含RotationQuaternion组件或RotationSpeed组件(或两者)的原型:
var query0 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationQuaternion)}
};
var query1 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationSpeed)}
};
EntityQuery m_Group = GetEntityQuery(new EntityQueryDesc[] {query0, query1});
参考:
https://blog.csdn.net/sigh667/article/details/73476938
https://docs.unity3d.com/Packages/[email protected]/manual/ecs_entities.html
https://docs.unity3d.com/Packages/[email protected]/api/Unity.Entities.ComponentType.html