0
点赞
收藏
分享

微信扫一扫

一维字符型数组算法整理

分湖芝蘭 2024-04-24 阅读 7

Unity DOTS中的baking(五)prefabs

在DOTS的baking过程中,prefabs会被烘焙成entity prefabs。entity prefabs也是一个entity,可以在运行时实例化,就像是prefab一样。我们可以使用EntityPrefabReference这个struct,将prefab注册到baker中。

首先,我们需要定义一个包含EntityPrefabReference的ECS component:

public struct Config : IComponentData
{
    public EntityPrefabReference PrefabReference;
}

然后,再定义一个authoring component,用来给entity添加Config这个component:

public class ConfigAuthoring : MonoBehaviour
{
    public GameObject Prefab;

    class Baker : Baker<ConfigAuthoring>
    {
        public override void Bake(ConfigAuthoring authoring)
        {
            var prefabEntity = new EntityPrefabReference(authoring.Prefab);

            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent(entity, new Config
            {
                PrefabReference = prefabEntity,
            });
        }
    }
}

此时我们使用传入的prefab新建了一个EntityPrefabReference类型的对象。自此,prefab的baking过程就结束了。接下来,就是要如何加载这个entity prefab了。

要加载 EntityPrefabReference 引用的prefab,还需要将 RequestEntityPrefabLoaded 这个component添加到entity中。当prefab加载结束时,Unity会将PrefabLoadResult这个component添加到包含RequestEntityPrefabLoaded的同一entity中。我们可以在统一的一个system中完成这项工作:

public partial struct LoadPrefabSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<Config>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        state.Enabled = false;

        var config = SystemAPI.GetSingleton<Config>();
        var configEntity = SystemAPI.GetSingletonEntity<Config>();

        state.EntityManager.AddComponentData(configEntity, new RequestEntityPrefabLoaded
        {
            Prefab = config.PrefabReference
        });
    }
}

最后,在使用这个prefab的system中,只有当获取到PrefabLoadResult这个component时,system才会执行:

    public partial struct SpawnSystem : ISystem
    {
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Config>();
            state.RequireForUpdate<PrefabLoadResult>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var config = SystemAPI.GetSingleton<Config>();

            var configEntity = SystemAPI.GetSingletonEntity<Config>();
            if (!SystemAPI.HasComponent<PrefabLoadResult>(configEntity))
            {
                return;
            }

            var prefabLoadResult = SystemAPI.GetComponent<PrefabLoadResult>(configEntity);
            var entity = state.EntityManager.Instantiate(prefabLoadResult.PrefabRoot);
            var random = Random.CreateFromIndex((uint) state.GlobalSystemVersion);
            state.EntityManager.SetComponentData(entity,
                LocalTransform.FromPosition(random.NextFloat(-5, 5), random.NextFloat(-5, 5), 0));
        }
    }

同样地,我们来窥探一下Unity内部的实现机制。首先是构造EntityPrefabReference时,调用的构造函数为:

public EntityPrefabReference(GameObject prefab) : this(
    AssetDatabase.GUIDFromAssetPath(AssetDatabase.GetAssetPath(prefab)))
{
}

可以看到,这里保存了prefab对应的guid,后续是通过guid来进行prefab加载的。我们再来看看请求加载prefab时使用的RequestEntityPrefabLoaded,它只包含了EntityPrefabReference一个成员:

/// <summary>
/// Component to signal the request to convert the referenced prefab.
/// </summary>
public struct RequestEntityPrefabLoaded : IComponentData
{
    /// <summary>
    /// The reference of the prefab to be converted.
    /// </summary>
    public EntityPrefabReference Prefab;
}

容易猜到,Unity内部有一个System,会遍历所有包含RequestEntityPrefabLoaded的Entity。它就是WeakAssetReferenceLoadingSystem,这个System创建时拥有一个WeakAssetReferenceLoadingData类型的Component:

struct WeakAssetReferenceLoadingData : IComponentData
{
    public struct LoadedPrefab
    {
        public int RefCount;
        public Entity SceneEntity;
        public Entity PrefabRoot;
    }

