Unity WebGL内存优化:部分Deux

本文最初发布在 Kongregate的Developer Blog上

现在,Unity WebPlayer几乎已经成为过去,确保Unity WebGL内容流畅运行比以往任何时候都更为重要。 我们从开发人员那里听到的主要痛点与最终用户在尝试玩WebGL游戏时遇到的可怕的“内存不足”错误有关。 对于使用32位版本浏览器的播放器来说,这尤其令人沮丧,因为它们不太可能为Unity堆提供大量连续的可用内存块。

本文将介绍更多有关诊断和解决Unity WebGL游戏中与内存相关的问题的技巧。

监视内存使用情况

在分析和优化Unity WebGL游戏时,跟踪多种内存使用情况非常重要。 第一个是Unity堆,默认为256MB,可以在“ WebGL内存大小”下的“发布设置”界面中进行更改。我们谈到了针对此内存块的一些优化技术(以及为什么要使其尽可能小) )在上一篇博客文章中。 重申一下,您需要为Unity堆保留的内存越少,浏览器将可用于其他事物(例如音频,IndexedDB数据库等)的内存就越大。

由于减小Unity堆的大小非常重要,因此我们在WebGL实用程序包中添加了一个非常简单的内存统计信息类(基于Unity团队的出色博客),以帮助开发人员跟踪此信息并将其报告给浏览器控制台。

只需将包导入到您的Unity项目中,然后将“ WebGL Memory Stats”脚本添加到场景中的GameObject中,有关空闲/总内存的日志条目将在您选择的间隔内在浏览器控制台中进行记录:

重要的是要注意,此处Unity / Emscripten提供的数据似乎是内存使用率的高水位线。 也就是说,清理对象时,已用/可用内存不会更改。 但是,它对于衡量和调整要为Unity堆分配多少内存的工具仍然至关重要。 您应该使用此工具来跟踪已使用的最大内存量,然后在此之上添加一个安全网,同时舍入到下一个最高的16MB。

在此示例中,您可以看到我们的应用程序正在为堆请求高达256MB(默认)的内存,而实际上我们应该请求约32MB的内存:

资产捆绑和IDBFS:沉默的杀手

内存相关问题的另一个来源是Unity使用的IndexedDB文件系统。 每当您缓存资产捆绑包或使用任何与文件系统相关的方法时,它们都将存储在由IndexedDB支持的虚拟文件系统中。

您可能没有意识到,一旦您的Unity应用程序启动,该虚拟文件系统便被加载到内存中并持久保存在内存中。 这意味着,如果您对资产捆绑包使用默认的Unity缓存机制,则会将所有这些捆绑包的大小添加到游戏的内存需求中, 即使未加载它们

您可以使用开发人员工具的“应用程序”标签在Chrome中跟踪此内存使用情况。 如您在这里看到的,缓存中存储了多个捆绑包,所选的捆绑包接近20MB:

为了了解这对总内存消耗的影响,我们可以使用Chrome(或Firefox)在应用程序生命周期的各个时间点拍摄堆快照。 对于此演示,我们在应用程序首次加载且没有任何缓存时拍摄第一个快照,然后在加载一些大型资产包之后重新拍摄,在重新加载页面后再次拍摄。

这是一个加载使用WWW.LoadFromCacheOrDownload的WebGL项目的示例。

您可以看到,在初始加载后,我们正在使用大约230MB的内存。 加载所有资产捆绑包后,我们的容量为300MB,然后重新加载页面( 不是资产捆绑包)后,我们的容量仍接近300MB。 这个不好。

幸运的是,Unity团队的优秀成员为我们提供了一个名为CachedXMLHttpRequest的插件。 与非缓存的UnityWebRequest调用CachedXMLHttpRequest使用时, CachedXMLHttpRequest使用单独的IndexedDB缓存来存储下载的文件,这些文件在内存中不保持持久性,从而减少了内存使用量。 让我们看一下使用CachedXMLHttpRequest的示例:

内存使用量最初约为236MB,然后在加载和缓存资产捆绑包后增加至237MB,然后在重新加载页面后又降至226MB。 这很了不起,因为我们消除了IndexedDB虚拟文件系统引起的永久性内存膨胀。

