我在FreeFire学到的(2025春季版)
2025-04-02 07:30:20

最近有时间稍微整理一下自己在FreeFire项目组学到的东西,这篇比较虚,主要是对组里的一些实践的整理以及自己的一些反思。

其实我2022年加入Garena的时候也不是太清楚FreeFire这个游戏是什么样子,也不知道我大概会做啥,只是觉得钱给的多就来了。现在呆了2年多了,可以说有一些心得体会。说实话我有后悔的时候,有觉得呆着很难受的时候,但是我很清楚这个产品这么成功一定有做的很对的地方,而且这样的体验可能在其他国内厂商都不会有,所以我现在是心怀感激地写下这篇文章,感谢这段经历。

Constraints有哪些

以做产品的思路而言,要理清楚当前这个项目的Constraints有哪些,从我的(引擎程序员)的视角来看:

  • 玩家那边一般基建一般,网速不行,所以游戏的包体要小,更新要少,旧的资源包也要兼容尽量不要重新打包
  • 玩家中间低端机很多,大量32位机器、1G内存手机,所以对内存的控制需要极为严格,PSS和RSS都需要关注
  • 东南亚和拉美天热,手机很容易发热,所以对性能要求严苛
  • 游戏DAU很大(1亿),即使只有很小的百分比,也对应了绝对数量很多的玩家,所以对风险的控制要更加严格
  • 项目组规模很大,所以任何对于引擎的优化与改动最好不要给其他部门造成流程上额外的麻烦

另一个不可忽视的方面是人力。就个人体验而言,其实整体而言组里还挺忙的,组里人虽然看起来很多,虽然并不经常涉及特别深度的渲染任务,但是细细算下职责很多:

  • 永无止境的性能优化:包体、帧率、卡顿、内存、Crash率;也包括相关的自动化工具的开发与维护
  • 每个版本都有新的功能、新的美术资源需要性能评估,要开发工具给美术自查,也有需要手动看的部分,更有和美术、策划的来回掰扯
  • 引擎上的功能(包括渲染需求)开发,支持GPP、UGC等相关的需求
  • 客户端其他的功能,打包、热更、服装系统等等
  • 美术工具的开发与维护,lightmap、streaming相关的工具等等

投入产出比——怎么做优化

老大最喜欢说我们工作讲究的是投入产出比,最理想的情况是花最少的投入来达到最高的优化。喜欢的是低投入高产出和高投入高产出,不喜欢的是低投入低产出和高投入低产出。所以对于优化效果的事前评估就很重要,这决定了要花多少时间在上面是合适的,效果一般的优化可能就3天之后老大就来催了,做不出来算了,只有收益够大才会继续花时间。

最理想的优化当然是:工作量小、风险小、几乎没有流程改动(包括美术制作流程,做包流程等)。所以虽然我个人不太赞成,但能理解引擎组整体给美术组施加的压力,因为削减美术效果确实完美符合上述三点要求:工作量很小(告诉他们砍哪些就行),没有风险,也没有流程改动。另外举一个例子,给地图上PVS,也算是收益很大,风险偏小(出bug会导致部分物品的显隐有问题),但是会需要改动流程:PVS会需要烘培场景。我印象中这个也做了挺久,然后也是梳理清楚了做包和烘焙的流程怎么搞才上线的,效果也很好。

这边推崇的流程是:

  • 对于美术资源而言,尽可能早的设立和使用标准,至少在铺量开始生成资源时,就需要这些规范和工具。因为如果一开始没有这些规范和工具,到后期我们再去优化和操作会浪费大量的人力。
  • 尽可能晚去优化,优化会让流程和代码变得复杂和难看,所以当没有性能问题或者项目还没定型的时候,没必要为了优化而优化(这不等于编码时和资源制作时不考虑性能,两者存在区别)。

虽然我个人并不是完全赞成第二点,但是我理解这确实是一种很实际的态度。从纯粹的性能角度而言肯定是越早考虑性能越好,很多时候后期代码已经定型了其实很难改的动,但是对游戏而言先开发出来玩法然后测试很重要,另外毕竟Gameplay组和性能组是分开的两个组,职责不同,不能强行要求Gameplay考虑性能。

风险控制——开关的艺术

性能优化造成的风险主要来自两方面,一是资源,二是代码。资源几乎都支持热更,所以这方面风险不大。对于代码,则是分两部分:首先是C#的客户端代码,这个改起来风险不大,因为比较大的优化会加开关,而且从根本上来说大部分客户端的代码支持代码热更,风险是可控的;另一个部分是引擎的C++代码的优化,这里风险很大,因为一旦线上出现问题,只能通过版本强制更新来解决,这对产品影响很大,所以几乎所有的引擎层优化都带开关。

一些原则:

  • 每一个功能都需要有单独的变量来控制,一个功能对应一个开关,避免多个开关操作一个功能
  • 在C++端,当开关关了之后,逻辑必须和之前一致

