关于ZAKER 合作
游戏葡萄 前天

上线十年,月活过亿,《开心消消乐》如何用 100 天完成小游戏迁移?

先做减法,再做加法。

整理 / 林致

6 月 25 日,在腾讯举办的微信小游戏开发者大会上,乐元素的祥一分享了《开心消消乐》迁移小游戏平台的完整历程。

这款上线超过十年的三消游戏,至今依然保持月活超 1.3 亿、畅销榜常驻 Top20 的稳定表现。2024 年初正式上线微信小游戏后,很快再次吸引了大量玩家关注。

在这场迁移中,他们遇到的最大难题是:原本跑在手机 App 上的复杂动画和上万关卡内容,如何在小游戏这种性能受限的环境里流畅运行?团队最终用「先做减法,再做加法」的思路,从剔除冗余功能到并行推进各项开发,硬是在 100 天内完成了上线。

以下为分享内容整理,为方便阅读,内容有所调整。

大家好,我叫祥一,来自乐元素,今天给大家分享《开心消消乐》团队将 APP 手游迁移到小游戏的整个过程。

《开心消消乐》是一款国民游戏,相信在座的很多人或者自己的亲友都曾玩过这款游戏。

我们在 2014 年在 iOS 平台上线,到目前为止,游戏运营已经有 11 年以上的时间。我们陆陆续续又发布了安卓版本,并在 2024 年上线了鸿蒙版本。目前,主线关卡已经超过一万关,每周会更新 30 个以上的关卡。

在这么多关卡内容和活动玩法的基础上,将这款 App 游戏迁移到小游戏平台,工作量是非常大的。因为历史积累下来的功能、活动和代码非常多,而且还需要兼容已有的平台,所以整体工作的复杂度比较高。

我们迁移的主要挑战是将 App 端的整个技术架构迁移到小游戏端。

App 以前是用 Cocos 加 Lua 开发的,现在要迁移到小游戏端,而小游戏只能运行在 App 中的一个 GS 环境下。如果在小游戏中继续用 Lua 去运行,就会形成一个虚拟机中套一个 Lua 虚拟机的模式。但我们无法避免这种模式,否则 App 开发业务和小游戏开发业务就需要走两套代码,开发成本会非常高。

因此,在小游戏端,我们选择的架构是基于 WebGL,用 Unity 导出代码,并且业务逻辑依然跑在 Lua 中。不过,这种情况下小游戏中 Lua 的运行效率会相对低一些。

我们在前期把最核心的内容提炼出来,选择了最小上线规模。做第一版时,主线关卡需要上线 1005 关,后期调整到了 2010 关。

另外,《开心消消乐》是一款已经在运营的游戏,所以我们希望给用户提供一致的体验。无论是在 App 上玩还是在小游戏中玩,我们都希望用户账号是互通的,数据资产是一致的,参与的活动、领取的道具和素材资源在两个平台都可以通用。

因此,我们需要一个通用的体系,一些核心功能、道具和支付都需要支持。

此外,在小游戏上我们也希望能够提供良好的体验,帧率需要达标,启动时间也需要达标。

对我们来说,挑战最大的一点是时间非常紧迫。我们接到任务的时候,大约只有三个月的时间需要完成上线,所以当时的时间压力非常大。

小游戏的运行性能也是一个挑战,因为它运行在 GS 环境下,效率本身就打了很大的折扣。根据官方公布的测试结果和我们自己的测算,可用性能大概只有 Net5 的三分之一左右,而且还无法使用多线程相关的技术,因此在性能优化上面临很大的挑战。

迁移工作的第一个步骤是先确认我们的最小验证集。

《开心消消乐》的核心玩法就是打关,如果打关无法正常进行,后续工作基本也无法开展。UI 的展示主要使用的是 Spine 动画,如果运行效率非常低,后续几乎所有方案都需要推倒重来。在最小验证集通过之后,我们开展了业务逻辑移植、小游戏平台能力接入、测试和优化,最后完成上线并进行功能迭代和玩法优化。

前期的最小验证集对我们来说是挑战最大的一部分。

我们的游戏是在 Cocos2dx 基础上开发的 App 版本,当时是为了满足产品需求以及快速上线验证,功能开发也很顺利。但随着这几年的运营,我们发现产品在表现力、玩法内容以及 3D 建模等方面都有了更多新的需求。

因此,我们此前就已经开始准备 Cocos 向 Unity 的迁移。这次迁移也借机将发行小游戏时 Unity 版本导出小游戏作为主攻目标。不过在客户端上,我们还需要验证运行时能否在 WebGL 上正常运行。

幸运的是,我们 Cocos 导出的版本在去除联网功能后,在 WebGL 版本上高端机可以打出 50 帧左右,低端机也能达到十几帧,这让我们看到了希望,至少运行起来没有太大问题。

在 Unity 上,我们同样需要验证运行效果。我们测试了一个典型的 Spine 动画场景,放入了很多动画,运行效率基本达标,但仍有不少动作需要进一步优化。

