当前位置:   article > 正文

【DOTS】Unity DOTS Entity常用组件类型说明_icleanupcomponentdata

icleanupcomponentdata

总览

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;
}
  • 1
  • 2
  • 3
  • 4

将使用兼容类型的属性添加到结构中,以定义组件的数据。

如果不向组件添加任何属性,它将充当标记组件。(标记组件:标记组件是非托管组件,不存储任何数据,也不占用空间。)

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;
}
  • 1
  • 2
  • 3
  • 4

2.3 Manage the lifecycle of external resources 管理外部资源的生命周期

如果引用外部资源,最佳的做法是实现 ICloneableIDisposable

例如,对于存储对 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) };
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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;
}
  • 1
  • 2
  • 3
  • 4

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

::: info
注意
清理组件是非托管组件,并且具有与非托管组件相同的所有限制。
:::

4.2 创建清理组件

要创建清理组件,请创建一个继承自ICleanupComponentData的结构。

下面的代码示例显示了一个空的清理组件:

public struct ExampleCleanupComponent : ICleanupComponentData
{

}
  • 1
  • 2
  • 3
  • 4

::: info
注意
空的清理组件通常就足够了,但您可以添加属性来存储清理目标原型所需的信息。
:::

4.2.1 执行清理

您可以使用清理组件来帮助您管理销毁时需要清理的实体。Unity 会阻止您销毁包含清理组件的实体。

当您尝试销毁附加了清理组件的实体时,Unity 会删除所有非清理组件。该实体仍然存在,直到您从中删除所有清理组件。

要对特定原型的实体执行清理,请执行以下操作:

  1. 创建一个新的标记组件,并将该标记组件添加到原型中。
  2. 创建一个新的清理组件,其中包含清理特定实体原型所需的信息。
  3. 创建一个系统,该系统:
    1. 获取新创建的目标原型的实体。这些实体包含标记组件,但不包含清理组件
    2. 将清理组件添加到这些实体。
  4. 创建一个系统,该系统:
    1. 获取已临时销毁并需要清理的实体。这些实体包含清理组件,但不包含标记组件。
    2. 对实体执行适当的清理工作。
    3. 从实体中移除清理组件。

4.3 清理共享组件

清理共享组件是托管共享组件,具有清理组件的销毁语义。它们可用于标记需要相同信息进行清理的实体。

4.4 创建清理共享组件

要创建清理共享组件,请创建继承自ICleanupSharedComponentData的结构。

下面的代码示例显示了一个空的系统清理组件:

public struct ExampleSharedCleanupComponent : ICleanupSharedComponentData
{
    public int Value;
}
  • 1
  • 2
  • 3
  • 4

5. Tag components (标记组件)

标记组件是非托管组件,不存储任何数据,也不占用空间。

从概念上讲,标签组件实现与游戏对象标签类似的用途,它们在查询中很有用,因为您可以按实体是否具有标签组件来筛选实体。例如,您可以将它们与清理组件和筛选器实体一起使用来执行清理。

5.1 创建标记组件

若要创建标记组件,请创建没有任何属性的非托管组件。

以下代码示例演示了一个标记组件:

public struct ExampleTagComponent : IComponentData
{

}
  • 1
  • 2
  • 3
  • 4

6. Dynamic Buffer components(动态缓冲区组件)

动态缓冲区组件是充当可调整大小的数组的组件。

6.1 引入动态缓冲区组件

动态缓冲区组件是充当可调整大小的非托管结构数组的组件。您可以使用它来存储实体的数组数据,例如实体要在其中导航的航点位置。

除了数据之外,每个缓冲区还存储一个 Length,Capacity a 和一个内部指针:

  • Length是缓冲区中的元素数。它从以下位置开始,并在将值追加到缓冲区时递增。
  • Capacity是缓冲区中的存储量。它开始时与内部缓冲区容量相匹配。Capacity设置可调整缓冲区的大小。
  • null指针指示动态缓冲区数据的位置。最初,它表示数据与实体一起位于块中,如果 Unity 将数据移动到块之外,则指针设置为指向新数组。有关 Unity 如何存储动态缓冲区组件的详细信息,请参阅容量。

6.2 能力

