Notifications
Article
Unity可寻址资源系统:加速工作流程
Updated 9 days ago
2.1 K
12
设想一下,你正在领导由程序员和艺术家组成的团队,要把精美的PS4 VR游戏移植到Oculus Quest。但是你只有一个月时间减半内存预算,并在六个月时间内完成移植过程。
那么首先应该怎么做?我们可以在这时候考虑使用Unity Addressables可寻址资源系统。
我们必须同时处理多项复杂的任务。根据开发者在不同领域的技术水平,不同的开发者会有不同的考虑对象。如果一定要选出最耗费时间的任务,你会选择什么任务?
我猜超过70%的读者会说:在把游戏移植到Oculus Quest平台时,CPU和GPU的性能是最大的问题。我非常赞同这种看法。性能是VR游戏中最难提升的方面。
对于这类优化,我们需要对产品有深入的了解,但这是很耗时的过程。有时我们甚至无法进一步优化,只能牺牲大量游戏内容和图形效果。如果移植出来的成品不符合玩家的期待,这也是非常麻烦的问题。
性能这个词可能会让开发者的背后一凉。Oculus Quest平台的性能是怎样的?项目在Oculus Quest的运行效果如何?
实际上,如果对Oculus Quest有一定开发经验,你可能知道,如果把Oculus Quest作为移动硬件来看,它的功能是非常强大的。
有人可能不同意我的说法,因为手机在打开第二个浏览器标签页时往往会明显变慢,所以他们反对移动平台有很好的性能。
你是不是也是这么想呢?Oculus Quest的主要区别在于它的主动冷却系统,该系统对可用CPU/GPU硬件级别提供了其它移动平台没有的大幅提升效果。这个冷却系统有强力的风扇,可以防止灰尘吹到使用者的头发上,避免硬件CPU融化并流到使用者的脸上,这么说是否让你想起了《权力的游戏》里的类似场景。
此外对于Oculus Quest的软件方面,和通用的Android变体相比,专用操作系统更加适合对虚拟现实渲染进行优化。而且在过去几年中,移动硬件正在快速赶上独立平台。
但是不可否认的是,要实现持续在72 fps下渲染是非常困难的,特别是从高端平台移植的VR项目。准确来说,在谈到Oculus Quest的移植时,我们必须把该平台设想为使用带有屏幕、电池、四个摄像头和一个风扇的高通骁龙835处理器的设备。
这款设备的缺点从其它角度看也可以是优点。该移动平台是经过大量研究的硬件,如今有很多已知的技巧可以快速把其CPU和GPU的负载降低到可以接受的程度。
如果你对此感兴趣,可以在未来的文章中阅读到相关内容。本文中,我们主要考虑性能。
这项任务中的关注点在于,和PS4相比,Oculus Quest上的一项硬件指标减少了一半:RAM容量。PS4的RAM内存是8 GB,而Oculus Quest是4 GB。
这是粗略估算的结果,因为在两个平台上,操作系统都不会允许使用所有内存,这样它可以跟踪一些子系统,让生态系统正常工作。在Oculus Quest上,我们最多可以使用约2.2 GB的RAM内存,否则会出现问题。
出现问题是什么意思呢?其实,合适的内存处理对游戏来说非常重要。这是因为我们有两个限制:
  • 严格的内存限制:如果超过特定使用量阈值,操作系统会直接关闭游戏
  • 缓和的内存限制:除了特定限制外,在用户最小化游戏窗口,脱下VR设备,或走出Oculus Guardian区域时,操作系统也可能会关闭游戏。