工作流的目标和整体框架已确定,接下来的核心工作包括代码和资源的迁移——相关内容需要迁移到 WebGL 上。

在小游戏上,所有实时加载动作都是异步加载,而 App 上由于性能好,很多加载是同步的。这些在小游戏里无法使用,所以 App 端底层架构中最基础的文件加载、资源加载都需要重新迁移。

我们通过分析配置文件和 Lua 代码,将所有引用到的资源进行自动化分类,按不同的障碍名称、不同的关卡段分配到 Unity 的不同 BundleGroup 上,并自动化生成 Bundle。

经过以上几个步骤,我们基本完成了一个能够在客户端、Unity 和 Web 端正常运行的完整版本。下一步就是处理平台差异和适配的问题。

在小游戏平台,我们需要首次接入许多第三方接口,还需要对接小程序的 API 和开发能力,支持登录、支付、广告等相关功能。

第一个版本跑起来后,我们很自然地遇到了很多问题,主要包括卡顿发热、帧率不高、内存不足导致的卡死或报错、效果不符合预期等。

由于最小验证集阶段对美术资源压缩率要求非常高,技术层面主要是保证跑起来和可见,效果方面美术团队肯定无法接受。因此,后期需要在美术压缩纹理上适当提升,逐步完善效果。纹理品质等方面需要与美术团队一起在效果和资源之间寻找平衡,争取既能跑起来又能满足效果要求。

后面还会介绍很多优化手段,但优化的前提是能够形成量化指标。只有量化了性能数据,后续的具体优化动作、过程和效果才有依据。

我们使用的性能分析工具大家也比较熟悉,比如 Unity 的 UnityProfiler、MemoryProfiler、FrameDebugger,这些工具比较完备,也是我们选择 Unity 的原因之一。

微信开发者工具也提供了成熟的工具,如 Performance 工具和 CPUslowdown 功能,可以放大 CPU 的运行负担,帮助我们更容易发现 CPU 层面的问题。

在开发机上跑得再好、再流畅,也不能代表用户的实际体验效果,因此最终我们真正关心的是真机上的表现。

将真机 Profile 和 Performance 工具导出的数据导入到 Chrome 工具中后,我们看到的还原效果与开发机上的效果基本一致,这套工具也非常好用。

对于小游戏的实际优化手段,文档和开发者最佳实践中也列出了非常多的细项,我们基本上都一一落实。不过对我们来说,最核心的优化还是集中在两个方面:内存优化和计算优化。其他大多数优化措施都是围绕这两点的扩展或延伸。

在小游戏,尤其是微信小游戏上,iOS 的高性能 + 模式非常关键。它决定了我们的可用内存和效率提升。

在 iOS 高性能 + 模式下,微信小游戏会把小游戏运行在一个单独的进程中,内存空间的分配完全不同,这对内存使用帮助很大。另外,WASM 分包对内存分化效果显著;降低渲染分辨率也是一种立竿见影的优化措施。

虽然方法简单,但对于我们最初 App 端设计 720 宽的渲染效果而言,将渲染降低到目标分辨率再放大,不论是对帧率的提升还是内存占用的降低,都非常明显。预加载资源和用户数据在小游戏上也极为敏感,不管是使用量还是加载速度,尤其影响启动时间。因此,能并行处理的操作我们尽量并行执行,以显著提高加载速度和启动效率。

在内存优化方面,通用的手段主要是解决内存泄漏问题。

由于存在虚拟机套虚拟机的结构,各层内存都必须精确控制,Lua 和 GS 环境本身也可能出现内存泄漏。初期移植阶段我们以速度优先,后期在迭代过程中逐步解决了大量内存泄漏问题。同时,资源按需加载、压缩纹理格式、WASM 分包等措施都对提升加载速度、降低内存占用有明显帮助。对象池的使用也能缓解 GC 的压力。

Unity 对小游戏导出的优化工作也做了很多对标改进,因此通过 Unity 导出在性能上有明显提升。对于 GC 频率,iOS 和安卓的处理策略不同。微信小游戏在 JS 层会每 10 秒自动 GC 一次,但在 Lua 上我们起初没有设置定时 GC,这导致大掉落或关卡运行时可能引发内存问题。后来我们在 iOS 上定时 GC,在安卓上考虑到低性能设备无法频繁 GC,只在每局结束后触发一次 GC。

WASM 分包是效果显著的内存优化点。我们的总函数量大约 11 万个,首包包含约 1.8 万个函数,未压缩情况下带符号表的包大小约 55MB。分包后首包约 15.8MB,分包文件约 40MB,两者不带符号表时容量接近不分包时的体积。分包后代码量反而增加,是因为引入了大量相关检测、参数准备、异常处理等工作,导致代码存在冗余。

此外,通过 br 压缩可显著降低首包体积,从 15.8MB 压缩到 3.4MB。分包最大好处在于内存占用大幅降低。官方文档指出 GS 代码约 1MB 对应内存占用 10MB,分包 40MB 大约能降低 400MB 的 GS 内存占用,为美术素材等留出空间,效果提升明显。

在计算优化方面,我们重点解决了几个问题。