动态缓冲区的初始容量由缓冲区存储的类型定义。默认情况下,容量默认为 128 字节以内的元素数。有关详细信息,请参阅 DefaultBufferCapacityNumerator。您可以使用 InternalBufferCapacity 属性指定自定义容量。有关如何创建动态缓冲区组件类型的信息,请参阅创建动态缓冲区组件类型。

最初,Unity 将动态缓冲区数据直接存储在组件所属实体的块中。如果动态缓冲区的长度大于容量,Unity 会将动态缓冲区数据复制到块外部的数组中。如果动态缓冲区的长度稍后缩小到小于容量,Unity 仍会将数据存储在块外部;如果 Unity 将动态缓冲区数据移出区块,则永远不会将数据移回区块中。

原始内部缓冲区容量是区块的一部分,Unity 仅在 Unity 解除分配区块本身时解除分配它。这意味着,如果动态缓冲区长度超过内部容量,并且 Unity 将数据复制到块外部,则块内会浪费空间。最佳做法是尽可能使用区块中的数据。为此,请确保大多数实体不超过缓冲区容量,但如果实体不使用容量,也不要将容量设置得太高。如果动态缓冲区的大小变化太大,最佳做法是将其数据存储在块外部。为此,请将InternalBufferCapacity0设置为0。
还有其他选项可用于存储数组数据:

  • Blob 资产:存储紧密打包的只读结构化数据(包括数组),多个实体可以共享单个 Blob 资产。因为它们是只读的,所以您可以同时从多个线程访问它们。
  • 托管组件:存储本机或托管对象的数组。但是,与动态缓冲区组件数据相比,访问托管组件数据的限制性更强,性能更低。此外,您还需要手动克隆和释放数组数据。
  • 共享组件:与托管组件类似,它们存储本机或托管对象的数组,您的实体可以将索引存储到这些较大的数组中。它们具有与托管组件相同的限制和性能注意事项。

6.3 结构变化

结构更改可能会破坏或移动动态缓冲区引用的数组,这意味着动态缓冲区的任何句柄在结构更改后都将失效。您必须在发生任何结构更改后重新获取动态缓冲区。例如:

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];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

6.4 创建动态缓冲区组件

要创建动态缓冲区组件,请创建一个继承自IBufferElementData的结构。此结构定义动态缓冲区类型的元素,还表示动态缓冲区组件本身。
若要指定缓冲区的初始容量,请使用该属性:InternalBufferCapacity
下面的代码示例演示了一个缓冲区组件:

[InternalBufferCapacity(16)]
public struct ExampleBufferComponent : IBufferElementData
{
    public int Value;
}
  • 1
  • 2
  • 3
  • 4
  • 5

与其他组件一样,您可以向实体添加动态缓冲区组件。但是,您可以使用 DynamicBuffer 表示动态缓冲区组件,并使用特定于动态缓冲区组件的 API(如 EntityManager.GetBuffer)与它们进行交互。例如:

public void GetDynamicBufferComponentExample(Entity e)
{
	    DynamicBuffer<ExampleBufferComponent> myDynamicBuffer = EntityManager.GetBuffer<ExampleBufferComponent>(e);
} 
  • 1
  • 2
  • 3
  • 4

6.5 访问区块中的动态缓冲区

