資源分離打包與加載
游戲中會有很多地方使用同一份資源谜叹。比如匾寝,有些界面共用同一份字體、同一張圖集荷腊,有些場景共用同一張貼圖艳悔,有些怪物使用同一個Animator,等等女仰。在制作游戲安裝包時將這些公用資源從其它資源中分離出來猜年,單獨打包。比如若資源A和B都引用了資源C疾忍,則將C分離出來單獨打一個bundle乔外。在游戲運行時,如果要加載A一罩,則先加載C杨幼;之后如果要加載B,因為C的實例已經(jīng)在內(nèi)存聂渊,所以只要直接加載B差购,讓B指向C即可。如果打包時不將C從A和B分離出來汉嗽,那么A的包里會有一份C歹撒,B的包里也會有一份C,冗余的C會將安裝包撐大诊胞;并且在運行時暖夭,如果A和B都加載進內(nèi)存,內(nèi)存里就會有兩個C實例撵孤,增大了內(nèi)存占用迈着。
資源分離打包與加載是最有效的減小安裝包體積與運行時內(nèi)存占用的手段。一般打包粒度越細邪码,這兩個指標就越性2ぁ;而且當兩個renderQueue相鄰的DrawCall使用了相同的貼圖闭专、材質(zhì)和shader實例時奴潘,這兩個DrawCall就可以合并旧烧。但打包也并不是越細就越好。如果運行時要同時加載大量小bundle画髓,那么加載速度將會非常慢——時間都浪費在協(xié)程之間的調(diào)度和多批次的小I/O上了掘剪;而且DrawCall合并不見得會提高性能,有時反而會降低性能奈虾,后文會提到夺谁。因此需要有策略地控制打包粒度。一般只字體和貼圖這種體積較大的公用資源肉微。
可以用AssetDatabase.GetDependencies得知一份資源使用了哪些其它資源匾鸥。
2 ?貼圖透明通道分離,壓縮格式設(shè)為ETC/PVRTC
最初我們使用了DXT5作為貼圖壓縮格式碉纳,希望能減小貼圖的內(nèi)存占用勿负,但很快發(fā)現(xiàn)移動平臺的顯卡是不支持的。因此對于一張1024x1024大小的RGBA32貼圖劳曹,雖然DXT5可將它從4MB壓縮到1MB奴愉,但系統(tǒng)將它送進顯卡之前,會先用CPU在內(nèi)存里將它解壓成4MB的RGBA32格式(軟件解壓)厚者,然后再將這4MB送進顯存躁劣。于是在這段時間里迫吐,這張貼圖就占用了5MB內(nèi)存和4MB顯存库菲;而移動平臺往往沒有獨立顯存,需要從內(nèi)存里摳一塊作為顯存志膀,于是原以為只占1MB內(nèi)存的貼圖實際卻占了9MB熙宇!
所有不支持硬件解壓的壓縮格式都有這個問題。經(jīng)過一番調(diào)研溉浙,我們發(fā)現(xiàn)安卓上硬件支持最廣泛的格式是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)寺酪。它們渲染時的內(nèi)存占用則是2x0.5+2x0.5=2MB坎背。
3 關(guān)閉貼圖的讀寫選項
Unity中導入的每張貼圖都有一個啟用可讀可寫(Read/Write Enabled)的開關(guān),對應的程序參數(shù)是TextureImporter.isReadable寄雀。選中貼圖后可在Import Setting選項卡中看到這個開關(guān)得滤。只有打開這個開關(guān),才可以對貼圖使用Texture2D.GetPixel咙俩,讀取或改寫貼圖資源的像素耿戚,但這就需要系統(tǒng)在內(nèi)存里保留一份貼圖的拷貝,以供CPU訪問阿趁。一般游戲運行時不會有這樣的需求膜蛔,因此我們對所有貼圖都關(guān)閉了這個開關(guān),只在編輯中做貼圖導入后處理(比如對原始貼圖分離透明通道)時打開它脖阵。這樣皂股,上文提到的1024x1024大小的貼圖,其運行時的2MB內(nèi)存占用又可以少一半命黔,減小到1MB呜呐。
4 減少場景中的GameObject數(shù)量
有一次我們將場景中的GameObject數(shù)量減少了近2萬個,游戲在iPhone 3S上的內(nèi)存占用立馬減了20MB悍募。這些GameObject雖然基本是在隱藏狀態(tài)(activeInHierarchy為false)蘑辑,但仍然會占用不少內(nèi)存。這些GameObject身上還掛載了不少腳本坠宴,每個GameObject中的每個腳本都要實例化洋魂,又是一比不菲的內(nèi)存占用。因此后來我們規(guī)定場景中的GameObject數(shù)量不得超過1萬喜鼓,并且將GameObject數(shù)量列為每周版本的性能監(jiān)測指標副砍。
5?圖集
整理圖集的主要目的是節(jié)省運行時內(nèi)存(雖然有時也能起到合并DrawCall的作用)。從這個角度講庄岖,顯示一個界面時送進顯存的圖集尺寸之和是越小越好豁翎。一般有如下方法可以幫助我們做到這點:
1)在界面設(shè)計上,盡量讓美術(shù)將控件設(shè)計為可以做九宮格拉伸隅忿,即UISprite的類型為Sliced心剥。這樣美術(shù)就可以只切出一張小圖,我們在Unity中將它拉大背桐。當然蝉仇,一個控件做九宮格也就意味著其頂點數(shù)量從4個增加到至少16個(九宮格的中心格子采用Tiled做平鋪類型的話碰镜,頂點數(shù)會更多)酥诽,構(gòu)建DrawCall的開銷會更大(見第6點)爽锥,但一般只要DrawCall安排合理(同樣見第6點)就不會有問題。
2)同樣是在界面設(shè)計上,盡量讓美術(shù)將圖案設(shè)計成對稱的形式纷责。這樣切圖的時候捍掺,美術(shù)就可以只切一部分,我們在Unity中將完整的圖案拼出來再膳。比如對一個圓形圖案挺勿,美術(shù)可以只切出四分之一;對一張臉喂柒,美術(shù)可以只切出一半不瓶。不過,與第1)點類似灾杰,這個方法同樣有其它性能代價——一個圖案所對應的頂點數(shù)和GameObject數(shù)量都增多了蚊丐。第4點已經(jīng)提到,GameObject數(shù)量的增多有時也會顯著占用更多內(nèi)存艳吠。因此一般只對尺寸較大的圖案采用這個方法麦备。
3)確保不要讓不必要的貼圖素材駐留內(nèi)存,更不要在渲染時將無關(guān)的貼圖素材送進顯存昭娩。為此需要將圖集按照界面分開凛篙,一般一張圖集只放一個界面的素材,一個界面中的UISprite也不要使用別的界面的圖集栏渺。假設(shè)界面A和界面B上都有一個小小的一模一樣的金幣圖標呛梆,不要因為在制作時貪圖方便,就讓界面A的UISprite直接引用界面B中的金幣素材磕诊;否則界面A顯示的時候填物,會將整個界面B的圖集也送進顯存,而且只要A還在內(nèi)存中秀仲,B的圖集也會駐留內(nèi)存融痛。對于這種情況壶笼,應該在A和B的圖集中各放一個一模一樣的金幣圖標神僵,A中的UISprite只使用A的圖集,B中的UISprite只使用B的圖集覆劈。
不過保礼,如果兩個界面之間存在大量相同的素材,那么這兩個界面就可以共用同一張圖集责语。這樣可以減少所有界面的總內(nèi)存占用量炮障。具體操作時需要根據(jù)美術(shù)的設(shè)計進行權(quán)衡。一般界面之間相同的通用的素材越多坤候,程序的內(nèi)存負擔就越小胁赢。但界面之間相同的東西太多的話,美術(shù)效果可能就不生動白筹,這是美術(shù)和程序之間又一個需要尋求平衡的地方智末。
另外谅摄,數(shù)量龐大的圖標資源(如物品圖標)不要做在圖集里,而應該采用UITexture系馆。
4)減少圖集中的空白地方送漠。圖集中完全透明的像素和不透名的像素所占的內(nèi)存空間其實是一樣的。因此在素材量不變的情況下由蘑,要盡量減少圖集中的空白闽寡。有時一張1024x1024的圖集中,素材所占的面積還沒超過一半尼酿,這時可以考慮將這張圖集切成兩張512x512的圖集爷狈。(有人會問為什么不能做成一張1024x512的圖集,這是因為iOS平臺似乎要求送進顯存的貼圖一定是方形裳擎。)當然淆院,兩張不同圖集的DrawCall是無法合并的,但這并不是什么問題(見第6點)句惯。
應該說土辩,圖集的整理在具體操作時并沒有一成不變的標準,很多時候需要權(quán)衡利弊來最終決定如何整理抢野,因為不管哪種措施都會有別的性能代價拷淘。
8 降低貼圖素材分辨率
這一招說白了其實就是減小貼圖素材的尺寸。比如對一張在原畫里尺寸是100x80的指孤,我們將它導入Unity后會把它縮小到50x40启涯,即縮小兩倍。游戲?qū)嶋H使用的是縮小后的貼圖恃轩。不過這一招是必然會顯著降低美術(shù)品質(zhì)的结洼,美術(shù)立馬會發(fā)現(xiàn)畫面變得更模糊,因此一般不到程序撐不住的時候不會采用叉跛。
9 界面的延遲加載和定時卸載策略
如果一些界面的重要性較低松忍,并且不常被使用,可以等到界面需要打開顯示的時候才從bundle加載資源筷厘,并且在關(guān)閉時將卸載出內(nèi)存鸣峭,或者等過一段時間再卸載。不過這個方法有兩個代價:一是會影響體驗酥艳,玩家要求打開界面時摊溶,界面的顯示會有延遲;二是更容易出bug充石,上層寫邏輯時要考慮異步情況莫换,當程序員要訪問一個界面時,這個界面未必會在內(nèi)存里。因此目前為止我們?nèi)晕磳嵤┰摲桨咐辍D壳爸皇沁M入一個新場景時溃列,卸載上一個場景用到但新場景不會用到的界面。
以上的9個方法中膛薛,4听隐、5、6需要在一定程度上從策劃和美術(shù)的角度考慮問題哄啄,并且需要持續(xù)保持監(jiān)控以維護優(yōu)化狀態(tài)(因為在設(shè)計上總是會有新界面的需求或改動老界面的需求)雅任;其它都是一勞永逸的解決方案,只要實施穩(wěn)定后咨跌,就不需要再在上面花費精力沪么。不過2和8都是會降低美術(shù)品質(zhì)的方法,尤其是8锌半。如果美術(shù)對品質(zhì)的降低程度實在忍不了的話禽车,也可能不會允許采用這兩個方法。
10避免頻繁調(diào)用GameObject.SetActive
我們游戲的某些邏輯會在一幀內(nèi)頻繁調(diào)用GameObject.SetActive刊殉,顯示或隱藏一些對象殉摔,數(shù)量達到一百多次之多。這類操作的CPU開銷很大(尤其是NGUI的UIWidget在激活的時候會做很多初始化工作)记焊,而且會觸發(fā)大量GC逸月。后來我們改變了顯示和隱藏對象的方法——讓對象一直保持激活狀態(tài)(activeInHierarchy為true),而原來的SetActive(false)改為將對象移到屏幕外遍膜,SetActive(true)改為將對象移回屏幕內(nèi)碗硬。這樣性能就好多了。