上线十年,月活过亿,《开心消消乐》如何用100天完成小游戏迁移?
昨天 17:019 浏览综合

先做减法,再做加法。
整理/林致
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进行了美术和技术两方面的优化,包括减少内存输出和提高使用效率。

此外,我们引入了Mes***,将Spine动画计算过程中的三角形网格预先计算好并存储起来,运行时直接引用静态Mesh资源,以内存换取CPU性能。这种方法在无法提前计算骨骼位置、需要与业务逻辑紧密关联的场景中无法使用,例如连续显示进度的星星瓶等。但我们在这些场景中也进行了优化,将连续进度细化为10个阶段,以降低计算压力,效果基本能达到预期。
在API相关优化方面,小游戏对文件操作和API调用性能有限,且嵌套虚拟机结构增加了开销。在App端,为实现崩溃状态恢复,玩家每次操作都需将状态写入磁盘。这在小游戏端导致明显卡顿,因此我们去掉了小游戏端的频繁文件操作。
同理,音效播放也受到类似限制,我们简化了音乐播放功能,裁剪掉不必要的代码以提升效率并减少代码量。震动效果也经过优化,在小游戏中只保留高、中、低三种震动等级,去掉曲线控制,通过封装函数将震动耗时从20毫秒以上降到几毫秒以内。
Lua代码优化也是重点。我们对比了Lua文本模式,发现加载效率影响不大,但文本代码体积更小,内存占用更低。虽然查错时可读性下降,但结合字节码和文本混用,能在保持性能的同时确保定位问题时信息完整。
经过上述各项优化,我们在约100天内完成迁移并于8月上线测试。这期间没有新增业务逻辑,仅完成从原生到小游戏的迁移,工作量之大可见一斑。这离不开团队各部门的协同配合和多任务并行推进。
总结经验,我们的核心做法包括:
1.先做减法,再做加法。优先剔除一切不必要内容,验证最小可用框架。一旦验证通过,再逐步补充新功能。技术选型如果一开始走弯路,代价会非常高。
2.尽量让所有任务并行,做好相关支持工作来加速开发进程。引擎优化、API接入、Spine渲染优化、业务移植、美术迭代、产品设计均可并行。只有把所有东西都并行起来,才能把整个时间往前移。
3.产品做好短期和长期规划,为此制定可行的开发计划。《开心消消乐》作为运营十年的游戏,内容量庞大,必须规划好哪些内容真正需要迁移到小游戏平台,避免无效开发。
4.与公司内部和外部专家保持交流,以快速获取有效方案。项目过程中,我们得到了微信小游戏团队和Unity团队的大力支持,极大推动了方案落地。

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