重要的是要注意,当使用CachedXMLHttpRequest时,由于对包的处理方式不同,WebGL堆的使用量可能会增加,因此您应注意不要同时加载太多的包(无论如何这都是最佳做法),并且始终确保卸载完成后,他们。 优化资产捆绑包在本文中有点超出范围,但是总的来说,您希望使其尽可能细化,以免将未使用的资产保留在内存中,并避免由于加载大型资产捆绑包而导致的内存高峰。

Kongregate WebGL实用程序包

不幸的是, CachedXMLHttpRequest的原始版本存在一些错误:

  • 在Firefox私有浏览模式下显示错误对话框
  • 在iframe中与Safari一起使用时,该插件不起作用
  • 同步XHR请求用于重新验证资源

我们已经发布了作为WebGL实用工具包的一部分的更新版本,该版本可以直接替换,可以解决上述问题,并添加以下功能:

  • 支持从Unity异步查询缓存以确定项目是否存在
  • 添加了为不应缓存的项目和不应重新验证的项目配置黑名单的功能
  • 允许从Unity清除缓存

我们将尝试与Unity WebGL团队联系,以将这些功能合并到官方插件中,因为在将多个项目转换为CachedXMLHttpRequest时我们发现它们是必需的。

转换为CachedXMLHttpRequest

如果打算将项目转换为使用CachedXMLHttpRequest ,则可能要使用Caching类上的方法来清理以前缓存的资产包,或将IndexedDB的最大已用磁盘空间设置为较小的数量,以确保您的旧磁盘资产未占用磁盘或内存上的任何空间。

如果要删除缓存,则需要在访问任何资产捆绑包或其他文件之前执行此操作,最好在初始场景中存在的GameObject的Awake方法中进行。 我们发现,对于某些版本的Unity, CleanCache方法可能不可靠。 在这种情况下,您可以使用以下代码直接使用IndexedDB清除缓存,但要注意,这将清除整个域的缓存,这一点很重要。 应该在包含Unity Loader之前将其插入到WebGL index.html或模板中。

(function clearCache() {
var idb = window.indexedDB || window.mozIndexedDB ||
window.webkitIndexedDB || window.msIndexedDB;
if (!idb) return;

var open;
try { open = idb.open('/idbfs') } catch(e) { return; }

var errorHandler = function(e){ e.preventDefault(); e.stopImmediatePropagation(); };
open.onabort = open.onerror = errorHandler;
open.onupgradeneeded = function(upgradeEvent) {
upgradeEvent.target.transaction.abort();
};

open.onsuccess = function(openEvent) {
var db = openEvent.target.result;
db.onerror = db.onabort = errorHandler;

try {
var store = db.transaction('FILE_DATA', 'readwrite').objectStore('FILE_DATA');
store.openCursor().onsuccess = function(cursorEvent) {
var cursor = cursorEvent.target.result;
if (cursor) {
if (cursor.key.indexOf('/UnityCache/Shared/') !== -1) cursor.delete();
cursor.continue();
}
};
} catch(e) {}

db.close();
};
})();

当前版本CachedXMLHttpRequest中有一个小错误,它将导致它在Firefox私有浏览模式下生成错误对话框。 可以通过在加载UnityLoader.js脚本之前运行以下JavaScript代码来解决此问题, 如果您正在使用插件的Kongregate版本,则不需 UnityLoader.js

window.addEventListener('error', function(e){
if (e.message.indexOf('InvalidStateError') !== -1) {
e.stopImmediatePropagation();
}
}, false);

您可能还需要修改CORS配置以公开ETag头,并允许HEAD HTTP请求通过CachedXMLHttpRequest加载的任何资产。

使用CachedXMLHttpRequest时要注意的CachedXMLHttpRequest是,默认情况下将缓存所有使用WWWUnityWebRequest进行的GET请求。 如果您在查询字符串上生成带有缓存清除器或时间戳的请求,则可能会出现问题。 该类的Kongregate版本允许您配置黑名单,以防止根据需要缓存资产。

WebAudio内存使用情况

另一个需要注意的重要事情是游戏播放的音频未压缩地存储在内存中。 根据您的使用情况,这可能会导致内存使用量大幅增加。

同样,Unity团队在商店中也有资产可以帮助解决此问题。 不幸的是,当前的解决方案似乎在循环音频方面存在问题,因此它对音乐可能没有太大帮助,音乐可能是游戏中大型音频文件的主要来源。

参考文献

  • Unity博客文章详细介绍了Unity WebGL堆
  • Kongregate WebGL实用程序包:包括CachedXMLHttpRequest的改进版本以及WebGL内存报告器。