1. 程式人生 > >學習unity的一些經驗和見解

學習unity的一些經驗和見解

個人認為Unity相比於其他引擎易用性較好的原因主要有:

基於元件(Component)的關卡設計更符合直覺
Unity通過將系統或使用者的指令碼抽象為可重用的元件(Component)並附著在遊戲物件上來控制遊戲的行為。相較於傳統的基於指令碼的開發方式,關卡設計師可以更靈活、更快速地搭建介面和關卡,有一種“搭積木”的感覺。雖然這種設計犧牲了一部分擴充套件性(例如難以實現巢狀Prefab),但對初學者來講是非常友好的。

虛幻引擎4.7版本的更新也效仿Unity,向元件化的方向靠攏,使關卡結構更容易理解和維護。

使用Mono作為指令碼執行的平臺
C#/Mono相比C++和其他指令碼語言,有更好的穩定性和抽象能力,有容易使用的.NET框架和易於移植的各類開源庫,相對完善的語言服務(如GC和反射)也使開發複雜邏輯容易許多。雖然降低了門檻的同時也讓低質量的程式碼更容易產生,但不管對於初學者還是老鳥來說,我覺得都是利大於弊的。

引擎本身的功能相對簡單 + 豐富的Asset Store外掛

和虛幻等超級引擎相比,Unity提供的功能算是非常基礎的,元件數量和各元件可配置的內容都不多,所以在學習的時候更容易產生比較直觀的感受,不至於迷失在細節當中。另一方面,Asset Store模式的成功造就了大量功能強大的第三方外掛,填補了Unity開發中的各種空白,進一步降低了開發門檻。

而難於精通方面,我覺得主要原因在於:

對綜合能力要求高
首先,不僅對Unity,對任何遊戲引擎或者對遊戲開發本身來說,想要精通都是很難的一件事。因為遊戲客戶端開發本身是一項綜合性非常強的活動,整合的技術非常多,例如:
  • 建模
  • 關卡製作
  • 指令碼邏輯
  • 網路通訊
  • 平臺特性整合
  • 動畫製作
  • 特效製作
  • 工作流整合
  • 除錯和優化
而對於將這些技術粘合在一起的Unity工程師來說,雖然不需要精通方方面面,但將團隊成員的工作高效率高質量地結合在一起,也是非常考驗其能力的,只有長期地,全方面地參與整個開發過程並瞭解團隊成員的工作方式,才能逐漸成長為一名優秀的Unity工程師。

舉例來說,Unity工程師需要:
  • 與設計和美術團隊溝通,評估設計對於遊戲效能的影響,實現原型,進行各種效能測試。
  • 與伺服器工程師溝通,確定互相之間的介面和協議內容的細節。
  • Unity本身沒有一個Gameplay Framework,場景管理、遊戲資料管理等相對底層的框架都需要Unity工程師來搭建,如何減少團隊中其他成員的錯誤實踐,是主程式的責任,也很考驗其架構能力和對Unity Runtime的理解。
  • 一些特效需要自己編寫Shader才能實現。
  • 動畫師和美術團隊產生的資產,根據專案的需要常常要自己寫Pipeline來匯入和進行優化處理。一部分還需要以合適的方式打包,供客戶端增量下載,需要對Unity的資源管線有較好的理解才可以。而且看似簡單的決定之中,常常蘊含了很多效能上的考量。
  • 根據關卡設計師的需要,製作編輯器擴充套件工具,提高其工作效率。這方面的文件稀少,同時也需要對Unity獨特的序列化機制有比較深的理解才行。
  • 想利用iOS和Android以及各種平臺特有的功能時,需要針對特定的平臺編寫一些Native外掛,如本地Push通知,自定義系統鍵盤,系統彈窗等等。要懂一些iOS開發和Android開發的知識才能駕馭。
有些做Unity的工程師可能只是搭建關卡,寫一些控制指令碼,而優秀的Unity工程師的價值往往在於其能夠承擔更多的團隊職責。所以說想要精通這些,真的需要付出很多的時間和努力才行。

難在細節