显然,我们不希望这些问题出现在游戏中。你是否可以想象到一名玩家在丢失2小时游戏进度后的愤怒?这时候,玩家会打开游戏的商店页面,对游戏给出言辞过激的负面评价。
实际上,2.2 GB的可用RAM内存其实并不多。对于从一开始就跟踪统计数据的新项目来说,这并不是问题,但如果要移植项目到性能大幅降低的硬件,这会是一个大问题。
如果过去处理过类似的移植过程,你一定明白减半游戏的RAM内存预算有多么困难。这项任务取决于游戏架构是否对这种改动做好准备,但在大多数情况下,该任务会让开发者非常难以处理。
减小内存压力的最常用方法包括:调整资源压缩设置,优化脚本,减少着色器变体等。
根据项目的具体情况,调整纹理导入设置是我们的首选方法,但如果需要的话,我们也可以压缩网格、动画和音频。问题在于:这些方法本身非常复杂,而且有一定限制。
不是所有平台都支持相同的导入设置,面向多个设备构建会大幅提升构建管线的开销,更不用说QA阶段,艺术,设计和编程方面的复杂性。开发者要考虑:某款Android设备是否支持ASTC或ETC2?
我们也希望构建64位的版本,同时在32位版本上留住玩家。我们要想好到底要创建多少个APK版本,更糟糕的是,游戏的每次更新都要对每个版本分别管理和测试。我们希望让工作变得轻松,所以我们不应该只依赖这些方法。
我们希望更进一步,让事情尽可能简单,特别是在进行移植的时候。为了性能而重新设计游戏是最坏的选择,不如不进行移植。
由于这些原因,本文会提供实现性能提升的好方法:我将告诉你如何在几小时内减半项目的内存预算。
这难道不是很棒吗?
读者或许会问:这对我来说真的有可能吗?答案是:虽然具体情况取决于项目的起点,但以我的经验来看,答案是肯定的。Unity Addressable可寻址资源系统会起到很大作用。开发者必须使用并掌握该方法的工作流,这个工作流程会帮助你赢得月度员工的称号。
感兴趣的话,请继续阅读。
本文中,我们会介绍传统资源管理方法和可寻址资源管理系统。为了展示这个过程,我们会把简单的旧项目移植到全新的Unity Addressables可寻址资源系统。
现在,或许你会问:为什么不直接展示你参与的实际项目?
如果这是一个不存在任何竞争的世界,我会给你展示我制作的所有内容。但在现实世界,这样做会让我被炒鱿鱼,甚至可能会锒铛入狱。
我要做的是提供指导,和你一起处理相同的示例项目,这个过程中遇到的难题很可能在你未来的项目出现。我们首先会加入Unity Addressables资源包。
本文中,我会介绍Unity Addressables可寻址资源系统,让你可以在数分钟内实现自己的Unity Addressables可寻址资源系统。

使用Unity Addressables的原因

现在我们要转到这个重要部分。我们的目标是分析如何轻松改进内存,快速实现改进效果。方法有很多,其中最有效也最简单的实现方法是:加载初始场景,打开性能分析器。为什么这样做?
因为没有优化过的游戏架构可以在游戏中任意时间点频繁进行分析,所以最快捷的检查方法是:对初始场景进行性能分析。原因是:过度使用内存的类单例脚本通常包含对项目中每个资源的引用,这只为了方便在需要时使用
换句话说,很多游戏经常有开销很大的脚本,它会造成资源引用过度消耗性能。无论在特定时间是否使用某些资源,这类脚本组件会一直独立加载所有资源。
这会造成怎样的后果?
后果会根据具体情况而定。如果游戏容易受到内存容量的限制,那么这是风险很大的使用方法,因为游戏无法随着开发者添加资源而扩展,例如:未来加入的DLC内容。
如果开发者要面向各种各样的设备开发,例如:Android设备,开发者没有一个固定的内存预算,每种设备会提供不同的内存预算,因此开发者必须考虑最糟糕的情况。
在游戏期间,如果用户要回复Facebook上的一条信息,操作系统可能在任意时间决定关闭应用。之后用户返回游戏时会惊讶地发现:所有游戏进度都没了。
这有意思吗?
当然没有。除非你工作过度且睡眠不足,否则这样的情况只会让你苦笑。
更深入考虑这个问题的话,如果之后你决定把游戏移植到性能较弱的平台,同时实现跨平台游戏,这项技术挑战会非常困难,所以只能祝你好运了。
另一方面,是否有传统资源管理方法可以提供很好使用效果的情况呢?答案是肯定的。
如果正在为PS4等同类平台开发项目,大多数要求已经在一开始决定好,使用全局对象的优点可能会胜过新内存管理系统的额外复杂度。
如果有足够好的使用效果,使用传统全局对象满足所有需求是一种很好的解决方案。这样可以简化代码,预加载所有引用的资源。
在任何情况下,传统的内存管理方法不适合打算挑战硬件极限的开发者。由于你在阅读这篇文章,我猜你想提升自己的技能。那么我们现在就开始吧。
开始使用Unity Addressables可寻址资源系统。

Unity Addressables:对项目的要求

如果只打算阅读文字而已,你只要有一个屏幕就够了,但如果要和我一起操作,你需要的东西有:
  • 熟练的双手。
  • 活跃的大脑。
  • Unity 2019.2.0f1或类似版本。
  • 从GitHub上下载的Level 1项目,请通过命令行下载压缩文件。
  • 使用Unity Addressables的意愿。