若要访问区块中的所有动态缓冲区,请使用 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++)
            {
                // ...
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

6.6 为多个实体重用动态缓冲区

如果 IJobEntity 的所有实体都需要相同的缓冲区,则可以在计划作业之前将该缓冲区作为主线程上的局部变量获取。

下面的代码示例演示如何对多个实体使用相同的动态缓冲区。它假定存在一个名为MyElement的动态缓冲区,并且存在另一个名为OtherComponent的组件。

::: info
请注意
如果使用ScheduleParallel,则不能并行地写入动态缓冲区。相反,您可以使用一个EntityCommandBuffer.Parallelwriter并行记录更改。但是,任何结构更改都会使缓冲区失效。
:::

6.7 从jobs访问动态缓冲区

如果jobs需要在其代码中查找一个或多个缓冲区,则作业需要使用BufferLookup查找表。您在系统中创建它们,然后将它们传递给需要它们的jobs。

6.7.1 修改jobs

在需要随机访问动态缓冲区的jobs中:

1. 添加ReadOnly BufferLookup成员变量。
2. 在IJobEntity。方法,按实体索引BufferLookup查找表。这提供了对附加到实体的动态缓冲区的访问。
  • 1
  • 2
public partial struct AccessDynamicBufferJob : IJobEntity
{
    [ReadOnly] public BufferLookup<ExampleBufferComponent> BufferLookup;
    public void Execute()
    {
        // ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6.7.2 修改systems

在创建 job实例的systems 中:

1. 添加一个BufferLookup成员变量。
2. 在OnCreate中,使用SystemState。GetBufferLookup来分配BufferLookup变量。
3. 在OnUpdate的开始,对BufferLookup变量调用Update。这将更新查找表。
4. 创建作业的实例时,将查找表传递给作业。
  • 1
  • 2
  • 3
  • 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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

6.8 使用实体命令缓冲区修改动态缓冲区

EntityCommandBuffer (ECB)记录了为实体添加、移除或设置缓冲区组件的命令。有一些特定于缓冲区的动态api与常规组件api不同。

ECB只能记录未来发生的命令,因此它只能通过以下方式操纵动态缓冲组件:

  • SetBuffer:返回一个DynamicBuffer,记录线程可以用它填充数据。在回放时,这些缓冲区内容覆盖任何现有的缓冲区内容。如果目标实体已经包含缓冲区组件,SetBuffer不会失败。如果不止一个线程在同一个实体上记录了SetBuffer命令,那么在回放之后,只有最后一个命令根据sortKey顺序添加的内容是可见的。SetBuffer具有与AddBuffer相同的功能,除了AddBuffer首先将缓冲区添加到组件中,如果它不存在的话。
  • AppendToBuffer:向实体上的现有缓冲区组件添加一个缓冲区元素,并保留任何现有的缓冲区内容。多个线程可以安全地附加到同一实体上的同一缓冲区组件,记录命令的sortKey决定结果元素的顺序。如果目标实体不包含类型为T的缓冲区组件,则AppendToBuffer将在回放时失败。因此,最佳实践是在每个AppendToBuffer命令之前使用AddComponent,以确保目标缓冲区组件存在。
  • 如果T是一个IBufferElementData,你可以安全地使用AddComponent和RemoveComponent方法来添加一个空缓冲区或删除一个现有的缓冲区。这些方法可以安全地从多个线程中使用,并且添加现有组件或删除不存在的组件不会导致错误。

下面的代码示例遍历了一些通用的动态缓冲区特定的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 });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

当您设置DynamicBuffer的长度、容量和内容时,ECS将这些更改记录到EntityCommandBuffer中。当您回放EntityCommandBuffer时,ECS会对动态缓冲区进行更改。

6.9 重新解释动态缓冲区

您可以重新解释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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

::: info
请注意
重新解释的缓冲区共享原始缓冲区的安全句柄,因此受到所有相同的安全限制。
:::

7. Chunk components(块组件)

块组件是一种按块而不是按实体存储值的组件。它们提供与共享组件相似的功能,但在一些基本方面有所不同。

7.1 引入块组件

块组件存储每个块而不是每个实体的值。它们的主要目的是作为一种优化,因为您可以在每个块级别上运行代码,以检查是否要为每个块中的所有实体处理某些行为。例如,块组件可以存储其中所有实体的边界。你可以检查边界是否在屏幕上,如果是,只处理那个块中的实体。
块组件提供与共享组件相似的功能,但在以下方面有所不同:

  • 数据块组件值在概念上属于数据块本身,而不属于数据块的单个实体。
    设置一个块组件的值并不是一个结构性的改变。
  • 与共享组件不同,Unity不会删除唯一的块组件值:具有相同块组件值的块存储自己的单独副本。
  • 块组件总是非托管的:您不能创建托管块组件。
  • 当实体的原型改变或实体的共享组件值改变时,Unity将实体移动到一个新的块,但这些移动不会修改源或目标块的块组件值。

7.2 创建一个块组件

块组件的定义与非托管组件相同。这意味着您需要创建一个继承IComponentData的常规结构来创建一个块组件。块组件和非托管组件之间的区别在于如何将它们添加到实体中。
下面的代码示例显示了一个非托管组件:

public struct ExampleChunkComponent : IComponentData
{
    public int Value;
}
  • 1
  • 2
  • 3
  • 4

若要将非托管组件用作块组件,请使用EntityManager.AddChunkComponentData(Entity)将其添加到实体中。

7.3 使用块组件

与其他组件类型相比,块组件使用一组不同的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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

::: 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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

8. Enableable components(可在运行时Entiy启用或者禁用的组件)

使用可启用组件在运行时禁用或启用实体上的各个组件。这在处理您期望频繁更改且不可预测的状态时非常有用,因为它们比添加或删除组件创建更少的结构更改。

8.1 Enableable components overview 可启用组件概述

你可以在IComponentData和IBufferElement Data组件上使用enableable组件,在运行时禁用或启用实体上的各个组件。要使组件可启用,可以从IEnableable Component继承它们。
可启用组件非常适合您希望经常更改且不可预测的状态,或者在逐帧基础上状态排列的数量很高的状态。添加和删除组件是管理低频状态变化的组件的较好方法,在这种情况下,您希望状态持续许多帧。
您还可以使用可启用组件而不是一组零大小的标记组件来表示实体状态。这减少了唯一实体原型的数量,并鼓励更好地使用块以减少内存消耗。

8.2 Structural changes 结构变化

与添加和删除组件不同,可启用组件不会创建结构更改。在确定实体是否与实体查询匹配时,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)静静地什么也不做,因为组件已经存在。