任何事想精通,細節都是非常重要的,比如:
  • 記憶體管理
    • 避免和排查指令碼中的記憶體洩漏。例如沒有清空的委託和靜態閉包造成的引用。
    • 優化GC,瞭解Mono和.NET GC演算法的差別。比如Unity採用的是較老版本的Boehm GC,不分世代,GC分頁為1KB,記憶體碎片無法合併,單執行緒,缺少LOH,託管堆一旦擴大就很難向系統返還記憶體。如果按照.NET的思路優化GC,有時候並沒有理想的效果。而Unity最近引入的IL2CPP執行時,採用了新版本的Boehm GC,演算法又有所改變,優化策略也應適當調整。
    • 瞭解Unity的記憶體模型,哪些資源分配在Native堆上,哪些分配在Mono的託管堆上。Native上不同種類的資源分別用哪些方法能夠釋放乾淨。Native堆上的引用計數是如何工作的。如何減輕Unity自動釋放場景時的壓力。AssetBundle的記憶體結構是怎麼樣的,各個部分如何不依賴GC而精準地釋放。
    • 暫時隱藏起來的圖片或物體如何暫時性地釋放,在顯示時又如何重新載入回記憶體。
    • 瞭解哪些API和操作會分配記憶體,什麼時候使用值型別更好或更不好。這都需要對C#有很深入的理解。
  • 網路和下載
    • AssetBundle如何打包,什麼樣的圖片用什麼粒度打包效率最高。
    • 如果使用Json反序列化資料,怎麼才能避免記憶體碎片降低整個App的效能。
    • 不斷更新的素材需要增量下載,Unity內建的下載方式瓶頸在哪,怎麼自己實現一個比Unity內建API更高效的下載機制和更精準的快取控制機制。
  • 指令碼執行
    • 能夠將較複雜的操作(如反序列化資料,較重的IO操作)放在後臺執行緒執行,再排程回主執行緒更新遊戲介面,以避免UI卡頓。Unity的執行緒優先順序又是怎樣的?
    • 能否理解Unity Coroutine的迭代器本質,怎樣對Coroutine中的異常進行處理,如何使Coroutine具有返回值,Coroutine啟動時將分配多少記憶體,為什麼複雜的Coroutine會使用更多的記憶體,如何將多個Coroutine合併為一個從而消除記憶體分配。
    • 在與Native外掛(如iOS外掛)互動時,如何讓C#與Objective C或Java共享記憶體,從而減少大型資料封送造成的CPU負擔。
    • iOS平臺上AOT異常的根本原因是什麼,有哪幾種。值型別和泛型的組合更容易引發AOT異常的原因是什麼,如何繞過。怎麼安全的使用Linq,使用C#標準事件為什麼會觸發AOT異常,怎樣避免。Mono AOT的trampoline又是什麼,哪種風格的程式碼更容易耗盡trampoline並引發AOT異常。
    • Unity的C#編譯器有哪些弱點,哪些程式碼通過Visual Studio編譯成DLL再放到Unity中可以提高執行效率和記憶體使用效率。
    • 場景載入緩慢等Unity內部表現出來的問題如何從自身指令碼下手進行優化。或者說不同效能問題在自己程式碼中的Critical Path都是哪些部分。
  • 渲染
    • Draw Call是什麼。不同AssetBundle中的小圖片如何Batch到一個Draw Call中。
    • 移動平臺常用的Tile-based GPU有哪些弱點,如何避免。
    • Retina等高清螢幕上製作2D遊戲時,如何動態為圖片生成最小的Mesh網格以節約fill rate。
  • 團隊協作
    • 自己編寫Gameplay框架的情況下,如何控制隊友程式碼中的記憶體洩漏。
    • 使用版本管理系統時會產生哪些難以解決的衝突,如何建立開發規則。自己開發的框架或工具是否能有效避免隊友間發生衝突。
    • 如何實現資源管線的自動化。
    • 如何將各種奇葩動畫編輯器的輸出轉換成Unity標準的動畫資源。
    • 製作編輯器擴充套件時,是否能正確序列化複雜的資料結構。能否讓自己的工具和指令碼也實現所見即所得,讓隊友更快的搭建場景。
    • 是否會使用Gizmo和Handle來擴充套件場景編輯器。
    • 當不得不修改MonoBehaviour的定義時,怎樣讓已經上線的老版本中的資料正確地反序列化到新版本。
    • 要有能力編寫模組劃分明確,依賴關係合理清晰的可重用程式碼和元件,作為公司的資產加速新專案的開發。
這些都是Unity開發中會不斷面對的問題,如果不能從始至終中控制住這些細節,積累起來往往會使團隊效率底下,難以產出高質量的應用。