小游戏性能大约只有 Net5 的三分之一,计算优化如果不到位,性能压力会很大。我们去掉了大量 try-catch 函数,因为 WASM 转换后代码膨胀且检查开销高。虚拟机嵌套结构导致参数传递存在多层装箱、拆箱,参数量大或参数个数多时影响更为明显。

我们也调整了小游戏的补帧逻辑。《开心消消乐》的运行逻辑分为逻辑运算和渲染运算。逻辑帧定在 30 帧,如果大掉落时单帧运算超时,可能会出现卡顿。若持续卡顿,在用户体验上就像进入 " 子弹时间 "。在 App 端,大掉落通常只影响 1 至 2 帧,很快能追回。但在小游戏上无法追帧,会导致连锁卡顿。

因此我们优化补帧策略,仅追部分帧,合并可合并的逻辑,减少雪崩现象。同时,我们优化了 Lua-C# 参数传递和 JS 接口调用,重点在业务逻辑上改进 Lua 代码结构,以应对 Lua 执行效率的局限。

在优化 Spine 动画的实践中,我们始终围绕两个核心问题展开:计算消耗和内存占用。

Spine 是《开心消消乐》关卡内的主要表现形式,所有关卡障碍和小动物绝大多数都采用 Spine 动画。在 App 端,Spine 动画表现效果好,优化空间大,但在小游戏端,这类动画带来了明显的计算压力和内存问题。

在内存方面,我们的优化措施包括降低顶点数、减少网格,以减轻计算负担。同时,在播放一致的 Spine 动画进入静止状态后,我们会将其替换为静态图,以降低内存占用和计算开销。对于可以替换的部分,我们尽量替换;对于无法替换的动态内容,我们采取减帧或抽帧的方式减少开销。

另一个重点是去除或优化 Clip 效果。在 App 端,美术为了表现力大量使用 Clip,但小游戏端无法很好支持,因此我们和美术团队一起去除了不必要的 Clip,并对必须保留的 Clip 进行了美术和技术两方面的优化,包括减少内存输出和提高使用效率。

此外,我们引入了 Mesh 动画,将 Spine 动画计算过程中的三角形网格预先计算好并存储起来,运行时直接引用静态 Mesh 资源,以内存换取 CPU 性能。这种方法在无法提前计算骨骼位置、需要与业务逻辑紧密关联的场景中无法使用,例如连续显示进度的星星瓶等。但我们在这些场景中也进行了优化,将连续进度细化为 10 个阶段,以降低计算压力,效果基本能达到预期。

在 API 相关优化方面,小游戏对文件操作和 API 调用性能有限,且嵌套虚拟机结构增加了开销。在 App 端,为实现崩溃状态恢复,玩家每次操作都需将状态写入磁盘。这在小游戏端导致明显卡顿,因此我们去掉了小游戏端的频繁文件操作。

同理,音效播放也受到类似限制,我们简化了音乐播放功能,裁剪掉不必要的代码以提升效率并减少代码量。震动效果也经过优化,在小游戏中只保留高、中、低三种震动等级,去掉曲线控制,通过封装函数将震动耗时从 20 毫秒以上降到几毫秒以内。

Lua 代码优化也是重点。我们对比了 Lua 文本模式,发现加载效率影响不大,但文本代码积更小,内存占用更低。虽然查错时可读性下降,但结合字节码和文本混用,能在保持性能的同时确保定位问题时信息完整。

经过上述各项优化,我们在约 100 天内完成迁移并于 8 月上线测试。这期间没有新增业务逻辑,仅完成从原生到小游戏的迁移,工作量之大可见一斑。这离不开团队各部门的协同配合和多任务并行推进。

总结经验,我们的核心做法包括:

1. 先做减法,再做加法。优先剔除一切不必要内容,验证最小可用框架。一旦验证通过,再逐步补充新功能。技术选型如果一开始走弯路,代价会非常高。

2. 尽量让所有任务并行,做好相关支持工作来加速开发进程。引擎优化、API 接入、Spine 渲染优化、业务移植、美术迭代、产品设计均可并行。只有把所有东西都并行起来,才能把整个时间往前移。

3. 产品做好短期和长期规划,为此制定可行的开发计划。《开心消消乐》作为运营十年的游戏,内容量庞大,必须规划好哪些内容真正需要迁移到小游戏平台,避免无效开发。

4. 与公司内部和外部专家保持交流,以快速获取有效方案。项目过程中,我们得到了微信小游戏团队和 Unity 团队的大力支持,极大推动了方案落地。

以上就是我的分享,谢谢大家!

游戏葡萄招聘内容编辑,

点击「阅读原文」可了解详情

推荐阅读

鹰角音律联觉 | 苏丹的游戏 | 星穹铁道音乐会

成都慢半拍 | 漕河泾「抬棺人」| 91ACT 复活

对话射雕制作人 | 33 号远征队 | 黑暗世界:因与果

游戏行业书籍推荐

点击下方名片,关注公众号

(星标可第一时间收到推送和完整封面)

相关标签

最新评论

没有更多评论了