我把《塞尔达传说》搬上苹果设备,「独占」还剩什么?

52 个小时,两天多一点,不到三天。
我把 Switch 版《塞尔达传说:旷野之息》,从 Mac 搬到了 iPhone,做出了 iOS 测试版。
等不来官方的苹果版本,那就自己动手。海拉鲁的门,凭什么只能用任天堂的机器打开?
以这份测试版为起点,我最终实现了 macOS、iOS、iPadOS 三平台适配:Mac 短测接近 250 FPS,iPhone、iPad mini 跑到 60 FPS。设备的次元壁打破了,林克也来苹果全家桶串门了。
这道「独占」的门,我是怎么推开的?

能跑《暗黑破坏神 II》,那《塞尔达》呢?
Armigo 是我为《旷野之息》做的一套兼容层,让 Switch 版游戏能在苹果设备上运行。大部分游戏代码可以直接在苹果芯片上执行,需要另外适配的是系统服务和图形功能。
开源项目 d2r-mobile 已经让 Switch 版《暗黑破坏神 II:重制版》在苹果设备上跑了起来。我只是在它的基础上继续开发。
但换成《旷野之息》,刚启动就卡住了。它需要调用的多项系统服务,原项目根本没有实现。
我先补上账号、游戏内容加载和音频等支持,让游戏能继续启动。画面也得重新处理:两款游戏虽然都用 Switch 的 NVN 图形接口,但《旷野之息》用到的一些功能,原项目没有支持;已有功能的调用方式也不完全一样,仍得逐项适配。

就算接口不报错了,画面也未必正确。游戏要求画一个物体、清空画面,或者更新一张贴图,兼容层都得完成对应的操作。有没有做对,得看实际画出来的结果。
又折腾了好一阵,我终于拿到了第一张截图。满怀期待地点开一看——

一整块品红。
截图倒是保存下来了,可林克和神庙一个都没画出来。折腾了这么一通,离真正看到游戏画面还差得远。
神庙出现了,主角却是一团黑影
修掉启动时遇到的问题后,屏幕上终于出现了游戏标题。那一刻,恨不得敲锣打鼓、普天同庆(好吧,主要还是我自己在庆祝)。

再往后,复苏神庙也出现了,蓝色的光、熟悉的石台都有了。
可躺在石台上的林克,却黑成了一团。镜头拉近了,也看不清皮肤和衣服,只能看见一个人形轮廓。

乍一看,很像是贴图没加载。查下去才发现,是光照算错了:翻译着色器时,一串条件分支漏了后半截,结果把环境光算成了零。
所以,光把画面调亮没用,得先把漏掉的那部分光照计算补回来。
修好之后,还是那座神庙,林克的肤色、身上的衣服,还有周围的细节,终于都看清了。

神庙里的画面正常了。可一到室外,新的问题又冒了出来。
不是湖,是地面没了
树和岩壁都在,远处的山也能看见。乍一看,画面已经像那么回事了。可仔细一看,地面却东缺一块、西缺一块,林克的腿还陷在土里。

这些缺口看着像是地形模型没加载完整,查下去才发现,是地形高度数据出了问题。有些数据没有正确传给显卡,有些已经算好的结果又被后续更新覆盖了。地面的高度画错了,林克看起来就像陷在土里。
把数据传递和更新的问题修好后,地面终于恢复了正常,林克也稳稳地站在了草地上。

画面看着正常了,还得实际玩一玩。我开始操控林克走路、转动镜头、攀爬岩壁,检查有没有截图里看不出来的问题。

除了画面,声音和操作也要逐项检查:游戏能不能正常出声,手柄的按键和摇杆有没有反应,手机上的触控是否好用。这些都没问题了,才能好好玩。
现成基础之外,我还补了什么
d2r-mobile 证明了这条路走得通,也提供了程序加载、系统调用和图形转换的基础。但到了《旷野之息》,不少关键功能都得重新适配,有些还得另外实现。前面的黑影和地面缺失,只是其中最容易看出来的两个问题。
按我对这次开发工作量的估计,d2r-mobile 的现成实现能直接解决的部分还不到 30%。剩下大量工作,是针对《旷野之息》补功能、修错误、优化性能,再把它适配到不同设备上。
声音的处理方式就不一样。《暗黑破坏神 II:重制版》用 FMOD 处理声音,再把处理好的音频交给系统播放。原项目已经做好了后面这一步。《旷野之息》用的却是另一套原生音频渲染器,现成的播放接口并不能解决全部问题。我得让游戏自带的音频渲染器在 CPU 上正常运行,补上驱动它的线程、输出设备和播放缓冲,才能把声音送到苹果设备的扬声器里。混音和音效仍由原来的游戏程序处理,但让这套流程在苹果设备上正常工作,还得另外写代码。
画面也远不止翻译几个接口。《旷野之息》会用到原项目还没支持的纹理处理和绘图功能,也会在绘制过程中反复修改数据。多条线程同时准备绘图命令时,还要保证它们不会互相覆盖。这些功能既要做对,也要尽量减少额外的性能开销。
这次适配的还是《旷野之息》1.9.0。和之前准备的 1.8.2 相比,几个程序模块都变了。旧版本里确认过的函数位置、数据结构和接口参数,到了新版都得重新检查,不能直接照搬。
DLC 又多了一整套要处理的事情。没装 DLC 时,游戏查询追加内容,应该得到一个空列表。装上以后,就要能查到对应的内容,再打开文件,读取版本和地图资源,和本体一起使用。文件放在目录里,不等于游戏就能认出来。
我把追加内容查询改为按配置好的 DLC 目录返回结果,并补上目录加载和资源读取的支持。运行日志里,已经能看到游戏识别出两项追加内容,并且真的打开了 DLC 的版本文件和地图资源。到这里,1.9.0 本体、更新和 DLC 的加载流程才一起跑通了。
从原生音频到新版程序和 DLC,这些都是现成代码不能直接替我完成的工作。解决了能不能玩,后面还要继续解决能不能玩得流畅。