项目代码库包含三项提交内容,每个对应着本文中的一个技能水平部分。有时我会把项目文件搞混,这时我会提交修复内容。
从GitHub下载地址直接下载ZIP格式的项目。
或者访问GitHub代码库。

Level 1开发者:传统资源管理方法

这部分介绍最简单的资源管理方法。在本例中,这意味着在我们的组件中,会有天空盒材质的直接引用列表。
如果跟我一起进行操作,设置过程只需简单的三步:
Level 1的简单步骤:
  1. 通过Git下载项目。在前一部分可以找到ZIP文件的链接,如果熟悉使用命令行,你也可以运行命令行来获取项目文件。
  2. 在Unity打开项目。打开Unity Hub,添加Add按钮。找到下载目录,打开项目文件。
  3. 按下Play按钮。在Windows系统,按下Ctrl+P,在Mac系统,按下CMD+P。
很好,我们可以按下按钮修改天空盒。这种方法很原始,也很乏味。我们不会在此使用可寻址资源系统。
很快我们就会明白为什么要忍受这种短暂的无聊。
首先,我们的项目结构是怎样的?项目结构围绕着两个主要系统。一方面,我们有SkyboxManager游戏对象。其中的组件是保存天空盒材质引用的主要脚本,可以根据UI事件来切换材质。
using UnityEngine;
public class SkyboxManager : MonoBehaviour
{
[SerializeField] private Material[] _skyboxMaterials;
public void SetSkybox(int skyboxIndex)
{
RenderSettings.skybox = _skyboxMaterials[skyboxIndex];
}
}
SkyboxManager对象为UI系统提供功能,可以通过使用RenderSettings API,应用特定材质到场景设置。
第二,我们有CanvasSkyboxSelector。该游戏对象包含画布组件,它会渲染一组垂直分布的按钮。点击每个按钮时,它会调用之前提到的Manager函数,根据按钮id交换渲染的天空盒。
换句话说,每个按钮的OnClick事件会在Manager对象调用SetSkybox函数。
或许这个场景会让你想到《X2: The Threat》游戏,但现在我们要开始处理了。按下Ctrl+7或Cmd+7,或者点击Window > Analysis > Profiler,打开性能分析器。
如果你熟悉这个工具的话,可能知道如何使用顶部的录制按钮。录制几秒后,停止录制,查看各项指标信息:CPU,内存等。是否有特别注意的指标信息?
项目的性能结果很好,考虑到项目的大小,这不是什么令人惊讶的事。你可以把该项目转换为VR体验,但是用户不会从中获得《Eve: Valkyrie》的类似游戏体验。
此时我们会专注于内存部分。简单的视图模式会展示以下内容:
考虑到我们只会在一段时间展示一个天空盒,纹理大小的数值非常夸张。这是我们会在很多未优化项目中发现的情况。
但在示例场景中,我们只使用了几个天空盒而已。而其它项目中往往会有角色,星球,音效,音乐等更多内容。
如果你的任务是处理诸多资源,那么我很高兴你在阅读这篇文章。我会帮助你从传统方法过渡到可扩展的方法。
现在我们要把内存分析部分切换为细节模式,然后进行查看。
这是什么情况?所有天空盒纹理都加载到内存中,但每次只会显示一个天空盒。你是否看出了特点?这个傻瓜的架构使用了400MB左右的内存。
这样的结果无法令人满意,特别是考虑到这只是游戏的一小部分。解决这个问题是我们下一部分的重点。
请跟着我一起做。

小结

  • 传统资源管理方法需要直接的引用。
  • 因此要一直加载所有对象。
  • 项目无法以这种方法扩展。

Level 2开发者:Unity Addressables工作流程

