[Unity ECS] 使用Scriptableobject和 Blob asset,创建实用类

63 篇文章 11 订阅

英文原文:

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 生成器时,我确实在这里和那里偶然发现了一些障碍和错误。我仍在努力,但当我取得任何重大进展时,我会确保写另一篇文章/指南。下一篇见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值