赞
踩
在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 | 在给定环境中只有一个实例的组件 |
最常用的存储数据的数据类型。
非托管组件可以存储以下类型的字段:
要创建非托管组件,只需要继承 IComponentData 即可:
public struct ExampleUnmanagedComponent : IComponentData
{
public int Value;
}
将使用兼容类型的属性添加到结构中,以定义组件的数据。
如果不向组件添加任何属性,它将充当标记组件。(标记组件:标记组件是非托管组件,不存储任何数据,也不占用空间。)
与非托管组件不同,托管组件可以存储任何类型的属性。但是,它们的存储和访问会占用更多资源,并且具有以下限制:
如果托管组件中的属性使用托管类型(引用类型?),则可能需要手动添加clone、compare、serialize该属性的功能。
创建一个类,该类继承 IcomponentData :
public class ExampleManagedComponent : IComponentData
{
public int Value;
}
如果引用外部资源,最佳的做法是实现 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) };
}
}
与非托管组件不同,Unity 不会将托管组件直接存储在区块中,相反,Unity 将它们存储在一个大数组中。然后,块存储相关托管组件的数组索引。这意味着,当您访问实体的托管组件时,Unity 会处理额外的索引查找。这使得托管组件不如非托管组件优化。
托管组件的性能影响意味着应尽可能改用非托管组件
共享组件根据其共享组件的值将实体分组到块中,这有助于重复数据消除。
共享组件根据其共享值,将Entity分组到相同区块中存储,这有助于消除重复数据。
Unity将具有相同共享组件值的所有Entity存储到一起,这将会删除Entity之间的重复值。
可以创建托管或非托管的共享组件,他们有着同样的限制与优点。
对于每个World,Shared components将值存储在独立于ECS区块的数组中, 而该world中的原来的ECS区块,会存储句柄,用来查找对应在数组中的值。多个区块可以存储相同的共享组件句柄,也就是说可以使用相同共享组件的实体数量没有限制。
如果要更改Entity的共享组件的值,那么Unity就会将该Entity移动到使用新共享组件值的区块。这意味着修改实体的共享组件值,是一种结构性更改。
如果需要修改ECS比较共享组件实例的方式,可以为共享组件实现 IEquatable,如果执行此操作,ECS 将使用您的实现来检查共享组件的实例是否相等。如果共享组件是非托管的,则可以将 [BurstCompile] 属性添加到共享组件结构、Equals方法和GetHashCode方法中以提高性能
可以创建托管和非托管共享组件。
要创建非托管共享组件,请创建一个实现标记接口的结构。ISharedComponentData
下面的代码示例演示了一个非托管共享组件:
public struct ExampleUnmanagedSharedComponent : ISharedComponentData
{
public int Value;
}
若要创建托管共享组件,请创建一个实现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();
}
}
如果可能,请使用非托管共享组件,而不是托管共享组件。这是因为 Unity 将非托管共享组件存储在 Burst 编译代码可通过非托管共享组件 API(如 SetUnmanagedSharedComponentData)访问的位置。与托管组件相比,这提供了性能优势。
更新实体的共享组件值是一种结构更改,这意味着 Unity 将实体移动到另一个块。出于性能原因,请尽量避免频繁执行此操作。
块中的所有实体必须共享相同的共享组件值。这意味着如果您为大量实体提供唯一的共享组件值,它会将这些实体分割成许多几乎为空的块。
例如,如果一个原型有500个实体和一个共享组件,每个实体都有一个唯一的共享组件值,Unity将每个实体存储在一个单独的块中。这浪费了每个块中的大部分空间,也意味着要循环遍历原型的所有实体,Unity必须循环遍历所有500个块。这否定了ECS块布局的好处,并降低了性能。为了避免这个问题,尽量少使用唯一的共享组件值。如果500个示例实体只共享10个唯一的共享组件值,Unity可以将它们存储在10个块中。
要小心使用具有多个共享组件类型的原型。原型块中的所有实体必须具有相同的共享组件值组合,因此具有多个共享组件类型的原型容易受到碎片的影响。
清理组件与常规组件类似,但当您销毁包含一个实体的实体时,Unity 会删除所有非清理组件。该实体仍然存在,直到您从中删除所有清理组件。这对于标记销毁时需要清理的实体非常有用。
下面的代码示例说明了包含清理组件的实体的生命周期:
// 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
注意
清理组件是非托管组件,并且具有与非托管组件相同的所有限制。
:::
要创建清理组件,请创建一个继承自ICleanupComponentData的结构。
下面的代码示例显示了一个空的清理组件:
public struct ExampleCleanupComponent : ICleanupComponentData
{
}
::: info
注意
空的清理组件通常就足够了,但您可以添加属性来存储清理目标原型所需的信息。
:::
您可以使用清理组件来帮助您管理销毁时需要清理的实体。Unity 会阻止您销毁包含清理组件的实体。
当您尝试销毁附加了清理组件的实体时,Unity 会删除所有非清理组件。该实体仍然存在,直到您从中删除所有清理组件。
要对特定原型的实体执行清理,请执行以下操作:
清理共享组件是托管共享组件,具有清理组件的销毁语义。它们可用于标记需要相同信息进行清理的实体。
要创建清理共享组件,请创建继承自ICleanupSharedComponentData的结构。
下面的代码示例显示了一个空的系统清理组件:
public struct ExampleSharedCleanupComponent : ICleanupSharedComponentData
{
public int Value;
}
标记组件是非托管组件,不存储任何数据,也不占用空间。
从概念上讲,标签组件实现与游戏对象标签类似的用途,它们在查询中很有用,因为您可以按实体是否具有标签组件来筛选实体。例如,您可以将它们与清理组件和筛选器实体一起使用来执行清理。
若要创建标记组件,请创建没有任何属性的非托管组件。
以下代码示例演示了一个标记组件:
public struct ExampleTagComponent : IComponentData
{
}
动态缓冲区组件是充当可调整大小的数组的组件。
动态缓冲区组件是充当可调整大小的非托管结构数组的组件。您可以使用它来存储实体的数组数据,例如实体要在其中导航的航点位置。
除了数据之外,每个缓冲区还存储一个 Length,Capacity a 和一个内部指针:
动态缓冲区的初始容量由缓冲区存储的类型定义。默认情况下,容量默认为 128 字节以内的元素数。有关详细信息,请参阅 DefaultBufferCapacityNumerator。您可以使用 InternalBufferCapacity 属性指定自定义容量。有关如何创建动态缓冲区组件类型的信息,请参阅创建动态缓冲区组件类型。
最初,Unity 将动态缓冲区数据直接存储在组件所属实体的块中。如果动态缓冲区的长度大于容量,Unity 会将动态缓冲区数据复制到块外部的数组中。如果动态缓冲区的长度稍后缩小到小于容量,Unity 仍会将数据存储在块外部;如果 Unity 将动态缓冲区数据移出区块,则永远不会将数据移回区块中。
原始内部缓冲区容量是区块的一部分,Unity 仅在 Unity 解除分配区块本身时解除分配它。这意味着,如果动态缓冲区长度超过内部容量,并且 Unity 将数据复制到块外部,则块内会浪费空间。最佳做法是尽可能使用区块中的数据。为此,请确保大多数实体不超过缓冲区容量,但如果实体不使用容量,也不要将容量设置得太高。如果动态缓冲区的大小变化太大,最佳做法是将其数据存储在块外部。为此,请将InternalBufferCapacity0设置为0。
还有其他选项可用于存储数组数据:
结构更改可能会破坏或移动动态缓冲区引用的数组,这意味着动态缓冲区的任何句柄在结构更改后都将失效。您必须在发生任何结构更改后重新获取动态缓冲区。例如:
public void DynamicBufferExample(Entity e) { // Acquires a dynamic buffer of type MyElement. //获取MyElement类型的动态缓冲区。 DynamicBuffer<MyElement> myBuff = EntityManager.GetBuffer<MyElement>(e); // This structural change invalidates the previously acquired DynamicBuffer. //这个结构改变使先前获得的DynamicBuffer无效。 EntityManager.CreateEntity(); // A safety check will throw an exception on any read or write actions on the buffer. //安全检查将抛出一个异常,任何读取或写入操作的缓冲区。 var x = myBuff[0]; // Reacquires the dynamic buffer after the above structural changes. //在上述结构改变后重新获取动态缓冲区。 myBuff = EntityManager.GetBuffer<MyElement>(e); var y = myBuff[0]; }
要创建动态缓冲区组件,请创建一个继承自IBufferElementData的结构。此结构定义动态缓冲区类型的元素,还表示动态缓冲区组件本身。
若要指定缓冲区的初始容量,请使用该属性:InternalBufferCapacity
下面的代码示例演示了一个缓冲区组件:
[InternalBufferCapacity(16)]
public struct ExampleBufferComponent : IBufferElementData
{
public int Value;
}
与其他组件一样,您可以向实体添加动态缓冲区组件。但是,您可以使用 DynamicBuffer 表示动态缓冲区组件,并使用特定于动态缓冲区组件的 API(如 EntityManager.GetBuffer)与它们进行交互。例如:
public void GetDynamicBufferComponentExample(Entity e)
{
DynamicBuffer<ExampleBufferComponent> myDynamicBuffer = EntityManager.GetBuffer<ExampleBufferComponent>(e);
}
若要访问区块中的所有动态缓冲区,请使用 ArchetypeChunk.GetBufferAccessor 方法。这将采用 BufferTypeHandle 并返回 BufferAccessor。如果将BufferAccessor索引T,则返回T块的缓冲区类型:
以下代码示例演示如何访问块中某个类型的每个动态缓冲区。
[InternalBufferCapacity(16)] public struct ExampleBufferComponent : IBufferElementData { public int Value; } public partial class ExampleSystem : SystemBase { protected override void OnUpdate() { var query = new EntityQueryBuilder(Allocator.Temp) .WithAllRW<ExampleBufferComponent>() .Build(EntityManager); NativeArray<ArchetypeChunk> chunks = query.ToArchetypeChunkArray(Allocator.Temp); for (int i = 0; i < chunks.Length; i++) { UpdateChunk(chunks[i]); } chunks.Dispose(); } private void UpdateChunk(ArchetypeChunk chunk) { // Get a BufferTypeHandle representing dynamic buffer type ExampleBufferComponent from SystemBase. // 从SystemBase获取一个表示动态缓冲区类型的BufferTypeHandle。 BufferTypeHandle<ExampleBufferComponent> myElementHandle = GetBufferTypeHandle<ExampleBufferComponent>(); // Get a BufferAccessor from the chunk. // 从块中获取BufferAccessor。 BufferAccessor<ExampleBufferComponent> buffers = chunk.GetBufferAccessor(ref myElementHandle); // Iterate through all ExampleBufferComponent buffers of each entity in the chunk. // 遍历块中每个实体的所有ExampleBufferComponent缓冲区。 for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++) { DynamicBuffer<ExampleBufferComponent> buffer = buffers[i]; // Iterate through all elements of the buffer. // 遍历缓冲区的所有元素。 for (int j = 0; j < buffer.Length; j++) { // ... } } } }
如果 IJobEntity 的所有实体都需要相同的缓冲区,则可以在计划作业之前将该缓冲区作为主线程上的局部变量获取。
下面的代码示例演示如何对多个实体使用相同的动态缓冲区。它假定存在一个名为MyElement的动态缓冲区,并且存在另一个名为OtherComponent的组件。
::: info
请注意
如果使用ScheduleParallel,则不能并行地写入动态缓冲区。相反,您可以使用一个EntityCommandBuffer.Parallelwriter并行记录更改。但是,任何结构更改都会使缓冲区失效。
:::
如果jobs需要在其代码中查找一个或多个缓冲区,则作业需要使用BufferLookup查找表。您在系统中创建它们,然后将它们传递给需要它们的jobs。
在需要随机访问动态缓冲区的jobs中:
1. 添加ReadOnly BufferLookup成员变量。
2. 在IJobEntity。方法,按实体索引BufferLookup查找表。这提供了对附加到实体的动态缓冲区的访问。
public partial struct AccessDynamicBufferJob : IJobEntity
{
[ReadOnly] public BufferLookup<ExampleBufferComponent> BufferLookup;
public void Execute()
{
// ...
}
}
在创建 job实例的systems 中:
1. 添加一个BufferLookup成员变量。
2. 在OnCreate中,使用SystemState。GetBufferLookup来分配BufferLookup变量。
3. 在OnUpdate的开始,对BufferLookup变量调用Update。这将更新查找表。
4. 创建作业的实例时,将查找表传递给作业。
public partial struct AccessDynamicBufferFromJobSystem : ISystem { private BufferLookup<ExampleBufferComponent> _bufferLookup; public void OnCreate(ref SystemState state) { _bufferLookup = state.GetBufferLookup<ExampleBufferComponent>(true); } public void OnUpdate(ref SystemState state) { _bufferLookup.Update(ref state); var exampleBufferAccessJob = new AccessDynamicBufferJob { BufferLookup = _bufferLookup }; exampleBufferAccessJob.ScheduleParallel(); } }
EntityCommandBuffer (ECB)记录了为实体添加、移除或设置缓冲区组件的命令。有一些特定于缓冲区的动态api与常规组件api不同。
ECB只能记录未来发生的命令,因此它只能通过以下方式操纵动态缓冲组件:
下面的代码示例遍历了一些通用的动态缓冲区特定的EntityCommandBuffer api。它假设一个动态缓冲区称为MyElement存在。
private void Example(Entity e, Entity otherEntity) { EntityCommandBuffer ecb = new(Allocator.TempJob); // Record a command to remove the MyElement dynamic buffer from an entity. // 记录命令,从实体中移除MyElement动态缓冲区。 ecb.RemoveComponent<MyElement>(e); // Record a command to add a MyElement dynamic buffer to an existing entity. // This doesn't fail if the target entity already contains the buffer component. // The data of the returned DynamicBuffer is stored in the EntityCommandBuffer, // so changes to the returned buffer are also recorded changes. // 记录命令,将MyElement动态缓冲区添加到现有实体。 // 如果目标实体已经包含了buffer组件,这不会失败。 // 返回的DynamicBuffer的数据存储在Entit中y命令缓冲区, // 因此对返回缓冲区的更改也会记录更改。 DynamicBuffer<MyElement> myBuff = ecb.AddBuffer<MyElement>(e); // After playback, the entity will have a MyElement buffer with // Length 20 and these recorded values. // 播放后,实体将有一个MyElement buffer // 长度为20和这些记录值。 myBuff.Length = 20; myBuff[0] = new MyElement { Value = 5 }; myBuff[3] = new MyElement { Value = -9 }; // SetBuffer is like AddBuffer, but safety checks will throw an exception at playback if // the entity doesn't already have a MyElement buffer. // SetBuffer类似于AddBuffer,但是安全检查会在播放if时抛出异常 // 实体没有MyElement缓冲区。 DynamicBuffer<MyElement> otherBuf = ecb.SetBuffer<MyElement>(otherEntity); // Records a MyElement value to append to the buffer. Throws an exception at // playback if the entity doesn't already have a MyElement buffer. // ecb.AddComponent<MyElement>(otherEntity) is a safe way to ensure a buffer // exists before appending to it. // 记录MyElement值,并将其追加到缓冲区。 // 如果实体没有MyElement缓冲区,则播放。 // ecb.AddComponent<MyElement>(otherEntity)是确保缓冲区的安全方法 // 在追加之前存在。 ecb.AppendToBuffer(otherEntity, new MyElement { Value = 12 }); }
当您设置DynamicBuffer的长度、容量和内容时,ECS将这些更改记录到EntityCommandBuffer中。当您回放EntityCommandBuffer时,ECS会对动态缓冲区进行更改。
您可以重新解释DynamicBuffer以获得另一个DynamicBuffer< U>,其中T和U具有相同的大小。如果您希望将组件的动态缓冲区重新解释为组件所附加的实体的动态缓冲区,这将非常有用。这种重新解释对相同的内存进行了别名化,因此改变一个索引i处的值会改变另一个索引i处的值。
::: info
请注意
Reinterpret方法只强制原类型和新类型具有相同的大小。例如,您可以将int重新解释为float,因为这两种类型都是32位。决定重新解释是否符合你的目的是你的责任。
:::
下面的代码示例展示了如何解释动态缓冲区。它假设存在一个名为MyElement的动态缓冲区,并包含一个名为Value的int字段。
public class ExampleSystem : SystemBase { private void ReinterpretEntitysChunk(Entity e) { DynamicBuffer<MyElement> myBuff = EntityManager.GetBuffer<MyElement>(e); // Valid as long as each MyElement struct is four bytes. // 只要MyElement结构体是4字节就有效。 DynamicBuffer<int> intBuffer = myBuff.Reinterpret<int>(); intBuffer[2] = 6; // same effect as: myBuff[2] = new MyElement { Value = 6 }; // The MyElement value has the same four bytes as int value 6. // MyElement值与int值6有相同的四个字节 MyElement myElement = myBuff[2]; Debug.Log(myElement.Value); // 6 } }
::: info
请注意
重新解释的缓冲区共享原始缓冲区的安全句柄,因此受到所有相同的安全限制。
:::
块组件是一种按块而不是按实体存储值的组件。它们提供与共享组件相似的功能,但在一些基本方面有所不同。
块组件存储每个块而不是每个实体的值。它们的主要目的是作为一种优化,因为您可以在每个块级别上运行代码,以检查是否要为每个块中的所有实体处理某些行为。例如,块组件可以存储其中所有实体的边界。你可以检查边界是否在屏幕上,如果是,只处理那个块中的实体。
块组件提供与共享组件相似的功能,但在以下方面有所不同:
块组件的定义与非托管组件相同。这意味着您需要创建一个继承IComponentData的常规结构来创建一个块组件。块组件和非托管组件之间的区别在于如何将它们添加到实体中。
下面的代码示例显示了一个非托管组件:
public struct ExampleChunkComponent : IComponentData
{
public int Value;
}
若要将非托管组件用作块组件,请使用EntityManager.AddChunkComponentData(Entity)将其添加到实体中。
与其他组件类型相比,块组件使用一组不同的api来添加、删除、获取和设置它们。例如,要向实体添加块组件,可以使用EntityManager.AddChunkComponentData而不是常规的EntityManager.AddComponent.
下面的代码示例展示了如何添加、设置和获取块组件。它假设存在一个名为ExampleChunkComponent的块组件和一个名为ExampleComponent的非块组件:
private void ChunkComponentExample(Entity e) { // Adds ExampleChunkComp to the passed in entity's chunk. // 将ExampleChunkComp添加到传入实体的块中。 EntityManager.AddChunkComponentData<ExampleChunkComp>(e); // Finds all chunks with an ExampleComponent and an ExampleChunkComponent. // To distinguish chunk components from a regular IComponentData, You must // specify the chunk component with ComponentType.ChunkComponent. // 查找所有带有ExampleComponent和ExampleChunk组件的块。 // 你必须将块组件与常规IComponentData区分开来 // 使用ComponentType指定块组件。ChunkComponent。 EntityQuery query = GetEntityQuery(typeof(ExampleComponent), ComponentType.ChunkComponent<ExampleChunkComp>()); NativeArray<ArchetypeChunk> chunks = query.ToArchetypeChunkArray(Allocator.Temp); // Sets the ExampleChunkComp value of the first chunk. // 设置第一个chunk的ExamplechunkComp值。 EntityManager.SetChunkComponentData<ExampleChunkComp>(chunks[0], new ExampleChunkComp { Value = 6 }); // Gets the ExampleChunkComp value of the first chunk. //获取第一个块的ExamplechunkComp值。 ExampleChunkComp exampleChunkComp = EntityManager.GetChunkComponentData<ExampleChunkComp>(chunks[0]); Debug.Log(exampleChunkComp.Value) // 6 }
::: info
请注意
如果您只想从块组件中读取而不写入它,定义查询时使用 ComponentType.ChunkComponentReadOnly。
将查询中包含的组件标记为只读有助于避免不必要的jobs调度约束
:::
尽管块组件属于块本身,但在实体上添加或删除块组件会更改其原型并导致结构更改。
::: info
请注意
Unity将新创建的块组件值初始化为这些类型的默认值。
:::
你也可以通过任何块的实体来获取和设置块的组件:
private void ChunkComponentExample(Entity e)
{
// Sets the ExampleChunkComp value of the entity's chunk.
// 设置实体块的ExampleChunkComp值。
EntityManager.SetChunkComponentData<MyChunkComp>(e, new MyChunkComp { Value = 6 });
// Sets the ExampleChunkComp value of the entity's chunk.
// 设置实体块的ExampleChunkComp值。
MyChunkComp myChunkComp = EntityManager.GetChunkComponentData<MyChunkComp>(e);
Debug.Log(myChunkComp.Value) // 6
}
使用可启用组件在运行时禁用或启用实体上的各个组件。这在处理您期望频繁更改且不可预测的状态时非常有用,因为它们比添加或删除组件创建更少的结构更改。
你可以在IComponentData和IBufferElement Data组件上使用enableable组件,在运行时禁用或启用实体上的各个组件。要使组件可启用,可以从IEnableable Component继承它们。
可启用组件非常适合您希望经常更改且不可预测的状态,或者在逐帧基础上状态排列的数量很高的状态。添加和删除组件是管理低频状态变化的组件的较好方法,在这种情况下,您希望状态持续许多帧。
您还可以使用可启用组件而不是一组零大小的标记组件来表示实体状态。这减少了唯一实体原型的数量,并鼓励更好地使用块以减少内存消耗。
与添加和删除组件不同,可启用组件不会创建结构更改。在确定实体是否与实体查询匹配时,ECS将禁用的组件视为实体没有该组件。这意味着具有禁用组件的实体不匹配需要该组件的查询,而匹配排除该组件的查询(假设它满足所有其他查询条件)。
现有组件操作的语义不会改变。EntityManager认为具有禁用组件的实体仍然具有该组件。例如,如果T组件在实体E上被禁用,这些方法将执行以下操作:
方法 | 结果 |
---|---|
HasComponent(E) | Return true |
GetComponent(E) | Returns component T’s current value. |
SetComponent(E,value) | 更新组件T的值。 |
RemoveComponent(E) | 从E中移除组件T。 |
AddComponent(E) | 静静地什么也不做,因为组件已经存在。 |
你只能启用IComponentData和IBufferElementData组件。为此,实现IEnableableComponent接口。
当您使用可启用组件时,目标实体不会更改其原型,ECS不会移动任何数据,组件的现有值保持不变。这意味着您可以在工作线程上运行的作业上启用和禁用组件,而无需使用实体命令缓冲区或创建同步点。
然而,为了防止竞争条件,对可启用组件具有写访问权限的作业可能会导致主线程操作阻塞,直到作业完成,即使作业没有在任何实体上启用或禁用组件。
默认情况下,所有可启用的组件都在使用CreateEntity()创建的新实体上启用。从预制件实例化的实体继承预制件的启用或禁用状态。
要使用可启用的组件,你可以在EntityManager、ComponentLookup、EntityCommandBuffer和ArchetypeChunk上使用以下方法:
public partial struct EnableableComponentSystem : ISystem { public void OnUpdate(ref SystemState system) { Entity e = system.EntityManager.CreateEntity(typeof(Health)); ComponentLookup<Health> healthLookup = system.GetComponentLookup<Health>(); // true bool b = healthLookup.IsComponentEnabled(e); // disable the Health component of the entity // 禁用实体的运行状况组件 healthLookup.SetComponentEnabled(e, false); // though disabled, the component can still be read and modified // 虽然被禁用,组件仍然可以被读取和修改同一标准的 Health h = healthLookup[e]; } }
您可以使用ComponentLookup. setcomponentenabled (Entity,bool)来安全地启用或禁用工作线程中的实体,因为不需要进行结构更改。作业必须对组件t具有写访问权限。避免启用或禁用另一个线程可能在作业中处理的实体上的组件,因为这通常会导致竞争条件。
具有组件T disabled的实体匹配查询,就好像它根本没有组件T disabled一样。
例如,实体E有T1 (enabled)、T2 (disabled)和T3 (disabled)三个组件:
所有EntityQuery方法都会自动处理可启用的组件。例如,query.calculateentitycount()计算匹配查询的实体的数量,并考虑到它们的哪些组件是启用的和禁用的。有两个例外:
下面以查询已使用EntityManager禁用的组件为例。IsComponentEnabled:
public partial struct EnableableHealthSystem : ISystem { public void OnUpdate(ref SystemState system) { Entity e1 = system.EntityManager.CreateEntity(typeof(Health), typeof(Translation)); Entity e2 = system.EntityManager.CreateEntity(typeof(Health), typeof(Translation)); // true (components begin life enabled) // true(组件开始启用生命) bool b = system.EntityManager.IsComponentEnabled<Health>(e1); // disable the Health component on the first entity // 禁用第一个实体上的Health组件 system.EntityManager.SetComponentEnabled<Health>(e1, false); EntityQuery query = new EntityQueryBuilder(Allocator.Temp).WithAll<Health, Translation>().Build(ref system); // the returned array does not include the first entity // 返回的数组不包含第一个实体 var entities = query.ToEntityArray(Allocator.Temp); // the returned array does not include the Health of the first entity // 返回的数组不包括第一个实体的运行状况 var healths = query.ToComponentDataArray<Health>(Allocator.Temp); // the returned array does not include the Translation of the first entity // 返回的数组不包括第一个实体的翻译 var translations = query.ToComponentDataArray<Translation>(Allocator.Temp); // This query matches components whether they're enabled or disabled // 无论组件是启用还是禁用,这个查询都会匹配组件 var queryIgnoreEnableable = new EntityQueryBuilder(Allocator.Temp).WithAll<Health, Translation>().WithOptions(EntityQueryOptions.IgnoreComponentEnabledState).Build(ref system); // the returned array includes the Translations of both entities // 返回的数组包含两个实体的翻译 var translationsAll = queryIgnoreEnableable.ToComponentDataArray<Translation>(Allocator.Temp); } }
为了安全和确定地处理可启用组件,所有同步EntityQuery操作(除了那些忽略过滤的操作)都会自动等待对查询的可启用组件具有写访问权限的任何正在运行的作业完成。所有异步EntityQuery操作(以Async结尾的操作)也会自动在这些正在运行的作业上插入一个输入依赖项。
异步EntityQuery收集和分散操作,如EntityQuery。ToEntityArrayAsync()调度一个作业来执行请求的操作。这些方法必须返回一个Nativelist而不是NativeArray,因为查询匹配的实体数量直到作业运行时才知道,但是容器必须立即返回给调用者。
该列表的初始容量根据可以匹配查询的实体的最大数量保守地调整大小,尽管其最终长度可能更低。在异步收集或分散作业完成之前,对列表(包括其当前长度、容量或基指针)的任何读写都会导致JobsDebugger安全错误。但是,您可以安全地将该列表传递给相关的后续作业。
单例组件是指在给定世界中只有一个实例的组件。例如,如果一个世界中只有一个实体具有类型为T的组件,则T是单例组件。
如果一个单例组件被添加到另一个实体中,那么它就不再是一个单例组件。此外,单例组件可以存在于另一个世界,而不会影响其单例状态。
Entities包包含了几个api,你可以使用它们来处理单例组件:
Namespace | Method |
---|---|
EntityManager | CreateSingleton |
EntityQuery | GetSingletonEntity |
GetSingleton | |
GetSingletonRW | |
TryGetSingleton | |
HasSingleton | |
TryGetSingletonBuffer | |
TryGetSingletonEntity | |
GetSingletonBuffer | |
SetSingleton | |
SystemAPI | GetSingletonEntity |
GetSingleton | |
GetSingletonRW | |
TryGetSingleton | |
HasSingleton | |
TryGetSingletonBuffer | |
TryGetSingletonEntity | |
GetSingletonBuffer | |
SetSingleton |
在知道组件只有一个实例的情况下,使用单例组件api是很有用的。例如,如果你有一个单人应用程序,并且只需要一个PlayerController组件的实例,你可以使用单例api来简化你的代码。此外,在基于服务器的体系结构中,客户端实现通常只跟踪其实例的时间戳,因此单例api很方便,并且简化了大量手工编写的代码。
单例组件在系统代码的依赖完成中有特殊情况的行为。通过正常的组件访问,诸如EntityManager之类的api。GetComponentData或SystemAPI。GetComponent确保任何可能在工作线程上写入相同组件数据的运行作业在返回所请求的数据之前都已完成。
但是,单例API调用不能确保首先完成正在运行的作业。Jobs Debugger会记录无效访问的错误,您要么需要用EntityManager手动完成依赖项。CompleteDependency beforeero或EntityManager。completedependencybeforeerw,或者需要重构数据依赖关系。
如果您使用GetSingletonRW来获得对组件的读/写访问权限,您也应该小心。因为返回了对组件数据的引用,所以可以在作业读取或写入数据的同时修改数据。GetSingletonRW的最佳实践是:
要向实体添加组件,请使用实体所在世界的EntityManager。您可以向单个实体添加组件,也可以同时向多个实体添加组件。
将组件添加到实体是一种结构变化,这意味着实体移动到不同的块中。这意味着您不能直接从jobs向实体添加组件。相反,您必须使用EntityCommandBuffer来记录以后添加组件的意图。
下面的代码示例创建一个新实体,然后从主线程向该实体添加一个组件。
public partial struct AddComponentToSingleEntitySystemExample : ISystem
{
public void OnCreate(ref SystemState state)
{
var entity = state.EntityManager.CreateEntity();
state.EntityManager.AddComponent<Rotation>(entity);
}
}
下面的代码示例获取每个带有附加ComponentA组件的实体,并从主线程向它们添加一个ComponentB组件。
struct ComponentA : IComponentData {}
struct ComponentB : IComponentData {}
public partial struct AddComponentToMultipleEntitiesSystemExample : ISystem
{
public void OnCreate(ref SystemState state)
{
var query = state.GetEntityQuery(typeof(ComponentA));
state.EntityManager.AddComponent<ComponentB>(query);
}
}
要从实体中删除组件,请使用实体所在世界的EntityManager。
::: warning
重要的
从实体中移除组件是一种结构变化,这意味着实体移动到不同的原型块中。
:::
你可以直接从主线程中删除实体中的组件。下面的代码示例获取每个带有附加Rotationcomponent的实体,然后删除旋转组件。
public partial struct RemoveComponentSystemExample : ISystem
{
public void OnCreate(ref SystemState state)
{
var query = state.GetEntityQuery(typeof(Rotation));
state.EntityManager.RemoveComponent<Rotation>(query);
}
}
因为从实体中删除组件是一种结构变化,所以不能直接在工作中执行。相反,您必须使用EntityCommandBuffer来记录您以后要删除组件的意图。
将组件添加到实体后,系统就可以访问、读取和写入组件值。根据您的用例,您可以使用几种方法来实现这一点。
有时,您可能希望一次读取或写入一个实体的单个组件。要做到这一点,在主线程上,你可以让EntityManager读取或写入单个实体的组件值。EntityManager保留了一个查找表来快速查找每个实体的块和块中的索引。
对于大多数工作,您将需要读取或写入一个或一组块中所有实体的组件:
为了延迟组件值的更改,请使用EntityCommandBuffer,它记录了您写入(但不读取)组件值的意图。这些更改仅在稍后在主线程上播放EntityCommandBuffer时发生。
Collections包提供原生容器类型,如Nativelist和NativeHashMap,以及不安全容器,如UnsafeList。您可以在组件中使用这些容器类型。
原生容器和不安全容器都是值类型,而不是引用类型。不安全容器和原生容器之间的主要区别是:
原生容器比不安全容器更安全,并且始终满足期望。
Functionality | Native containers | Unsafe containers |
---|---|---|
Compatible with Jobs Debugger(与Jobs兼容?) | yes | no |
Can be used in job worker threads(可以在Jobs线程使用吗?) | yes | yes |
Can be used on main thread(可以在主线程使用吗?) | yes | yes |
Usable with ComponentLookup on main thread(可与主线程上的 ComponentLookup 一起使用?) | yes | yes |
Usable with ComponentLookup in job worker threads(可与jobs工作线程中的 ComponentLookup 一起使用?) | no | yes |
Can contain other NativeContainers(可以包含其他 NativeContainer?) | no | yes(这在技术上是支持的,但会影响性能。) |
Can contain other UnsafeContainers(可以包含其他 UnsafeContainers?) | yes | yes |
这些限制不适用于在主线程之外使用本机容器(尽管在jobs结构中不可能这样做)。组件。例如,原生容器可以嵌套其他原生容器
方面是一个类似于对象的包装器,您可以使用它将实体组件的子集组合成单个c#结构体。方面对于组织组件代码和简化系统中的查询非常有用。Unity为相关组件组提供了预定义的方面,或者您可以使用IAspect接口定义自己的方面。
方面可以包括以下项目:
要创建方面,请使用IAspect接口。你必须将aspect声明为只读部分结构体,并且该结构体必须实现IAspect接口:
using Unity.Entities;
readonly partial struct MyAspect : IAspect
{
// Your Aspect code
}
您可以使用RefRW或RefRO将组件声明为方面的一部分。要声明一个缓冲区,请使用DynamicBuffer。
有关可用字段的更多信息,请参阅IAspect文档。在方面内声明的字段定义了必须查询哪些数据才能使方面实例在特定实体上有效。要使一个字段可选,使用[optional]属性。
要将DynamicBuffer和嵌套方面声明为只读,请使用[ReadOnly]属性。
使用refo和refw字段为方面中的组件提供只读或读写访问。当您想在代码中引用一个方面时,使用in来覆盖所有引用,使其变为只读,或者使用ref来尊重方面中声明的只读或读写访问。如果使用in来引用对组件具有读写访问权限的方面,则可能会在写尝试时抛出异常。
要在系统中创建方面实例,请调用SystemAPI.GetAspect:
// Throws if the entity is missing any of
// the required components of MyAspect.
// 如果实体缺少MyAspect所需的任何组件,则抛出。
MyAspect asp = SystemAPI.GetAspect<MyAspect>(myEntity);
要在系统外部创建方面实例,请使用EntityManager.GetAspect。
如果你想迭代一个方面,你可以使用SystemAPI.Query:
public partial struct MySystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach (var cannonball in SystemAPI.Query<CannonBallAspect>())
{
// use cannonball aspect here
}
}
}
在这个例子中,CannonBallAspect在一个坦克主题的游戏中设置了炮弹组件的变换、位置和速度。
struct CannonBall : IComponentData { public float3 Speed; } // Aspects must be declared as a readonly partial struct // aspect必须声明为只读的局部结构体 readonly partial struct CannonBallAspect : IAspect { // An Entity field in an Aspect gives access to the Entity itself. // This is required for registering commands in an EntityCommandBuffer for example. // 方面中的实体字段提供对实体本身的访问。 // 这是在EntityCommand Buffer中注册命令所必需的。 public readonly Entity Self; // Aspects can contain other aspects. //方面可以包含其他方面。 // A RefRW field provides read write access to a component. If the aspect is taken as an "in" // parameter, the field behaves as if it was a RefRO and throws exceptions on write attempts. // refw字段提供对组件的读写访问。如果将方面作为“输入” // 参数,该字段的行为就好像它是一个refo,并在写尝试时抛出异常。 readonly RefRW<LocalTransform> Transform; readonly RefRW<CannonBall> CannonBall; // Properties like this aren't mandatory. The Transform field can be public instead. // But they improve readability by avoiding chains of "aspect.aspect.aspect.component.value.value". // 这样的属性不是强制性的。Transform字段可以是公共的。 // 但是它们避免了“aspect.aspect.aspect.component.value.value”链,从而提高了可读性 public float3 Position { get => Transform.ValueRO.Position; set => Transform.ValueRW.Position = value; } public float3 Speed { get => CannonBall.ValueRO.Speed; set => CannonBall.ValueRW.Speed = value; } }
要在其他代码中使用这个方面,你可以像请求一个组件一样请求CannonBallAspect:
using Unity.Entities;
using Unity.Burst;
// It's best practice to Burst-compile your code
// 最好的做法是快速编译代码
[BurstCompile]
partial struct CannonBallJob : IJobEntity
{
void Execute(ref CannonBallAspect cannonBall)
{
// Your game logic
}
}
源生成器通过分析现有代码在编译过程中生成代码。实体包生成方法和类型,允许你使用你的方面与Unity API的其他部分。有关Unity中源生成器的更多信息,请参阅Roslyn分析器和源生成器的用户手册文档。
方面源生成器在方面中生成其他方法struct使其可用于其他api,如IJobEntity和SystemAPI.Query < MyAspect >()。
将这个方面的组件需求添加到原型列表中。如果一个组件已经存在于列表中,它就不会添加一个重复的组件。但是,如果这方面需要,它会用读写覆盖只读需求。
public void AddComponentRequirementsTo(
ref UnsafeList<ComponentType> all,
ref UnsafeList<ComponentType> any,
ref UnsafeList<ComponentType> none,
bool isReadOnly)
为特定实体的组件数据创建方面结构的实例。
public AspectT CreateAspect(
Entity entity,
ref SystemState systemState,
bool isReadOnly)
创建一个IEnumerable,您可以使用它来遍历查询实体方面。
public static Enumerator Query(EntityQuery query, TypeHandle typeHandle)
完成此方面具有读访问权所需的依赖链。这完成了组件、缓冲区等的所有写依赖关系,以允许读取。
public static void CompleteDependencyBeforeRO(ref SystemState systemState)
完成该组件具有读/写访问所需的依赖链。这就完成了组件、缓冲区等的所有写依赖关系,以允许读取,并且它完成了所有读依赖关系,以便您可以对其进行写入。
public static void CompleteDependencyBeforeRW(ref SystemState state)
方面源生成器声明嵌套在所有实现IAspect的方面结构中的新类型。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。