    public NativeParallelMultiHashMap<EntityPrefabReference, Entity> InProgressLoads;
    public NativeParallelHashMap<EntityPrefabReference, LoadedPrefab> LoadedPrefabs;
}

InProgressLoads是一个一对多的HashMap,它记录了当前有哪些Entity发起了加载同一个prefab的请求。LoadedPrefabs则是一对一的HashMap,它保存了当前已经加载完成的prefab信息,包含引用计数,prefab对应的Entity。这个Component在System的OnCreate方法执行时就被添加到System上,直到System销毁时在OnDestroy方法中移除。

System的OnUpdate方法才是重头戏。它负责prefab引用计数的管理,加载和卸载prefab的操作。

[BurstCompile]
public void OnUpdate(ref SystemState state)
{
    var ecb = new EntityCommandBuffer(Allocator.Temp);
    foreach(var (req, e) in SystemAPI.Query<RequestEntityPrefabLoaded>().WithEntityAccess().WithNone<PrefabAssetReference>())
    {
        StartLoadRequest(ref state, e, req.Prefab, ref ecb);
    }
    ecb.Playback(state.EntityManager);

    var sceneEntitiesToUnload = new NativeList<Entity>(Allocator.Temp);
    var prefabIdsToRemove = new NativeList<EntityPrefabReference>(Allocator.Temp);
    var inProgressLoadsToRemove = new NativeParallelHashMap<EntityPrefabReference, Entity>(16, Allocator.Temp);
    var PARSToRemove = new NativeList<Entity>(Allocator.Temp);
    ref var loadingData = ref state.EntityManager.GetComponentDataRW<WeakAssetReferenceLoadingData>(state.SystemHandle).ValueRW;
    foreach (var (prefabReference, e) in SystemAPI.Query<PrefabAssetReference>().WithEntityAccess().WithNone<RequestEntityPrefabLoaded>())
    {
        if (loadingData.LoadedPrefabs.TryGetValue(prefabReference._ReferenceId, out var loadedPrefab))
        {
            if (loadedPrefab.RefCount > 1)
            {
                loadedPrefab.RefCount--;
                loadingData.LoadedPrefabs[prefabReference._ReferenceId] = loadedPrefab;
            }
            else
            {
                prefabIdsToRemove.Add(prefabReference._ReferenceId);

                sceneEntitiesToUnload.Add(loadedPrefab.SceneEntity);
            }
        }
        else
        {
            inProgressLoadsToRemove.Add(prefabReference._ReferenceId, e);
        }
        PARSToRemove.Add(e);
    }

    for (int i = 0; i < sceneEntitiesToUnload.Length; i++)
    {
        SceneSystem.UnloadScene(state.WorldUnmanaged, sceneEntitiesToUnload[i], SceneSystem.UnloadParameters.DestroyMetaEntities);
    }

    for (int i = 0; i < prefabIdsToRemove.Length; i++)
    {
        loadingData.LoadedPrefabs.Remove(prefabIdsToRemove[i]);
    }

    foreach (var k in inProgressLoadsToRemove.GetKeyArray(Allocator.Temp))
    {
        loadingData.InProgressLoads.Remove(k, inProgressLoadsToRemove[k]);
    }

    for (int i = 0; i < PARSToRemove.Length; i++)
    {
        state.EntityManager.RemoveComponent<PrefabAssetReference>(PARSToRemove[i]);
    }
}

方法稍微有点长,但可以划分为几个部分。第一块是查询带有RequestEntityPrefabLoaded但是又没有PrefabAssetReference的Entity,前者表示请求加载prefab,后者表示该请求System是否已经在处理中。这个Component可以避免prefab的重复加载。

对于满足条件的query,则会调用StartLoadRequest方法:

void StartLoadRequest(ref SystemState state, Entity entity, EntityPrefabReference loadRequestWeakReferenceId, ref EntityCommandBuffer ecb)
{
    ref var loadingData = ref state.EntityManager.GetComponentDataRW<WeakAssetReferenceLoadingData>(state.SystemHandle).ValueRW;
    if (loadingData.LoadedPrefabs.TryGetValue(loadRequestWeakReferenceId, out var loadedPrefab))
    {
        ecb.AddComponent(entity, new PrefabAssetReference { _ReferenceId = loadRequestWeakReferenceId});
        ecb.AddComponent(entity, new PrefabLoadResult { PrefabRoot = loadedPrefab.PrefabRoot});
        loadedPrefab.RefCount++;
        loadingData.LoadedPrefabs.Remove(loadRequestWeakReferenceId);
        loadingData.LoadedPrefabs.Add(loadRequestWeakReferenceId, loadedPrefab);
        return;
    }

    ecb.AddComponent(entity, new PrefabAssetReference { _ReferenceId = loadRequestWeakReferenceId });

    if (!loadingData.InProgressLoads.ContainsKey(loadRequestWeakReferenceId))
    {
        var loadParameters = new SceneSystem.LoadParameters { Flags = SceneLoadFlags.NewInstance };
        var sceneEntity = SceneSystem.LoadPrefabAsync(state.WorldUnmanaged, loadRequestWeakReferenceId, loadParameters);
        ecb.AddComponent(sceneEntity, new WeakAssetPrefabLoadRequest { WeakReferenceId = loadRequestWeakReferenceId });
    }

    loadingData.InProgressLoads.Add(loadRequestWeakReferenceId, entity);
}

方法首先检查prefab是否已经加载完成,如果完成了则直接往发起请求的Entity上添加PrefabAssetReferencePrefabLoadResult这两个Component,前者表示System已经处理了,后者表示prefab加载完成的结果。然后还要更新下prefab的引用计数。

如果还未加载完成,则要判断是已经在加载中,还是还没发起加载请求。如果还不在加载中,则需要调用SceneSystem.LoadPrefabAsync发起加载请求,该方法返回一个表示prefab当前加载状态的Entity,如果prefab加载完成,Entity上会添加一个PrefabRoot类型的Component,它包含了加载完成的prefab。

/// <summary>
/// Load a prefab by its weak reference id.
/// A PrefabRoot component is added to the returned entity when the load completes.
/// </summary>
/// <param name="world">The <see cref="World"/> in which the prefab is loaded.</param>
/// <param name="prefabReferenceId">The weak asset reference to the prefab.</param>
/// <param name="parameters">The load parameters for the prefab.</param>
/// <returns>An entity representing the loading state of the prefab.</returns>
public static Entity LoadPrefabAsync(WorldUnmanaged world, EntityPrefabReference prefabReferenceId, LoadParameters parameters = default)
{
    return LoadSceneAsync(world, prefabReferenceId.Id.GlobalId.AssetGUID, parameters);
}

发起加载请求后,还要为Scene Entity添加WeakAssetPrefabLoadRequest类型的Component。这个component的作用是为了让prefab加载完成时通知到当前System,类似注册了一个回调函数。只有带有该Component的Scene Entity完成prefab加载时,才会调用System的CompleteLoad方法:

if (prefabRoot != Entity.Null)
{
    var sceneEntity = EntityManager.GetComponentData<SceneEntityReference>(sectionEntity).SceneEntity;
    EntityManager.AddComponentData(sceneEntity, new PrefabRoot {Root = prefabRoot});
    if (EntityManager.HasComponent<WeakAssetPrefabLoadRequest>(sceneEntity))
    {
        WeakAssetReferenceLoadingSystem.CompleteLoad(ref systemState,
            sceneEntity,
            prefabRoot,
            EntityManager.GetComponentData<WeakAssetPrefabLoadRequest>(sceneEntity)
                .WeakReferenceId);
    }
}

CompleteLoad方法更新System的WeakAssetReferenceLoadingData信息,所有请求加载该prefab的Entity都会添加PrefabAssetReferencePrefabLoadResult这两个Component,并且Entity的数量就是该prefab的引用计数。如果没有这样的Entity,则直接卸载这个prefab。