8.3 Use enableable components 使用可启用的组件

你只能启用IComponentData和IBufferElementData组件。为此,实现IEnableableComponent接口。

当您使用可启用组件时,目标实体不会更改其原型,ECS不会移动任何数据,组件的现有值保持不变。这意味着您可以在工作线程上运行的作业上启用和禁用组件,而无需使用实体命令缓冲区或创建同步点。

然而,为了防止竞争条件,对可启用组件具有写访问权限的作业可能会导致主线程操作阻塞,直到作业完成,即使作业没有在任何实体上启用或禁用组件。

默认情况下,所有可启用的组件都在使用CreateEntity()创建的新实体上启用。从预制件实例化的实体继承预制件的启用或禁用状态。

8.4 Enableable component methods 启用组件方法

要使用可启用的组件,你可以在EntityManager、ComponentLookup、EntityCommandBuffer和ArchetypeChunk上使用以下方法:

  • IsComponentEnabled(实体e):返回true如果实体e有组件T,并且它是启用的。如果实体e有组件T,返回false,但它被禁用。判断实体e是否没有组件T,或者T是否没有实现IEnableableComponent。
  • SetComponentEnabled(实体e, bool enable):如果实体e有组件,则根据enable的值启用或禁用它。判断实体e是否没有组件T,或者T是否没有实现IEnableableComponent
    例如:
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];

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

您可以使用ComponentLookup. setcomponentenabled (Entity,bool)来安全地启用或禁用工作线程中的实体,因为不需要进行结构更改。作业必须对组件t具有写访问权限。避免启用或禁用另一个线程可能在作业中处理的实体上的组件,因为这通常会导致竞争条件。

8.5 Querying enableable components 查询可启用的组件

具有组件T disabled的实体匹配查询,就好像它根本没有组件T disabled一样。
例如,实体E有T1 (enabled)、T2 (disabled)和T3 (disabled)三个组件:

  • 它不匹配同时需要T1和T2的查询
  • 它匹配一个需要T1而不需要T2的查询
  • 它不会匹配将T2和T3作为可选组件的查询,因为它至少没有启用其中一个组件。

所有EntityQuery方法都会自动处理可启用的组件。例如,query.calculateentitycount()计算匹配查询的实体的数量,并考虑到它们的哪些组件是启用的和禁用的。有两个例外:

  • 以IgnoreFilter结尾的方法名将所有组件视为已启用。这些方法不需要同步点,因为只有结构更改才会影响它们的结果。它们往往比考虑过滤的变体更有效。
  • 使用EntityQueryOptions创建的查询。IgnoreComponentEnabledState,忽略匹配中所有实体的当前启用/禁用状态原型,确定它们是否与查询匹配。

下面以查询已使用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);

    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

8.6 Asynchronous operations 异步操作

