英文原文:
https://neilmarkcorre.wordpress.com/2020/12/01/working-with-scriptable-objects-and-blob-assets-and-creating-a-utility-class/
因此,您希望将可编写脚本的对象转换为可在 ECS 系统中使用的数据——但是,您无法使用纯 ECS 直接访问 SO。今天,我将与您分享如何将可编写脚本的对象转换为 Blob 资产,并分享我在学习 Blob 资产时创建的实用程序类。
首先,让我们快速定义什么是 blob 资产——它是一种不可变的数据,您的系统可以跨多个线程访问它。这意味着,您可以在运行并行作业时使用这些,这对于使用 Unity 的 DOTS 创建高性能系统非常重要。同样重要的是要注意,您只能在 blob 资产中使用 blittable 类型,如果要使用字符串,则可以使用 BlobString 或 FixedString。
如果您想了解有关 Blob 资产入门的更多信息,来自 Squeaky Wheel 的 Marnel 编写了一份指南来帮助您。 Code Monkey 创建了一个易于理解的视频,“什么是 Blob 资产?”。此外,检查 Unity 的关于 Blob 资产的测试脚本,它们对理解事物的工作原理有很大帮助。
在我们开始之前 - 就上下文而言,我正在制作一个 NPC 生成器作为研究 ECS 的测试项目。此处的代码对于包是稳定的 - Entities 0.16.0-preview.21 和 Hybrid Renderer 0.8.0-preview.19。
Blob Assets 的最佳用例是将可编写脚本的对象(设计人员友好,可以在 Unity 的编辑器中编辑)转换为可用于burst Job的数据。
如果您遇到此错误(我遇到了包 - Entities 0.17.0-preview.42 和 Hybrid Renderer 0.11.0-preview.44 - 其他依赖包应相应更新):
error ConstructBlobWithRefTypeViolation: You may not build a type TBlobAssetType with Construct as TBlobAssetType is a reference or pointer. Only non-reference types are allowed in Blobs.
Unity 已经意识到这个问题(有关更多信息,请参阅此线程),但该修复程序包含在 Entities 0.18 中,截至撰写此编辑块时,该修复程序还没有 ETA。同时,您可以从这篇文章中实施此解决方法:
“For now, you can comment out loop on line 149 in BlobAssetSafetyVerifier.cs”
我会看看是否可以找到其他解决方法,在那之前我已经实施了上述解决方法,并且本教程中的项目应该仍然有效。
转换可编写脚本的对象
让我们首先创建我们要转换的 ScriptableObject。为简单起见,让我们为我们想要生成的最大 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;
}
我们还需要一个游戏对象来保存这个可编写脚本的对象,
public class DataContainer : MonoBehaviour {
[SerializeField]
private NpcManagerData npcManagerData;
// Accessor for the conversion system
public NpcManagerData NpcManagerData => this.npcManagerData;
}
为了将其转换为 blob 资产,我们需要定义 blob 资产的结构。对于我们的目的,结构将非常相似。如果需要,您可以稍后在转换系统中进行计算或特定的转换逻辑。
public struct NpcDataBlobAsset {
public int TotalNumberOfNpcs;
public int TotalFriends ;
}
在这一点上,同样重要的是要注意 Scriptable Objects 是“场景数据”——这意味着它们存在于 Unity 的“game object”世界中。也就是说,我们需要一种方法将这些“场景数据”转换为 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 在 Unite Copenhagen 2019 期间关于“将场景数据转换为 DOTS”的演讲。需要注意的是,GameObjectConversionSystems 在游戏对象/场景世界(游戏对象存在于预制件、脚本化对象等的地方)和实体或“DOTS 世界”(您的系统所在的地方)之间的世界中工作,这一点很重要。我可能在这里过于简单化了,因为你可以有多个世界。在这种情况下,GameObjectConversionSystems 仍然位于 GameObject/Scene 世界和您的世界之间。
在我们测试代码之前,请确保您在子场景中有 DataContainer(您放置对可编写脚本对象的引用的那个)。因为当您在 Inspector 中“关闭”它们时,子场景会将其中的游戏对象转换为实体。有关更多信息,请参阅 Unity 的 Unite Copenhagen 演讲。这是编辑器中的层次结构,
运行游戏将在控制台打印这些,
很酷,现在我们将可编写脚本的对象转换为可以使用 TestGameDataSystem 中的静态 BlobAssetReference 访问的 Blob 资产。让我们再添加两件事——一个可以在我们的Job system中访问的具有 blob 资产引用的实体,以及承诺的 blob 资产实用程序类。
在 DOTS/ECS 系统中访问 Blob 资产
首先,我们需要一个将附加到实体的组件数据,
// 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。
现在,让我们创建一个简单的系统,看看我们是否可以从作业系统访问 blob 资产并运行游戏,
[UpdateInGroup(typeof(SimulationSystemGroup))]
public class TestSystem : SystemBase {
protected override void OnUpdate() {
if (!TestGameDataSystem.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 Asset 实用类
Cool!接下来,让我们创建另一个 blob asset,但这次让我们将 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 资产。从可编写脚本的对象开始,
[CreateAssetMenu(fileName = "NamesData", menuName = "Game/NamesData")]
public class NamesData : ScriptableObject {
public List<string> firstNames;
public List<string> lastNames;
}
现在是 blob Asset,
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 生成器时,我确实在这里和那里偶然发现了一些障碍和错误。我仍在努力,但当我取得任何重大进展时,我会确保写另一篇文章/指南。下一篇见!