0
点赞
收藏
分享

微信扫一扫

Unity DOTS中的baking(三)过滤baking的输出

Gaaidou 2024-02-07 阅读 9

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上:

Unity DOTS中的baking(三)1

而在editor world下,就只剩下MyComponent这一个component了:

Unity DOTS中的baking(三)2

接下来我们来研究研究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属性会在GatherComponentChangesBuildPackedGatherComponentChangesJob两个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创建的时候执行一次,代码中就是获取当前包含MyComponentTempBakingTypecomponent的entity,如果存在这样的entity,则system才会执行OnUpdate。那么,根据我们之前的假设,这里的OnUpdate只会执行一次。实际上也是如此:

Unity DOTS中的baking(三)3

类似地,暗地里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);
        }
    }
}

BakingTypeMyComponentTempBakingType都是用来过滤component的,Unity还提供了一种过滤entity的方式,即给需要过滤掉的entity上添加一个名为BakingOnlyEntity的component。

AddComponent<BakingOnlyEntity>(entity);

这样这个entity就只会存在于conversation world中:

Unity DOTS中的baking(三)4

Unity DOTS中的baking(三)5

背后的实现也很简单,就是有一个专门处理这个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

举报

相关推荐

0 条评论