游戏中我们往往会从Level 1部分的情况开始,但在我们知道游戏规则后,是时候离开原来的安全区,开始提升技能。这也是这部分的重点。
获取Level 2项目文件:
直接从GitHub地址下载ZIP格式的项目文件。
或者访问GitHub项目页面进行下载。
通过之前对性能分析器的观察,我们知道:虽然每次只会使用一个天空盒,但是项目中所有天空盒都会加载到内存中。因为我们可能会遇到不同资源变体数量的限制,所以这并不是有可扩展性的解决方案。那么建议是什么?不要限制玩家的乐趣。
现在让我告诉你如何摆脱传统资源管理方法的束缚。首先添加新工具Unity Addressables可寻址资源系统的API。
首先我们要安装Addressables资源包,打开Window > Package Manager,窗口如下图所示:
安装好Addressables资源包后,我们要把材质标记为可寻址资源,在检视窗口勾选Addressable。
这会让Unity在可寻址资源数据库包含这些材质和纹理依赖。该数据库会在我们的构建中用来打包资源为多个部分,从而在游戏中任意时候轻松加载。
现在厉害的部分要来了。打开Window > Asset Management > Addressables,此时会打开我们的数据库。
之前的操作很简单。下面是有趣的部分
我们现在要查看之前的SkyboxManager脚本。我们注意到,该脚本仍旧保存着资源的直接引用。我们不希望这样。
我们要告诉该脚本如何使用间接引用,即使用AssetReference.
下面我们要优化脚本组件:
using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; public class SkyboxManager : MonoBehaviour { [SerializeField] private List<AssetReference> _skyboxMaterials; private AsyncOperationHandle _currentSkyboxMaterialOperationHandle; public void SetSkybox(int skyboxIndex) { StartCoroutine(SetSkyboxInternal(skyboxIndex)); } private IEnumerator SetSkyboxInternal(int skyboxIndex) { if (_currentSkyboxMaterialOperationHandle.IsValid()) { Addressables.Release(_currentSkyboxMaterialOperationHandle); } var skyboxMaterialReference = _skyboxMaterials[skyboxIndex]; _currentSkyboxMaterialOperationHandle = skyboxMaterialReference.LoadAssetAsync(); yield return _currentSkyboxMaterialOperationHandle; RenderSettings.skybox = _currentSkyboxMaterialOperationHandle.Result; } }
我们做的事情如下:
主要改动发生在第7行,我们在此利用AssetReference保存了一组间接引用。这项改动非常重要,因为这样修改后,材质在被引用时不会自动加载。我们必须明显指定加载操作,它们才会进行加载。之后,请在编辑器重新指定该字段。
第13行:由于我们现在使用异步工作流程,因此我们倾向于使用协程。我们会开启一个新的协程,用于处理天空盒材质的变化。
我们会在第18到20行,检查我们是否有天空盒材质的句柄,如果有的话,我们会放出之前渲染的天空盒。
每次我们使用Addressables API执行这类加载操作时,我们都会得到为之后操作保存的句柄。句柄是一种数据结构,包含有关特定可寻址资源管理的数据。
第23行,我们会把特定可寻址引用解析到天空盒材质,然后调用它的LoadAssetAsync函数,我们在第25行使用yield关键字,因此我们会在进一步处理前等待这项操作。由于使用了泛型,所以我们不需要开销较大的调用。
最后,在材质和依赖加载后,我们会在第26行修改场景的天空盒。材质会在Result字段提供,该字段属于我们用来加载材质的句柄。
注意:这些代码目前不适合用到正式产品中。不要在实际项目使用它。为了让内容保持简单,和稳定性相比,我会更注重简洁性。
构想到此结束。现在我们要看该方法的实际效果。
我们要执行的步骤如下:
  1. 在Addressables窗口中,单击Build Player Content,构建内容。
  2. 然后为自己选择的平台构建版本。
  3. 运行版本,把它和内存分析器连接起来。
  4. 准备好大吃一惊。
资源构建过程是不是很不错?
我喜欢出色的性能分析结果,而这是我们看到最出色的性能分析结果。让人满意的性能分析结果可以改善多个方面。首先,它意味着玩家可以在低端设备上玩你的游戏。其次,它也会让开发者感到高兴,带来更多收入。
这就是Addressables可寻址资源系统的强大功能。
Addressables系统会给团队带来少量成本。一方面,程序员必须支持异步工作流程,使用协程很容易实现该工作流程。
其次,设计师需要学习Addressables系统的功能,例如:可寻址分组,积累经验来做出聪明的决策。最后,如果打算在线托管资源,开发人员要设置架构,从而在网络上提供资源。
我们实现的效果如下:
  • 适当的内存管理功能。
  • 更快的初始加载速度。
  • 更快的安装速度,减少应用的存储大小。
  • 更好的设备兼容性。
  • 异步架构。
  • 能够在网上存储内容,把数据和代码互相分离。
我很高兴取得这些效果,这是付出努力的很好回报。
请记得在自己的求职面试提到自己的Addressables系统使用经验。

中级部分:实例化和引用计数

