英文原文:
https://neilmarkcorre.wordpress.com/tag/gameobjectconversionsystem/
所以,你想把scriptable objects转换为可以在ECS系统中使用的数据–但是,你不能用纯ECS直接访问scriptable objects。今天,我将与大家分享如何将scriptable objects转换为Blob assets,同时也分享我在研究Blob assets时创建的一个实用类。
首先,让我们快速定义一下什么是blob assets–它是一个不可变的数据,可以被你的系统在多个线程中访问。这意味着,你可以在运行并行Job时使用这些数据,这对于使用Unity的DOTS创建高性能的系统非常重要。同样重要的是,你只能在你的blob assets中使用blittable类型,如果你想使用字符串,就使用BlobString或FixedString。
如果你想了解更多关于blob assets的入门知识,Squeaky Wheel的Marnel写了一份指南来帮助你。Code Monkey制作了一个简单易懂的视频,“什么是Blob assets”。另外,请查看Unity关于Blob assets的测试脚本,它们对理解工作原理有很大帮助。
在我们开始之前–为了了解情况,我正在制作一个NPC生成器,作为学习ECS的一个测试项目。
Blob assets的最佳用例是将Scriptable Objects(对设计师友好,可在Unity的编辑器中编辑)转换为可在工作中使用的数据,并进行burst。
转换Scriptable Objects
让我们首先创建我们想要转换的Scriptable Objects。为了简单起见,让我们为我们想要生成的NPC的最大数量添加一个整数,为了有趣起见,我们还可以添加一个NPC可以拥有的最大朋友数量。
[CreateAssetMenu(fileName = "NpcManagerData", menuName = "Game/NpcManagerData")]
public class NpcManagerData : ScriptableObject {
[SerializeField]
private int totalNumberOfNpcs;
[SerializeField]
private int totalFriends;
public int TotalNumberOfNpcs => this.totalNumberOfNpcs;
public int TotalFriends => this.totalFriends;
}
我们还需要一个gameobject,它将容纳这个scriptable object。
public class DataContainer : MonoBehaviour {
[SerializeField]
private NpcManagerData npcManagerData;
// Accessor for the conversion system
public NpcManagerData NpcManagerData => this.npcManagerData;
}
为了将其转换为blob assets,我们需要定义blob assets的结构。对于我们的目的来说,这个结构将是非常相似的。如果有需要,你可以在以后的转换系统中设置计算或特定的转换逻辑。
public struct NpcDataBlobAsset {
public int TotalNumberOfNpcs;
public int TotalFriends ;
}
在这一点上,还需要注意的是,Scriptable Objects是 “场景数据”–意味着它们存在于Unity的 "gameobject"世界中。也就是说,我们需要一种方法来将这些 "场景数据 "转换为DOTS。我们可以通过使用GameObjectConversionSystem来实现这一点。
[UpdateInGroup(typeof(GameObjectConversionGroup))]
public class TestGameDataSystem : GameObjectConversionSystem {
// We made this static so that other systems can access the blob asset.
// We'll modify this later to work with job systems.
// For now, let's keep it simple.
public static BlobAssetReference<NpcDataBlobAsset> NpcBlobAssetReference;
protected override void OnCreate() {
base.OnCreate();
// Let's debug here to make sure the system ran
Debug.Log("Prefab entities system created!");
}
protected override void OnUpdate() {
// Access the DataContainer attached to a gameObject here and copy the data to a blob asset
this.Entities.ForEach((DataContainer container) => {
// We use a using block since the BlobBuilder needs to be disposed after using it
using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp)) {
// Take note of the "ref" keywords. Unity will throw an error without them, since we're working with structs.
ref NpcDataBlobAsset npcDataBlobAsset = ref blobBuilder.ConstructRoot<NpcDataBlobAsset>();
// Copy data. We'll work with lists/arrays later.
npcDataBlobAsset.TotalNumberOfNpcs = container.NpcManagerData.TotalNumberOfNpcs;
npcDataBlobAsset.TotalFriends = container.NpcManagerData.TotalFriends;
// Store the created reference to the memory location of the blob asset
NpcBlobAssetReference = blobBuilder.CreateBlobAssetReference<NpcDataBlobAsset>(Allocator.Persistent);
}
});
// Print to check if the conversion was successful.
// Note that we have to access the "Value" of where the reference is pointing to.
Debug.Log($"At prefab entities initialization: total npc count is {NpcBlobAssetReference.Value.TotalNumberOfNpcs.ToString()}");
}
}
如果你对上面的代码不熟悉,可以查看Unity在2019年哥本哈根Unite会议期间关于 "将场景数据转换为DOTS "的演讲。需要注意的是,GameObjectConversionSystems在GameObject/Scene世界(存在Prefabs、Scriptable Objects等游戏对象的地方)和Entity或 “DOTS世界”(你的系统所在的地方)之间的世界中工作。我在这里可能过于简化了,因为你可以有多个世界。在这种情况下,GameObjectConversionSystems仍然位于GameObject/Scene世界和你的世界之间。
在我们测试代码之前,请确保你的DataContainer(就是你把对可脚本对象的引用放在子场景中的那个)。因为当你在Inspector中 "关闭 "它们时,子场景会将其中的gameObjects转换为Entite。更多信息请参见Unity的Unite Copenhagen讲座。下面是hierarchy中的层次结构的样子。
运行游戏将在控制台中打印这些内容。
酷,现在我们将Scriptable Object 转换为Blob assets,可以使用TestGameDataSystem中的静态BlobAssetReference进行访问。让我们再添加两样东西–一个带有Blob assets引用的实体,可以在我们的 Job system 中访问,以及承诺的 Blob assets 实用类。
在DOTS/ECS系统中访问Blob assets
首先,我们需要一个组件数据,我们将把它附加到实体上。
// This will be used by job systems to access blob asset data,
// since we cannot access static non-readonly fields in jobs
public struct BlobAssetReferences : IComponentData {
public BlobAssetReference<NpcDataBlobAsset> NpcManager;
}
接下来,让我们把这个组件添加到一个新的实体中,我们将在生成NpcDataBlobAsset之后,在前面的GameObjectConversionSystem中直接创建这个实体。
// ...the rest of the GameObjectConversionSystem earlier
Debug.Log($"At prefab entities initialization: total npc count is {NpcBlobAssetReference.Value.TotalNumberOfNpcs.ToString()}");
// We use the default world here since this is attached to a gameobject in a subscene which is in itself, a World.
// We have 3 worlds at this point: Default, Subscene, and Subscene entity conversion world
EntityManager defaultEntityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
Entity gameDataEntity = defaultEntityManager.CreateEntity();
BlobAssetReferences blobAssetReferences = new BlobAssetReferences {
NpcManager = NpcBlobAssetReference
};
defaultEntityManager.AddComponentData(gameDataEntity, blobAssetReferences);
}
}
在GameObjectConversionSystems中创建实体时要小心,因为那里有2个EntityManager,因为我们在GameObjectConversionSystems中工作时有2个世界–一个用于转换世界,另一个用于默认世界。如果你正在创建一个你想在默认世界中使用的实体,请使用World.DefaultGameObjectInjectionWorld.EntityManager而不是GameObjectConversionSystems中的EntityManager。
现在,让我们创建一个简单的系统,看看我们是否能从Job system中访问blob assets并运行游戏。
[UpdateInGroup(typeof(SimulationSystemGroup))]
public class TestSystem : SystemBase {
protected override void OnUpdate() {
if (!GameDataSystem.NpcManager.IsCreated) {
// Don't do anything if the NpcManager is not yet created
return;
}
this.Entities.ForEach((Entity entity, int entityInQueryIndex, ref BlobAssetReferences blobAssetReferences) => {
NpcDataBlobAsset npcDataBlobAsset = blobAssetReferences.NpcManager.Value;
for (int i = 0; i < npcDataBlobAsset.TotalNumberOfNpcs; i++) {
// you can now access the NpcDataBlobAsset here
}
}).ScheduleParallel();
}
}
Blob Assets实用类
酷!接下来,让我们再创建一个blob Assets,但这次让我们把TestGameDataSystem中的BlobBuilder重构为一个实用类,这样我们就可以简化代码,使其更容易阅读。
public static class BlobAssetUtils {
private static BlobBuilder BLOB_BUILDER;
// We expose this to the clients to allow them to create BlobArray using BlobBuilderArray
public static BlobBuilder BlobBuilder => BLOB_BUILDER;
// We allow the client to pass an action containing their blob creation logic
public delegate void ActionRef<TBlobAssetType, in TDataType>(ref TBlobAssetType blobAsset, TDataType data);
public static BlobAssetReference<TBlobAssetType> BuildBlobAsset<TBlobAssetType, TDataType>
(TDataType data, ActionRef<TBlobAssetType, TDataType> action) where TBlobAssetType : struct {
BLOB_BUILDER = new BlobBuilder(Allocator.Temp);
// Take note of the "ref" keywords. Unity will throw an error without them, since we're working with structs.
ref TBlobAssetType blobAsset = ref BLOB_BUILDER.ConstructRoot<TBlobAssetType>();
// Invoke the client's blob asset creation logic
action.Invoke(ref blobAsset, data);
// Store the created reference to the memory location of the blob asset, before disposing the builder
BlobAssetReference<TBlobAssetType> blobAssetReference = BLOB_BUILDER.CreateBlobAssetReference<TBlobAssetType>(Allocator.Persistent);
// We're not in a Using block, so we manually dispose the builder
BLOB_BUILDER.Dispose();
// Return the created reference
return blobAssetReference;
}
}
至于我们的TestGameDataSystem,OnUpdate函数将看起来像。
protected override void OnUpdate() {
this.Entities.ForEach((DataContainer container) => {
// Use the Utility class here - pass the container data, then the conversion logic as an action
NpcBlobAssetReference = BlobAssetUtils.BuildBlobAsset(container.NpcManagerData, delegate(ref NpcDataBlobAsset blobAsset, NpcManagerData data) {
blobAsset.TotalNumberOfNpcs = data.TotalNumberOfNpcs;
blobAsset.TotalFriends = data.TotalFriends;
});
});
// ...the rest of the code
}
看起来已经很干净了。现在,为了让你对Blob资产的使用情况有更多的了解,让我分享一个简单的名字库(使用列表),我为我的NPC创建并转换为一个Blob assets。从Scriptable Object开始。
[CreateAssetMenu(fileName = "NamesData", menuName = "Game/NamesData")]
public class NamesData : ScriptableObject {
public List<string> firstNames;
public List<string> lastNames;
}
现在是blob assets。
public struct NamesBlobAsset {
// Blob arrays are memory location offsets from the original Blob Asset Reference.
// At least, that's how I understood the manual.
public BlobArray<FixedString32> FirstNames;
public BlobArray<FixedString32> LastNames;
}
最后,TestGameDataSystem中的创建逻辑。
protected override void OnUpdate() {
this.Entities.ForEach((DataContainer container) => {
// ...the NpcBlobAssetReference generation code here
NamesLibraryReference = BlobAssetUtils.BuildBlobAsset(container.NamesData, delegate(ref NamesBlobAsset blobAsset, NamesData data) {
// Cache the blob builder from the utility class so we can generate blob arrays
BlobBuilder blobBuilder = BlobAssetUtils.BlobBuilder;
BlobBuilderArray<FixedString32> firstNamesArrayBuilder = blobBuilder.Allocate(ref blobAsset.FirstNames, data.firstNames.Count);
BlobBuilderArray<FixedString32> lastNamesArrayBuilder = blobBuilder.Allocate(ref blobAsset.LastNames, data.lastNames.Count);
for (int i = 0; i < data.firstNames.Count; ++i) {
// Copy the data from the list to the BlobBuilderArray
firstNamesArrayBuilder[i] = new FixedString32(data.firstNames[i]);
}
for (int i = 0; i < data.lastNames.Count; ++i) {
lastNamesArrayBuilder[i] = new FixedString32($" {data.lastNames[i]}");
}
// We don't have to worry about "storing" the created array to the blob asset here
// since that is already handled in the BlobAssetUtils. This is just the creation logic
// that is passed to the utility class.
});
});
// ...the rest of the code
}
就这样了。在使用Unity的DOTS时,Blob Assets是一种很好的存储数据的方式,但仍有很多东西需要学习。在尝试制作一个简单的NPC生成器时,我确实遇到了一些障碍和错误。我还在努力,但当我取得任何重大进展时,我一定会再写一篇文章/指南。在下一篇文章中见!