所以我覺得Unity難以精通之處就在於對細節知識的把握,以及在整合團隊價值的過程中如何做到揚長避短。Unity開發團隊中常常不是所有人都會這個引擎有很深刻的認識,大家術業有專攻,有人搭場景,有人做後端,對自己不熟悉的領域,難免有錯誤的認知和實踐。我們尚可以通過時間和努力精通細節,然而到頭來真正缺少的,其實是團隊成員間的信任所帶來的溝通成本的降低。

【更新】
回答一些朋友的疑問。

關於資料來源
細節問題很難有系統的資料來源,而像AssetBundle Dynamic Batch這種幾乎找不到答案的問題只能自己慢慢摸索。在此列舉幾個最主要的知識來源。
  1. 官方手冊。例如搜尋Unity Optimization可以找到官方在GPU,CPU和Mobile方面的幾篇優化手冊。官方資料常常包含最核心也最容易被忽略的原則,深入理解往往會有新的收穫。
  2. 官方部落格。部落格上不時會有一些技術類文章,如關於IL2CPP和序列化機制的知識幾乎只能看那幾篇部落格。Technology
  3. Unite的視訊和Slide。對某些領域有比較深入的探討,特別在記憶體管理,AssetBundle和程式碼組織方面。YouTube和SlideShare上搜Unite或Unity能找到。值得注意的是Unite的分會場,如日本和韓國,有時會有一些更加深入的分析。如 slideshare.net 的頁面
  4. Unity的Mono fork:Unity-Technologies/mono · GitHub ,關於GC和AOT方面是第一手的資料。比如gc的配置,Enumerable類的實現如何導致Linq容易觸發AOT異常,以及泛型CompareExchange中存在的JIT Hack導致C#事件和一些執行緒同步操作觸發AOT異常等等。另外GC方面也可以參考ivmai/bdwgc · GitHub ,有詳細的機制解釋。
  5. 隨Unity安裝的PDB除錯檔案。Unity的安裝目錄下其實是有Editor和Player當中所有C++原始碼的PDB檔案的,而且居然都是private PDB。需要探查Unity內部資料結構和過程實現的時候,通過WinDbg配合這些PDB檔案除錯Unity程序可以獲得很多最底層的資訊。當然,如果公司買了Unity的原始碼就不必這樣麻煩了。

關於優化策略
不只是Unity,優化程式最重要的原則就是先測量。而且在沒有豐富經驗和自信的情況下,不要自己寫測量程式碼,而要依賴Unity自己Profiler和Profiler API。這裡只說一些Unity特有的內容。

使用Profiler時,切忌猜測。一定要弄清各種資料的精確涵義,如Self %,Self ms,GC Alloc等,如果弄不清楚,優化常常是南轅北轍。另外診斷CPU Spike時,一定要開啟Deep Profile,否則只能看到誤導性很強的表面資料。找到真正的Hot Line才能著手優化改善效能。

當Hot Line在自己的程式碼中時,可以嘗試將CPU密集的操作分派到後臺執行緒,然後在需要與Unity API互動時排程回來。粒度較好的操作可以嘗試用Coroutine分派到其他幀分別執行。大量GC Alloc造成的Spike需要重新設計記憶體分配策略,小物件(目前版本是小於1KB)較多的時候也可以嘗試預先擴大託管堆(如分配很多小於1KB的緩衝區,然後再釋放),這樣可以加速後續記憶體分配。因為Boehm GC的堆擴充套件策略是時間線性而非空間線性的,所以每次擴大後的容量都是翻倍的,需要注意。

而當Hot Line在Unity API中時,常常是自己的錯誤實踐造成的,需要重新審視設計。一方面要減少昂貴API的呼叫次數,一方面要降低Unity內部處理資料的規模。例如場景載入緩慢時,可能需要簡化場景本身,然後在場景啟動後再手動、增量地載入場景中的其他內容。

另外要了解一些底層知識。例如App啟動時效能較差的原因,在非AOT平臺上可能是因為大量的JIT編譯造成的,而在AOT平臺上則可能是因為初始化程式碼過於複雜導致CPU快取命中率很低,和作業系統頻繁地Page Fault。這也是為什麼啟動程式碼一定要精簡,並且要儘量實現批處理。

GPU方面,如果沒有複雜的特效,瓶頸常常在Draw Call和Fill Rate上。Draw Call需要Batch,能共享的材質一定要共享。Fill Rate的問題通常在高解析度的2D遊戲中比較明顯,Profiler中的Transparency渲染佔比很大時就應該著手優化。GPU優化策略上Unity相比其他引擎並沒有很多特異的方面,準確測量的基礎上通常能找到比較通用的解決方法。