歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~
由 QQ會員技術團隊 釋出在雲+社群
1. Unity編輯器基礎
從原理上講,遊戲開發就是將一系列變動的場景呈現在玩家面前,並根據玩家的輸入修改遊戲畫面;而遊戲畫面則是通過呼叫目標作業系統上的圖形影像庫來繪製的。比較知名的圖形影像庫有Windows上的DirectX,*nix系統、macOS和iOS等系統上用到的OpenGL以及Android用到的Vulkan等。
一般來講,底層的圖形影像API只能進行最基本的三角形繪製,但是,因為是通過計算機的GPU進行的操作,具有平行計算的優勢,在短短六十分之一秒時間內,也可以繪製出成千上萬個三角形,而這麼多小三角形堆疊起來看,視覺效果也就和真實場景差別不大了。
現代遊戲引擎一般都會把遊戲人物的“建模”工作交給第三方,引擎本身只負責遊戲場景和人物的繪製以及內部互動邏輯。第三方建模軟體通過模擬人物的真實3D外觀來將虛擬人物表面“三角形化”,附帶上游戲人物在做出不同動作時的外觀資料,最後生成遊戲引擎可識別格式的檔案,這個過程就是所謂的3D建模。
3D模型製作完成後,會由遊戲引擎進行繪製,這個過程一般稱作“著色”(Shading)。著色的核心是叫做“著色器(Shader)”的GPU程式 – GPU通過輸入一些引數資訊,然後執行著色器程式就能生成最終的遊戲影像。
GPU需要的引數資訊主要有兩種:一是紋理,二是材質。
紋理是指一個模型的表面,可以理解成一件衣服平鋪起來的樣子。如果是一個三維物體,其表面的紋理可以想象成是把它的表面拆開,然後壓扁後的樣子。什麼是材質呢?材質(Material)從字面上理解的話就是材料,比如木頭和大理石,看起來就是不一樣的效果。同樣的紋理,用不一樣的材質來繪製,會得到不一樣的效果圖,因為材質有一些關鍵的引數,會影響著色器的繪製效果。
比較重要的一個引數是反射率(Albedo)。
光滑材質的反射率比較高,看起來就會亮一些。在自然白光的照射下,這樣的材質看起來會偏白,如果沿著光照方向看過去,會出現光斑效果(太陽光照射下的湖面看起來會有一種很耀眼的效果)。粗糙材質的反射率比較低,看起來就比較柔和。典型的高反射率材質比如光滑的金屬表面,典型的低反射率材質有布料、地面等。在3D場景中,反射率高的物體受周圍物體的影響更大。譬如,一個平靜的湖面會倒映出地面的建築物。因此,高反射率的材質通常需要更多的繪製步驟。
材質的另一個重要引數是法向圖(Normal Map)。
法向就是物體表面的方向。法向圖表示的是材質的表面細節,比如凹槽、斑點、凸起或者空洞等,法向圖通常以紋理圖來表示。然而不同於一般的紋理圖,法向圖的每個畫素點稱作“紋素(texel)”,它表示的是紋理在此位置處的光照反射方向,紋素的RGB分量分別對應反射方向的XYZ分量。
一個3D模型的表面紋理被分割成一個個小三角形,而法向圖就表示此表面的每個畫素點位置的光照反射方向。方向不同的三角形繪製出來和周圍的三角形看起來顏色是不同的,從而產生了視覺上的凸起/凹陷效果。這種物體的表面細節,如果在3D建模階段通過修改模型外觀的方式來實現的話,會增加很多物體表面的細小的繪製操作。通過材質的法向圖來實現,將物體“表面”和物體的實際皮膚剝離開了,可以實現同一個人物穿上不同衣服的效果。
如上圖所示,右邊的物體採用左邊的法向圖來繪製,注意看凸起位置的顏色
2. C#指令碼語言
2.1 為什麼需要指令碼?
長久以來,遊戲引擎開發都採用底層語言如C++來進行,這對於遊戲上層開發來說,並不友好。很難想象如果使用一款引擎修改某個人物的動作,還需要直接呼叫C++底層的介面,這樣既不安全,也不方便。因此,一般引擎從設計之初就會把封裝好的繪製介面通過某些上層語言暴露出來,給遊戲製作方使用。這些上層語言就叫做遊戲指令碼語言。
lua是指令碼語言裡面比較流行的一種,因其虛擬機器小巧、API豐富、可靈活定製而深受遊戲引擎開發商的喜愛。Unity使用了C#和Unity Script(現已廢棄)來作為指令碼語言。C#語言因為建立在.NET IL之上而具有跨平臺擴充套件性。這樣,遊戲開發者只需要一套程式碼就可在多個平臺執行。
2.2 IL是什麼?
IL(Intermediate Language,在.NET平臺下是CIL,Common Intermediate Language)是一種中間語言格式,類似於Java的位元組碼(byte code),這種格式的程式碼需要一個虛擬機器來“解釋”執行。IL的所有指令都是基於虛擬堆疊的:呼叫函式前,先將引數push到虛擬堆疊裡面;函式執行的時候,從虛擬堆疊裡面取出引數,然後將結果壓入虛擬堆疊。由於呼叫方式簡單,IL語言的指令集也比較精簡。
IL作為指令碼語言的獨到之處在於可以將C#上層語言的各種特性(如泛型、協程等)轉換成基本的IL指令集,但是這樣的轉換也是有代價的 — 轉換後的IL指令比普通的函式呼叫多出數倍。因此,在遊戲開發中,不宜在每一幀中都進行這一類的呼叫。
另外,IL語言執行需要一個虛擬機器翻譯成目標平臺的機器碼,雖然.NET虛擬機器已經比較高效了(可參考.NET與Java的對比),但是和平臺原生程式碼比起來,依然有一些差距。在iOS平臺上,由於蘋果禁止使用JIT方式,IL指令需要預先編譯成目標平臺庫檔案,然後在最終二進位制檔案打包的時候作為第三方庫連結進去。Unity遊戲幾乎所有的遊戲邏輯都是通過指令碼來實現的,一個大型遊戲,成千上萬個指令碼,AOT方式打包造成的效率低下,是不得不考慮的問題。因此,Unity在5.3.4版本中引入了il2cpp技術。
2.3 il2cpp原理
顧名思義,il2cpp就是把中間語言轉換成cpp程式碼的工具。上面我們講到,在iOS平臺上,由於無法使用JIT方式執行IL指令,所以需要先將遊戲指令碼打包成.NET Managed Assembly(這裡的Managed是指二進位制檔案是在.NET層面打包的,可能會依賴.NET底層庫,可以理解為“安全的”庫檔案。另外有些庫檔案是通過直接封裝C/C++介面方式生成的,由於有如指標之類的底層記憶體操作,所以稱作是Unmanaged Assembly),然後和.NET CLR的Assembly連結之後生成最終的平臺二進位制檔案。il2cpp的作用是去掉連結.NET CLR的步驟,將C#指令碼生成的Managed Assembly“翻譯”成C++檔案,最後用目標平臺的編譯器編譯這些C++檔案來生成最終的遊戲可執行檔案。
il2cpp會先讀取.NET二進位制檔案,解析其中的符號,然後將其中C#方法轉換成對應的C方法。雖然名為il2cpp,但其實它只用到了很少部分的C++特性,絕大多數轉換後的程式碼都是C函式。
在遊戲執行前,il2cpp會啟動一個小的虛擬機器,用於動態解析C方法。其會將所有方法的簽名放在一個叫做global-metadata.dat的檔案裡,方法呼叫的時候會先從此檔案裡讀取C函式地址,然後再呼叫。
獲取函式指標的方法是這個:
inline Il2CppMethodPointer il2cpp_codegen_resolve_icall (const char* name){
Il2CppMethodPointer method = il2cpp::vm::InternalCalls::Resolve (name); if (!method)
{
il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetMissingMethodException(name));
} return method;
}
複製程式碼
[ 圖九:獲取函式指標 ]
Unity確保了所有采用il2cpp平臺實現的遊戲,其metadata的格式都是一樣的。metadata載入時採用了記憶體對映技術,上述函式實際上會從一張記憶體的資料表裡查詢方法名對應的鍵值,也即目標函式的地址。
為何Unity要採用檔案來記錄方法名?一是遊戲有動態解析方法的需求;再者是這樣可以隱藏掉遊戲內部邏輯的實現,起到一部分混淆的作用;最後還有一個重要的原因是Unity編輯器裡可以設定指令碼執行時候的延遲時間,而這些資訊可以很方便的放在檔案裡。
Unity C#層面的介面暴露給遊戲開發者,開發者通過C#指令碼編寫遊戲邏輯,然後通過il2cpp將指令碼翻譯成C++檔案,接著連結上Unity C#介面的底層C++實現,最終生成遊戲的二進位制檔案,這就是Unity遊戲開發的大致過程。
按照Unity的說法,通過il2cpp方式打包有多種好處:
- 跨平臺相容性更好。基本上所有遊戲平臺都支援C++程式碼,而.NET/Mono執行時卻不一定能在所有平臺上執行;
- 效率更高。Unity給出的資料顯示採用il2cpp打包之後,遊戲的執行效率提升了1.5到2.0倍。
以上就是遊戲開發的一些基本知識。
相關閱讀
此文已由作者授權騰訊雲+社群釋出,轉載請註明文章出處