[Unity] 使用Scriptable Objects 和 Blob Assets,并创建一个实用类

63 篇文章 11 订阅

英文原文:
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生成器时,我确实遇到了一些障碍和错误。我还在努力,但当我取得任何重大进展时,我一定会再写一篇文章/指南。在下一篇文章中见!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值