|
当我谈论资源管理的时候我在谈论什么?
平衡 和 预算
平衡: 这是资源管理的首要原则,遵循着“申请”和“释放”平衡的原则。只有这样才能达到需要时加载,不需要的时候及时释放资源的目的。预算: 这是资源管理的前提约束条件,现实世界中很多资源都是有限的。在移动游戏开发过程中的常见预算就有内存和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目录加载开始说起,项目开天辟地之际,我们的主人公小白就写下了第一行代码:- public class Initializer01 : MonoBehaviour
- {
- void Start()
- {
- Debug.Log("Hello World!");
- }
- }
复制代码 一切都很好,直到有一天,技术主管老白说:“要有图”。于是便有了图:- public class Initializer02 : MonoBehaviour
- {
- public UITexture backgroundTexture;
- void Start()
- {
- // 从 Resource 加载名为 Unity_Logo 图片资源
- Texture texture = Resources.Load<Texture>(&#34;Unity_Logo&#34;);
- backgroundTexture.mainTexture = texture; // 将图片资源赋值给 backgroundTexture.mainTexture
- backgroundTexture.MakePixelPerfect(); // 显示图片原始大小,防止拉伸
- }
- void OnDestroy()
- {
- // 卸载资源,backgroundTexture.mainTexture 指向的图片资源
- Resources.UnloadAsset(backgroundTexture.mainTexture);
- }
- }
复制代码 这里小白用了 NGUI 作为 UI 插件,在 Start 中加载图片资源,在 OnDestroy 中释放图片资源,符合我们之前说的资源管理的“平衡”原则,一切都很美好,直到小白遇到了加载两张同样的资源:
当小白将上面的脚本挂在两个不同的 GameObejct 上,并分别指定不同的显示图片,加载显示的是同样的图片。但是当销毁其中一个 GameObject 时,另外的图片也消失不见了!
小白猜测是由于同名资源尽管加载两次,但实际上指向的是同一份资源,Unity 这么做也有它的道理,如果不复用同名资源,那么加载一次就会多一份资源拷贝,这是无意义的,下面的测试代码也验证了这个结论:- public class Initializer04 : MonoBehaviour
- {
- void Start()
- {
- // 从 Resource 加载两次 Unity_Logo 图片资源
- Texture texture1 = Resources.Load<Texture>(&#34;Unity_Logo&#34;);
- Texture texture2 = Resources.Load<Texture>(&#34;Unity_Logo&#34;);
- Debug.Log(object.ReferenceEquals(texture1, texture2)); // True
- }
- }
复制代码 这是小白遇到的第一个问题:资源引用计数。
接着又出现了第二个问题:技术主管老白刚从网上看到一篇文章,决定把资源加载改成异步,这样做是为了防止卡顿。
小白心里估算了一下改成异步可能会出现问题:
假设异步加载耗时10s,玩家在5s的时候就等得不耐烦了,关闭界面直接销毁了 GameObject。此时并没有获取到资源,无需释放,等10s的时候,资源加载完成,因为此时对象已经被销毁了,会导致逻辑报错和资源泄露。在同一个对象载入同一个资源时,即一个对象对资源有两个引用,那么逻辑上要支持“先入先出”式地卸载,即下面的代码应该只有第二个有回调:
- // 同时加载两次相同的资源
- ResourceManager.Instance.GetResourceAsset<Texture>(&#34;Unity_Logo&#34;, this).Then(回调1...)
- ResourceManager.Instance.GetResourceAsset<Texture>(&#34;Unity_Logo&#34;, this).Then(回调2...)
- // 卸载资源,只有回调2有作用,第一个引用已被卸载
- ResourceManager.Instance.DestroyResourceAsset(&#34;Unity_Logo&#34;, this);
复制代码 这是小白遇到的第二个问题: 异步加载。
小白略微思考了一下这两个问题,这还只是比较简单的 Resources目录 资源管理,后面还有 AssetBundle 资源管理,于是小白写下了这样的代码:- public class Initializer05 : MonoBehaviour
- {
- void Start()
- {
- Debug.Log(&#34;Bad World!&#34;);
- }
- }
复制代码 于是,小白卒。
可能的解决方案
让我们把 资源引用计数 和 异步加载 结合起来看,异步加载 分三个阶段:未开始、进行中、已完成。异步加载一个资源,发现资源一共可能是三种状态:
未开始: 开始加载资源,状态切换到进行中,设置资源引用计数,设置加载完成后回调。进行中: 设置资源引用计数,设置加载完成后回调。已完成: 设置资源引用计数,然后直接返回该资源。
资源管理还涉及到卸载,这里我们使问题简单化,卸载操作是“同步”的:首先让一个资源和对象解除关联,如果一个资源所有的引用都解除了,那么就可以卸载这个资源了。卸载时也会遇到资源状态问题:
未开始: 报错,不能卸载未加载的资源。进行中: 解除对象对资源的引用,加载完成逻辑如果发现资源已经没对象引用了,需要卸载。已完成: 解除对象对资源的引用,如果资源没有对象引用,卸载。
根据之前的文章,资源加载涉及到复杂的异步操作,我们可以借助 Promises 来完成,那么优雅的接口看起来是这样子的:- public class Initializer06 : MonoBehaviour
- {
- public UITexture backgroundTexture;
- void Start()
- {
- ResourceManager.Instance.GetResourceAsset<Texture>(&#34;Unity_Logo&#34;, this)
- .Then(texture =>
- {
- backgroundTexture.mainTexture = texture; // 将图片资源赋值给 backgroundTexture.mainTexture
- backgroundTexture.MakePixelPerfect(); // 显示图片原始大小,防止拉伸
- })
- .Catch(ex =>
- {
- // 如果资源加载完成对象已经解除引用了,异常
- Debug.LogException(ex);
- });
- }
- void OnDestroy()
- {
- // 解除资源对象的引用
- ResourceManager.Instance.DestroyResourceAsset(&#34;Unity_Logo&#34;, this);
- }
- }
复制代码 尾声
好了,资源管理到这里就说完了,根据这篇文章和前置的 AssetBundle 相关文章,相信聪明的你封装一个资源管理并不是一件难事。如果你实在是懒,可以参考 github代码 ,其中封装了 Resources目录、WWW 和 AssetBundle的资源管理,用到的依赖的库 有 UniRx 和 C-Sharp-Promise。
最后,惯例感谢各个开源库和Unity ;-)。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|