目前的进度很好。我们把Unity Addressables工作流程应用到了天空盒,过程十分简单。这种简单特点的原因在于,我们每次只会启用一个天空盒,因此管理天空盒生命周期的方法非常直观。
但我们在游戏中要管理的东西不只是天空盒。
举个例子,我们可能会给游戏角色添加变体。这个角色可能是玩家角色,也可能是敌人角色。这样难度就变大了,现在我们不只是要卸载资源并加载新资源。可能的情况是,我们打算卸载的资源正在被其它实例使用。
这种情况下如何进行内存管理呢?
幸运的是,Unity可以使用自带的集成引用计数器来处理这种情况。这意味着:每次我们实例化某个资源时,例如:角色预制件,它会自动增加引用数量。
每次我们卸载资源时,引用计数会减小。如果引用数量减小为0,即不存在任何实例,那么该资源便可从内存卸载。这些操作可以使用下列结构完成:
var operationHandle = prefabMaterialReference.InstantiateAsync(transform, true);
yield return operationHandle;
// ...
Addressables.ReleaseInstance(operationHandle);
注意:过去Unity版本的Instantiate和Destroy不会更新引用计数,这些计数只受到Addressables系统的InstantiateAsync和ReleaseInstance变体的影响。
因此,我们要避免在使用Addressables系统时,使用到Instantiate和Destroy。

可选:其它加载方式

如果打算使用硬编码的资源字符串标识符,而不是使用AssetReferences,我们可以在Manager类中使用其它结构:
它们的行为是相似的,该结构会返回开发者可以命令的async操作句柄。由于它有泛型形式,Result字段会设为我们最初传入方法调用的类型。
_currentSkyboxMaterialOperationHandle = Addressables.LoadAssetAsync<Material>("Skybox" + skyboxIndex);
我们传入函数的字符串标识符不仅可以在资源检视窗口设置,也可以在Addressables主窗口设置。
此外,我们也可以加载某个标签的所有资源。这样有助于预加载所有会在后面关卡生成的敌人。

小结

  • Addressables资源管理方法可以很好地扩展。
  • Addressables系统加入了异步行为。
  • 别忘了在改动后准备内容。

小结

本文开始时,我介绍了自己遇到的挑战:在一个月内,为面向Oculus Quest开发的项目减少一半的内存预算。那么结果如何?我完成了任务。
这款强大的Unity资源包即Addressables系统很好地帮助了我。该资源包帮助我按时完成耗费时间的任务,避免对大量游戏内容进行重新设计,通过导入设置几乎无法实现这些目标。
Addressables可寻址资源系统是一个很有前景的系统。一方面,Unity开发人员正在努力改进该系统。另一方面,它适用于正式制作,而且提供完善的文档。
这意味着:我们可以现在就把它用到项目中。如果按照本文和指南的内容,只需少量时间,你就可以在自己的项目使用该系统。
我们可以把它看做一项中期投入。我们要投入一些时间,让项目在未来数月或来年得到提升。不仅是我们会从中受益,由于游戏可以支持更多设备,更多玩家也将能够玩你的游戏。
请分享你的看法。

作者Rubén Torres Bonet简介

Rubén Torres Bonet是一名优秀的游戏开发者,如今他希望帮助读者开发出更好的游戏。Rubén Torres Bonet参与过多个游戏项目的开发,包括面向Oculus Quest,Android/iOS,PS4,Xbox One,这些游戏在全球的累计安装量超过3000万次。他参与过的游戏包括:《Time Stall》,《Catan Universe》,《Anne Frank House VR》,《Diamond Dash》,《Jelly Splash》,《Blackguards Definitive Edition》。
原文链接:https://thegamedev.guru/unity-addressables-learn-workflows/
开发过程中遇到问题?在这里提问:connect.unity.com/g/discussion
觉得这篇内容不错or有待提高?请在下方评论区留言。我们会根据大家的需求,优化内容产出^_^
Tags:
Unity开发者原创
9
Comments
Pengyue Li
Staff
7 days ago
Developer Relations Engineer
Sir Wu为什么一定要用2019.2呢?2018.4可以吗?
2018.3以上都可以
0
SW
Sir Wu
7 days ago
男爵
为什么一定要用2019.2呢?2018.4可以吗?
0
Pengyue Li
Staff
8 days ago
Developer Relations Engineer
hyc就比如写个扇形子弹弹幕,需要根据i值做子弹角的间隔计算,这不就要用到i值了?
是,那就得自己写逻辑了
0
hyc
8 days ago
Pengyue LiCompleted的参数里有实例化好的GameObject对象,不需要依赖 i
就比如写个扇形子弹弹幕,需要根据i值做子弹角的间隔计算,这不就要用到i值了?
0
Pengyue Li
Staff
8 days ago
Developer Relations Engineer
hyc就比如for循环100次是一帧内的事,Completed回调最快也得在下一帧,拿到的 i 值已经是第99了。
Completed的参数里有实例化好的GameObject对象,不需要依赖 i
0