为了安全和确定地处理可启用组件,所有同步EntityQuery操作(除了那些忽略过滤的操作)都会自动等待对查询的可启用组件具有写访问权限的任何正在运行的作业完成。所有异步EntityQuery操作(以Async结尾的操作)也会自动在这些正在运行的作业上插入一个输入依赖项。
异步EntityQuery收集和分散操作,如EntityQuery。ToEntityArrayAsync()调度一个作业来执行请求的操作。这些方法必须返回一个Nativelist而不是NativeArray,因为查询匹配的实体数量直到作业运行时才知道,但是容器必须立即返回给调用者。
该列表的初始容量根据可以匹配查询的实体的最大数量保守地调整大小,尽管其最终长度可能更低。在异步收集或分散作业完成之前,对列表(包括其当前长度、容量或基指针)的任何读写都会导致JobsDebugger安全错误。但是,您可以安全地将该列表传递给相关的后续作业。

9. Singleton components 单例组件

单例组件是指在给定世界中只有一个实例的组件。例如,如果一个世界中只有一个实体具有类型为T的组件,则T是单例组件。
如果一个单例组件被添加到另一个实体中,那么它就不再是一个单例组件。此外,单例组件可以存在于另一个世界,而不会影响其单例状态。

9.1 Singleton component APIs 单例组件api

Entities包包含了几个api,你可以使用它们来处理单例组件:

NamespaceMethod
EntityManagerCreateSingleton
EntityQueryGetSingletonEntity
GetSingleton
GetSingletonRW
TryGetSingleton
HasSingleton
TryGetSingletonBuffer
TryGetSingletonEntity
GetSingletonBuffer
SetSingleton
SystemAPIGetSingletonEntity
GetSingleton
GetSingletonRW
TryGetSingleton
HasSingleton
TryGetSingletonBuffer
TryGetSingletonEntity
GetSingletonBuffer
SetSingleton

在知道组件只有一个实例的情况下,使用单例组件api是很有用的。例如,如果你有一个单人应用程序,并且只需要一个PlayerController组件的实例,你可以使用单例api来简化你的代码。此外,在基于服务器的体系结构中,客户端实现通常只跟踪其实例的时间戳,因此单例api很方便,并且简化了大量手工编写的代码。

9.2 Dependency completion 依赖完成

单例组件在系统代码的依赖完成中有特殊情况的行为。通过正常的组件访问,诸如EntityManager之类的api。GetComponentData或SystemAPI。GetComponent确保任何可能在工作线程上写入相同组件数据的运行作业在返回所请求的数据之前都已完成。

但是,单例API调用不能确保首先完成正在运行的作业。Jobs Debugger会记录无效访问的错误,您要么需要用EntityManager手动完成依赖项。CompleteDependency beforeero或EntityManager。completedependencybeforeerw,或者需要重构数据依赖关系。

如果您使用GetSingletonRW来获得对组件的读/写访问权限,您也应该小心。因为返回了对组件数据的引用,所以可以在作业读取或写入数据的同时修改数据。GetSingletonRW的最佳实践是:

  • 仅用于访问组件中的原生容器。这是因为本机容器有自己与Jobs Debugger兼容的安全机制,与ECS组件安全机制是分开的。
  • 检查作业调试器是否有错误。任何错误都表示依赖项您需要重组或手动完成的问题。

二、Add components to an entity 向实体添加组件

要向实体添加组件,请使用实体所在世界的EntityManager。您可以向单个实体添加组件,也可以同时向多个实体添加组件。

将组件添加到实体是一种结构变化,这意味着实体移动到不同的块中。这意味着您不能直接从jobs向实体添加组件。相反,您必须使用EntityCommandBuffer来记录以后添加组件的意图。

1. Add a component to a single entity 向单个实体添加组件

下面的代码示例创建一个新实体,然后从主线程向该实体添加一个组件。

public partial struct AddComponentToSingleEntitySystemExample : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        var entity = state.EntityManager.CreateEntity();
        state.EntityManager.AddComponent<Rotation>(entity);
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2. Add a component to multiple entities 将组件添加到多个实体中

下面的代码示例获取每个带有附加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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

三、Remove components from an entity 从实体中删除组件

要从实体中删除组件,请使用实体所在世界的EntityManager。
::: warning
重要的
从实体中移除组件是一种结构变化,这意味着实体移动到不同的原型块中。
:::

2. From the main thread 从主线程

