在过去几周里,我一直致力于开发我最喜欢的拼图游戏之一的VR版本,即Nonogram,也称为Picross或Griddlers。这些谜题要求你通过使用列和行计数来确定网格中哪些单元格应被着色。我认为这将非常适合一款轻松休闲的VR游戏。我称之为Lava Flow。
由于Lava Flow是一款休闲游戏,我希望它能够快速加载。我的目标是使整个游戏尽可能小,并在3G网络连接下加载时间不超过10秒。我还希望它以每秒60帧(fps)或更高的速度稳定运行。稳定的帧率是开发WebVR应用程序时最重要的考虑因素。
首先进行衡量
在优化性能或大小之前,我需要了解我的起点。查看应用程序实际大小的最佳方法是使用Firefox开发者工具的网络选项卡。以下是如何使用它。
打开Firefox开发者工具的网络选项卡。点击禁用缓存,然后重新加载页面。在页面底部,我可以看到页面的总大小。当我开始这个项目时,它的大小是1.8MB。
我的应用程序使用了流行的开源3D库three.js。毫不奇怪,最大的部分是three.js本身。我并没有使用最小化版本,所以它超过了1MB!通过加载three.min.js,大小现在变为536KB vs 1110KB。不到原来的一半。
游戏使用了两个岩石的3D模型。它们使用GLB格式,这种格式经过压缩和优化,适用于网页使用。我的两个岩石每个都小于3KB,因此没有太多优化空间。我编写的JavaScript代码是一堆小文件。我可以在以后使用压缩器来减少获取次数,但现在还不用担心。
图像压缩
接下来最大的资源是两种熔岩纹理。它们是PNG格式,总共加起来有561KB。
我将两种熔岩纹理重新压缩为中等质量的JPEG格式。此外,由于凹凸贴图图像不需要与彩色纹理一样高分辨率,我将其从512×512调整为128×128。这使得大小从234KB降至143KB,以及339KB降至13KB。视觉上没有太大差别,但现在页面大小降至920KB。
接下来两个最大的文件是一个JSON字体和GLTFLoader JavaScript库。这两个文件都可以进行gzip压缩,所以我暂时不担心它们。
音频
现在让我们玩一下游戏,确保一切正常。看起来不错。等等,那是什么?一个新的网络请求?当然,是音频文件!声音只有在第一次用户交互时才会触发。然后它会下载超过10MB的MP3文件。这些文件没有被DefaultLoader统计到,因为我通过音频标签而不是JavaScript加载它们。糟糕!
我并不想等待背景音乐加载,但预加载音效会比较好。此外,音频元素无法提供我需要的音效控制,它们也不支持3D功能。因此,我将它们移至使用Audio对象并使用AudioLoader在three.js中加载它们。现在它们在应用程序启动时会完全加载,并且会被统计到下载时间中。
包括所有音频(除了背景音乐)后,总共是2.03MB。越来越好了。
卡顿
有一个奇怪的故障,在重建游戏棋盘时,整个场景会暂停。我需要弄清楚发生了什么事。为了帮助调试问题,我需要在VR沉浸模式下查看每秒帧数。大多数three.js应用程序使用的标准stats.js模块实际上是通过在WebGL画布上叠加一个DOM元素来实现的。大多数情况下这都可以,但在沉浸模式下就不行了。
为了解决这个问题,我创建了一个名为JStats的小型类,它将统计信息绘制到一个锚定在VR视图顶部的正方形中。这样,无论你朝哪个方向看,你都能在沉浸模式下看到它。
我还创建了一个简单的计时器类,以便我可以测量某个特定函数的运行时间。经过一些测试,我确认每次玩家进入新关卡时,运行setupGame函数所需时间在250到450毫秒之间。
我深入研究了代码,发现了两件事。首先,每个单元格都使用自己的圆角矩形几何体和材质副本。由于这对于每个单元格都是一样的,我可以只创建一个副本,然后在每个单元格上重复使用它。我发现的另一件事是我每次更改关卡时都会创建文字叠加层。这是不必要的。我们只需要每个文字对象的副本。它们可以在关卡之间重复使用。通过将其移动到单独的setupText函数,我节省了几百毫秒。事实证明,三角形文字的成本非常高。现在平均棋盘设置时间约为100毫秒,即使在移动头盔上也不应该会引起注意。
作为最终测试,我使用网络监控器将网络速度降低到3G速度,并禁用了缓存。我的目标是在1秒内首次绘制屏幕,并在10秒内让游戏准备好玩。网络屏幕显示,需要12.36秒。差不多啦!
两步前进,一步后退
在开发游戏的过程中,我意识到还有一些东西缺失了。
- 应该有一个进度条来指示游戏正在加载。
- 当鼠标/指针进入和离开方块时,方块需要音效。
- 每次完成关卡时,应该播放音乐。
- 启动画面需要一个酷炫的字体。
进度条很容易构建,因为DefaultLoadingManager提供了回调函数。我在HTML叠加层中创建了一个进度条,如下所示
<progress id="progress" value="0" max="100"></progress>
然后,每当加载管理器告诉我某个东西已加载时,就更新它。
THREE.DefaultLoadingManager.onProgress = (url, loaded, total) => { $("#progress").setAttribute('value',100*(loaded/total)) }
结合一些CSS样式,它看起来像这样
对抗膨胀
接下来是音乐和音效。添加额外的音乐会增加133KB + 340KB,这使得应用程序的大小增加了近半个兆字节。糟糕。
我可以通过字体稍微减小一些大小。目前,我使用的是three.js的标准字体之一,它们采用JSON格式。这并不是一种非常高效的格式。根据字体的不同,文件大小可能从100KB到600KB不等。事实证明,three.js现在可以直接加载TrueType字体,而无需先将其转换为JSON格式。我选择的字体叫做Hobby of Night,由deFharo设计,它只有80KB。但是,要加载TTF文件,需要TTFLoader.js(4KB)和opentype.min.js(124KB)。因此,我加载的文件比以前更多了,但至少opentype.min.js可以在所有字体之间进行分摊。由于我现在只使用一种字体,所以这并没有帮助,但在将来会有所帮助。所以我又使用了大约100KB的空间。
我今天学到的教训是,优化总是两步前进,一步后退。我必须调查所有内容,花时间完善游戏和加载体验。
目前游戏大小约为2.5MB。在良好的3G条件下,加载时间为13.22秒。
音频再谈
当我添加新的音效时,我想到了一个问题。它们都来自FreeSound.org,该网站通常以WAV格式提供音效。我使用iTunes将它们转换为MP3,但iTunes可能没有使用最优化的格式。查看其中一个文件,我发现它以192KBps的速率编码,这是iTunes的默认设置。使用命令行工具,我敢肯定我可以进一步压缩它们。
我安装了ffmpeg
并使用以下命令重新转换了30秒的歌曲
ffmpeg -i piano.wav -codec:a libmp3lame -qscale:a 5 piano.mp3
它从348KB降至185KB。这节省了163KB!总的来说,声音从10MB降至4.7MB,大大减少了我的应用程序的大小。现在,在不加载背景音乐的情况下启动游戏的总下载大小为2.01MB。
有时你会得到免费的礼物
我将游戏加载到我的网络服务器这里,并在我的VR头盔上测试它,以确保一切正常。然后,我再次尝试在Firefox中加载公共版本,并打开了网络选项卡。我注意到了一些奇怪的地方。总下载大小变小了!在状态栏中显示:2.01 MB/1.44 MB 已传输。在我的本地网络服务器上进行开发时,它显示:2.01 MB/2.01 MB 已传输。这是巨大的差异。这是什么原因造成的?
我怀疑这是因为我的公共网络服务器进行了gzip压缩,而我的本地网络服务器没有。对于MP3文件,这没有区别,但对于高度可压缩的文件(如JavaScript)而言,差别很大。例如,three.min.js文件未压缩时为536.08KB,但压缩后只有惊人的135.06KB。压缩起着巨大作用。现在下载大小只有1.44MB,在良好的3G条件下的下载时间为8.3秒。成功!
我通常在本地网络服务器上进行所有开发工作,只有在项目准备就绪时才使用公共网络服务器。这告诉我们,要观察所有内容,并始终进行测量。
最后
这些是我在优化应用程序时学到的经验。制作第一个版本的遊戲很容易,但要准备发布却很困难。这是一个漫长的过程,但最终的结果非常值得。原型和游戏之间的区别在于对细节的精雕细琢。我希望这些技巧能帮助你创建自己的绝佳 WebVR 体验。
关于 Josh Marinacci
我是一名作家、研究员和转型工程师。以前在 Sun 的 Swing 团队、Palm 的 webOS 团队和诺基亚研究院工作过。我传播良好用户体验的理念。我和我的妻子和天才乐高积木建造师孩子住在阳光明媚的俄勒冈州尤金市。
6 条评论