从能玩,到接近 250 FPS
画面终于正常了,接下来就是把帧率提上去。
检查性能时发现,兼容层自己就在浪费时间。游戏只改了几个绘图参数,兼容层却要中断绘制,重新做一遍准备工作。一帧里反复折腾几百次,时间就这么耗掉了。
接下来就是不断测试、持续优化,把这些重复工作一点点减掉。减少更新绘图参数时的中断,就是其中一项。在一次 1600×900、解除帧率限制的测试里,这项改进让 GPU 每帧耗时从 8.9 毫秒降到了 4.6 毫秒,几乎减半。
之后,我又继续优化了数据搬运、线程调度等部分。帧率就这样一点点提了上来,最终 Mac 在短测中跑到了接近 250 FPS。
但帧率上去了,林克不会也跟着跑快了吧?
如果帧数翻了几倍,跑步、物理效果和游戏里的时间也跟着加速,那就成了开倍速玩游戏。所以,我还调整了游戏的计时方式,让它按实际经过的时间更新。在那次接近 250 FPS 的短测里,现实过去约十秒,游戏逻辑也只推进了约十秒。这段测试里,帧率提上去了,游戏时间没有跟着快进。

适配 iPhone、iPad 和手柄
macOS 跑通之后,我接着开始研究 iPhone 和 iPad 上的适配。移动设备的散热条件不同,除了帧率,还得看长时间玩下来会不会发热、掉帧。我在 iPhone 上连续测试了十五分钟,帧率接近 60;测试时室温约 29°C,手机插着电,几分钟后系统就检测到温度偏高。想玩得久一点,发热和耗电还得继续优化。
iPad 也单独做了适配和测试,用的是搭载 A17 Pro 的 iPad mini,同样跑到了 60 FPS。从 Mac 到 iPhone,再到 iPad,三个平台都跑通了。
操作上,我既做了屏幕触控,也适配了手柄。拿起手机或平板,可以直接用虚拟按键和摇杆控制林克;接上手柄,也能用熟悉的实体按键来玩。按键映射、摇杆输入,以及切到别的 App 再回来能不能继续操作,都要逐项处理。声音能不能正常播放、被系统打断后能不能恢复,也得实际测试。
开头说的 52 小时,算的是两条开发记录之间的时间:一条是 Mac 首次启动,一条是 iOS 测试版的代码提交。之后,我又继续修问题、调操作、优化性能,才有了现在的效果。
「独占」还意味着什么?
这次折腾,也让我开始想一些代码之外的问题。游戏能在别的设备上跑了,「独占」靠的又是什么?我有一些自己的想法,也想听听大家怎么看。
买下的是作品,还是某个平台的入场券?
对我来说,买下一款喜欢的游戏,就会希望很多年后还能打开它。换了设备,或者原来的主机坏了,都不该轻易成为和它告别的理由。
这次移植,让我有了继续玩它的另一种办法。但如果设备退役、商店下线之后,普通玩家只能重新买一遍,或者自己折腾一套兼容层,我觉得「买下了这款游戏」这句话,还是少了点分量。
游戏明明还在手里,却因为主机坏了、修不好,就再也玩不了。到了这时候,我还能说自己拥有这款游戏吗?
平台还能靠什么留住玩家?
两边都用 ARM 处理器,确实省下了一部分工作。但操作系统不同,游戏需要的功能还是得逐项适配。
这次做下来,我发现这些兼容问题可以逐项查明原因,再动手解决。但这也不等于说,公司多年经营的平台优势就这么没了。
内容当然是答案之一。服务、便利、联机、更新,以及下一部真正想玩的作品,也都可能是答案。
在我看来,有了别的选择之后,玩家仍然愿意留下,才更能说明一个平台的价值。单靠「换了机器就玩不了」,恐怕越来越难。
保存经典和创造经典,是两条路吗?
老主机总有一天会越来越难买到,坏了也未必修得好。让老游戏在新设备上也能玩,至少能让喜欢它的人继续玩下去。我觉得,这件事本身就值得做。
但它不是全部。
《旷野之息》之所以值得搬,不是因为它能跑 60 帧,而是因为海拉鲁的草地、风声、地形和谜题让人愿意停下来。兼容层能把游戏画面显示出来,却不会因为多跑了一百帧,就替谁造出另一个海拉鲁。
所以,我觉得这两件事没必要二选一。我希望喜欢的旧游戏能一直玩下去,也期待下一部让我愿意花时间探索的新作。经典值得保存,一个平台也得拿出新作品,让人对下一次冒险有所期待。
我把 ARM 和西班牙语里表示「朋友」的 amigo 拼在一起,给这个项目起名 Armigo。把老朋友接到新设备上,是这次折腾做的事。至于下一位值得认识的朋友,还得有人去创造。