你可以直接从主线程中删除实体中的组件。下面的代码示例获取每个带有附加Rotationcomponent的实体,然后删除旋转组件。

public partial struct RemoveComponentSystemExample : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        var query = state.GetEntityQuery(typeof(Rotation));
        state.EntityManager.RemoveComponent<Rotation>(query);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3. From a job 从job

因为从实体中删除组件是一种结构变化,所以不能直接在工作中执行。相反,您必须使用EntityCommandBuffer来记录您以后要删除组件的意图。

四、Read and write component values of entities 读取和写入实体的组件值

将组件添加到实体后,系统就可以访问、读取和写入组件值。根据您的用例,您可以使用几种方法来实现这一点。

1. Access a single component 访问单个组件

有时,您可能希望一次读取或写入一个实体的单个组件。要做到这一点,在主线程上,你可以让EntityManager读取或写入单个实体的组件值。EntityManager保留了一个查找表来快速查找每个实体的块和块中的索引。

2. Access multiple components 访问多个组件

对于大多数工作,您将需要读取或写入一个或一组块中所有实体的组件:

  • ArchetypeChunk直接读写块的组件数组。
  • EntityQuery有效地检索与查询匹配的数据块集。
  • IJobEntity在使用Job的查询中遍历组件。

3.Deferring component value changes 延迟组件值的更改

为了延迟组件值的更改,请使用EntityCommandBuffer,它记录了您写入(但不读取)组件值的意图。这些更改仅在稍后在主线程上播放EntityCommandBuffer时发生。

五、Native container component support 原生容器组件支持

Collections包提供原生容器类型,如Nativelist和NativeHashMap,以及不安全容器,如UnsafeList。您可以在组件中使用这些容器类型。
原生容器和不安全容器都是值类型,而不是引用类型。不安全容器和原生容器之间的主要区别是:

  • 您只能对本机容器使用Jobs调试器。
  • 本机容器将引用复制到其底层数据。

原生容器比不安全容器更安全,并且始终满足期望。

1.Component limitations 组件的局限性

FunctionalityNative containersUnsafe containers
Compatible with Jobs Debugger(与Jobs兼容?)yesno
Can be used in job worker threads(可以在Jobs线程使用吗?)yesyes
Can be used on main thread(可以在主线程使用吗?)yesyes
Usable with ComponentLookup on main thread(可与主线程上的 ComponentLookup 一起使用?)yesyes
Usable with ComponentLookup in job worker threads(可与jobs工作线程中的 ComponentLookup 一起使用?)noyes
Can contain other NativeContainers(可以包含其他 NativeContainer?)noyes(这在技术上是支持的,但会影响性能。)
Can contain other UnsafeContainers(可以包含其他 UnsafeContainers?)yesyes

这些限制不适用于在主线程之外使用本机容器(尽管在jobs结构中不可能这样做)。组件。例如,原生容器可以嵌套其他原生容器

六、Aspect overview 方面概念

方面是一个类似于对象的包装器,您可以使用它将实体组件的子集组合成单个c#结构体。方面对于组织组件代码和简化系统中的查询非常有用。Unity为相关组件组提供了预定义的方面,或者您可以使用IAspect接口定义自己的方面。
方面可以包括以下项目:

  • 一个单独的Entity字段,用于存储实体的ID
  • refw 和refo 字段,用于访问类型为T的组件数据,其中T实现了IComponentData。
  • EnabledRefRW和EnabledRefRO字段用于访问实现IEnableableComponent的组件的启用状态。
  • DynamicBuffer字段来访问实现的缓冲区元素缓IBufferElementData
  • 任何ISharedComponent字段访问共享组件值为只读。
  • 其他方面类型

1. Create an aspect 创建一个方面

要创建方面,请使用IAspect接口。你必须将aspect声明为只读部分结构体,并且该结构体必须实现IAspect接口:

using Unity.Entities;

