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上添加PrefabAssetReference
和PrefabLoadResult
这两个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都会添加PrefabAssetReference
和PrefabLoadResult
这两个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_EDITORif (loadingData.LoadedPrefabs.TryGetValue(weakReferenceId, out var loadedPrefab)){// Prefab was reloaded, patch all references to point to the new prefab rootloadedPrefab.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 immediatelySceneSystem.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引用计数也会自动正确地更新。这是怎么做到的呢?
答案就在RequestEntityPrefabLoaded
和PrefabAssetReference
这两个Component上。默认Entity是同时拥有这两个Component的,但是PrefabAssetReference
是ICleanupComponentData,意味着Entity销毁时它也不会被销毁,这样被销毁的Entity身上,只有一个PrefabAssetReference
,就会在System的OnUpdate时触发prefab的清理工作。
Reference
[1] Prefabs in baking