在兩周前舉辦的 Cocos 「十周年系列沙龍」北京站,來自字節(jié)跳動的高級軟件工程師林良喜、渲染引擎開發(fā)工程師黃俊銘、高級算法工程師郭冠軍為現(xiàn)場開發(fā)者分享了大力教育教具游戲聯(lián)合團(tuán)隊制作的教具游戲技術(shù)實現(xiàn)方案,深受開發(fā)者喜愛。
在征得同意后,我們將通過本文對幾位的演講進(jìn)行梳理總結(jié),從 AI 識別原理、定制化渲染引擎以及游戲?qū)崿F(xiàn)三個方面進(jìn)行分享,把他們寶貴的技術(shù)經(jīng)驗分享給因為各種原因沒能去到沙龍現(xiàn)場的開發(fā)者伙伴們!
字節(jié)跳動布局線上教育成立大力教育品牌,旗下有多款已公開的產(chǎn)品,其中瓜瓜龍啟蒙是專為 2-8 歲孩子打造的在線教育產(chǎn)品,內(nèi)容涵蓋英語、思維和語文等多學(xué)科的趣味 AI 互動課程,多維度體系化教學(xué),全面助力孩子成長。
3-6 歲兒童正處于動手敏感期,孩子數(shù)學(xué)思維的啟蒙須高效把握好這個時期,充分調(diào)動起他們的多重感官,才能更好地培養(yǎng)孩子的數(shù)感。與市面上傳統(tǒng)的 AI 啟蒙課程相比,瓜瓜龍教具游戲玩法更加新穎,一改屏幕點選、拖動等課程交互形式,升級為結(jié)合實體智能教具動手實操。
通過瓜瓜龍啟蒙的獨(dú)家“黑科技”,孩子邊聽課邊將題目的正確答案從教具中挑選出來擺放在“底座”上,iPad 和手機(jī)上方的“棱鏡”均可進(jìn)行智能識別立即判斷答案是否正確,給到孩子及時反饋。那么這些是如何實現(xiàn)的呢?
攝像頭拍攝原始的鏡頭數(shù)據(jù),原始數(shù)據(jù)傳給 AI 識別模塊;
通過 AI 模塊進(jìn)行算法識別,對攝像頭原始數(shù)據(jù)處理生成結(jié)構(gòu)化的數(shù)據(jù);
結(jié)構(gòu)化的數(shù)據(jù)通過客戶端傳給游戲 JS 運(yùn)行環(huán)境。
教具游戲設(shè)計的挑戰(zhàn)在于配套了 28 類卡牌,每類卡牌又由幾十個卡牌組成,需要對這 28 類卡牌進(jìn)行識別和定位,同時讓卡牌定位算法的召回率高于 99% 和識別算法的精度大于 99%。AI 識別這里的原理概括就是通過輸入原始的圖片數(shù)據(jù),經(jīng)過兩類的神經(jīng)網(wǎng)絡(luò):分類和定位對卡牌進(jìn)行定位和分類。我們 AI 算法識別第一個面臨的難點就是數(shù)據(jù)采集成本高,原始數(shù)據(jù)會出現(xiàn)目標(biāo)阻擋、卡牌破損、多余卡牌、以及其他極端環(huán)境等問題。為了解決這個困難,我們采用了數(shù)據(jù)合成策略的方法,也就是在理想的情況下采集了卡牌的數(shù)據(jù)和計算出了卡牌的定位和分類信息,然后把這些卡牌通過一定的變形合成之后,與隨機(jī)背景合成形成新的訓(xùn)練樣本,從而提升了神經(jīng)網(wǎng)絡(luò)的效果(召回率)。AI 算法識別遇到的第二個難點就是識別結(jié)果的“晃動”,前后兩幀的定位框晃動導(dǎo)致識別的類別會晃動,由于識別結(jié)果會實時地顯示在游戲中,識別結(jié)果晃動會讓用戶體驗變差。解決這個問題的方法比較簡單,我們通過串聯(lián)前后幀的信息來抹平前后幀晃動的情況,具體是通過緩存前一幀的灰度圖和框信息來實現(xiàn)的。幀差法檢測上一幀卡牌位置是否發(fā)生變化,如果沒發(fā)生變化,保留框,同時合并上一幀剩余檢測框和當(dāng)前幀檢測框,做 nms 進(jìn)行過濾 。AI 算法識別遇到的第三個難點是檢測模型性能要求高的問題,我們的檢測模型需要達(dá)到 60 FPS 的標(biāo)準(zhǔn)且內(nèi)存占用在 10M 以下。我們這里采用的解決方案是用了學(xué)術(shù)界性能和效果都不錯的、一個比較小型的網(wǎng)絡(luò) ShuffleNet V2,同時通過神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)搜索(NAS)和對網(wǎng)絡(luò)權(quán)重的 INT8 量化使得單幀耗時在 17ms 以下同時召回率在 99.5% 以上。AI 算法遇到的第四個難點是除了提升召回率之外,如何提升類別識別的精度,且精度需要在 99% 以上。我們同樣把采集的數(shù)據(jù)貼在各種背景上后合成了各種分類數(shù)據(jù)。另外也會針對手遮擋、環(huán)境光等各種 badcase 單獨(dú)采集分類數(shù)據(jù)進(jìn)行模型迭代。在模型充分迭代 10 次左右以及在各種極端光線下測試通過后才會上線使用。AI 算法遇到的第五個難點是減少其他卡牌識別結(jié)果的干擾。由于用戶無法直接看到攝像頭的視野,因此視野內(nèi)很容易出現(xiàn)非目標(biāo)的卡牌,進(jìn)而被識別。我們這里采用環(huán)境監(jiān)測的方法來分割出用戶的桌墊,把桌墊外的卡牌識別結(jié)果過濾掉。同樣其他環(huán)境問題,比如光線、棱鏡遮擋有無桌墊環(huán)境問題也是通過采集對應(yīng)的數(shù)據(jù)訓(xùn)練不同的分類模型,當(dāng)環(huán)境出現(xiàn)異常時把實時的環(huán)境監(jiān)測結(jié)果反饋給游戲,讓游戲提示用戶調(diào)整識別環(huán)境。
在教具游戲場景下,底層游戲渲染引擎也扮演著一個重要的角色,支持游戲在教育業(yè)務(wù)場景下高效開發(fā)以及高性能運(yùn)行。MiniGameLite 是教具業(yè)務(wù)場景下使用的快速游戲集成解決方案,我們將從方案背景、技術(shù)原理、部分技術(shù)問題優(yōu)化以及未來展望幾個方面給大家介紹。第一部分是方案背景,首先我們提出一個問題,游戲如何與業(yè)務(wù)快速結(jié)合?需要將游戲嵌入到業(yè)務(wù)中,業(yè)務(wù)可以隨意放在任意位置;
需要快速地開發(fā)游戲,有一套完整的工具鏈路,可以快速集成到業(yè)務(wù)中;
需要定制游戲的接口去滿足業(yè)務(wù)的需求,比如教具場景下要通知游戲消息去做一些互動;
需要更高性能、占用內(nèi)存更小的游戲。
MiniGameLite 快速小游戲集成解決方案就解決了以上訴求:提供 View 視圖級別接入,快速嵌入業(yè)務(wù);
支持運(yùn)行小游戲,對接小游戲生態(tài),具有完整的工具鏈路;
提供業(yè)務(wù)方定制小游戲通道的能力,定制業(yè)務(wù)接口;
使用自研的渲染引擎,渲染鏈路更短更高效。
第二部分想跟大家分享背后的技術(shù)原理,首先從整體架構(gòu)圖看 MiniGameLite 方案部分實現(xiàn)的功能以及位置。最上層是 Cocos 等游戲引擎承載的小游戲,與小游戲生態(tài)對接;向下是對接 Android 、 iOS 業(yè)務(wù)方;中間就是 MiniGameLite 整體的解決方案,從上往下看,最上方是支持字節(jié)小游戲接口(比如 tt.createCanvas、tt.getSystemInfoSync、gl.clearColor 等),并支持 MiniGameLite 定制的消息通道接口,往下是 JS 引擎,Android 使用 V8 引擎, iOS 使用 JavaScriptCore 引擎,再往下是 JavaScript Binding,連接底層核心實現(xiàn),比如很重要的一部分 WebGL 到 OpenGL 的 Binding,緊接是 MiniGameLite 的核心模塊,包含核心小游戲渲染層、音頻模塊、文件系統(tǒng)、網(wǎng)絡(luò)等,接下來是 ?MiniGameLite 平臺接口層,提供給業(yè)務(wù)方簡單易用的接口,比如開始游戲、暫?;謴?fù)游戲等。我們拆解以上的幾個技術(shù)特點簡單講解,首先是 MiniGameLite 的視圖級別接入。MiniGameLite 提供簡單的接口,指定游戲 ID 以及游戲視圖,游戲?qū)嬅驿秩镜綄?yīng)的視圖上,不同于普通單進(jìn)程全屏小游戲,MiniGameLite 支持任意進(jìn)程指定視圖渲染,支持快速嵌入到業(yè)務(wù)中。游戲?qū)嬅驿秩镜?Canvas 中,然后在 Android 繪制到 SurfaceView 或者 TextureView 上,iOS 繪制到 CAEAGLLayer 上,最后嵌入到業(yè)務(wù)中。接下來講一下定制消息通道。背后的技術(shù)原理也比較簡單,MiniGameLite 提供了游戲?qū)右约皹I(yè)務(wù)層的消息通道,平臺層假如要通知游戲業(yè)務(wù)層消息,就是平臺層調(diào)用 JS 方法通知游戲方,游戲方通過暴露的消息通道接口監(jiān)聽函數(shù)收到消息,游戲業(yè)務(wù)需要通知平臺層消息時,平臺層預(yù)先注入實現(xiàn)好的 JS 方法,游戲方通過調(diào)用 JS 方法通知平臺層。關(guān)于 MiniGameLite 高性能渲染的部分,我們可以對比下瀏覽器渲染鏈路,瀏覽器支持了其他 DOM 元素的渲染,整體渲染鏈路比較復(fù)雜,包含樣式計算、重繪、重排等。MiniGameLite 專注于支持小游戲 Canvas 的渲染,整體渲染鏈路比較簡單,主要的工作是做了 WebGL 到 OpenGL 的 Binding 工作,減少了不必要的開銷,在某些 API 的使用上鏈路更短,避免了 DOM 元素帶來的消耗(比如 DrawImage API 等)。在 MiniGameLite 高性能渲染鏈路以外,我們還可以介紹下 MiniGameLite 在調(diào)用 WebGL 方面使用共享內(nèi)存的優(yōu)化。舉一個很簡單的例子,比如創(chuàng)建一個 canvas 使用 clearColor 將它染成紅色,這里的 clearColor 就是一個很常見的 WebGL 狀態(tài)。我們是不是需要在每次調(diào)用的時候直接調(diào)用到底層的 OpenGL 函數(shù)呢?答案不是的,我們使用共享內(nèi)存的方式維護(hù)了 GL 的狀態(tài),不會每次調(diào)用 clearColor 的時候調(diào)用 OpenGL 函數(shù),而是在真正繪制時對比當(dāng)前上下文狀態(tài)是否與對應(yīng)上下文狀態(tài)是否有更新,假如有更新再去應(yīng)用更新,這樣也高效地維護(hù)了 WebGL 狀態(tài),減少無用的更新。不過對于游戲開發(fā)者而言,引擎底層的優(yōu)化是一方面,游戲開發(fā)者需要更關(guān)注 drawcall 的使用,比如可以采用批處理等方式進(jìn)行優(yōu)化。第三部分我們講述下我們遇到的幾個技術(shù)問題以及優(yōu)化的方式。由于我們面向的是小游戲開發(fā)者,假如小游戲開發(fā)者沒有及時釋放 GL 資源,GL 資源將成為內(nèi)存殺手,這時候?qū)τ谝鎸有枰趺醋瞿?/strong>?我們知道 JS 引擎有垃圾回收機(jī)制,利用這一點我們也可以完成 GL 資源的垃圾回收,比如 V8 引擎,我們可以使用它的 SetWeak 方法,它將在對象引用只剩下一個弱持久引用時調(diào)用回調(diào)函數(shù)。利用這一點我們就可以對我們的 WebGL 資源掛載上 finalize,當(dāng)對象不再被引用時,我們自動調(diào)用析構(gòu),釋放 GL 資源。再分享我們遇到的一個技術(shù)問題,在某些機(jī)型上,底層 OpenGL 紋理異步釋放速度很慢,在某些場景下某些游戲頻繁地申請紋理資源(并且調(diào)用了釋放),但是由于釋放速度較慢,申請紋理資源過快就會導(dǎo)致 GL OOM 的問題。懶加載、復(fù)用緩存是優(yōu)化的常見手段,為了解決這個問題,我們針對特定場景下嘗試了以下優(yōu)化:首先,我們延遲了創(chuàng)建紋理的調(diào)用,createTexture 不立即創(chuàng)建紋理而是等到 Bind 使用時。其次,我們在釋放紋理時不會真正地調(diào)用底層紋理的釋放,而是將紋理放置于紋理池中,供下次創(chuàng)建循環(huán)使用,這樣也就解決了頻繁申請紋理的問題。不過對于游戲而言,關(guān)于內(nèi)存的使用,引擎層需要關(guān)注,游戲開發(fā)者也需要更多的關(guān)注:游戲可以優(yōu)化紋理使用,可以使用壓縮、合圖或者使用壓縮紋理的方式進(jìn)行優(yōu)化;游戲可以提早及時釋放游戲資源,防止達(dá)到內(nèi)存峰值。最后一部分我們講述 MiniGameLite 快速小游戲集成方案的未來展望。首先我們會打造更高性能、更小體積的業(yè)務(wù) SDK,其次我們在嘗試打造更強(qiáng)大的物理引擎,目前也與 Cocos 合作,下沉物理引擎到 Native 層增強(qiáng)小游戲的性能。最后是字節(jié)能力賦能,我們將結(jié)合字節(jié)跳動內(nèi)部特效、算法生態(tài),游戲結(jié)合攝像頭特效、攝像頭 AR 等能力賦能更多業(yè)務(wù)場景。
我們提供線上教育服務(wù),游戲跟隨課程上線,隨著課程開展,游戲需求量越來越大。為了適應(yīng)不同教學(xué)情況和滿足小朋友好奇心,我們需要豐富游戲玩法,為了更吸引小朋友,滿足小朋友動手需求,我們接入了教具識別。因此,我們需要解決的問題是,大量的、不同種類的教具游戲開發(fā)。我們統(tǒng)計了現(xiàn)有游戲玩法,把它們組合起來,抽出可以被復(fù)用的部分,設(shè)計游戲模板。在游戲模版中,根據(jù)功能不同分成三塊,分別是狀態(tài)機(jī),適配器和教具識別。為了大家更好理解,首先從這三張圖出發(fā),這里用點數(shù)游戲-抓老鼠的三張圖作為示例。第一張圖是關(guān)卡地圖,游戲以闖關(guān)的模式進(jìn)行,通過關(guān)卡后才能進(jìn)行下一關(guān)。第二張圖是進(jìn)入關(guān)卡后,開始游戲,這個游戲的玩法是,使用對應(yīng)點數(shù)卡牌回答問題,回答正確可以阻止老鼠進(jìn)入屋子,根據(jù)回答狀態(tài),我們可以分為未回答、回答正確、回答錯誤一次、回答錯誤兩次、回答錯誤三次、超時未回答等,每個狀態(tài)都有對應(yīng)邏輯。例如在未回答時,老鼠從主路走向中間的岔路,這時游戲開啟攝像頭,接收教具數(shù)據(jù),等待用戶答題。當(dāng)識別到正確教具,用戶回答正確,游戲狀態(tài)變?yōu)榛卮鹫_,這時游戲中出現(xiàn)了一只道具手拍向老鼠,結(jié)束當(dāng)前題目,開啟下一道題。我們把游戲中的每一個流程都當(dāng)成一種狀態(tài),為此設(shè)計了游戲狀態(tài)機(jī),用來管理游戲中的狀態(tài) ,狀態(tài)流轉(zhuǎn)類似 JS 中的 Promise,狀態(tài)只會正向流轉(zhuǎn),一旦發(fā)生改變,狀態(tài)將不可逆。用狀態(tài)機(jī)管理游戲有一個好處,可以使抽象邏輯更加清晰。因為游戲玩法太多了,每個流程對應(yīng)的功能可能都不一樣,那么我們可以把它理解為一個狀態(tài),規(guī)定這個狀態(tài)的輸入輸出,而中間是如何處理的,我們可以先不管,由具體邏輯負(fù)責(zé),下面會講到相關(guān)內(nèi)容。圍繞模板,分析玩法,我們把核心邏輯抽離出來,設(shè)計關(guān)卡管理器,根據(jù)游戲狀態(tài),設(shè)計了狀態(tài)機(jī)。如圖所示,游戲開始,我們進(jìn)入關(guān)卡,關(guān)卡開始、讀取狀態(tài)機(jī)、狀態(tài)機(jī)開始,開始接收識別數(shù)據(jù),狀態(tài)機(jī)根據(jù)識別數(shù)據(jù)改變狀態(tài),游戲處理對應(yīng)狀態(tài),狀態(tài)機(jī)達(dá)到退出條件,狀態(tài)機(jī)結(jié)束,銷毀當(dāng)前狀態(tài)機(jī),查詢是否有下一個狀態(tài)機(jī)。如果有,繼續(xù)執(zhí)行下一個狀態(tài)機(jī),游戲進(jìn)度加一;如果沒有下一個狀態(tài)機(jī),結(jié)束當(dāng)前關(guān)卡,游戲跳轉(zhuǎn)至下一關(guān)卡,根據(jù)關(guān)卡類型,執(zhí)行對應(yīng)狀態(tài)機(jī),游戲重新走一遍關(guān)卡流程;如果沒有下一個關(guān)卡,游戲通關(guān),通關(guān)后,結(jié)束游戲。在這一過程中,狀態(tài)機(jī)承擔(dān)了主要的游戲邏輯,由狀態(tài)機(jī)控制狀態(tài)流轉(zhuǎn),狀態(tài)流轉(zhuǎn)時控制狀態(tài)功能。這個流程的基本思路是,從玩法出發(fā),定義需要用到的狀態(tài),根據(jù)用戶的輸入,改變狀態(tài),流轉(zhuǎn)狀態(tài),最終達(dá)到退出游戲條件。而模板開發(fā)在這一過程中,只需要去實現(xiàn)對應(yīng)狀態(tài)功能。由于有多種玩法,因此在設(shè)計狀態(tài)機(jī)時,我們只需要確定核心狀態(tài),然后在適配器上實現(xiàn)新玩法帶來的新狀態(tài),就可以在同一份模板上實現(xiàn)不同游戲種類玩法。游戲玩法多樣性由適配器完成,根據(jù)狀態(tài)機(jī)核心流程,我們定義了一個基礎(chǔ)適配器,玩法適配器繼承了基礎(chǔ)適配器,在基礎(chǔ)適配器上開發(fā)。當(dāng)我們要開發(fā)一個新的玩法時,只需要開發(fā)新的適配器,實現(xiàn)對應(yīng)玩法,即可快速生成一套玩法模板。由于新玩法往往與其他游戲不同,我們需要根據(jù)玩法的特性,重寫狀態(tài)接口,使游戲狀態(tài)滿足游戲玩法。因為狀態(tài)接口和游戲狀態(tài)一一對應(yīng),因此,當(dāng)狀態(tài)發(fā)生流轉(zhuǎn)時,對應(yīng)的游戲玩法也隨之發(fā)生改變。在一些玩法里面,可能存在別的玩法不需要的能力,例如編程題中需要把教具信息轉(zhuǎn)化成程序指令、教具拖拽題需要拖動指定教具到正確區(qū)域,實現(xiàn)玩法時,我們需要增加 feature 能力完成對應(yīng)功能,feature 是指特定玩法功能,程序一般在狀態(tài)執(zhí)行時調(diào)用對應(yīng)的 feature,或者預(yù)留在代碼里,根據(jù)用戶輸入觸發(fā)對應(yīng) feature,通過 bridge 動態(tài)調(diào)用。我們所處業(yè)務(wù)的時間很緊張,每周都有大量游戲上線,行業(yè)和業(yè)務(wù)發(fā)展十分迅猛,過不了多久就會有大量線上游戲。量變形成質(zhì)變,怎么管理大量線上游戲成為一個難題,bridge 在這個背景下誕生。我們把游戲分成三大模塊,狀態(tài)機(jī)、適配器和教具識別,其中狀態(tài)機(jī)代表視圖層,控制游戲表現(xiàn),適配器代表控制層,控制游戲邏輯,教具識別代表數(shù)據(jù)層,表示用戶輸入。我們把適配器和教具識別單獨(dú)抽離出來,創(chuàng)建一個新工程,用來維護(hù)birdge。控制邏輯被抽離之后,可以簡化狀態(tài)機(jī)代碼,同時游戲邏輯更加清晰。狀態(tài)機(jī)只處理對應(yīng)視圖層,完成對應(yīng)狀態(tài)表現(xiàn),視圖層由邏輯層控制,開發(fā)者不用過多關(guān)注視圖層,可以更加專注游戲邏輯開發(fā)。為了能更好地管理游戲,抽離出來的 bridge 單獨(dú)維護(hù),所有游戲共用一份 bridge,通過 bridge 管理海量線上游戲。這樣,如果遇到邏輯變更或者修復(fù),我們只需要維護(hù)一份 birdge,即可影響所有游戲。那么,birdge怎么和狀態(tài)機(jī)結(jié)合呢?考慮游戲會和多端通信(minigamelite、AI lab 等),我們最終使用事件派發(fā)驅(qū)動游戲。根據(jù)模塊劃分,bridge 需要對接狀態(tài)機(jī)和 AI lab,AI lab 通過派發(fā)事件與 bridge 通信、傳輸教具信息,bridge 處理信息后通過事件驅(qū)動狀態(tài)機(jī)流轉(zhuǎn)狀態(tài),從而控制游戲邏輯。使用事件驅(qū)動,我們可以很方便在模塊之間通信,開發(fā)者只需要根據(jù)用戶輸入和當(dāng)前狀態(tài)判斷下一個狀態(tài),通過指定事件控制狀態(tài)流轉(zhuǎn)。遇到難題,假設(shè)我們有個通用組件叫瓜瓜幣,是游戲內(nèi)的積分,產(chǎn)品覺得這個組件不夠 Q,需要把它改得更加適合小朋友,這個時候我們的麻煩來了,怎么解決游戲更新?我們預(yù)留了解決方案,組件開發(fā)和動態(tài)下發(fā)。下面向大家分享我們組件倉庫的開發(fā)歷史,一開始我們也經(jīng)歷 copy 代碼的過程,但相信大家也知道這樣的痛苦。當(dāng)時還沒規(guī)劃組件倉庫、結(jié)合 Cocos,我們尋找市面上能夠滿足組件開發(fā)的技術(shù),后來我們選擇了 git subModule,一定程度上緩解 copy 代碼帶來的不便。但是因為子模塊維護(hù)操作比較麻煩,容易被誤修改等,我們后來開發(fā)組件倉庫時,最終使用了 @byted/cetus 管理組件倉庫。cetus 是我們的一個工具包,它是一個基于 git 的團(tuán)隊項目代碼管理工具,它適合內(nèi)部的包管理,它可以直接使用 git 倉庫作為依賴,cetus 僅需要很少的配置就可以實現(xiàn)倉庫的管理,在配置中,mode 表示組件開發(fā)模式,我們選擇克隆模式拉取整個倉庫,方便修改和提交,path 表示組件存放路徑,remoteUrl 表示拉取的組件倉庫地址,version 表示拉取版本的代碼,可以使用分支名或 commit hash,配置完成后,執(zhí)行 ct,空格 i,即可在對應(yīng)目錄下看到相應(yīng)的組件文件。通過 cetus,我們可以很便捷開發(fā)和維護(hù)組件。組件開發(fā)完后,我們會把通過測試的組件打包,發(fā)布到 CDN,提供游戲使用。游戲通過組件倉庫 id 加載倉庫下的 uuid-to-mtime.json 文件,這個文件比較特殊,存放了倉庫下面所有資源,包含了 uuid 與資源路徑映射關(guān)系,我們可以根據(jù)它獲取對應(yīng)倉庫所有資源。拿到 uuid-to-mtime.json 文件后,我們還需要改造數(shù)據(jù),由 relativePath 映射 uuid。因為一個倉庫里面包含很多組件,對于開發(fā)者而言,相對路徑對我們來說會更清晰些,我們通過相對路徑獲取遠(yuǎn)程倉庫內(nèi)指定組件。當(dāng)請求的組件路徑命中 relativePath 時,我們可以獲取到對應(yīng)組件的 uuid,根據(jù) Cocos 項目結(jié)構(gòu),我們可以很容易通過 uuid 解析出資源存放在 library 中的路徑,再通過路徑加載指定組件的資源。至此,游戲完成動態(tài)加載。至此,教育游戲的痛點難點我們都解決了,下面簡單介紹下我們用到的優(yōu)化手段。關(guān)于性能優(yōu)化,我們主要從包大小、內(nèi)存、drawCall 入手。優(yōu)化包大小的手段有很多,我們主要從資源入手,圖片、音頻壓縮,優(yōu)化合圖空白區(qū)域,優(yōu)化圖片像素格式,使用 jpg 格式,使用九宮格、平鋪或拉伸方式滿足大圖需求。對于字體文件,優(yōu)先考慮使用位圖,如果需要引入字體文件,使用 fontmin 去除未被使用的字符。在這么多資源中,setting.js 文件大小最容易被忽略,如果不注意資源放置,setting.js 文件可以增大很大,為了減少 setting.js 文件大小,避免在 resources 放置不需要動態(tài)加載的資源,碎圖合并成一張。除了業(yè)務(wù)相關(guān)資源,引擎中不需要引用的模塊也可以適當(dāng)刪減,去掉未使用的引擎模塊不單能夠減少包體積,還可以加快構(gòu)建速度。內(nèi)存優(yōu)化這里主要做了 3 個操作,對于靜態(tài)資源,可以勾選自動釋放資源讓引擎處理。針對動態(tài)資源,需要計算動態(tài)資源引用次數(shù),當(dāng)資源引用次數(shù)為 0 時釋放資源。圖片的分辨率盡量滿足 2 的冪次方,Cocos的渲染方式基于 OpenGL 的,OpenGL 載入紋理圖片時,所用內(nèi)存會自動擴(kuò)張到 2 的 N 次方,圖片分辨率滿足 2 的冪次方,可以避免因擴(kuò)張內(nèi)存導(dǎo)致內(nèi)存浪費(fèi),在分辨率滿足 2 的冪次方下,碎圖鋪滿合圖可以更有效利用內(nèi)存。進(jìn)行 drawCall 優(yōu)化前我們需要明白 drawCall 是什么,drawCall 是 cpu 對圖形繪制接口的調(diào)用,CPU 通過調(diào)用圖形庫接口,命令 GPU 進(jìn)行渲染操作。每一次繪制 CPU 都要調(diào)用 DrawCall,而在調(diào)動 DrawCall 前,CPU 還要進(jìn)行很多準(zhǔn)備工作:檢測渲染狀態(tài)、提交渲染所需要的數(shù)據(jù)、提交渲染所需要的狀態(tài)。而 GPU 本身具有很強(qiáng)大的計算能力,可以很快就處理完渲染任務(wù)。當(dāng) DrawCall 過多,CPU 就會很多額外開銷用于準(zhǔn)備工作,CPU 本身負(fù)載,而這時 GPU 可能閑置了。由于 drawCall 過高,導(dǎo)致 frame time 過高,fps 變低,用戶感覺體驗是游戲變卡了,甚至掉幀,那么我們優(yōu)化的時候就是減少 drawCall,盡量把小的 drawCall 合并到一個大的 drawCall 中,渲染合批。我們主要的方法是合圖,把紋理狀態(tài),材質(zhì),混合模式一致的圖片合張一張大圖,減少渲染次數(shù),使用位圖代替 Label,避免 Label 和 Sprite 相互打斷。還有一些常用的優(yōu)化,如使用對象池,設(shè)置合適的游戲幀率,降低物理引擎步長等,可以根據(jù)項目需要設(shè)置,通過比較小的改動帶來比較明顯的優(yōu)化。
以上就是大力教育所有關(guān)于教具的原理介紹喔,字節(jié)跳動布局線上教育成立大力教育品牌,旗下有多款已公開的產(chǎn)品。教具游戲是大力教育綜合字節(jié)內(nèi)部各種技術(shù)積累進(jìn)行的一個項目,大力教育結(jié)合 Cocos 還有很多應(yīng)用場景,對 Cocos 開發(fā)者也提供了很多非常不錯的機(jī)會,對教育行業(yè)感興趣同學(xué)可以聯(lián)系 [email protected] 投遞簡歷喔。
最后,再次感謝林良喜、黃俊銘、郭冠軍三位大大的傾情分享,「十周年系列沙龍」仍在繼續(xù)前進(jìn),12 月 26 日廣州站已經(jīng)在路上,尚未報名的童鞋戳鏈接沖鴨!