/// <summary>
/// Marks a prefab as loaded and cleans up the in progress state.
/// </summary>
/// <param name="state">The entity system state.</param>
/// <param name="sceneEntity">The entity representing the loading state of the scene.</param>
/// <param name="prefabRoot">The root entity of a converted prefab.</param>
/// <param name="weakReferenceId">The prefab reference used to initiate the load.</param>
public static void CompleteLoad(ref SystemState state, Entity sceneEntity, Entity prefabRoot, EntityPrefabReference weakReferenceId)
{
    ref var loadingData = ref state.EntityManager.GetComponentDataRW<WeakAssetReferenceLoadingData>(state.WorldUnmanaged.GetExistingUnmanagedSystem<WeakAssetReferenceLoadingSystem>()).ValueRW;
    if (!loadingData.InProgressLoads.TryGetFirstValue(weakReferenceId, out var entity, out var it))
    {
#if UNITY_EDITOR
        if (loadingData.LoadedPrefabs.TryGetValue(weakReferenceId, out var loadedPrefab))
        {
            // Prefab was reloaded, patch all references to point to the new prefab root
            loadedPrefab.PrefabRoot = prefabRoot;
            loadedPrefab.SceneEntity = sceneEntity;
            loadingData.LoadedPrefabs[weakReferenceId] = loadedPrefab;
            var query = state.GetEntityQuery(ComponentType.ReadOnly<RequestEntityPrefabLoaded>());
            foreach (var req in query.ToComponentDataArray<RequestEntityPrefabLoaded>(Allocator.Temp))
            {
                if (req.Prefab == weakReferenceId)
                    loadedPrefab.PrefabRoot = prefabRoot;
            }
            return;
        }
#endif
        //No one was waiting for this load, unload it immediately
        SceneSystem.UnloadScene(state.WorldUnmanaged, sceneEntity, SceneSystem.UnloadParameters.DestroyMetaEntities);
        return;
    }

    state.EntityManager.AddComponentData(prefabRoot, new RequestEntityPrefabLoaded() {Prefab =  weakReferenceId});
    int count = 0;
    do
    {
        state.EntityManager.AddComponentData(entity, new PrefabAssetReference { _ReferenceId = weakReferenceId});
        state.EntityManager.AddComponentData(entity, new PrefabLoadResult { PrefabRoot =  prefabRoot});
        count++;
    } while (loadingData.InProgressLoads.TryGetNextValue(out entity, ref it));
    loadingData.InProgressLoads.Remove(weakReferenceId);
    loadingData.LoadedPrefabs.Add(weakReferenceId, new WeakAssetReferenceLoadingData.LoadedPrefab { SceneEntity = sceneEntity, PrefabRoot = prefabRoot, RefCount = count});
}

看完加载的部分,再来看看卸载相关的逻辑。我们再次回到OnUpdate方法。接下来查询所有包含PrefabAssetReference并且不包含RequestEntityPrefabLoaded的Entity,这与前面恰好相反,它表示所有需要取消加载prefab的Entity。如果prefab已经加载完成了,此时需要将其引用计数减一;如果引用计数为0了,则需要将其卸载。如果prefab还在加载中,则需要从请求加载的hashmap中把Entity移除。最后,为了避免重复处理这类取消加载的Entity,处理完毕后需要移除它们身上的PrefabAssetReference

当然,实际上另一种情形更常见,就是Entity在其生命周期内请求加载了某个prefab之后一直在使用,直到它被destroy。这种情况外部并不需要做额外的操作,Entity所持有的prefab引用计数也会自动正确地更新。这是怎么做到的呢?

答案就在RequestEntityPrefabLoadedPrefabAssetReference这两个Component上。默认Entity是同时拥有这两个Component的,但是PrefabAssetReference是ICleanupComponentData,意味着Entity销毁时它也不会被销毁,这样被销毁的Entity身上,只有一个PrefabAssetReference,就会在System的OnUpdate时触发prefab的清理工作。

Reference

[1] Prefabs in baking

举报

相关推荐

0 条评论