- Unity 3D腳本編程:使用C#語言開發(fā)跨平臺游戲
- 陳嘉棟
- 4836字
- 2023-02-28 19:22:09
2.2 Mono如何扮演腳本的角色
Mono究竟為何被Unity 3D游戲引擎的開發(fā)人員選擇作為其腳本模塊的基礎(chǔ)呢?而Mono又是如何提供這種腳本的功能的呢?
如果需要利用Mono為應(yīng)用開發(fā)提供腳本功能,那么其中一個前提就是需要將Mono的運行時嵌入到應(yīng)用中,因為只有這樣才有可能使托管代碼和腳本能夠在原生應(yīng)用中使用。所以,可以發(fā)現(xiàn)將Mono運行時嵌入應(yīng)用中是多么重要。但在討論如何將Mono運行時嵌入原生應(yīng)用中之前,首先要清楚Mono是如何提供腳本功能的,以及Mono提供的到底是怎樣的腳本機制。
2.2.1 Mono和腳本
本節(jié)將會討論如何利用Mono來提高開發(fā)效率以及拓展性,而無須將已經(jīng)寫好的C/C++代碼重新用C#寫一遍,也就是Mono是如何提供腳本功能的。在第1章中曾經(jīng)說過,在過去開發(fā)游戲時,常常只使用一種編程語言。游戲開發(fā)者往往需要在高效率的低級語言和低效率的高級語言之間抉擇。例如,一個用C/C++開發(fā)的應(yīng)用的結(jié)構(gòu),如圖2-1所示。一個腳本語言開發(fā)的應(yīng)用的結(jié)構(gòu),如圖2-2所示。

圖2-1C/C++開發(fā)的應(yīng)用的結(jié)構(gòu)
可以看到低級語言和硬件打交道的方式更加直接,所以其效率更高。

圖2-2 腳本語言開發(fā)的應(yīng)用的結(jié)構(gòu)
可以看到高級語言并沒有直接和硬件打交道,所以其效率較低。
如果以速度作為衡量語言級別的標(biāo)準(zhǔn),那么語言從低級到高級的大概排名如下所示。
? 匯編語言。
? C/C++,編譯型靜態(tài)不安全語言。
? C#、Java,編譯型靜態(tài)安全語言。
? Python、Perl、JavaScript,解釋型動態(tài)安全語言。
開發(fā)者在選擇適合自己的開發(fā)語言時,的確面臨著很多現(xiàn)實的問題。高級語言對開發(fā)者而言效率更高,也更加容易掌握。但高級語言也并不具備低級語言的那種運行速度,甚至對硬件的要求更高,這在某種程度上的確也決定了一個項目到底是成功還是失敗。
因此,如何平衡兩者,或者說如何融合兩者的優(yōu)點,變得十分重要和迫切。腳本機制便在此時應(yīng)運而生。應(yīng)用的引擎由富有經(jīng)驗的開發(fā)人員使用C/C++開發(fā),而一些具體項目中功能的實現(xiàn),例如UI、交互等,則使用高級語言開發(fā)。
通過使用高級腳本語言,開發(fā)者便融合了低級語言和高級語言的優(yōu)點。同時提高了開發(fā)效率,如第1章中所講,引入腳本機制后開發(fā)效率提升了,可以快速地開發(fā)原型,而不必把大量的時間浪費在重編譯上。
腳本語言同時提供了安全的開發(fā)環(huán)境,也就是說開發(fā)者無須擔(dān)心C/C++開發(fā)的引擎中的具體實現(xiàn)細節(jié),也無須關(guān)注例如資源管理和內(nèi)存管理這些細節(jié),這在很大程度上簡化了應(yīng)用的開發(fā)流程。
而Mono則提供了這種腳本機制實現(xiàn)的可能性,即允許開發(fā)者使用JIT編譯的代碼作為腳本語言為他們的應(yīng)用提供拓展。
目前很多腳本語言的選擇趨向于解釋型語言,例如Cocos2d-JS使用的JavaScript。因此效率無法與原生代碼相比。而Mono則提供了一種將腳本語言通過JIT編譯為原生代碼的方式,提高了腳本語言的效率。例如Mono提供了一個原生代碼生成器,使你的應(yīng)用的運行效率盡可能快,同時提供了很多方便調(diào)用原生代碼的接口。
因為當(dāng)為一個應(yīng)用提供腳本機制時,往往需要和低級語言交互。而最常見的做法是提供句柄供腳本語言操作。這便不得不提到Mono運行時嵌入到應(yīng)用中的必要性。
2.2.2 Mono運行時的嵌入
既然明確了Mono運行時嵌入應(yīng)用的重要性,那么如何將它嵌入應(yīng)用中呢?
本節(jié)會為大家分析一下Mono運行時究竟如何被嵌入到應(yīng)用中、如何在原生代碼中調(diào)用托管方法,以及如何在托管代碼中調(diào)用原生方法。而眾所周知的是,Unity 3D游戲引擎本身是用C++寫成的,所以本節(jié)就以Unity 3D游戲引擎為例,假設(shè)此時已經(jīng)有了一個用C++寫好的應(yīng)用(Unity 3D),如圖2-3所示。

