找回密码
 立即注册
查看: 651|回复: 0

[笔记] 谈谈Unity资源管理

[复制链接]
发表于 2020-12-15 09:02 | 显示全部楼层 |阅读模式
当我谈论资源管理的时候我在谈论什么?

平衡预算
    平衡: 这是资源管理的首要原则,遵循着“申请”和“释放”平衡的原则。只有这样才能达到需要时加载,不需要的时候及时释放资源的目的。预算: 这是资源管理的前提约束条件,现实世界中很多资源都是有限的。在移动游戏开发过程中的常见预算就有内存和CPU:超过了内存预算可能导致游戏闪退,超过了单帧CPU预算的话会导致卡顿。
一点小小的历史



上面这张图是 Unite Austin 2017 上的一个演讲 slides 中的一页“重制版”。这张图高度概括了Unity资源管理的历史:从 “直接引用” 到 “Resources目录”,到现在的 “AssetBundle”,以及 Unity 2018中将推出的 “Unity Resource Manager”(图中未标出) 。
    直接引用: 直接引用是直接Unity编辑器中设置好相关引用,缺点是资源管理不便。Resources目录: 之前有文章介绍过,主要缺点是不能发布后更新,及可能会延长游戏启动时间。AssetBundle: 一切都很好,除了。。。额,除了比较复杂这一点之外,一切都很好。Unity Resource Manager: 官方翻译是: ResourceManager 是一个异步加载卸载资源的可扩展高层API,目的是通过一个接口来统一不同加载过程。听起来很美好,还未试用过,没有发言权。
现在我们正好站在一个风口浪尖上,前有 Unity的各种资源加载接口,后又刚好在 Unity 2018 将推出新的API来统一加载接口之际,我们来总结一下资源管理那么再合适不过了 ;-) 。
资源管理从入门到放弃

我们先从简单的Resources目录加载开始说起,项目开天辟地之际,我们的主人公小白就写下了第一行代码:
  1. public class Initializer01 : MonoBehaviour
  2. {
  3.     void Start()
  4.     {
  5.         Debug.Log("Hello World!");
  6.     }
  7. }
复制代码
一切都很好,直到有一天,技术主管老白说:“要有图”。于是便有了图:
  1. public class Initializer02 : MonoBehaviour
  2. {
  3.     public UITexture backgroundTexture;
  4.     void Start()
  5.     {
  6.         // 从 Resource 加载名为 Unity_Logo 图片资源
  7.         Texture texture = Resources.Load<Texture>("Unity_Logo");
  8.         backgroundTexture.mainTexture = texture; // 将图片资源赋值给 backgroundTexture.mainTexture
  9.         backgroundTexture.MakePixelPerfect();    // 显示图片原始大小,防止拉伸
  10.     }
  11.     void OnDestroy()
  12.     {
  13.         // 卸载资源,backgroundTexture.mainTexture 指向的图片资源
  14.         Resources.UnloadAsset(backgroundTexture.mainTexture);
  15.     }
  16. }
复制代码
这里小白用了 NGUI 作为 UI 插件,在 Start 中加载图片资源,在 OnDestroy 中释放图片资源,符合我们之前说的资源管理的“平衡”原则,一切都很美好,直到小白遇到了加载两张同样的资源:
当小白将上面的脚本挂在两个不同的 GameObejct 上,并分别指定不同的显示图片,加载显示的是同样的图片。但是当销毁其中一个 GameObject 时,另外的图片也消失不见了!
小白猜测是由于同名资源尽管加载两次,但实际上指向的是同一份资源,Unity 这么做也有它的道理,如果不复用同名资源,那么加载一次就会多一份资源拷贝,这是无意义的,下面的测试代码也验证了这个结论:
  1. public class Initializer04 : MonoBehaviour
  2. {
  3.     void Start()
  4.     {
  5.         // 从 Resource 加载两次 Unity_Logo 图片资源
  6.         Texture texture1 = Resources.Load<Texture>("Unity_Logo");
  7.         Texture texture2 = Resources.Load<Texture>("Unity_Logo");
  8.         Debug.Log(object.ReferenceEquals(texture1, texture2));      // True
  9.     }
  10. }
