年后新項(xiàng)目就要轉(zhuǎn)UE4了,本文主要是啟發(fā)式的總結(jié)一下這2年多使用Unity做手游的經(jīng)驗(yàn)和遇到的坑摇天。以下討論的Unity版本為5.6,本文僅代表個(gè)人的一些拙見哈哈。
打包構(gòu)建
為什么是先說打包违孝?因?yàn)槲矣X得出包的時(shí)間是影響團(tuán)隊(duì)工作效率最重要的因素。尤其是大項(xiàng)目泳赋,如果打包時(shí)間在一個(gè)小時(shí)以上雌桑,一般這個(gè)團(tuán)隊(duì)都避免不了996。再加上如果測(cè)試出現(xiàn)了一些卡流程的bug祖今,再重出包的效率真的低的可怕校坑。下面簡(jiǎn)單的說下我們項(xiàng)目是怎么做到打包時(shí)間控制在半小時(shí)內(nèi)的:
- AssetBundle與il2cpp代碼并行打包
不得不說,Unity在多核PC上的打包優(yōu)化做的真的爛千诬,完全不能充分利用多核機(jī)器的并行性能優(yōu)勢(shì)耍目。所以我們的做法很簡(jiǎn)單粗暴:就是弄2個(gè)工程,1個(gè)負(fù)責(zé)打ab包徐绑,另1個(gè)工程打il2cpp包邪驮,這2者并行,且ab包和il2cpp.so都備份傲茄,供下次打包有需要時(shí)使用毅访。 - 項(xiàng)目盡可能的早投入使用Lua類語言進(jìn)行開發(fā)
項(xiàng)目的il2cpp.so達(dá)到90M,C#代碼實(shí)在太多盘榨,每次轉(zhuǎn)il2cpp幾乎都要占用20-30分鐘喻粹,轉(zhuǎn)il2cpp都是全量轉(zhuǎn)(聽說新版Unity做了增量編譯),特別蛋疼草巡。后面接入了xLua后守呜,對(duì)C#代碼做了些裁剪,盡量讓framework層都用C#山憨,業(yè)務(wù)層都改用lua查乒,使得轉(zhuǎn)il2cpp時(shí)間大概控制在5-15mins。 - 記錄每次打的ab包的md5萍歉,只在md5發(fā)生變更時(shí)才打包
這個(gè)應(yīng)該很多項(xiàng)目都會(huì)做侣颂,理由也很簡(jiǎn)單,就是一些美術(shù)資源沒變更枪孩,就不用浪費(fèi)時(shí)間再打一次包了憔晒,直接復(fù)用上次打好的ab包就好藻肄。我們的實(shí)現(xiàn)方式是將ab包的bytes和路徑轉(zhuǎn)成一串md5,然后把所有ab包的md5都記在一個(gè)xml里拒担,通過這個(gè)xml文件嘹屯,每次打包就可以與之前的md5做一下diff,沒變更就直接跳過打ab包的流程从撼。
同時(shí)州弟,這個(gè)在做資源更新的時(shí)候,如果發(fā)現(xiàn)更新包體過大低零,也方便查找問題源頭婆翔。 - ab打包粒度控制
這個(gè)問題因項(xiàng)目而異,因?yàn)閡nity的ab包怎么打掏婶,在很多層面上啃奴,其實(shí)是個(gè)取舍問題。
至于我們項(xiàng)目是:- icon資源雄妥,可配置打圖集或者各打在同一bundle
- 對(duì)于每一個(gè)ui界面最蕾,它的prefab各自單獨(dú)打bundle,并記錄每一個(gè)界面依賴的資源bundle(如icon資源老厌、動(dòng)畫瘟则、特效的bundle),在加載ui的prefab前枝秤,先加載這個(gè)prefab依賴的bundle醋拧。
- 對(duì)于每一個(gè)npc模型,它的prefab淀弹、mesh趁仙、動(dòng)畫、材質(zhì)垦页、特效打在同一bundle
- 對(duì)于場(chǎng)景的資源,其所有依賴的資源跟場(chǎng)景打在同一bundle
- 如果同一個(gè)資源被2個(gè)或以上的ab包引用干奢,則把它打在common的bundle包里
- 配置表打在同一個(gè)bundle
- Lua按協(xié)議痊焊、配置、GamePlay這3部分分開打bundle
- Shader按模塊(如ui忿峻、character薄啥、effect等)分開打bundle
這里不討論為啥這么設(shè)計(jì)了,因?yàn)槠鋵?shí)這里也有很多問題逛尚,比如場(chǎng)景引用了一個(gè)npc垄惧,使得場(chǎng)景的ab包和npc的ab包都引用了npc資源,導(dǎo)致打在common包里了绰寞,我們的解決辦法是把npc在場(chǎng)景里的顯示人肉改成代碼動(dòng)態(tài)加載到逊;還有萬一shader在線上有bug铣口,整個(gè)shader和依賴這個(gè)shader的材質(zhì)要重新發(fā)一個(gè)完整的資源包。
- 構(gòu)建時(shí)打的log不要太多
半年前的版本發(fā)現(xiàn)打包時(shí)打了15w行的log觉壶,單是這個(gè)打log的操作就占了15分鐘脑题。所以除非要定位打包問題,建議在平時(shí)的日構(gòu)建盡可能的少打日志铜靶。當(dāng)然叔遂,有些log是引擎里的,有源碼的話直接注釋就好了争剿。
性能
這個(gè)話題太廣已艰,不同類型的游戲都有各種特定的優(yōu)化,這里只是簡(jiǎn)單提一些我覺得比較冷門的(需要引擎源碼)蚕苇。
- 引擎Background線程數(shù)量改成2哩掺,Unity默認(rèn)是16,引擎啟動(dòng)的時(shí)候這些線程就會(huì)創(chuàng)建捆蜀,相信大部分做手游都用不上這么多線程疮丛。
- 引擎啟動(dòng)的時(shí)候會(huì)創(chuàng)建Enlighten和Substance線程,如果不用的話辆它,可以把引擎編譯宏ENABLE_SUBSTANCE和ENABLE_RUNTIME_GI去掉誊薄。
- 在做一些類似皮膚換裝的需求時(shí),盡量把蒙皮和貼圖合并的邏輯做在引擎的C++層锰茉,可以避免頂點(diǎn)和貼圖拷貝操作呢蔫。
- il2cpp的反射信息是存儲(chǔ)在dense_hash_map上的,這個(gè)dense_hash_map在我們項(xiàng)目運(yùn)行時(shí)會(huì)占用30M左右的內(nèi)存飒筑,這個(gè)大小與項(xiàng)目運(yùn)行時(shí)加載的C#代碼量(反射信息的讀取是lazy load)成正比片吊,所以在低端機(jī)上,我們把這個(gè)存儲(chǔ)方式改成更省內(nèi)存的sparse_hash_map协屡,但其實(shí)cpu會(huì)更耗一點(diǎn)俏脊,最終大概省到10M左右,本質(zhì)是時(shí)間換空間的方案肤晓。
- 引擎資源的Remapper也是用dense_hash_map的爷贫,內(nèi)存很吃緊的情況下,也是可以改成sparse_hash_map的形式补憾。
- Strip Engine Code可以嘗試開下漫萄,能省一些編譯的引擎包量。但是會(huì)有很多坑盈匾,官方的解釋是如果開了會(huì)有問題腾务,就關(guān)掉,哈哈削饵,服不服岩瘦。
- 引擎里返回給C#層的數(shù)組類接口未巫,如GetVertices(),GetMaterials()等,都是有g(shù)c風(fēng)險(xiǎn)的担钮,因?yàn)樗鼤?huì)重新alloc一個(gè)C#的數(shù)組橱赠,然后把C++對(duì)象再一個(gè)個(gè)塞到這個(gè)數(shù)組里。例子如下:
void Update()
{
var mats = renderer.GetMaterials(); // gc
for(int i = 0; i < mats.Length;++i)
{
var mat = mats[i];
// mat.SetXXX()
}
}
簡(jiǎn)單的解決辦法當(dāng)然是緩存箫津,不用每次都調(diào)狭姨。其實(shí)也可以在引擎層加類似GetXXX(int index)和GetXXXCount()的接口,避免遍歷數(shù)組時(shí)造成的gc苏遥。所以上面的例子可以改寫成下面的形式饼拍。
void Update()
{
var matCount = renderer.GetMaterialCount();
for(int i = 0; i < matCount;++i)
{
var mat = renderer.GetMaterial(i);
// mat.SetXXX()
}
}
-
Time.realtimeSinceStartup在iOS上會(huì)走系統(tǒng)調(diào)用,非常耗田炭,盡可能的少調(diào)用师抄,如下圖是引擎的Lod系統(tǒng)時(shí)每個(gè)幾幀會(huì)調(diào)用這個(gè)接口,導(dǎo)致了一些無用的開銷教硫。改法是把他改成用Time.frameCount叨吮,不必用啟動(dòng)時(shí)間來做diff。
Time.realtimeSinceStartup的系統(tǒng)調(diào)用 - 在大多數(shù)的簡(jiǎn)單渲染場(chǎng)景下瞬矩,如ui茶鉴,Camera的多線程culling在反而比同步culling要耗。引擎內(nèi)部默認(rèn)是用JobSystem來實(shí)現(xiàn)多線程culling的景用,建議加個(gè)Camera的屬性涵叮,讓這個(gè)culling可以動(dòng)態(tài)改成同步執(zhí)行。
- il2cpp的內(nèi)存alloc是通過內(nèi)存池(也可以說分塊申請(qǐng))的形式分配的伞插,用instrument是截取不了準(zhǔn)確內(nèi)存申請(qǐng)的堆棧割粮,所以如果某個(gè)時(shí)刻分配了很多內(nèi)存,這個(gè)時(shí)候想找到代碼源頭就比較蛋疼了媚污。一個(gè)粗暴的解決辦法是舀瓢,在il2cpp向內(nèi)存池申請(qǐng)內(nèi)存時(shí),再alloc一個(gè)相同大小的內(nèi)存耗美,這個(gè)時(shí)候instrument就能截到堆棧了氢伟。當(dāng)然這個(gè)方法只在dev包的時(shí)候才可以做,否則代碼內(nèi)存會(huì)翻一翻幽歼。
坑
- 打外網(wǎng)包時(shí)要備份library,否則在打資源更新包時(shí)谬盐,如果library沒有備份甸私,而是重新import生成的,其中很多序列化id都是隨機(jī)生成的飞傀,會(huì)跟之前的不一致皇型,而結(jié)果就是在打資源更新包的時(shí)候會(huì)發(fā)現(xiàn)有一堆變更诬烹。
- 資源更新和一些cache不要存在Application.temporaryCachePath,在手機(jī)上有可能會(huì)被系統(tǒng)刪掉弃鸦。簡(jiǎn)單的改法是用Application.persistentDataPath绞吁,但對(duì)于iOS,這個(gè)目錄文件過大的話唬格,可能會(huì)不過審家破,最好的做法是放在Library/Application Support/{packageName}里,具體見蘋果的文檔File System Basics
-
盡量少直接引用fbx里的子asset购岗。舉個(gè)例子:如果一個(gè)prefab里引用了如下fbx的_cao這個(gè)mesh汰聋,那在打包的時(shí)候調(diào)用就會(huì)把fbx的_cao2、_shui2喊积、_shan這些mesh也打在包里烹困,因?yàn)檫@個(gè)prefab你調(diào)用GetDepdency的時(shí)候會(huì)發(fā)現(xiàn),這個(gè)prefab依賴的是_shui.fbx這個(gè)整體乾吻,所以打包的時(shí)候會(huì)把整個(gè)整體都打進(jìn)去髓梅。解決辦法是把fbx里的子asset單獨(dú)拆出來,并制作規(guī)范禁止直接引用fbx里的資源绎签。
多asset的fbx在被引用時(shí)要留意打包問題 - ip5和ip5c不能開啟Metal和OpenGL ES3枯饿,所以是不支持Half float;而Android則沒有這個(gè)問題辜御。
所以在處理mesh頂點(diǎn)格式時(shí)要注意鸭你,否則ip5和ip5c會(huì)崩潰。具體原因可參考引擎PrepareMeshDataForBuildTarget::VertexCompression PrepareMeshDataForBuildTarget::GetMaxVertexCompressionForPlatform(BuildTargetSelection targetPlatform)
的實(shí)現(xiàn)擒权。 - ab包的加載袱巨,在引擎里的實(shí)現(xiàn)會(huì)有各種lock/unlock,這也是為啥unity加載資源卡頓的問題不好解決的原因碳抄。我們后面把a(bǔ)b包做了多線程的異步加載(非協(xié)程的異步加載)愉老,途中改造了不少引擎代碼,到最后幀率才逐漸穩(wěn)定剖效。不過現(xiàn)在看來其實(shí)治標(biāo)不治本嫉入,因?yàn)殒i的消耗還是很高,Unity的官方駐場(chǎng)表示也沒法解決璧尸,畢竟AssetBundle這個(gè)系統(tǒng)設(shè)計(jì)的太臃腫咒林。。