圖2-3 用C++寫好的應(yīng)用
將你的Mono運行時嵌入到這個應(yīng)用后,應(yīng)用就獲取了一個完整的虛擬機運行環(huán)境。而這一步需要將“l(fā)ibmono”和應(yīng)用鏈接,一旦鏈接完成,C++應(yīng)用的地址空間如圖2-4所示。

圖2-4C++應(yīng)用的地址空間
而Mono的嵌入接口會將Mono運行時暴露給C++代碼。這樣通過這些接口,開發(fā)者就可以控制Mono運行時,以及依托于Mono運行時的托管代碼。
一旦Mono運行時初始化成功,那么下一步最重要的就是將CIL/.NET代碼加載進來。加載后的地址空間如圖2-5所示。

圖2-5 加載后的地址空間
那些C/C++代碼,通常稱為非托管代碼。而通過CIL編譯器生成CIL代碼,通常稱為托管代碼。
所以,將Mono運行時嵌入應(yīng)用,可以分為3個步驟。
(1)編譯C++程序和鏈接Mono運行時。
(2)初始化Mono運行時。
(3)C/C++和C#/CIL的交互。
首先需要將C++程序進行編譯并鏈接Mono運行時。此時會用到pkg-config工具。
在OS X系統(tǒng)上使用homebrew來進行安裝,在終端中輸入命令“brew install pkgconfig”,可以看到終端會輸出如下內(nèi)容。

終端輸出結(jié)束之后,證明pkg-config安裝完成。
接下來新建一個C++文件,將其命名為unity.cpp,作為原生代碼部分。需要將這個C++文件進行編譯,并和Mono運行時鏈接。
在終端輸入如下內(nèi)容。

此時,經(jīng)過編譯和鏈接后,unity.cpp和Mono運行時被編譯成了可執(zhí)行文件。
此時就需要將Mono的運行時初始化。所以再重新回到剛剛新建的unity.cpp文件中,要在C++文件中進行Mono運行時的初始化工作,即調(diào)用mono_jit_init方法,代碼如下。


mono_jit_init這個方法會返回一個MonoDomain(Mono程序域),用來作為盛放托管代碼的容器。其中參數(shù)managed_binary_path,即應(yīng)用運行域的名字。除了會返回MonoDomain之外,這個方法還會初始化默認框架版本,即2.0或4.0,這個主要由使用的Mono版本來決定。當(dāng)然,也可以手動指定版本。只需要調(diào)用下面的方法即可,代碼如下。

此時就獲取了一個應(yīng)用域——domain。但是當(dāng)Mono運行時被嵌入一個原生應(yīng)用時,它顯然需要一種方法來確定自己所需要的運行時程序集以及配置文件。在默認情況下,它會使用在系統(tǒng)中定義的位置。例如/usr/lib/mono目錄下的程序集,以及/etc/mono目錄下的配置文件。但是,如果應(yīng)用需要特定的運行時,顯然也需要指定其程序集和配置文件的位置。如圖2-6所示,在一臺電腦上可以存在很多不同版本的Mono,所以選擇指定版本的Mono就變得十分必要。

圖2-6 一臺電腦上存在的不同版本的Mono
為了選擇我們所需要的Mono版本,可以使用mono_set_dirs方法,代碼如下。

這樣就設(shè)置了Mono運行時的程序集和配置文件路徑。
當(dāng)然,Mono運行時在執(zhí)行一些具體功能時,可能還需要依靠額外的配置文件來進行。所以有時也需要為Mono運行時加載這些配置文件,通常使用mono_config_parse方法來進行加載這些配置文件的工作。
當(dāng)mono_config_parse的參數(shù)為NULL時,Mono運行時將加載Mono的配置文件(通常是/etc/mono/config)。當(dāng)然作為開發(fā)者,也可以加載自己的配置文件,只需要將自己的配置文件的文件名作為mono_config_parse方法的參數(shù)即可。
Mono運行時的初始化工作到此完成。接下來就需要加載程序集并運行它。這時需要用到MonoAssembly和mono_domain_assembly_open這個方法,代碼如下。


代碼會將當(dāng)前目錄下的ManagedLibrary.dll文件中的內(nèi)容加載到已經(jīng)創(chuàng)建好的domain中。此時需要注意的是,Mono運行時僅僅是加載代碼,而沒有立刻執(zhí)行這些代碼。
如果要執(zhí)行這些代碼,則需要調(diào)用被加載的程序集中的方法。或者當(dāng)有一個靜態(tài)的主方法時(也就是一個程序入口),可以很方便地通過mono_jit_exec方法來調(diào)用這個靜態(tài)入口,代碼如下。