复制代码
这是小白遇到的第一个问题:资源引用计数
接着又出现了第二个问题:技术主管老白刚从网上看到一篇文章,决定把资源加载改成异步,这样做是为了防止卡顿。
小白心里估算了一下改成异步可能会出现问题:
    假设异步加载耗时10s,玩家在5s的时候就等得不耐烦了,关闭界面直接销毁了 GameObject。此时并没有获取到资源,无需释放,等10s的时候,资源加载完成,因为此时对象已经被销毁了,会导致逻辑报错和资源泄露。在同一个对象载入同一个资源时,即一个对象对资源有两个引用,那么逻辑上要支持“先入先出”式地卸载,即下面的代码应该只有第二个有回调:
  1. // 同时加载两次相同的资源
  2. ResourceManager.Instance.GetResourceAsset<Texture>("Unity_Logo", this).Then(回调1...)
  3. ResourceManager.Instance.GetResourceAsset<Texture>("Unity_Logo", this).Then(回调2...)
  4. // 卸载资源,只有回调2有作用,第一个引用已被卸载
  5. ResourceManager.Instance.DestroyResourceAsset("Unity_Logo", this);
复制代码
这是小白遇到的第二个问题: 异步加载
小白略微思考了一下这两个问题,这还只是比较简单的 Resources目录 资源管理,后面还有 AssetBundle 资源管理,于是小白写下了这样的代码:
  1. public class Initializer05 : MonoBehaviour
  2. {
  3.     void Start()
  4.     {
  5.         Debug.Log("Bad World!");
  6.     }
  7. }
复制代码
于是,小白卒。
可能的解决方案

让我们把 资源引用计数异步加载 结合起来看,异步加载 分三个阶段:未开始、进行中、已完成。异步加载一个资源,发现资源一共可能是三种状态:
    未开始: 开始加载资源,状态切换到进行中,设置资源引用计数,设置加载完成后回调。进行中: 设置资源引用计数,设置加载完成后回调。已完成: 设置资源引用计数,然后直接返回该资源。
资源管理还涉及到卸载,这里我们使问题简单化,卸载操作是“同步”的:首先让一个资源和对象解除关联,如果一个资源所有的引用都解除了,那么就可以卸载这个资源了。卸载时也会遇到资源状态问题:
    未开始: 报错,不能卸载未加载的资源。进行中: 解除对象对资源的引用,加载完成逻辑如果发现资源已经没对象引用了,需要卸载。已完成: 解除对象对资源的引用,如果资源没有对象引用,卸载。
根据之前的文章,资源加载涉及到复杂的异步操作,我们可以借助 Promises 来完成,那么优雅的接口看起来是这样子的:
  1. public class Initializer06 : MonoBehaviour
  2. {
  3.     public UITexture backgroundTexture;
  4.     void Start()
  5.     {
  6.         ResourceManager.Instance.GetResourceAsset<Texture>("Unity_Logo", this)
  7.             .Then(texture =>
  8.             {
  9.                 backgroundTexture.mainTexture = texture; // 将图片资源赋值给 backgroundTexture.mainTexture
  10.                 backgroundTexture.MakePixelPerfect();    // 显示图片原始大小,防止拉伸
  11.             })
  12.             .Catch(ex =>
  13.             {
  14.                 // 如果资源加载完成对象已经解除引用了,异常
  15.                 Debug.LogException(ex);
  16.             });
  17.     }
  18.     void OnDestroy()
  19.     {
  20.         // 解除资源对象的引用
  21.         ResourceManager.Instance.DestroyResourceAsset("Unity_Logo", this);
  22.     }
  23. }
复制代码
尾声

好了,资源管理到这里就说完了,根据这篇文章和前置的 AssetBundle 相关文章,相信聪明的你封装一个资源管理并不是一件难事。如果你实在是懒,可以参考 github代码 ,其中封装了 Resources目录、WWW 和 AssetBundle的资源管理,用到的依赖的库 有 UniRx 和 C-Sharp-Promise。
最后,惯例感谢各个开源库和Unity ;-)。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-23 12:40 , Processed in 0.094300 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表