[Unity workflows] Unity Addressables 对象池

11 篇文章 0 订阅
9 篇文章 0 订阅

英文原文:https://thegamedev.guru/unity-addressables/pooling/

  如果您打算跳入 Unity Addressables 对象池,请小心。 您最好确保对象池不为空。

在这里插入图片描述
  在之前的帖子中,我向您展示了如何在游戏中无限制地加载内容。

  嗯,当然有限制,但如果你在实施资产内容管理系统方面做得很好,你就不太可能达到这些限制。 如果你听从我的建议,我很确定你会做对的。

  但到目前为止,我可以说我们遗漏了一个重要的拼图。 我的意思是,我们遗漏了很多,但今天我想关注一个非常具体的问题:延迟。

什么是延迟?

  延迟是从开始到完成某件事所花费的时间。 这是我们通常想要避免的某种延迟。

  例如,您在用微波炉烹制爆米花时会遇到延迟问题。 在那里,您启动微波炉并等待 3 到 5 分钟。 而且我们想马上吃爆米花,所以这种延迟很糟糕。

  当我们进入游戏领域时,事情变得比煮爆米花更糟糕。

  在游戏中,毫秒很重要。 超过 20 毫秒的一切都会让竞争性多人游戏变得更加不公平。

  但在这篇文章中,我们不是在谈论多人游戏。 我们将讨论使用 Addressables for Unity 加载和显示资产时遇到的延迟。

  实际上,我们会为此做点什么。

  我们将实现一个简单的 Unity Addressables Pooling 系统。

内容

  • 一级开发人员:简单的 Unity Addressables 加载
  • 二级开发人员:Unity Addressables Pooling
  • 三级开发人员:Smart Unity Addressables Pooling
    • 性能表现
    • 自动池化
    • 自动池化

一级开发人员:简单的 Unity Addressables 加载

是的,我知道。 我们已经这样做过好几次了。

  我们采用一个预制件,将其标记为可寻址,然后将其分配给一个脚本,该脚本会在需要时加载预制件。

  与基于直接引用的传统资产管理工作流程相比,这给您带来了巨大的好处。 简而言之,使用 Addressables 可以让你……

  • 加载时间更短 : 满足玩家对加载时间的要求
  • 微小的迭代时间 : 大大减少编辑器的播放和部署时间
  • 消除内存压力 : 定位更多设备 减少崩溃
  • 出售您的下一个 DLC : 几乎零努力地计划、制作和运送您的 DLC

要阅读更多相关信息,请访问我关于 Unity Addressables Benefits: 3 Ways to Save Your Game 的介绍性文章。

  在这篇博文中,我将坚持展示我极其复杂的示例项目设置。

在这里插入图片描述
  哦,没关系,它只是一个通过 Addressables API 实例化的预制件……

  这在大多数情况下都适用于任何游戏。

  然而…

  这个加载和实例化过程有一些延迟。 Unity 必须获取所需的资产包,加载预制件及其依赖项并实例化。

  加载过程应远低于 1 毫秒。

  但是当我们给这个对象增加更多的复杂性时,事情就会变得一团糟。 如果我们添加动画师、粒子系统、刚体等,Unity 最终肯定会从我们这里偷走 10 毫秒。 激活这些组件可能会花费大量时间。

  如果资产包是通过网络提供的,但它们还没有准备好,那么我们说的是几秒钟,甚至几分钟。

  如果在生成最终 Boss 时玩家已经到达地牢的尽头,您的游戏会有多恐怖?

在这里插入图片描述
  这是我的猜测:既有利可图又可怕。

  Unity 中的一个典型解决方案依赖于添加对象池。

  您可以在网上找到许多适用于 Unity 的对象池。 问题是,它们还没有为 Addressables 做好准备。

  但是现在,你会得到一个。


