Unity DOTS中的baking(三)过滤baking的输出
默认情况下,在conversation world(baker和baking system运行的环境)下产生的所有entities和components,都会作为baking环节的输出。在baking结束时,Unity必须将自上次baking以来发生变化的任何数据,都要复制到main world。不过,有些数据其实只在编辑时有用,我们希望可以在baking输出的时候将其舍弃掉。为此,Unity也提供了一些过滤的手段进行支持。
首先是属性BakingType
,它用来标记一个component,使其只会存在于conversation world,不会输出到main world中。我们来看一个例子:
public class MyAuthoring : MonoBehaviour
{
public int bakeIntData = 0;
public ImageGeneratorInfo info;
class MyBaker : Baker<MyAuthoring>
{
public override void Bake(MyAuthoring authoring)
{
Debug.Log("==========================Bake Invoked!========================== " + authoring.name);
DependsOn(authoring.info);
if(authoring.info == null) return;
//var transform = authoring.transform;
var transform = GetComponent<Transform>();
var entity = GetEntity(TransformUsageFlags.None);
AddComponent(entity, new MyComponent {
value = authoring.bakeIntData,
spacing = authoring.info.Spacing,
position = transform.position
});
AddComponent(entity, new MyComponentBakingType {
value = authoring.bakeIntData
});
}
}
}
public struct MyComponent : IComponentData
{
public int value;
public float spacing;
public float3 position;
}
[BakingType]
public struct MyComponentBakingType : IComponentData
{
public int value;
}
这个例子中,我们在Baker里为entity添加了两个component,其中MyComponentBakingType
是一个标记了BakingType属性的component。那么,在conversation world下,可以看到这两个component都存在于entity上:
而在editor world下,就只剩下MyComponent
这一个component了:
接下来我们来研究研究Unity在其背后做了哪些事情。首先对于BakingType属性,Unity在TypeManager中定义了一个与之匹配的常量:
/// <summary>
/// Bitflag set for component types decorated with the <seealso cref="BakingTypeAttribute"/> attribute.
/// </summary>
public const int BakingOnlyTypeFlag = 1 << 20;
然后在add component时,会对component所定义的属性进行判断:
bool isBakingOnlyType = Attribute.IsDefined(type, typeof(BakingTypeAttribute));
if (isBakingOnlyType)
typeIndex |= BakingOnlyTypeFlag;
TypeManager还对外暴露了IsBakingOnlyType
这一get属性:
public bool IsBakingOnlyType
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return (Value & TypeManager.BakingOnlyTypeFlag) != 0;
}
}
这一get属性会在GatherComponentChangesBuildPacked
和GatherComponentChangesJob
两个job中使用,它们用来收集发生变化的components,但包含BakingType属性的component会被过滤掉。这两个job由EntityManagerDiffer类触发:
/// <summary>
/// Generates a detailed change set for the world.
/// All entities to be considered for diffing must have the <see cref="EntityGuid"/> component with a unique value.
/// </summary>
/// <remarks>
/// The resulting <see cref="EntityChanges"/> must be disposed when no longer needed.
/// </remarks>
/// <param name="options">A set of options which can be toggled.</param>
/// <param name="allocator">The allocator to use for the results object.</param>
/// <returns>A set of changes for the world since the last fast-forward.</returns>
public EntityChanges GetChanges(EntityManagerDifferOptions options, AllocatorManager.AllocatorHandle allocator)
{
var changes = EntityDiffer.GetChanges(
ref m_CachedComponentChanges,
srcEntityManager: m_SourceEntityManager,
dstEntityManager: m_ShadowEntityManager,
options,
m_EntityQueryDesc,
m_BlobAssetCache,
allocator);
return changes;
}
从代码中可以猜测出,这里的变化指的是conversation world和shadow world之间的diff,conversation world就是baker和baking system运行的地方,而shadow world则是上一次baking环节输出的拷贝,Unity使用这个shadow world,与当前baking的输出进行对比,只把不同的components和entities拷贝到main world,然后再更新shadow world为当前的conversation world。那么,既然代码中调用的两个job会把包含BakingType属性的component过滤掉,很明显最后输出到main world的components就不包含它们了。
除了BakingType
属性之外,Unity还提供了TemporaryBakingType
属性标记一个component。这两者有什么区别呢?Unity官方文档中给出了一段说明:
可以得知,TemporaryBakingType
属于那种阅后即焚的操作,拥有该属性的component,会在触发相应的baking时add到entity上,然后在baking结束时component就会被销毁,这意味着该component也不会一直存在于conversation world。之后,如果不再触发相应的baking,那么该component在conversation world里也不复存在。
这么说有点枯燥,我们还是用代码进行实验,在前面例子的基础上,新增定义一个component,然后在authoring Bake函数中add一下:
[TemporaryBakingType]
public struct MyComponentTempBakingType : IComponentData
{
public int value;
}
public override void Bake(MyAuthoring authoring)
{
AddComponent(entity, new MyComponentTempBakingType
{
value = authoring.bakeIntData
});
}
由于它在conversation world中也是转瞬即逝,我们没法直接在编辑器观察到它。这里需要借助一下BakingSystem,BakingSystem是一类只存在baking过程中的system,它负责把各种baker产生的输出做进一步处理,而我们这里就是要观察baker中TemporaryBakingType
属性的component。
[WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)]
[BurstCompile]
public partial struct MyAuthoringBakingSystem : ISystem
{
EntityQuery m_query;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_query = SystemAPI.QueryBuilder().WithAll<MyComponentTempBakingType>().Build();
state.RequireForUpdate(m_query);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
Debug.Log("=================update my authoring baking system===================");
}
}
OnCreate会在system创建的时候执行一次,代码中就是获取当前包含MyComponentTempBakingType
component的entity,如果存在这样的entity,则system才会执行OnUpdate。那么,根据我们之前的假设,这里的OnUpdate只会执行一次。实际上也是如此:
类似地,暗地里Unity在TypeManager中定义了一个flag常量:
/// <summary>
/// Bitflag set for component types decorated with the <seealso cref="TemporaryBakingTypeAttribute"/> attribute.
/// </summary>
public const int TemporaryBakingTypeFlag = 1 << 21;
然后对外暴露名为TemporaryBakingType
的get属性:
/// <summary>
/// <seealso cref="TypeIndex.IsTemporaryBakingType"/>
/// </summary>
public bool TemporaryBakingType => IsTemporaryBakingType(TypeIndex);
最终这一属性会被Unity内部的BakingStripSystem
所使用,在OnCreate时会创建所有带有该attribute的entity query:
protected override void OnCreate()
{
var allTypes = TypeManager.AllTypes.Where(t => t.TemporaryBakingType).ToArray();
m_BakingComponentQueries = new NativeArray<(ComponentType, EntityQuery)>(allTypes.Length, Allocator.Persistent);
for(int i = 0; i < allTypes.Length; i++)
{
var componentType = ComponentType.FromTypeIndex(allTypes[i].TypeIndex);
EntityQueryDesc desc = new EntityQueryDesc()
{
All = new ComponentType[] {componentType},
Options = EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab
};
m_BakingComponentQueries[i] = (componentType, GetEntityQuery(desc));
}
}
然后每次update时,对符合条件的entity移除掉带有MyComponentTempBakingType
属性的component:
protected override void OnUpdate()
{
using (s_stripping.Auto())
{
foreach(var (componentType, query) in m_BakingComponentQueries)
{
EntityManager.RemoveComponent(query, componentType);
}
}
}
BakingType
和MyComponentTempBakingType
都是用来过滤component的,Unity还提供了一种过滤entity的方式,即给需要过滤掉的entity上添加一个名为BakingOnlyEntity
的component。
AddComponent<BakingOnlyEntity>(entity);
这样这个entity就只会存在于conversation world中:
背后的实现也很简单,就是有一个专门处理这个component的system,筛选出符合条件的entity,把它们一一销毁掉:
protected override void OnCreate()
{
_DestroyRemoveEntityInBake = new EntityQueryBuilder(Allocator.Temp)
.WithAny<RemoveUnusedEntityInBake, BakingOnlyEntity>()
.WithOptions(EntityQueryOptions.IncludeDisabledEntities | EntityQueryOptions.IncludePrefab)
.Build(this);
}
protected override void OnUpdate()
{
EntityManager.DestroyEntity(_DestroyRemoveEntityInBake);
}
Reference
[1] Filter baking output
[2] Baking worlds overview