資源分離打包與加載
游戲中會有很多地方使用同一份資源。比如,有些界面共用同一份字體、同一張圖集,有些場景共用同一張貼圖,有些怪物使用同一個Animator,等等。在制作游戲安裝包時將這些公用資源從其它資源中分離出來,單獨打包。比如若資源A和B都引用了資源C,則將C分離出來單獨打一個bundle。在游戲運行時,如果要加載A,則先加載C;之后如果要加載B,因為C的實例已經在內存,所以只要直接加載B,讓B指向C即可。如果打包時不將C從A和B分離出來,那么A的包里會有一份C,B的包里也會有一份C,冗余的C會將安裝包撐大;並且在運行時,如果A和B都加載進內存,內存里就會有兩個C實例,增大了內存占用。
資源分離打包與加載是最有效的減小安裝包體積與運行時內存占用的手段。一般打包粒度越細,這兩個指標就越小;而且當兩個renderQueue相鄰的DrawCall使用了相同的貼圖、材質和shader實例時,這兩個DrawCall就可以合並。但打包也並不是越細就越好。如果運行時要同時加載大量小bundle,那么加載速度將會非常慢——時間都浪費在協程之間的調度和多批次的小I/O上了;而且DrawCall合並不見得會提高性能,有時反而會降低性能,后文會提到。因此需要有策略地控制打包粒度。一般只字體和貼圖這種體積較大的公用資源。
可以用AssetDatabase.GetDependencies得知一份資源使用了哪些其它資源。
2 貼圖透明通道分離,壓縮格式設為ETC/PVRTC
最初我們使用了DXT5作為貼圖壓縮格式,希望能減小貼圖的內存占用,但很快發現移動平台的顯卡是不支持的。因此對於一張1024x1024大小的RGBA32貼圖,雖然DXT5可將它從4MB壓縮到1MB,但系統將它送進顯卡之前,會先用CPU在內存里將它解壓成4MB的RGBA32格式(軟件解壓),然后再將這4MB送進顯存。於是在這段時間里,這張貼圖就占用了5MB內存和4MB顯存;而移動平台往往沒有獨立顯存,需要從內存里摳一塊作為顯存,於是原以為只占1MB內存的貼圖實際卻占了9MB!
所有不支持硬件解壓的壓縮格式都有這個問題。經過一番調研,我們發現安卓上硬件支持最廣泛的格式是ETC,蘋果上則是PVRTC。但這兩種格式都是不帶透明(Alpha)通道的。因此我們將每張原始貼圖的透明通道都分離了出來,寫進另一張貼圖的紅色通道里。這兩張貼圖都采用ETC/PVRTC壓縮。渲染的時候,將兩張貼圖都送進顯存。同時我們修改了NGUI的shader,在渲染時將第二張貼圖的紅色通道寫到第一張貼圖的透明通道里,恢復原來的顏色:
fixed4 frag (v2f i) : COLOR
fixed4 col;
col.rgb = tex2D(_MainTex, i.texcoord).rgb;
col.a = tex2D(_AlphaTex, i.texcoord).r;
return col * i.color;
fixed4 frag (v2f i) : COLOR
{
fixed4 col;
col.rgb = tex2D(_MainTex, i.texcoord).rgb;
col.a = tex2D(_AlphaTex, i.texcoord).r;
return col * i.color;
}
這樣,一張4MB的1024x1024大小的RGBA32原始貼圖,會被分離並壓縮成兩張0.5MB的ETC/PVRTC貼圖(我們用的是ETC/PVRTC 4 bits)。它們渲染時的內存占用則是2x0.5+2x0.5=2MB。
3 關閉貼圖的讀寫選項
Unity中導入的每張貼圖都有一個啟用可讀可寫(Read/Write Enabled)的開關,對應的程序參數是TextureImporter.isReadable。選中貼圖后可在Import Setting選項卡中看到這個開關。只有打開這個開關,才可以對貼圖使用Texture2D.GetPixel,讀取或改寫貼圖資源的像素,但這就需要系統在內存里保留一份貼圖的拷貝,以供CPU訪問。一般游戲運行時不會有這樣的需求,因此我們對所有貼圖都關閉了這個開關,只在編輯中做貼圖導入后處理(比如對原始貼圖分離透明通道)時打開它。這樣,上文提到的1024x1024大小的貼圖,其運行時的2MB內存占用又可以少一半,減小到1MB。
4 減少場景中的GameObject數量
有一次我們將場景中的GameObject數量減少了近2萬個,游戲在iPhone 3S上的內存占用立馬減了20MB。這些GameObject雖然基本是在隱藏狀態(activeInHierarchy為false),但仍然會占用不少內存。這些GameObject身上還掛載了不少腳本,每個GameObject中的每個腳本都要實例化,又是一比不菲的內存占用。因此后來我們規定場景中的GameObject數量不得超過1萬,並且將GameObject數量列為每周版本的性能監測指標。
5 圖集
整理圖集的主要目的是節省運行時內存(雖然有時也能起到合並DrawCall的作用)。從這個角度講,顯示一個界面時送進顯存的圖集尺寸之和是越小越好。一般有如下方法可以幫助我們做到這點:
1)在界面設計上,盡量讓美術將控件設計為可以做九宮格拉伸,即UISprite的類型為Sliced。這樣美術就可以只切出一張小圖,我們在Unity中將它拉大。當然,一個控件做九宮格也就意味着其頂點數量從4個增加到至少16個(九宮格的中心格子采用Tiled做平鋪類型的話,頂點數會更多),構建DrawCall的開銷會更大(見第6點),但一般只要DrawCall安排合理(同樣見第6點)就不會有問題。
2)同樣是在界面設計上,盡量讓美術將圖案設計成對稱的形式。這樣切圖的時候,美術就可以只切一部分,我們在Unity中將完整的圖案拼出來。比如對一個圓形圖案,美術可以只切出四分之一;對一張臉,美術可以只切出一半。不過,與第1)點類似,這個方法同樣有其它性能代價——一個圖案所對應的頂點數和GameObject數量都增多了。第4點已經提到,GameObject數量的增多有時也會顯著占用更多內存。因此一般只對尺寸較大的圖案采用這個方法。
3)確保不要讓不必要的貼圖素材駐留內存,更不要在渲染時將無關的貼圖素材送進顯存。為此需要將圖集按照界面分開,一般一張圖集只放一個界面的素材,一個界面中的UISprite也不要使用別的界面的圖集。假設界面A和界面B上都有一個小小的一模一樣的金幣圖標,不要因為在制作時貪圖方便,就讓界面A的UISprite直接引用界面B中的金幣素材;否則界面A顯示的時候,會將整個界面B的圖集也送進顯存,而且只要A還在內存中,B的圖集也會駐留內存。對於這種情況,應該在A和B的圖集中各放一個一模一樣的金幣圖標,A中的UISprite只使用A的圖集,B中的UISprite只使用B的圖集。
不過,如果兩個界面之間存在大量相同的素材,那么這兩個界面就可以共用同一張圖集。這樣可以減少所有界面的總內存占用量。具體操作時需要根據美術的設計進行權衡。一般界面之間相同的通用的素材越多,程序的內存負擔就越小。但界面之間相同的東西太多的話,美術效果可能就不生動,這是美術和程序之間又一個需要尋求平衡的地方。
另外,數量龐大的圖標資源(如物品圖標)不要做在圖集里,而應該采用UITexture。
4)減少圖集中的空白地方。圖集中完全透明的像素和不透名的像素所占的內存空間其實是一樣的。因此在素材量不變的情況下,要盡量減少圖集中的空白。有時一張1024x1024的圖集中,素材所占的面積還沒超過一半,這時可以考慮將這張圖集切成兩張512x512的圖集。(有人會問為什么不能做成一張1024x512的圖集,這是因為iOS平台似乎要求送進顯存的貼圖一定是方形。)當然,兩張不同圖集的DrawCall是無法合並的,但這並不是什么問題(見第6點)。
應該說,圖集的整理在具體操作時並沒有一成不變的標准,很多時候需要權衡利弊來最終決定如何整理,因為不管哪種措施都會有別的性能代價。
8 降低貼圖素材分辨率
這一招說白了其實就是減小貼圖素材的尺寸。比如對一張在原畫里尺寸是100x80的,我們將它導入Unity后會把它縮小到50x40,即縮小兩倍。游戲實際使用的是縮小后的貼圖。不過這一招是必然會顯著降低美術品質的,美術立馬會發現畫面變得更模糊,因此一般不到程序撐不住的時候不會采用。
9 界面的延遲加載和定時卸載策略
如果一些界面的重要性較低,並且不常被使用,可以等到界面需要打開顯示的時候才從bundle加載資源,並且在關閉時將卸載出內存,或者等過一段時間再卸載。不過這個方法有兩個代價:一是會影響體驗,玩家要求打開界面時,界面的顯示會有延遲;二是更容易出bug,上層寫邏輯時要考慮異步情況,當程序員要訪問一個界面時,這個界面未必會在內存里。因此目前為止我們仍未實施該方案。目前只是進入一個新場景時,卸載上一個場景用到但新場景不會用到的界面。
以上的9個方法中,4、5、6需要在一定程度上從策划和美術的角度考慮問題,並且需要持續保持監控以維護優化狀態(因為在設計上總是會有新界面的需求或改動老界面的需求);其它都是一勞永逸的解決方案,只要實施穩定后,就不需要再在上面花費精力。不過2和8都是會降低美術品質的方法,尤其是8。如果美術對品質的降低程度實在忍不了的話,也可能不會允許采用這兩個方法。
10避免頻繁調用GameObject.SetActive
我們游戲的某些邏輯會在一幀內頻繁調用GameObject.SetActive,顯示或隱藏一些對象,數量達到一百多次之多。這類操作的CPU開銷很大(尤其是NGUI的UIWidget在激活的時候會做很多初始化工作),而且會觸發大量GC。后來我們改變了顯示和隱藏對象的方法——讓對象一直保持激活狀態(activeInHierarchy為true),而原來的SetActive(false)改為將對象移到屏幕外,SetActive(true)改為將對象移回屏幕內。這樣性能就好多了。
————————————————
版權聲明:本文為CSDN博主「Hus丶zZ」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_35037137/article/details/89851113