二级开发人员:Unity Addressables 对象池

  让我在这里警告您:对于池化系统的需求因项目而异。

  在这里,我将为您提供一个简单的系统,您可以根据自己的需要进行调整。

  这就是您希望从这个池化系统中得到的:

隐藏你的延迟 : 快速激活和停用
很少的编程开销 : 简单的 API:获取和返回
更少的内存压力 : 如果池被禁用,请释放内存。 如果启用池,则取回内存。

  如果您想知道:是的,我重新使用了上一节中的图标。 这里很忙。

  在我们进入代码之前,我将向您展示我准备的测试。

在这里插入图片描述

1.预热异步池

  到目前为止,预制件及其内容尚未加载到内存中。 池已启用并根据 Addressables 加载预制件。

  然后,它实例化几个对象并将它们全部停用,付出 Awake、Start、OnEnable 和 OnDisable 的代价。

  到目前为止,预制件内容在内存中。

在这里插入图片描述

2. 帮助我们的游戏玩法:从池中取出一个物品

  用户通过同步方法 Take() 从池中取出一个项目并将其放在场景中的某个位置。

  用户支付激活 (OnEnable) 时间,这取决于他们预制件的复杂性。

在这里插入图片描述

3.节省CPU时间:将物品归还到池中

  用户厌倦了他们的新玩具并将其放回池中。

  池将其停用并将其置于其层次结构下,付出 OnDisable 的代价。

在这里插入图片描述

4.释放内存:disable 池

  一段时间后,我们知道我们将不再需要这个项目。

  我们禁用池,因此它会释放所有使用的内存,即使间接引用仍然存在于池中。

这种方法的优势依赖于内存管理。 我们决定支付内存价格。

  使用传统的 Unity 对象池,我们一直在支付内存开销,即使预制件从未实例化。现在,代码看起来如何?

public class GamedevGuruPoolUserTest : MonoBehaviour
{
    [SerializeField] private AssetReference assetReferenceToInstantiate = null;

    IEnumerator Start()
    {
        var wait = new WaitForSeconds(8f);

        // 1. Wait for pool to warm up.
        yield return wait;

        // 2. Take an object out of the pool.
        var pool = GamedevGuruPool.GetPool(assetReferenceToInstantiate);
        var newObject = pool.Take(transform);

        // 3. Return it.
        yield return wait;
        pool.Return(newObject);

        // 4. Disable the pool, freeing resources.
        yield return wait;
        pool.enabled = false;

        // 5. Re-enable pool, put the asset back in memory.
        yield return wait;
        pool.enabled = true;
    }
}

这是一段非常正常的测试代码。

  如果有任何相关的内容需要提及,请参阅第 13 行。为什么我们要寻找将我们的资产传递给 GetPool 的池?

  这背后的想法是,您可能需要多个池,每种资产类型一个,因此您需要一种方法来识别您想要访问的池。

  我不是特别喜欢访问静态变量的静态方法,但您应该根据游戏的需要调整代码。

  顺便说一句,您不需要自己复制所有代码。 我准备了一个您可以免费访问的存储库。

访问 GitHub 存储库

池本身的代码如何?


namespace GamedevGuru
{
public class GamedevGuruPool : MonoBehaviour
{
    public bool IsReady { get { return loadingCoroutine == null; } }

    [SerializeField] private int elementCount = 8;
    [SerializeField] private AssetReference assetReferenceToInstantiate = null;
    
    private static Dictionary<object, GamedevGuruPool> allAvailablePools = new Dictionary<object, GamedevGuruPool>();
    private Stack<GameObject> pool = null;
    private Coroutine loadingCoroutine;

    public static GamedevGuruPool GetPool(AssetReference assetReference)
    {
        var exists = allAvailablePools
            .TryGetValue(assetReference.RuntimeKey, out GamedevGuruPool pool);
        if (exists)
        {
            return pool;
        }

        return null;
    }

