先做減法,再做加法。????????????????????????
整理/林致6月25日,在騰訊舉辦的微信小游戲開(kāi)發(fā)者大會(huì)上,樂(lè)元素的祥一分享了《開(kāi)心消消樂(lè)》遷移小游戲平臺(tái)的完整歷程。
這款上線超過(guò)十年的三消游戲,至今依然保持月活超1.3億、暢銷(xiāo)榜常駐Top20的穩(wěn)定表現(xiàn)。2024年初正式上線微信小游戲后,很快再次吸引了大量玩家關(guān)注。
在這場(chǎng)遷移中,他們遇到的最大難題是:原本跑在手機(jī)App上的復(fù)雜動(dòng)畫(huà)和上萬(wàn)關(guān)卡內(nèi)容,如何在小游戲這種性能受限的環(huán)境里流暢運(yùn)行?團(tuán)隊(duì)最終用「先做減法,再做加法」的思路,從剔除冗余功能到并行推進(jìn)各項(xiàng)開(kāi)發(fā),硬是在100天內(nèi)完成了上線。
以下為分享內(nèi)容整理,為方便閱讀,內(nèi)容有所調(diào)整。
大家好,我叫祥一,來(lái)自樂(lè)元素,今天給大家分享《開(kāi)心消消樂(lè)》團(tuán)隊(duì)將APP手游遷移到小游戲的整個(gè)過(guò)程。
《開(kāi)心消消樂(lè)》是一款國(guó)民游戲,相信在座的很多人或者自己的親友都曾玩過(guò)這款游戲。
我們?cè)?014年在iOS平臺(tái)上線,到目前為止,游戲運(yùn)營(yíng)已經(jīng)有11年以上的時(shí)間。我們陸陸續(xù)續(xù)又發(fā)布了安卓版本,并在2024年上線了鴻蒙版本。目前,主線關(guān)卡已經(jīng)超過(guò)一萬(wàn)關(guān),每周會(huì)更新30個(gè)以上的關(guān)卡。
在這么多關(guān)卡內(nèi)容和活動(dòng)玩法的基礎(chǔ)上,將這款A(yù)pp游戲遷移到小游戲平臺(tái),工作量是非常大的。因?yàn)闅v史積累下來(lái)的功能、活動(dòng)和代碼非常多,而且還需要兼容已有的平臺(tái),所以整體工作的復(fù)雜度比較高。
我們遷移的主要挑戰(zhàn)是將App端的整個(gè)技術(shù)架構(gòu)遷移到小游戲端。
App以前是用Cocos加Lua開(kāi)發(fā)的,現(xiàn)在要遷移到小游戲端,而小游戲只能運(yùn)行在App中的一個(gè)GS環(huán)境下。如果在小游戲中繼續(xù)用Lua去運(yùn)行,就會(huì)形成一個(gè)虛擬機(jī)中套一個(gè)Lua虛擬機(jī)的模式。但我們無(wú)法避免這種模式,否則App開(kāi)發(fā)業(yè)務(wù)和小游戲開(kāi)發(fā)業(yè)務(wù)就需要走兩套代碼,開(kāi)發(fā)成本會(huì)非常高。
因此,在小游戲端,我們選擇的架構(gòu)是基于WebGL,用Unity導(dǎo)出代碼,并且業(yè)務(wù)邏輯依然跑在Lua中。不過(guò),這種情況下小游戲中Lua的運(yùn)行效率會(huì)相對(duì)低一些。
我們?cè)谇捌诎炎詈诵牡膬?nèi)容提煉出來(lái),選擇了最小上線規(guī)模。做第一版時(shí),主線關(guān)卡需要上線1005關(guān),后期調(diào)整到了2010關(guān)。
另外,《開(kāi)心消消樂(lè)》是一款已經(jīng)在運(yùn)營(yíng)的游戲,所以我們希望給用戶提供一致的體驗(yàn)。無(wú)論是在App上玩還是在小游戲中玩,我們都希望用戶賬號(hào)是互通的,數(shù)據(jù)資產(chǎn)是一致的,參與的活動(dòng)、領(lǐng)取的道具和素材資源在兩個(gè)平臺(tái)都可以通用。
因此,我們需要一個(gè)通用的體系,一些核心功能、道具和支付都需要支持。
此外,在小游戲上我們也希望能夠提供良好的體驗(yàn),幀率需要達(dá)標(biāo),啟動(dòng)時(shí)間也需要達(dá)標(biāo)。
對(duì)我們來(lái)說(shuō),挑戰(zhàn)最大的一點(diǎn)是時(shí)間非常緊迫。我們接到任務(wù)的時(shí)候,大約只有三個(gè)月的時(shí)間需要完成上線,所以當(dāng)時(shí)的時(shí)間壓力非常大。
小游戲的運(yùn)行性能也是一個(gè)挑戰(zhàn),因?yàn)樗\(yùn)行在GS環(huán)境下,效率本身就打了很大的折扣。根據(jù)官方公布的測(cè)試結(jié)果和我們自己的測(cè)算,可用性能大概只有Net5的三分之一左右,而且還無(wú)法使用多線程相關(guān)的技術(shù),因此在性能優(yōu)化上面臨很大的挑戰(zhàn)。
遷移工作的第一個(gè)步驟是先確認(rèn)我們的最小驗(yàn)證集。
《開(kāi)心消消樂(lè)》的核心玩法就是打關(guān),如果打關(guān)無(wú)法正常進(jìn)行,后續(xù)工作基本也無(wú)法開(kāi)展。UI的展示主要使用的是Spine動(dòng)畫(huà),如果運(yùn)行效率非常低,后續(xù)幾乎所有方案都需要推倒重來(lái)。在最小驗(yàn)證集通過(guò)之后,我們開(kāi)展了業(yè)務(wù)邏輯移植、小游戲平臺(tái)能力接入、測(cè)試和優(yōu)化,最后完成上線并進(jìn)行功能迭代和玩法優(yōu)化。
前期的最小驗(yàn)證集對(duì)我們來(lái)說(shuō)是挑戰(zhàn)最大的一部分。
我們的游戲是在Cocos2dx基礎(chǔ)上開(kāi)發(fā)的App版本,當(dāng)時(shí)是為了滿足產(chǎn)品需求以及快速上線驗(yàn)證,功能開(kāi)發(fā)也很順利。但隨著這幾年的運(yùn)營(yíng),我們發(fā)現(xiàn)產(chǎn)品在表現(xiàn)力、玩法內(nèi)容以及3D建模等方面都有了更多新的需求。
因此,我們此前就已經(jīng)開(kāi)始準(zhǔn)備Cocos向Unity的遷移。這次遷移也借機(jī)將發(fā)行小游戲時(shí)Unity版本導(dǎo)出小游戲作為主攻目標(biāo)。不過(guò)在客戶端上,我們還需要驗(yàn)證運(yùn)行時(shí)能否在WebGL上正常運(yùn)行。
幸運(yùn)的是,我們Cocos導(dǎo)出的版本在去除聯(lián)網(wǎng)功能后,在WebGL版本上高端機(jī)可以打出50幀左右,低端機(jī)也能達(dá)到十幾幀,這讓我們看到了希望,至少運(yùn)行起來(lái)沒(méi)有太大問(wèn)題。
在Unity上,我們同樣需要驗(yàn)證運(yùn)行效果。我們測(cè)試了一個(gè)典型的Spine動(dòng)畫(huà)場(chǎng)景,放入了很多動(dòng)畫(huà),運(yùn)行效率基本達(dá)標(biāo),但仍有不少動(dòng)作需要進(jìn)一步優(yōu)化。
工作流的目標(biāo)和整體框架已確定,接下來(lái)的核心工作包括代碼和資源的遷移——相關(guān)內(nèi)容需要遷移到WebGL上。
在小游戲上,所有實(shí)時(shí)加載動(dòng)作都是異步加載,而App上由于性能好,很多加載是同步的。這些在小游戲里無(wú)法使用,所以App端底層架構(gòu)中最基礎(chǔ)的文件加載、資源加載都需要重新遷移。
我們通過(guò)分析配置文件和Lua代碼,將所有引用到的資源進(jìn)行自動(dòng)化分類,按不同的障礙名稱、不同的關(guān)卡段分配到Unity的不同BundleGroup上,并自動(dòng)化生成Bundle。
經(jīng)過(guò)以上幾個(gè)步驟,我們基本完成了一個(gè)能夠在客戶端、Unity和Web端正常運(yùn)行的完整版本。下一步就是處理平臺(tái)差異和適配的問(wèn)題。
在小游戲平臺(tái),我們需要首次接入許多第三方接口,還需要對(duì)接小程序的API和開(kāi)發(fā)能力,支持登錄、支付、廣告等相關(guān)功能。
第一個(gè)版本跑起來(lái)后,我們很自然地遇到了很多問(wèn)題,主要包括卡頓發(fā)熱、幀率不高、內(nèi)存不足導(dǎo)致的卡死或報(bào)錯(cuò)、效果不符合預(yù)期等。
由于最小驗(yàn)證集階段對(duì)美術(shù)資源壓縮率要求非常高,技術(shù)層面主要是保證跑起來(lái)和可見(jiàn),效果方面美術(shù)團(tuán)隊(duì)肯定無(wú)法接受。因此,后期需要在美術(shù)壓縮紋理上適當(dāng)提升,逐步完善效果。紋理品質(zhì)等方面需要與美術(shù)團(tuán)隊(duì)一起在效果和資源之間尋找平衡,爭(zhēng)取既能跑起來(lái)又能滿足效果要求。
后面還會(huì)介紹很多優(yōu)化手段,但優(yōu)化的前提是能夠形成量化指標(biāo)。只有量化了性能數(shù)據(jù),后續(xù)的具體優(yōu)化動(dòng)作、過(guò)程和效果才有依據(jù)。
我們使用的性能分析工具大家也比較熟悉,比如Unity的UnityProfiler、MemoryProfiler、FrameDebugger,這些工具比較完備,也是我們選擇Unity的原因之一。
微信開(kāi)發(fā)者工具也提供了成熟的工具,如Performance工具和CPUslowdown功能,可以放大CPU的運(yùn)行負(fù)擔(dān),幫助我們更容易發(fā)現(xiàn)CPU層面的問(wèn)題。
在開(kāi)發(fā)機(jī)上跑得再好、再流暢,也不能代表用戶的實(shí)際體驗(yàn)效果,因此最終我們真正關(guān)心的是真機(jī)上的表現(xiàn)。
將真機(jī)Profile和Performance工具導(dǎo)出的數(shù)據(jù)導(dǎo)入到Chrome工具中后,我們看到的還原效果與開(kāi)發(fā)機(jī)上的效果基本一致,這套工具也非常好用。
對(duì)于小游戲的實(shí)際優(yōu)化手段,文檔和開(kāi)發(fā)者最佳實(shí)踐中也列出了非常多的細(xì)項(xiàng),我們基本上都一一落實(shí)。不過(guò)對(duì)我們來(lái)說(shuō),最核心的優(yōu)化還是集中在兩個(gè)方面:內(nèi)存優(yōu)化和計(jì)算優(yōu)化。其他大多數(shù)優(yōu)化措施都是圍繞這兩點(diǎn)的擴(kuò)展或延伸。
在小游戲,尤其是微信小游戲上,iOS的高性能+模式非常關(guān)鍵。它決定了我們的可用內(nèi)存和效率提升。
在iOS高性能+模式下,微信小游戲會(huì)把小游戲運(yùn)行在一個(gè)單獨(dú)的進(jìn)程中,內(nèi)存空間的分配完全不同,這對(duì)內(nèi)存使用幫助很大。另外,WASM分包對(duì)內(nèi)存分化效果顯著;降低渲染分辨率也是一種立竿見(jiàn)影的優(yōu)化措施。
雖然方法簡(jiǎn)單,但對(duì)于我們最初App端設(shè)計(jì)720寬的渲染效果而言,將渲染降低到目標(biāo)分辨率再放大,不論是對(duì)幀率的提升還是內(nèi)存占用的降低,都非常明顯。預(yù)加載資源和用戶數(shù)據(jù)在小游戲上也極為敏感,不管是使用量還是加載速度,尤其影響啟動(dòng)時(shí)間。因此,能并行處理的操作我們盡量并行執(zhí)行,以顯著提高加載速度和啟動(dòng)效率。
在內(nèi)存優(yōu)化方面,通用的手段主要是解決內(nèi)存泄漏問(wèn)題。
由于存在虛擬機(jī)套虛擬機(jī)的結(jié)構(gòu),各層內(nèi)存都必須精確控制,Lua和GS環(huán)境本身也可能出現(xiàn)內(nèi)存泄漏。初期移植階段我們以速度優(yōu)先,后期在迭代過(guò)程中逐步解決了大量?jī)?nèi)存泄漏問(wèn)題。同時(shí),資源按需加載、壓縮紋理格式、WASM分包等措施都對(duì)提升加載速度、降低內(nèi)存占用有明顯幫助。對(duì)象池的使用也能緩解GC的壓力。
Unity對(duì)小游戲?qū)С龅膬?yōu)化工作也做了很多對(duì)標(biāo)改進(jìn),因此通過(guò)Unity導(dǎo)出在性能上有明顯提升。對(duì)于GC頻率,iOS和安卓的處理策略不同。微信小游戲在JS層會(huì)每10秒自動(dòng)GC一次,但在Lua上我們起初沒(méi)有設(shè)置定時(shí)GC,這導(dǎo)致大掉落或關(guān)卡運(yùn)行時(shí)可能引發(fā)內(nèi)存問(wèn)題。后來(lái)我們?cè)趇OS上定時(shí)GC,在安卓上考慮到低性能設(shè)備無(wú)法頻繁GC,只在每局結(jié)束后觸發(fā)一次GC。
WASM分包是效果顯著的內(nèi)存優(yōu)化點(diǎn)。我們的總函數(shù)量大約11萬(wàn)個(gè),首包包含約1.8萬(wàn)個(gè)函數(shù),未壓縮情況下帶符號(hào)表的包大小約55MB。分包后首包約15.8MB,分包文件約40MB,兩者不帶符號(hào)表時(shí)容量接近不分包時(shí)的體積。分包后代碼量反而增加,是因?yàn)橐肓舜罅肯嚓P(guān)檢測(cè)、參數(shù)準(zhǔn)備、異常處理等工作,導(dǎo)致代碼存在冗余。
此外,通過(guò)br壓縮可顯著降低首包體積,從15.8MB壓縮到3.4MB。分包最大好處在于內(nèi)存占用大幅降低。官方文檔指出GS代碼約1MB對(duì)應(yīng)內(nèi)存占用10MB,分包40MB大約能降低400MB的GS內(nèi)存占用,為美術(shù)素材等留出空間,效果提升明顯。
在計(jì)算優(yōu)化方面,我們重點(diǎn)解決了幾個(gè)問(wèn)題。
小游戲性能大約只有Net5的三分之一,計(jì)算優(yōu)化如果不到位,性能壓力會(huì)很大。我們?nèi)サ袅舜罅縯ry-catch函數(shù),因?yàn)閃ASM轉(zhuǎn)換后代碼膨脹且檢查開(kāi)銷(xiāo)高。虛擬機(jī)嵌套結(jié)構(gòu)導(dǎo)致參數(shù)傳遞存在多層裝箱、拆箱,參數(shù)量大或參數(shù)個(gè)數(shù)多時(shí)影響更為明顯。
我們也調(diào)整了小游戲的補(bǔ)幀邏輯?!堕_(kāi)心消消樂(lè)》的運(yùn)行邏輯分為邏輯運(yùn)算和渲染運(yùn)算。邏輯幀定在30幀,如果大掉落時(shí)單幀運(yùn)算超時(shí),可能會(huì)出現(xiàn)卡頓。若持續(xù)卡頓,在用戶體驗(yàn)上就像進(jìn)入“子彈時(shí)間”。在App端,大掉落通常只影響1至2幀,很快能追回。但在小游戲上無(wú)法追幀,會(huì)導(dǎo)致連鎖卡頓。
因此我們優(yōu)化補(bǔ)幀策略,僅追部分幀,合并可合并的邏輯,減少雪崩現(xiàn)象。同時(shí),我們優(yōu)化了Lua-C#參數(shù)傳遞和JS接口調(diào)用,重點(diǎn)在業(yè)務(wù)邏輯上改進(jìn)Lua代碼結(jié)構(gòu),以應(yīng)對(duì)Lua執(zhí)行效率的局限。
在優(yōu)化Spine動(dòng)畫(huà)的實(shí)踐中,我們始終圍繞兩個(gè)核心問(wèn)題展開(kāi):計(jì)算消耗和內(nèi)存占用。
Spine是《開(kāi)心消消樂(lè)》關(guān)卡內(nèi)的主要表現(xiàn)形式,所有關(guān)卡障礙和小動(dòng)物絕大多數(shù)都采用Spine動(dòng)畫(huà)。在App端,Spine動(dòng)畫(huà)表現(xiàn)效果好,優(yōu)化空間大,但在小游戲端,這類動(dòng)畫(huà)帶來(lái)了明顯的計(jì)算壓力和內(nèi)存問(wèn)題。
在內(nèi)存方面,我們的優(yōu)化措施包括降低頂點(diǎn)數(shù)、減少網(wǎng)格,以減輕計(jì)算負(fù)擔(dān)。同時(shí),在播放一致的Spine動(dòng)畫(huà)進(jìn)入靜止?fàn)顟B(tài)后,我們會(huì)將其替換為靜態(tài)圖,以降低內(nèi)存占用和計(jì)算開(kāi)銷(xiāo)。對(duì)于可以替換的部分,我們盡量替換;對(duì)于無(wú)法替換的動(dòng)態(tài)內(nèi)容,我們采取減幀或抽幀的方式減少開(kāi)銷(xiāo)。
另一個(gè)重點(diǎn)是去除或優(yōu)化Clip效果。在App端,美術(shù)為了表現(xiàn)力大量使用Clip,但小游戲端無(wú)法很好支持,因此我們和美術(shù)團(tuán)隊(duì)一起去除了不必要的Clip,并對(duì)必須保留的Clip進(jìn)行了美術(shù)和技術(shù)兩方面的優(yōu)化,包括減少內(nèi)存輸出和提高使用效率。
此外,我們引入了Mesh動(dòng)畫(huà),將Spine動(dòng)畫(huà)計(jì)算過(guò)程中的三角形網(wǎng)格預(yù)先計(jì)算好并存儲(chǔ)起來(lái),運(yùn)行時(shí)直接引用靜態(tài)Mesh資源,以內(nèi)存換取CPU性能。這種方法在無(wú)法提前計(jì)算骨骼位置、需要與業(yè)務(wù)邏輯緊密關(guān)聯(lián)的場(chǎng)景中無(wú)法使用,例如連續(xù)顯示進(jìn)度的星星瓶等。但我們?cè)谶@些場(chǎng)景中也進(jìn)行了優(yōu)化,將連續(xù)進(jìn)度細(xì)化為10個(gè)階段,以降低計(jì)算壓力,效果基本能達(dá)到預(yù)期。
在API相關(guān)優(yōu)化方面,小游戲?qū)ξ募僮骱虯PI調(diào)用性能有限,且嵌套虛擬機(jī)結(jié)構(gòu)增加了開(kāi)銷(xiāo)。在App端,為實(shí)現(xiàn)崩潰狀態(tài)恢復(fù),玩家每次操作都需將狀態(tài)寫(xiě)入磁盤(pán)。這在小游戲端導(dǎo)致明顯卡頓,因此我們?nèi)サ袅诵∮螒蚨说念l繁文件操作。
同理,音效播放也受到類似限制,我們簡(jiǎn)化了音樂(lè)播放功能,裁剪掉不必要的代碼以提升效率并減少代碼量。震動(dòng)效果也經(jīng)過(guò)優(yōu)化,在小游戲中只保留高、中、低三種震動(dòng)等級(jí),去掉曲線控制,通過(guò)封裝函數(shù)將震動(dòng)耗時(shí)從20毫秒以上降到幾毫秒以內(nèi)。
Lua代碼優(yōu)化也是重點(diǎn)。我們對(duì)比了Lua文本模式,發(fā)現(xiàn)加載效率影響不大,但文本代碼體積更小,內(nèi)存占用更低。雖然查錯(cuò)時(shí)可讀性下降,但結(jié)合字節(jié)碼和文本混用,能在保持性能的同時(shí)確保定位問(wèn)題時(shí)信息完整。
經(jīng)過(guò)上述各項(xiàng)優(yōu)化,我們?cè)诩s100天內(nèi)完成遷移并于8月上線測(cè)試。這期間沒(méi)有新增業(yè)務(wù)邏輯,僅完成從原生到小游戲的遷移,工作量之大可見(jiàn)一斑。這離不開(kāi)團(tuán)隊(duì)各部門(mén)的協(xié)同配合和多任務(wù)并行推進(jìn)。
總結(jié)經(jīng)驗(yàn),我們的核心做法包括:
1.先做減法,再做加法。優(yōu)先剔除一切不必要內(nèi)容,驗(yàn)證最小可用框架。一旦驗(yàn)證通過(guò),再逐步補(bǔ)充新功能。技術(shù)選型如果一開(kāi)始走彎路,代價(jià)會(huì)非常高。
2.盡量讓所有任務(wù)并行,做好相關(guān)支持工作來(lái)加速開(kāi)發(fā)進(jìn)程。引擎優(yōu)化、API接入、Spine渲染優(yōu)化、業(yè)務(wù)移植、美術(shù)迭代、產(chǎn)品設(shè)計(jì)均可并行。只有把所有東西都并行起來(lái),才能把整個(gè)時(shí)間往前移。
3.產(chǎn)品做好短期和長(zhǎng)期規(guī)劃,為此制定可行的開(kāi)發(fā)計(jì)劃?!堕_(kāi)心消消樂(lè)》作為運(yùn)營(yíng)十年的游戲,內(nèi)容量龐大,必須規(guī)劃好哪些內(nèi)容真正需要遷移到小游戲平臺(tái),避免無(wú)效開(kāi)發(fā)。
4.與公司內(nèi)部和外部專家保持交流,以快速獲取有效方案。項(xiàng)目過(guò)程中,我們得到了微信小游戲團(tuán)隊(duì)和Unity團(tuán)隊(duì)的大力支持,極大推動(dòng)了方案落地。
以上就是我的分享,謝謝大家!