readonly partial struct MyAspect : IAspect
{
    // Your Aspect code
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2. Fields 字段

您可以使用RefRW或RefRO将组件声明为方面的一部分。要声明一个缓冲区,请使用DynamicBuffer。

有关可用字段的更多信息,请参阅IAspect文档。在方面内声明的字段定义了必须查询哪些数据才能使方面实例在特定实体上有效。要使一个字段可选,使用[optional]属性。

要将DynamicBuffer和嵌套方面声明为只读,请使用[ReadOnly]属性。

3. Read-only and read-write access 只读和读写权限

使用refo和refw字段为方面中的组件提供只读或读写访问。当您想在代码中引用一个方面时,使用in来覆盖所有引用,使其变为只读,或者使用ref来尊重方面中声明的只读或读写访问。如果使用in来引用对组件具有读写访问权限的方面,则可能会在写尝试时抛出异常。

4. Create aspect instances in a system 在系统中创建方面实例

要在系统中创建方面实例,请调用SystemAPI.GetAspect:

// Throws if the entity is missing any of 
// the required components of MyAspect.
// 如果实体缺少MyAspect所需的任何组件,则抛出。
MyAspect asp = SystemAPI.GetAspect<MyAspect>(myEntity);
  • 1
  • 2
  • 3
  • 4

要在系统外部创建方面实例,请使用EntityManager.GetAspect。

4.1 Iterate over an aspect 迭代一个方面

如果你想迭代一个方面,你可以使用SystemAPI.Query:

public partial struct MySystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var cannonball in SystemAPI.Query<CannonBallAspect>())
        {
            // use cannonball aspect here
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5. Example 示例

在这个例子中,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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

要在其他代码中使用这个方面,你可以像请求一个组件一样请求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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

6. Aspect source generation 方面源生成

源生成器通过分析现有代码在编译过程中生成代码。实体包生成方法和类型,允许你使用你的方面与Unity API的其他部分。有关Unity中源生成器的更多信息,请参阅Roslyn分析器和源生成器的用户手册文档。

6.1 Aspect-generated methods 方面生成完成方法

方面源生成器在方面中生成其他方法struct使其可用于其他api,如IJobEntity和SystemAPI.Query < MyAspect >()。

6.2 AddComponentRequirementsTo

将这个方面的组件需求添加到原型列表中。如果一个组件已经存在于列表中,它就不会添加一个重复的组件。但是,如果这方面需要,它会用读写覆盖只读需求。

Declaration
public void AddComponentRequirementsTo(
    ref UnsafeList<ComponentType> all, 
    ref UnsafeList<ComponentType> any, 
    ref UnsafeList<ComponentType> none, 
    bool isReadOnly)
  • 1
  • 2
  • 3
  • 4
  • 5
Parameters
  • all:原型必须匹配所有组件需求。
  • any: prototype必须匹配任何组件需求。
  • none:原型必须不匹配任何组件需求。
  • isReadOnly:设置为true,使所有组件只读。

6.3 CreateAspect

为特定实体的组件数据创建方面结构的实例。

Declaration
public AspectT CreateAspect(
    Entity entity, 
    ref SystemState systemState, 
    bool isReadOnly)
  • 1
  • 2
  • 3
  • 4
Parameters
  • entity:从中创建方面结构的实体。
  • systemState:从中提取数据的系统状态。
  • isReadOnly:设置为true,使所有对数据的引用都为只读。

6.4 Query

创建一个IEnumerable,您可以使用它来遍历查询实体方面。

Declaration
public static Enumerator Query(EntityQuery query, TypeHandle typeHandle)
  • 1
Parameters
  • query:要枚举的EntityQuery。
  • typeHandle:方面的类型句柄。

6.5 CompleteDependencyBeforeRO

完成此方面具有读访问权所需的依赖链。这完成了组件、缓冲区等的所有写依赖关系,以允许读取。

Declaration
public static void CompleteDependencyBeforeRO(ref SystemState systemState)
  • 1
Parameters
  • state:包含存储所有依赖关系的EntityManager的SystemState。

6.6 CompleteDependencyBeforeRW

完成该组件具有读/写访问所需的依赖链。这就完成了组件、缓冲区等的所有写依赖关系,以允许读取,并且它完成了所有读依赖关系,以便您可以对其进行写入。

Declaration
public static void CompleteDependencyBeforeRW(ref SystemState state)
  • 1
Parameters
  • state:包含存储所有依赖关系的EntityManager的SystemState。

6.7 Aspect-generated types

方面源生成器声明嵌套在所有实现IAspect的方面结构中的新类型。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/112378
推荐阅读
相关标签
  

闽ICP备14008679号