Write groups(写入组)
一个常见的ECS模式是,一个系统读取一组输入组件并写入另一个组件作为其输出。然而,在某些情况下,你可能想覆盖一个系统的输出,并使用一个基于不同输入组的不同系统来更新输出组件。写入组提供了一个机制,让一个系统覆盖另一个系统,即使你不能改变另一个系统。
一个目标组件类型的写入组由ECS应用WriteGroup特性的所有其他组件类型组成,并以该目标组件类型为参数。作为一个系统创建者,你可以使用写组,这样你的系统的用户就可以排除你的系统在其他情况下选择和处理的实体。这种过滤机制让系统用户根据自己的逻辑更新被排除的实体的组件,而让你的系统对其余的实体正常运行。
为了利用写入组,你必须在你的系统中的查询上使用写入组过滤器选项。这就从查询中排除了所有的实体,这些实体有一个来自写入组的组件,这些组件在查询中被标记为可写。
要覆盖一个使用写入组的系统,把你自己的组件类型标记为该系统的输出组件类型的写入组的一部分。原来的系统会忽略任何有你的组件的实体,你可以用你自己的系统更新这些实体的数据。
Write groups example(写入组的例子)
在这个例子中,你使用一个外部包,根据游戏中所有角色的健康状态为其着色。为此,包里有两个组件:HealthComponent和ColorComponent。
public struct HealthComponent : IComponentData
{
public int Value;
}
public struct ColorComponent : IComponentData
{
public float4 Value;
}
此外,软件包中还有两个系统:
- ComputeColorFromHealthSystem,它从HealthComponent读取并写入ColorComponent。
- RenderWithColorComponent,它从ColorComponent中读取
为了表示当玩家使用力量提升,他们的角色变得无敌,你可以在角色的实体上附加一个InvincibleTagComponent。在这种情况下,角色的颜色应该变成一种单独的、不同的颜色,而上面的例子并没有考虑到这一点。
您可以创建自己的系统来覆盖ColorComponent值,但理想情况下,ComputeColorFromHealthSystem不会一开始就为您的实体计算颜色。它应该忽略任何具有InvincibleTagComponent的实体。当屏幕上有成千上万的玩家时,这就变得更加重要了。不幸的是,这个系统来自另一个包,它不知道InvincibleTagComponent的情况。这时,一个写入组就很有用了。它允许系统在查询中忽略实体,当你知道它计算的值无论如何都会被覆盖。你需要两样东西来支持这个:
- InvincibleTagComponent必须标记为ColorComponent的写入组的一部分。ColorComponent的写入组由所有以typeof(ColorComponent)为参数的WriteGroup属性的组件类型组成。
[WriteGroup(typeof(ColorComponent))]
struct InvincibleTagComponent : IComponentData {}
- ComputeColorFromHealthSystem必须明确地支持写入组。为了实现这一点,系统需要为其所有查询指定 EntityQueryOptions.FilterWriteGroup 选项。
你可以像这样实现ComputeColorFromHealthSystem。
...
protected override void OnUpdate() {
Entities
.WithName("ComputeColor")
.WithEntityQueryOptions(EntityQueryOptions.FilterWriteGroup) // support write groups
.ForEach((ref ColorComponent color, in HealthComponent health) => {
// compute color here
}).ScheduleParallel();
}
...
当这个执行时,会发生以下情况:
- 系统检测到你向ColorComponent写了东西,因为它是一个旁注的参数
- 它查找ColorComponent的写入组并在其中找到InvincibleTagComponent。
- 它排除了所有具有InvincibleTagComponent的实体。
这样做的好处是,这允许系统根据系统未知的、可能生活在不同包中的类型来排除实体。
NOTE
关于更多的例子,请看Unity.Transforms的代码,它对每一个更新的组件都使用了写组,包括LocalToWorld。
Creating write groups(创建写入组)
要创建写入组,在写入组中每个组件类型的声明中添加WriteGroup属性。WriteGroup属性需要一个参数,它是组中的组件用来更新的组件类型。一个组件可以是一个以上的写入组的成员。
例如,如果你有一个系统,只要实体上有组件A或B,就会写到组件W,那么你可以为W定义一个写入组,如下所示:
public struct W : IComponentData
{
public int Value;
}
[WriteGroup(typeof(W))]
public struct A : IComponentData
{
public int Value;
}
[WriteGroup(typeof(W))]
public struct B : IComponentData
{
public int Value;
}
NOTE
你不会把写入组的目标(上面例子中的组件W)添加到它自己的写入组
Enabling write group filtering(启用写入组过滤功能)
要启用写组过滤,请在你的作业上设置FilterWriteGroups标志。
public class AddingSystem : SystemBase
{
protected override void OnUpdate() {
Entities
// support write groups by setting EntityQueryOptions
.WithEntityQueryOptions(EntityQueryOptions.FilterWriteGroup)
.ForEach((ref W w, in B b) => {
// perform computation here
}).ScheduleParallel();}
}
对于查询描述对象,在创建查询时设置该标志:
public class AddingSystem : SystemBase
{
private EntityQuery m_Query;
protected override void OnCreate()
{
var queryDescription = new EntityQueryDesc
{
All = new ComponentType[] {
ComponentType.ReadWrite<W>(),
ComponentType.ReadOnly<B>()
},
Options = EntityQueryOptions.FilterWriteGroup
};
m_Query = GetEntityQuery(queryDescription);
}
// Define IJobChunk struct and schedule...
}
当你在查询中启用写入组过滤时,查询会将可写组件的写入组中的所有组件添加到查询的None列表中,除非你明确地将它们添加到全部或任何列表中。因此,查询只有在明确要求该实体上的每个组件来自特定的写组时才会选择该实体。如果一个实体有一个或多个来自该写入组的额外组件,查询会拒绝它。
在上面的示例代码中,查询:
- 排除任何有组件A的实体,因为W是可写的,而A是W的写入组的一部分。
- 即使B是W的写入组的一部分,它也被明确地指定在All列表中,也不排除任何具有组件B的实体。
Overriding another system that uses write groups(覆盖另一个使用写入组的系统)
如果一个系统在其查询中使用了写入组过滤,你就用你自己的系统来覆盖该系统并向这些组件写入。要覆盖该系统,将你自己的组件添加到其他系统所写的组件的写入组中。因为写入组过滤排除了查询没有明确要求的写入组中的任何组件,其他系统会忽略任何有你的组件的实体。
例如,如果你想通过指定旋转的角度和轴来设置你的实体的方向,你可以创建一个组件和一个系统来把角度和轴的值转换成四元数,并把它写到Unity.Transforms.Rotation组件。为了防止Unity.Transforms系统更新Rotation,不管除了你的组件之外还有什么其他组件,你可以把你的组件放在Rotation的写组中:
using System;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
[Serializable]
[WriteGroup(typeof(Rotation))]
public struct RotationAngleAxis : IComponentData
{
public float Angle;
public float3 Axis;
}
然后你可以用RotationAngleAxis组件更新任何实体,而不需要争论。
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Transforms;
public class RotationAngleAxisSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Rotation destination, in RotationAngleAxis source) =>
{
destination.Value
= quaternion.AxisAngle(math.normalize(source.Axis), source.Angle);
}).ScheduleParallel();
}
}
Extending another system that uses write groups(扩展另一个使用写入组的系统)
如果你想扩展另一个系统而不是覆盖它,或者你想让未来的系统覆盖或扩展你的系统,那么你可以在你自己的系统上启用写组过滤功能。然而,当你这样做时,两个系统都没有默认处理任何组件的组合。你必须明确地查询和处理每个组合。
在前面的例子中,它定义了一个包含组件A和B的写入组,目标是组件W。如果你在写入组中添加一个新的组件,称为C,那么知道C的新系统可以查询包含C的实体,而且这些实体是否也有组件A或B并不重要。然而,如果新系统也启用了写组过滤,这就不再正确了。如果你只需要组件C,那么写入组过滤就会排除任何具有A或B的实体。相反,你必须明确地查询每个有意义的组件组合。
NOTE
你可以在适当的时候使用查询的Any子句。
var query = new EntityQueryDesc
{
All = new ComponentType[] {
ComponentType.ReadOnly<C>(),
ComponentType.ReadWrite<W>()
},
Any = new ComponentType[] {
ComponentType.ReadOnly<A>(),
ComponentType.ReadOnly<B>()
},
Options = EntityQueryOptions.FilterWriteGroup
};
如果你有任何包含写入组中没有明确提到的组件组合的实体,那么写到写入组目标的系统及其过滤器就不会处理它们。然而,如果你有任何如果这些类型的实体,这很可能是程序中的一个逻辑错误,它们不应该存在。