我们有各种各样的开关方式:

  • 如果是新加的接口,比如某个unity接口的无GC版或者优化版,可以在C++层把原来的逻辑复制一份,在上面做优化。开关也比较简单,在C#层做开关就行,true就走接口,false就走原来的逻辑
  • 如果不是新的接口,而是一些引擎层逻辑的修改,则是在原有的逻辑上写 if else,原则就是上文提到的:当开关关了之后,逻辑必须和之前一致
  • 如果是在客户端吃热更之前就生效的优化,比如启动时间相关的优化,则会使用自适应关闭机制,简单来说就是如果客户端发现启动crash数次则自动关闭优化(如果CrowdStrike也是这么谨慎就好了)。此时可能网络下发的开关客户端会读到,但是需要下一次启动才会读取并且生效

当然我们也会有一些无法加开关的优化,比如改了资源的序列化方式这种,这种就只能靠多测试 + 先去测试服小范围对外测试了。也就是如果收益够大,可以稍微麻烦下其他部门帮忙安排相关事宜。如果收益真的够大,也可以尝试一定的风险,有些偶现的crash和ANR真的也没法测出来,也确实没办法,FF这个每日1亿DAU的量级确实太大了。

最后举一个例子,我之前在升级FF使用的Unity引擎,这个是无论如何无法加开关的。这个是前后经历了内部QA测试、测试服测试、测试服二测(两次测试服都是作为可选更新上线的,不是全部用的新引擎包)、小范围上线测试,现在还在小范围上线测试中。我们还详细制定了失败的fallback方案,首先本来就是作为可选更新上线的,影响范围可控;其次我们随时准备用标准包(旧引擎版本的包)去覆盖安装新包。

魔改引擎——外挂式开发

由于关闭代码后逻辑必须和之前一致这个限制,其实整体的代码开发可以说就是“外挂”一套逻辑,无独有偶,我回忆起2077也基于他们的proxy外挂了一套可以streaming的ECS,我称之为外挂式开发的艺术,见 https://youtu.be/JaCf2Qmvy18?t=889 CDPR和我们面对的限制不太一样,他们一方面强依赖于UE5的各种机制,而且会需要升级引擎享受UE新版本的功能,另一方面项目也需要自行修改UE引擎的逻辑,所以他们选择了自己起一套轻量的系统,这样升级的时候merge代码不会是地狱一般的体验。FF选择的方案可以说和CDPR是是殊途同归。

我在升级引擎的时候也确实感受到了这种开发外挂式开发方法的好处,merge优化到新版本Unity的时候确实没那么痛苦。其实这也是一个投入产出比的问题,升级引擎本质上是在借势,因为Unity或者UE这几年的更新多少都有一些可以为项目带来收益的优化。选择外挂式开发,虽然牺牲了开发体验,也从根本上限制了能做的事情的范围,但是换来了升级,也就是利用人家开发团队这几年的优化的方便。

另外提一嘴Editor的流程,虽然组里会去频繁修改引擎代代码,但是给项目组使用的Editor更新频率相当低,甚至可以说是几乎不更新。一方面可以说是基建不好,做这个优先级也不高,另一方面这也是刻意的选择。选择去一直滚动更新Editor一是费时(开发时间和其他项目人员的更新时间),二是出于风险的考虑,不更新就不会出问题,更新了如果出问题就会导致大范围停工。所以组里的策略是性能优化只更新做版本的机器上的Editor,然后代码里通过宏来开关优化代码。项目组人员使用的Editor更新的频率非常非常低,只会偶尔修一些crash或者提高Editor响应速度才会更新。想到以前在腾讯做UE项目,那可是天天要更新 + 重编UE,酸爽无比,所以我十分理解这个策略。当然这样不是没有坏处,越是不更新,Editor和手机上的游戏行为就越是容易不一致,这也导致排查Bug变得非常麻烦。

问题

其实我这部分洋洋洒洒写了很多我想吐槽的地方,但是想想哪里又不是草台班子。我把我遇到的一些问题分了下类:

  • 人力不足和优化优先级不高导致的,这些问题主要在开发效率方面,比如各种工具要么缺失要么太难用,这个在慢慢变好,但是很慢
  • 各种遗留代码和祖宗之法不可变导致的,比如streaming系统,另外Editor和手机上太多表现不一致了导致查bug很麻烦
  • 承接上一条,虽然可能可以优化但是风险很大的,比如打个apk包要2h起步,可能存在优化流程的空间,但是改坏了咋办

我觉得更重要的是要理解哪些是为了规避风险而刻意为之,哪些则是单纯被忽视了的优化点,然后再有针对性地推动改变。

结语

我不知道我和FF的旅程还有多久,但是我很感谢FF教会我的东西,这是一段很有价值的旅程。

Prev
2025-04-02 07:30:20