近期使用了 cocos creator 來開發一些遊戲化的課中互動。Cocos 是一個優秀的國產遊戲引擎,能夠經過 javascript 寫出跨平臺的遊戲。看完文檔,吭哧吭哧搞完,看似完美運行,然而體驗會上,你們提出加載時黑屏時間長、手機發燙嚴重、閃退、卡頓等問題。頭疼,只能想辦法優化。javascript
通過幾天的優化,性能才漸漸達標,其間踩了很多坑,因此打算將一些性能問題排查和優化的手段記錄起來,分享給有須要的同窗。html
雖然 Cocos 屬於遊戲開發範疇,但與前端開發中遇到的性能問題仍是有不少共通之處,無非是加載速度、CPU、內存這三個指標。接下來分別從這三個指標來闡述一些優化手段。前端
Cocos 的啓動大體能夠分爲5個階段:java
其中 Cocos 引擎加載和運行的耗時,業務側是沒法改動的,這部分黑屏時間沒法優化。那麼黑屏時間優化就只剩 Cocos 靜態資源加載了。git
靜態資源加載的手段有兩個:github
資源壓縮主要是針對圖片資源的壓縮,tinify支持 png 和 jpg 格式圖片的在線壓縮,通常能夠壓縮掉 75% 的大小,而且在視覺上不會有明顯的差別,十分推薦。算法
若是接受必定程度的失真,在 cocos creator 編輯器中也可以對 png 和 jpg 圖片進行壓縮。 canvas
若是是 png 格式圖片就 png,jpg 格式則選 jpg,選擇後能夠調整圖片質量,圖片質量越低,大小越小,失真也會越多。 瀏覽器
資源緩存分爲硬盤緩存和內存緩存。緩存
對於原生端,資源自己是存在本地的。對於 Web 端,能夠經過 http 的緩存,或者 PWA 來實現資源在硬盤的緩存。
資源還能夠緩存在內存中,通常來講,遊戲中會有多個場景,例如遊戲中會有不少關卡,每一個關卡一個場景。若是一個場景不會重複進入,那麼場景資源能夠不用緩存。若是場景須要重複進入,那麼緩存一下,能夠加速第二次打開的速度。
通常來講,硬盤的存儲空間比較大,多作硬盤的存儲問題不大。可是內存通常空間比較寶貴,不能啥資源都一股腦往裏塞,容易形成內存佔用率高,而且可能存在內存泄漏的風險,因此通常來講只緩存一些常駐的資源。
因爲遊戲中須要大量的計算與繪製,自己是比較吃cpu的。因此在遊戲過程當中, CPU 的優化是很是重要的。若是 CPU 負載太高,會形成設備發熱嚴重、幀率下降甚至是卡退。
CPU 是負責解析執行指令的,那麼cpu高負載的緣由主要就是須要執行的指令過多,尤爲是一些耗時的指令。在遊戲中,主要是繪製指令的調用,也就是 drawcall。還有其餘的一些計算量比較大的系統,例如物理系統、碰撞系統。另外就是結點的建立與銷燬,以及業務代碼中一些 update 邏輯。
對於 drawcall 的優化,理想的狀況是 drawcall 的次數越少越好。要了解優化 drawcall 的意義和方法,首先要知道在執行 drawcall 後, CPU 作了什麼操做。
CPU 對於圖形處理不太擅長,因此通常都是將圖形處理丟給 GPU (Graphics Processing Unit,圖形處理器)去作,這就是爲何打大型遊戲須要比較好的顯卡的緣由,其實就是須要性能更強大的gpu。
CPU 要將數據交給 GPU 渲染,也不是啥都不用幹的。CPU 須要把要渲染的數據,寫入到數據緩衝區(顯存),並設置渲染狀態(紋理、着色器等),而後 GPU 纔去取數據計算並渲染。
因爲 GPU 的圖形處理能力強,因此每次給一點數據和一次性給一堆數據處理速度是差很少的。可是對於 CPU 來講,若是頻繁調用 drawcall,每次一點點數據,那麼 CPU 就會忙得焦頭爛額。因此優化 drawcall 的最有效方式就是批處理了。
批處理的方式就是合圖了。所謂合圖,就是將要渲染的紋理圖合成一個大的圖集,一次性送給 GPU 去渲染。例若有 3 個 sprite,3 個 sprite 有本身的紋理,若是不合圖,那麼就須要 3 次 drawcall。若是開啓了合圖,那麼只須要 1 次 drawcall。
3 個星星圖標的 sprite,顯示 drawcall 是 4,爲何不是 3 呢,由於相機的背景自己須要一次 drawcall,因此星星總共須要 3 次 drawcall。
添加圖集後,能夠看到 drawcall 就變成 2 了,說明星星如今只須要 1 次 drawcall。
除了 sprite 能夠合圖,label 組件 (font) 也能支持合圖。實際上,渲染字體也是將紋理送到gpu去渲染。
字體分爲兩種實現方式,一種是位圖字體 (Bitmap font),一種是 Free type 字體。
所謂位圖字體,就是將全部字符所有都打到一張圖片中,這樣作簡單粗暴,效率也比較高,由於至關於字體都是預渲染好的。缺點是在字符集比較大時,例如全部漢字,那麼字符的圖片可能會比較大,內存佔用率會比較高。而且不夠靈活,由於圖片的分辨率固定,在高分屏中,位圖字體會出現一些鋸齒。
另一種是 Free type 字體,例如ttf格式的字體。不一樣於位圖字體使用像素來表示字體,Free type 字體只是定義了字體的渲染數據,須要在運行時實時計算而後渲染。這樣的字體就不存在放縮問題,但須要必定的計算消耗,因此通常須要經過緩存來優化。
對於只有數字和英文字母,而且文本結點比較多或者常常變化的狀況,能夠考慮使用位圖字體進行優化,能夠有效下降文字渲染形成的 drawcall 數。
咱們來看看這樣一個簡單例子。場景中有 3 個 label 結點,字體的格式爲 ttf 格式。
預覽一下,發現 drawcall 是 4,前面提到了相機默認會有一次 drawcall,說明 3 個文本結點帶來了 3 次 drawcall,若是是大量文本結點或者文本結點常常變化,將會形成大量的 drawcall。
若是咱們使用 BMFont,能夠看到 drawcall 當即降爲 2,也就是 3 個結點只繪製了 1 次,帶來的 drawcall 優化很是可觀。
對於系統自帶字體,Cocos 也會爲每一個 label 組件建立字符紋理,而且默認不參加合圖。
Cocos 爲 label 組件提供了相似 BMFont 的功能,咱們可使用 Cache Mode
來優化 CPU 。
Cache Mode 值爲 NONE
的時候,Cocos 會爲每一個 label 組件的文本建立字符紋理,而且默認不參加合圖。
值爲 BITMAP
的時候,Cocos 會爲每一個 label 組件的文本建立字符紋理,可是能夠參加動態合圖(後面會講到),批量繪製。
值爲 CHAR
的時候,Cocos 會爲字體生成一張單獨的字符圖集,並緩存起來。後續的新的文本,能夠直接從字符圖集緩存中獲取,不須要從新渲染。(事實上 Cocos 官方文檔對此的描述是」下次遇到相同字符再也不從新繪製」,但就個人理解來講仍是須要繪製的,不然爲何屏幕顯示的文字會更新呢,因此應該只是複用了渲染的數據)。
相較於自動圖集這種靜態合圖方式, Cache Mode 爲 BITMAP
使用的是動態合圖。靜態合圖的方式是在構建時生成合圖,而動態合圖是運行時生成合圖。靜態合圖會減小一些運行時的消耗,可是一些動態加載圖片資源沒辦法應用靜態合圖,這時候能夠經過動態合圖進行優化。關於如何使用動態合圖,Cocos 官方文檔已經講得很詳細,這裏再也不贅述,能夠直接查看:docs.cocos.com/creator/man…
前面咱們說到合圖是下降 drawcall 是一種常見而且有效的手段,可是使用合圖的方式會佔用必定的內存,因此同時要關注內存指標。另外須要注意的是,合圖以後並不意味着就可以批量渲染,參與合圖的 sprite 或者 label 結點的須要是連續的。仍是上面那個星星的例子,場景中有 3 顆星星,也就是 3 個 sprite,本來須要 3 次 drawcall,合圖以後只須要 1 次 drawcall。咱們在第一和第二個星星中間,加入一個 sprite 結點,批量渲染就會被打破:
插入紅色小方塊後,drawcall 變成4。分別是相機背景 drawcall + 第一個星星 drawcall + 紅色方塊 drawcall + 第三和第四個星星的 drawcall。第一個星星原本能夠和第三和第四個星星一塊兒批量渲染的,被紅色方塊的渲染打斷了。
咱們再將小方塊的位置調整一下,調到第一個星星的前面。
能夠看到,儘管顯示上沒有任何變化,可是 drawcall 變成了3次。
因此,儘可能讓參與合圖的結點連續,中間不插入其餘的 sprite 類的結點,以避免打破批次渲染。
此外,mask 組件也多是 drawcall 數量上升的元兇之一。mask 在 Cocos 中,主要是用來實現一些形狀,例如圓角。
爲何這麼說呢,咱們來看個例子:
場景中有一個白色方塊。
總的 drawcall 是 2,因此渲染方塊須要 1 次 drawcall。
若是想要顯示圓形,能夠經過加 mask 組件來遮罩。
能夠看到 drawcall 從 2 變成了 4,說明使用了 mask 以後,會產生 2 次 drawcall。很神奇哦,這是什麼原理呢?
cocos文檔中的解釋是這樣的:
結論就是使用 mask 組件的結點,繪製總共須要 3 次 drawcall,使用 mask 組件不能與相鄰的結點合批渲染,即便它們使用的是相同的圖集。因此,儘可能少用 mask,若是要實現圓角等效果,結點的尺寸也比較固定,可讓設計同窗直接給圖。
固然若是你和我同樣想細扣裏面的細節,什麼是模板緩衝?爲何必定要 3 次 drawcall ?能夠看接下的詳細解釋,須要一點 OpenGL 知識,若是不想深刻細節能夠直接跳過:
什麼是模板測試?
模板測試其實就是經過模板緩衝區中的設置,來決定某些區域要不要渲染。
詳細學習請見:[learnopengl-cn.readthedocs.io/zh/latest/0…learnopengl-cn.readthedocs.io/zh/latest/0… Advanced OpenGL/02 Stencil testing/)
使用 mask 組件的結點渲染三步驟
能夠經過spector.JS來查看渲染幀信息。這是圓形渲染相關的三個幀:
第 1 幀渲染:
渲染命令以下,意思是經過 6 個頂點畫出 2 個三角形,實際上就是本來的小方塊。
可是實際上這裏並無將小方塊真正渲染出來。
模板緩衝狀態爲
這裏的意思是將小方塊區域對應的模板緩衝區位置的值直接置爲 0,也就是刷新該區域的模板緩衝區。
第 2 幀渲染:
渲染命令以下,意思是經過 186 個頂點,畫出 n(不少)個三角形,其實就是畫出圓形,由於在 OpenGL(Webgl)中,各類形狀都是經過三角形去拼出來的。
模板緩衝狀態爲
直接將圓形遮罩對應的模板緩衝區位置的值設成 1。
第 3 幀渲染:
渲染命令以下,與第一幀同樣,都是渲染出小方塊,此次會將方塊渲染出來。
模板緩衝狀態以下,意思是隻有緩衝區對應位置的值爲 1,纔會渲染出來,因此方形被遮罩出了圓形。
除了 drawcall,一些邏輯計算也會影響cpu的使用率。例如 widget 組件的計算時機:
若是選擇了 ALWAYS
,那麼每一幀都會從新計算結點的位置、大小,因此比較耗計算。能夠只選擇 ON_WINDOW_RESIZE
,只在窗口大小變化時,纔會從新計算。若是還須要在其餘時機計算 widget,能夠按需手動調用 widget.updateAlignment
。
另外,因爲 update
這個生命鉤子在每一幀都會調用,因此也須要注意在 update 中的邏輯是否執行過於頻繁,例如不停地打 log,或者不停地計算,都會影響 CPU 的性能。
結點的建立以及銷燬也是比較耗費性能的,因此要避免頻繁地進行結點的建立和銷燬操做,而且應該儘可能減小結點的數量。
因爲 Cocos 在 Web 中經過 canvas 進行繪製,沒辦法使用瀏覽器的開發者調試工具去查看結點,這裏推薦一個 Cocos 插件 ccc-devtools
,github 地址:github.com/potato47/cc…
若是發現結點數量過多,而且結點頻繁建立銷燬,例如遊戲中的小怪、子彈等數量比較多的重複物體,一般能夠經過回收工廠進行優化。回收工廠就是結點用完以後,不銷燬,而是緩存起來,下次獲取結點能夠直接複用緩存中的結點,而不須要從新建立。Cocos 自己提供了回收工廠的接口 NodePool
,能夠了解一下:docs.cocos.com/creator/man…
遊戲中的碰撞檢測,也會比較耗性能。咱們能夠儘可能使用box或者circle碰撞器,而少用多邊形碰撞器。
遊戲中比較佔用資源的主要是資源的緩存,例如圖片資源緩存。而資源分爲靜態資源和動態資源。
靜態資源指的是,場景一開始進入時便當即加載的資源。動態資源是指在場景中異步加載的資源,例如一些網絡圖片、音頻等經過 cc.loader.load
或者 cc.loader.loadRes
加載的資源。
咱們能夠經過 cc.loader._cache
查看當前場景下面的資源列表
也能夠經過前面提到的 ccc-devtool
可視化地查看資源列表,而且還能看到紋理資源的大小:
注意到一張圖片在內存中是比存在磁盤中要大不少的,由於在圖片存在磁盤中時,是通過編碼的,例如使用 png 和 jpg,數據量會小不少。可是存在內存中時,是解碼成像素值的,因此須要佔據的空間比較大。
內存要降下來,也無非兩種方式,一是減小沒必要要的資源、二是資源壓縮。
減小沒必要要的資源,例如:場景中的背景圖,在移動端中是一套,在 PC 端是一套。那麼應該是經過代碼判斷是什麼平臺,而後再動態加載對應資源的方式實現,而不是在場景中同時放置移動端和 PC 端的背景,而後控制顯隱的方式實現。這樣能夠減小一套資源的內存佔用。
對於背景,通常來講由設計直接給圖會比較大,若是是隻是純色或者經過簡單的背景重複或者變換能夠實現,能夠由開發來實現,這樣能夠把大背景圖優化掉。
另外,合圖的時候咱們注意只將比較相關的圖片進行合圖,不然意味着可能加載一整張合圖,只是用到其中的一個小圖,會形成不少內存空間的浪費。
資源壓縮,主要是指對圖片資源的壓縮,也稱紋理壓縮。
單純使用 tinify 等工具,對圖片大小進行壓縮,若是不改變圖片尺寸,是不會減小圖片資源在內存中的體積的,只能減少圖片在磁盤中的存儲體積。對於分辨率要求不高的資源,可使用2倍圖或者1倍圖,能夠減少資源在內存中的體積。
紋理壓縮算法,例如 Etc1, Etc2, PVRTC 等,能夠優化圖片在內存中的體積。jpg 和 png 格式雖然可以對圖片數據進行壓縮,可是並不能被gpu讀取,因此是須要 CPU 解碼以後再給到 GPU 渲染的。而通過紋理壓縮算法壓縮後的數據,是可以直接給gpu渲染的,因此紋理壓縮不只可以優化內存,還能優化 CPU。
須要注意的是,紋理壓縮通常都是有損壓縮,能夠選擇壓縮率。另外,紋理壓縮的算法依賴於設備的 GPU 可否解碼,因此針對不一樣的平臺,須要使用不一樣的紋理壓縮算法。
關於紋理壓縮算法的介紹,推薦看這篇文章:zhuanlan.zhihu.com/p/237940807…
Etc1 絕大部分的安卓設備支持,PVRTC 全部的 iOS 設備支持。
若是圖片不須要支持 alpha 通道,安卓選擇 Etc1 RGB、iOS 選擇 PVRTC 4bits RGB 便可。若是須要支持 alpha 通道,安卓選擇 Etc1 RGB Separate A
,iOS 選擇 PVRTC 4bits RGBA Separate A
。
對於不用的內存,咱們也要及時釋放,防止內存泄漏。分自動釋放和手動釋放兩種。
對於靜態資源的釋放,能夠經過勾選場景自動釋放選項來實現:
這樣在場景切換後,場景中的靜態資源就會被自動釋放了。
若是不想等到切換場景才釋放靜態資源,也可使用 cc.assetManager.releaseAsset
進行手動釋放。
有一個坑點是,動態加載的資源沒法在場景切換時,跟隨靜態資源自動釋放。須要經過 cc.setAutoReleaseRecursively
手動設置一下:
這樣資源在場景切換時,會自動釋放這部分動態加載的資源。也能夠經過 cc.loader.releaseRes
手動釋放動態加載資源。