當(dāng)然,最好總是能夠保證提供一個這樣的靜態(tài)入口,并且在啟動Mono運行時的時候通過調(diào)用mono_jit_exec方法來執(zhí)行這個靜態(tài)入口。因為這樣做可以為應(yīng)用域提供一些額外的信息。
舉一個將Mono運行時嵌入C/C++程序的例子,主要流程是加載一個由C#文件編譯成的DLL文件,然后調(diào)用一個C#的方法并輸出Hello World。
首先完成C#部分的代碼,代碼如下。

在這個文件中,實現(xiàn)了輸出Hello World的功能,然后將它編譯為DLL文件。這里也直接使用了Mono的編譯器——mcs。在終端命令行使用mcs編譯該cs文件。同時為了生成DLL文件,還需要加上“-t:library”選項,代碼如下。

這樣便得到了cs文件編譯后的DLL文件,叫ManagedLibrary.dll。
接下來完成C++部分的代碼。嵌入Mono的運行時,同時加載剛剛生成的ManagedLibrary.dll文件,并且執(zhí)行其中的main方法用來輸出Hello World,代碼如下。


從代碼可以看到,在C/C++代碼中調(diào)用C#的方法需要兩個步驟。第一步是要獲取目標(biāo)方法的MonoMethod句柄,第二步是調(diào)用該方法。
在本例中,首先獲取了一個MonoClass用來代表C#中定義的目標(biāo)類型。獲取MonoClass可以使用如下代碼。

接下來使用mono_method_desc_new方法來獲取一個MonoMethodDesc,代碼如下。

由于第二個參數(shù)為true,即需要包括命名空間。所以,要尋找的目標(biāo)函數(shù)是ManagedLibrary.MainTest:Main()方法。獲取了C#中的方法的MonoMethodDesc后,就可以根據(jù)這個MonoMethodDesc來獲得MonoMethod,即目標(biāo)方法的代表。獲取MonoMethod可以通過接口實現(xiàn),代碼如下。

此時就獲取了C#文件中的目標(biāo)方法的句柄。下一步便是如何調(diào)用該方法了。
可以直接使用mono_runtime_invoke()方法,通過托管代碼中的目標(biāo)方法的句柄來調(diào)用該目標(biāo)方法,代碼如下。

mono_runtime_invoke()方法中第一個參數(shù)便是剛剛獲取的MonoMethod,而第二個參數(shù)則相當(dāng)于“this”。所以若調(diào)用的是靜態(tài)方法,則此參數(shù)為NULL。然后編譯運行,可以看到屏幕上輸出了“Hello World”。
但是既然要提供腳本功能,將Mono運行時嵌入C/C++程序后,只是在C/C++程序中調(diào)用C#中定義的方法顯然還是不夠的。腳本機制的最終目的還是希望能夠在腳本語言中使用原生的代碼,所以下面將站在Unity 3D游戲引擎開發(fā)者的角度,繼續(xù)探索如何在C#文件(腳本文件)中調(diào)用C/C++程序中的代碼(游戲引擎)。
首先,假設(shè)要實現(xiàn)的是Unity 3D的組件系統(tǒng)。為了方便游戲開發(fā)者能夠在腳本中使用組件,首先要在C#文件中定義一個Component類,代碼如下。

與此同時,在Unity 3D游戲引擎(C/C++)中,則必然有和腳本中的Component相對應(yīng)的結(jié)構(gòu),代碼如下。

可以看到此時組件類Component只有一個屬性,即ID。再為組件類增加Tag屬性。
為了使托管代碼能夠和非托管代碼交互,需要在C#文件中引入命名空間System.Runtime.CompilerServices,同時需要提供一個IntPtr類型的句柄,以便托管代碼和非托管代碼之間引用數(shù)據(jù)(IntPtr類型被設(shè)計成整數(shù),其大小適用于特定平臺,即此類型的實例在32位硬件和操作系統(tǒng)中將是32位,在64位硬件和操作系統(tǒng)中將是64位。IntPtr對象常可用于保持句柄。例如,IntPtr的實例廣泛地用在System.IO.FileStream類中來保持文件句柄)。最后,將Component對象的構(gòu)建工作由托管代碼C#移交給非托管代碼C/C++,這樣游戲開發(fā)者只需要專注于游戲腳本即可,無須關(guān)注C/C++層面,即游戲引擎層面的具體實現(xiàn)邏輯。所以在此提供兩個方法,即用來創(chuàng)建Component實例的方法GetComponents和獲取ID的get_id_Internal方法。
這樣在C#端,定義了一個Component類,主要目的是為游戲腳本提供相應(yīng)的接口,而非具體邏輯的實現(xiàn)。在C#代碼中定義的Component類,代碼如下。