    public GameObject Take(Transform parent)
    {
        Assert.IsTrue(IsReady, $"Pool {name} is not ready yet");
        if (IsReady == false) return null;
        if (pool.Count > 0)
        {
            var newGameObject = pool.Pop();
            newGameObject.transform.SetParent(parent, false);
            newGameObject.SetActive(true);
            return newGameObject;
        }

        return null;
    }

    public void Return(GameObject gameObjectToReturn)
    {
        gameObjectToReturn.SetActive(false);
        gameObjectToReturn.transform.parent = transform;
        pool.Push(gameObjectToReturn);
    }
    

    void OnEnable()
    {
        Assert.IsTrue(elementCount > 0, "Element count must be greater than 0");
        Assert.IsNotNull(assetReferenceToInstantiate, "Prefab to instantiate must be non-null");
        allAvailablePools[assetReferenceToInstantiate.RuntimeKey] = this;
        loadingCoroutine = StartCoroutine(SetupPool());
    }

    void OnDisable()
    {
        allAvailablePools.Remove(assetReferenceToInstantiate);
        foreach (var obj in pool)
        {
            Addressables.ReleaseInstance(obj);
        }
        pool = null;
    }

    private IEnumerator SetupPool()
    {
        pool = new Stack<GameObject>(elementCount);
        for (var i = 0; i < elementCount; i++)
        {
            var handle = assetReferenceToInstantiate.InstantiateAsync(transform);
            yield return handle;
            var newGameObject = handle.Result;
            pool.Push(newGameObject);
            newGameObject.SetActive(false);
        }

        loadingCoroutine = null;
    }
}
}

  我知道,有点长,但我想把它贴在这里,这样我就可以解释发生了什么。

  就像我之前说的,在第 14 行中,我们为您提供了合适的池,正如在本文中我们的目标是每个预制件都有一个池。 我们为此使用运行时Key,这是我们用来识别可寻址资产的字符串。 其他变体可以包括使用泛型和枚举来代替使用单个池对象。

  在第 30-33 行中,我们从池中取出一个对象,我们将其作为父对象,然后激活它。 您可能希望向该函数添加更多参数,例如位置和旋转。

  我们在第 41-43 行做相反的事情。 就像孩子叛逆离开家一个小时后才回来一样,我们接受了它。 我们停用它并将其父级返回到我们的游戏对象池。

  然后是在第 52 行和第 60 行预热池并将其清空的时候了。我们通过实例化和停用 8 个预制件来预热池。 最后,我们调用 Addressables.ReleaseInstance 来释放内存。

  这里的策略很明确:当我们怀疑我们需要它时启用池,并在我们不需要时禁用/销毁它。


三级开发人员:智能 Unity Addressables 对象池

  Unity Addressables Pooling 系统有很多变体。

  这完全取决于您的游戏目标。

性能
  例如,您可以优先考虑性能。 如果是这种情况,您当然不希望在 pool 的 Take 和 Return 调用中activate/deactivate整个游戏对象。

  activate 非常昂贵。 你想要的是 enable/disable 某些组件,例如渲染器、动画器、画布等。你将停止支付绘制调用而不支付激活时间。

您还可以避免的是过多的设置父级,因为我们也为此付出了高昂的代价。

如果是这种情况,您可能想要使用 PerformancePool。

自动池化
  如果不需要性能并且您宁愿节省时间,您也可以让您的生活更轻松并且仍然可以获得池化的好处。

  您可以选择 AutomaticPool 组件,它会为您处理加载和卸载预制件。 更有趣的是,它会在用户不需要预制件的情况下经过一段时间后释放所有内存。

  如果您对这些即插即用组件感兴趣,您会很高兴知道它们包含在忙碌的开发人员课程的可寻址中。

  高技能的游戏开发人员和我将开始冲刺,以转变和提升我们在 Unity 中制作游戏的方式

  加入我们,寻求精通游戏性能。

你觉得这篇文章怎么样? 发表评论以分享您使用 Addressables 的经验。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值