還需要創(chuàng)建這個類的實例,并且訪問它的兩個屬性,所以再定義另一個類Main,來完成這項工作。Main的實現(xiàn),代碼如下。

完成了C#部分的代碼后,需要將具體的邏輯在非托管代碼端實現(xiàn)。而上文之所以要在Component類中定義ID屬性和Tag屬性,是為了使用兩種不同的方式訪問這兩個屬性,其中之一就是直接將句柄作為參數(shù)傳入到C/C++中。例如上文所講的get_id_Internal這個方法,它的參數(shù)便是句柄。第二種方法則是在C/C++代碼中通過Mono提供的mono_field_get_value方法直接獲取對應(yīng)的組件類型的實例。
所以組件Component類中的屬性獲取有兩種不同的方法,代碼如下。


由于在C#代碼中基本只提供接口,而不提供具體邏輯實現(xiàn),所以還需要在C/C++代碼中實現(xiàn)獲取Component組件的具體邏輯,然后再以在C/C++代碼中創(chuàng)建的實例為樣本,調(diào)用Mono提供的方法在托管環(huán)境中創(chuàng)建相同的類型實例,并且初始化。
由于C#中的GetComponents方法返回的是一個數(shù)組,所以需要使用MonoArray從C/C++中返回一個數(shù)組。所以C#代碼中GetComponents方法在C/C++中對應(yīng)的具體邏輯的代碼如下。

其中num_Components是uint32_t類型的字段,用來表示數(shù)組中組件的數(shù)量,為它賦值為5。然后通過Mono提供的mono_object_new方法來創(chuàng)建MonoObject的實例。而需要注意的是代碼中的Components[i],Components便是在C/C++代碼中創(chuàng)建的Component實例,這里用來給MonoObject的實例初始化賦值。
創(chuàng)建Component實例的過程代碼如下。

C/C++代碼中創(chuàng)建的Component的實例的ID為i,tag為i*4。
最后還需要將C#中的接口和C/C++中的具體實現(xiàn)關(guān)聯(lián)起來,即通過Mono的mono_add_internal_call方法來實現(xiàn),也即在Mono的運行時中注冊剛剛用C/C++實現(xiàn)的具體邏輯,以便將托管代碼(C#)和非托管代碼(C/C++)綁定,代碼如下。

這樣便使用非托管代碼(C/C++)實現(xiàn)了獲取組件、創(chuàng)建和初始化組件的具體功能,完整的代碼如下。



為了驗證是否成功地模擬了將Mono運行時嵌入“Unity 3D游戲引擎”中,需要將代碼編譯,并且查看輸出是否正確。
首先將C#代碼編譯為DLL文件,在終端直接使用Mono的mcs編譯器來完成這項工作,代碼如下。

運行后生成了ManagedLibrary.dll文件。然后將unity.cpp和Mono運行時鏈接,代碼如下。

運行后會生成一個a.out文件(OS X系統(tǒng))。執(zhí)行a.out,可以在終端上看到創(chuàng)建出的組件的ID和Tag的信息,如圖2-7所示。

圖2-7 Mono運行時嵌入C/C++的運行結(jié)果
通過本節(jié)內(nèi)容可以看到游戲腳本語言出現(xiàn)的必然性。同時也應(yīng)該更加明確Unity 3D的底層是C/C++實現(xiàn)的,但是它通過Mono提供了一套腳本機制,以方便游戲開發(fā)者快速地開發(fā)游戲,同時也降低了游戲開發(fā)的門檻。
但是Unity 3D游戲引擎作為一款開發(fā)跨平臺作品的工具,它還采用了Mono來提供自己的腳本模塊基礎(chǔ),那么Unity 3D的跨平臺能力就和Mono息息相關(guān)。
- 測試驅(qū)動開發(fā):入門、實戰(zhàn)與進階
- JavaScript語言精髓與編程實踐(第3版)
- Java從入門到精通(第4版)
- Practical DevOps
- Mastering Python Networking
- Linux操作系統(tǒng)基礎(chǔ)案例教程
- Android系統(tǒng)原理及開發(fā)要點詳解
- Visual Basic程序設(shè)計上機實驗教程
- 速學(xué)Python:程序設(shè)計從入門到進階
- RubyMotion iOS Develoment Essentials
- UI動效設(shè)計從入門到精通
- 原型設(shè)計:打造成功產(chǎn)品的實用方法及實踐
- C++17 By Example
- C語言程序設(shè)計實驗指導(dǎo)
- Spring Boot 3:入門與應(yīng)用實戰(zhàn)