天天看點

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html

從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

c#語言規範

閱讀目錄

  • 0x00 前言
  • 0x01 Why?從為何需要遊戲腳本開始
  • 0x02 What?Mono提供的腳本機制
  • 0x03 How?如何模拟Unity3D中的腳本機制
  • 0x04 後記

回到目錄

0x00 前言

在日常的工作中,我偶爾能遇到這樣的問題:“為何遊戲腳本在現在的遊戲開發中變得不可或缺?”。那麼這周我就寫篇文章從遊戲腳本聊起,分析一下遊戲腳本因何出現,而mono又能提供怎樣的腳本基礎。最後會通過模拟Unity3D遊戲引擎中的腳本功能,将Mono運作時嵌入到一個非托管(C/C++)程式中,實作腳本語言和“引擎”之間的分離。

回到目錄

0x01 Why?從為何需要遊戲腳本開始

首先聊聊為何現在的遊戲開發需要使用遊戲腳本這個話題。

為何需要有腳本系統呢?腳本系統又是因何而出現的呢?其實遊戲腳本并非一個新的名詞或者技術,早在暴雪的《魔獸世界》開始火爆的年代,人們便熟知了一個叫做Lua的腳本語言。而當時其實有很多網遊都不約而同的使用了Lua作為腳本語言,比如網易的大話西遊系列。

但是在單機遊戲流行的年代,我們卻很少聽說有什麼單機遊戲使用了腳本技術。這又是為什麼呢?因為當時的硬體水準不高,是以需要使用C/C++這樣的語言來盡量壓榨硬體的性能,同時,單機遊戲的更新換代并不如網遊那麼迅速,是以開發時間、版本疊代速度并非其考慮的第一要素,因而可以使用C/C++這樣開發效率不高的語言來開發遊戲。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

但是随着時間的推移,硬體水準逐年水漲船高,壓榨硬體性能的需求已經不再迫切。相反,此時網遊的興起卻對開發速度、版本更疊提出了更高的要求。是以開發效率并不高效,且投資巨大風險很高的C/C++便不再适應市場的需求了。而更加現實的問題是,随着java、.net甚至是javascript等語言的流行,程式員可以選擇的語言越來越多,這更加導緻了優秀的C/C++程式員所占比例越來越小。而網遊市場的不斷擴大,這種對人才的需求也同樣越來越大,這就造成了大量的人才空缺,也就反過來提高了使用C/C++開發遊戲的成本。而由于C/C++是門入門容易進階難的語言,其進階特性和高度靈活性帶來的高風險也是每個項目使用C/C++進行開發時,所不得不考慮的問題。

而一個可以解決這種困境的舉措便是在遊戲中使用腳本。可以說遊戲腳本的出現,不僅解決了由于C/C++難以精通而帶來的開發效率問題,而且還降低了使用C/C++進行開發的項目風險和成本。從此,腳本與遊戲開發相得益彰,互相促進,逐漸成為了遊戲開發中不可或缺的一個部分。

而到了如今手遊興起的年代,市場的需求變得更加龐大且變化更加頻繁。這就更加要求需要有腳本語言來提高項目的開發效率、降低項目的成本。

而作為遊戲腳本,它具體的優勢都包括哪些呢?

  1. 易于學習,代碼友善維護。适合快速開發。
  2. 開發成本低。由于上述第一點,因為易于學習,是以可以啟用新人,同時開發速度快,這些都是降低成本的方法。

是以,包括Unity3D在内的衆多遊戲引擎,都提供了腳本接口,讓開發者在開發項目時能夠擺脫C/C++(注:Unity3D本身是用C/C++寫的)的束縛,這其實是變相的降低了遊戲開發的門檻,吸引了很多獨立開發者和遊戲制作愛好者。

回到目錄

0x02 What?Mono提供的腳本機制

首先一個問題:Mono是什麼?

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

Mono是一個由Xamarin公司所贊助的開源項目。它基于通用語言架構(Common Language Infrastructure ,縮寫為CLI)和C#的ECMA 标準(Ecma-335、Ecam-334),提供了微軟的.Net架構的另一種實作。與微軟的.Net架構不同的是,Mono具備了跨平台的能力,也就是說它不僅能運作在Windows系統上,而且還可以運作在Mac OSX、Linux甚至是一些遊戲平台上。

是以把它作為跨平台的方案是像Unity3D這種開發跨平台遊戲的遊戲引擎的一個不錯的選擇。但Mono又是如何提供這種腳本的功能的呢?

如果需要利用Mono為應用開發提供腳本功能,那麼其中一個前提就是需要将Mono的運作時嵌入到應用中,因為隻有這樣才有可能使得托管代碼和腳本能夠在原生應用中使用。是以,我們可以發現,将Mono運作時嵌入應用中是多麼的重要。但在讨論如何将Mono運作時嵌入原生應用中去之前,我們首先要搞清楚Mono是如何提供腳本功能的,以及Mono提供的到底是怎樣的腳本機制。

Mono和腳本

本小節将會讨論如何利用Mono來提高我們的開發效率以及拓展性而無需将已經寫好的C/C++代碼重新用C#寫一遍,也就是Mono是如何提供腳本功能的。

常常使用一種程式設計語言開發遊戲是比較常見的一種情況。因而遊戲開發者往往需要在高效率的低級語言和低效率的進階語言之間抉擇。例如一個用C/C++開發的應用的結構如下圖:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

可以看到低級語言和硬體打交道的方式更加直接,是以其效率更高。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

可以看到進階語言并沒有和硬體直接打交道,是以其效率較低。

如果以速度作為衡量語言的标準,那麼語言從低級到進階的大體排名如下:

  • 彙編語言
  • C/C++,編譯型靜态不安全語言
  • C#、Java,編譯型靜态安全語言
  • Python, Perl, Javascript,解釋型動态安全語言

開發者在選擇适合自己的開發語言時,的确面臨着很多現實的問題。

進階語言對開發者而言效率更高,也更加容易掌握,但進階語言也并不具備低級語言的那種運作速度、甚至對硬體的要求更高,這在某種程度上的确也決定了一個項目到底是成功還是失敗。

是以,如何平衡兩者,或者說如何融合兩者的優點,便變得十分重要和迫切。腳本機制便在此時應運而生。遊戲引擎由富有經驗的開發人員使用C/C++開發,而一些具體項目中功能的實作,例如UI、互動等等則使用進階語言開發。

通過使用進階腳本語言,開發者便融合了低級語言和進階語言的優點。同時提高了開發效率,如同第一節中所講的,引入腳本機制之後開發效率提升了,可以快速的開發原型,而不必把大量的時間浪費在C/C++上。

腳本語言同時提供了安全的開發沙盒模式,也就是說開發者無需擔心C/C++開發的引擎中的具體實作細節,也無需關注例如資源管理和記憶體管理這些事情的細節,這在很大程度上簡化了應用的開發流程。

而Mono則提供了這種腳本機制實作的可能性。即允許開發者使用JIT編譯的代碼作為腳本語言為他們的應用提供拓展。

目前很多腳本語言的選擇趨向于解釋型語言,例如cocos2d-js使用的javascript。是以效率無法與原生代碼相比。而Mono則提供了一種将腳本語言通過JIT編譯為原生代碼的方式,提高了腳本語言的效率。例如,Mono提供了一個原生代碼生成器,使你的應用的運作效率盡可能高。同時提供了很多友善的調用原生代碼的接口。

而為一個應用提供腳本機制時,往往需要和低級語言互動。這便不得不提到将Mono的運作時嵌入到應用中的必要性了。那麼接下來,我将會讨論一下如何将Mono運作時嵌入到應用中。

Mono運作時的嵌入

既然我們明确了Mono運作時嵌入應用的重要性,那麼如何将它嵌入應用中就成為了下一個值得讨論的話題。

這個小節我會為大家分析一下Mono運作時究竟是如何被嵌入到應用中的,以及如何在原生代碼中調用托管方法,相應的,如何在托管代碼中調用原生方法。而衆所周知的一點是,Unity3D遊戲引擎本身是用C/C++寫成的,是以本節就以Unity3D遊戲引擎為例,假設此時我們已經有了一個用C/C++寫好的應用(Unity3D)。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

将你的Mono運作時嵌入到這個應用之後,我們的應用就擷取了一個完整的虛拟機運作環境。而這一步需要将“libmono”和應用連結,一旦連結完成,你的C++應用的位址空間就會像下圖一般:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

而在C/C++代碼中,我們需要将Mono運作時初始化,一旦Mono運作時初始化成功,那麼下一步最重要的就是将CIL/.NET代碼加載進來。加載之後的位址空間将會如下圖所示:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

那些C/C++代碼,我們通常稱之為非托管代碼,而通過CIL編譯器生成CIL代碼我們通常稱之為托管代碼。

是以,将Mono運作時嵌入我們的應用,可以分為三個步驟:

  1. 編譯C++程式和連結Mono運作時
  2. 初始化Mono運作時
  3. C/C++和C#/CIL的互動

讓我們一步一步的進行。首先我們需要将C++程式進行編譯并連結Mono運作時。此時我們會用到pkg-config工具。在Mac上使用homebrew來進行安裝,在終端中輸入指令“brew install pkgconfig”,可以看到終端會有如下的輸出内容:

==> Downloading https://homebrew.bintray.com/bottles/pkg-config-0.28.mavericks.bottle.2.tar.gz
######################################################################## 100.0%
==> Pouring pkg-config-0.28.mavericks.bottle.2.tar.gz
��  /usr/local/Cellar/pkg-config/0.28: 10 files, 604K      

結束之後,證明pkg-config安裝完畢。

接下來,我們建立一個C++檔案,命名為unity.cpp,作為我們的原生代碼部分。我們需要将這個C++檔案進行編譯,并和Mono運作時連結。

在終端輸入:

g++ unity.cpp  -framework CoreFoundation -lobjc -liconv `pkg-config --cflags --libs mono-2`      

此時,經過編譯和連結之後,我們的unity.cpp和Mono運作時被編譯成了可執行檔案。

到此,我們需要能夠将Mono的運作時初始化。是以再重新回到剛剛建立的unity.cpp檔案中,我們要在C++檔案中來進行運作時的初始化工作,即調用mono_jit_init方法。代碼如下:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/mono-config.h>

MonoDomain* domain;

domain = mono_jit_init(managed_binary_path);      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

mono_jit_init這個方法會傳回一個MonoDomain,用來作為盛放托管代碼的容器。其中的參數managed_binary_path,即應用運作域的名字。除了會傳回MonoDomain之外,這個方法還會初始化預設架構版本,即2.0或4.0,這個主要由使用的Mono版本來決定。當然,我們也可以手動指定版本。隻需要調用下面的方法即可:

domain = mono_jit_init_version ("unity", ""v2.0.50727);      

到此,我們擷取了一個應用域——domain。但是當Mono運作時被嵌入一個原生應用的時候,它顯然需要一種方法來确定自己所需要的運作時程式集以及配置檔案。預設情況下它會使用在系統中定義的位置。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

如圖,可以看到,在一台電腦上可以存在很多不同版本的Mono,如果我們的應用需要特定的運作時的話,我們顯然也需要指定其程式集和配置檔案的位置。

為了選擇我們所需要的Mono版本,可以使用mono_set_dirs方法:

mono_set_dirs("/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc");      

這樣,我們就設定了Mono運作時的程式集和配置檔案路徑。

當然,Mono運作時在執行一些具體功能的時候,可能還需要依靠額外的配置檔案來進行。是以我們有時也需要為Mono運作時加載這些配置檔案,通常我們使用mono_config_parse 方法來進行加載這些配置檔案的工作。

當mono_config_parse 的參數為NULL時,Mono運作時将加載Mono的配置檔案。當然作為開發者,我們也可以加載自己的配置檔案,隻需要将我們自己的配置檔案的檔案名作為mono_config_parse方法的參數即可。

Mono運作時的初始化工作到此完成。接下來,我們就需要加載程式集并且運作它了。這裡我們需要用到MonoAssembly和mono_domain_assembly_open

這個方法。

const char* managed_binary_path = "./ManagedLibrary.dll";
MonoAssembly *assembly;  
assembly = mono_domain_assembly_open (domain, managed_binary_path); 
if (!assembly)   
   error ();      

上面的代碼會将目前目錄下的ManagedLibrary.dll檔案中的内容加載進已經建立好的domain中。此時需要注意的是Mono運作時僅僅是加載代碼而沒有立刻執行這些代碼。

如果要執行這些代碼,則需要調用被加載的程式集中的方法。或者當你有一個靜态的主方法時(也就是一個程式入口),你可以很友善的通過mono_jit_exec

方法來調用這個靜态入口。

下面我将為各位舉一個将Mono運作時嵌入C/C++程式的例子,這個例子的主要流程是加載一個由C#檔案編譯成的DLL檔案,之後調用一個C#的方法輸出Hello World。

首先,我們完成C#部分的代碼。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
namespace ManagedLibrary
{
   public static class MainTest
   {
       public static void Main()
       {
         System.Console.WriteLine("Hello World");
       }
   }
}
      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

在這個檔案中,我們實作了輸出Hello World的功能。之後我們将它編譯為DLL檔案。這裡我也直接使用了Mono的編譯器——mcs。在終端指令行使用mcs編譯該cs檔案。同時為了生成DLL檔案,還需要加上-t:library選項。

mcs ManagedLibrary.cs -t:library      

這樣,我們便得到了cs檔案編譯之後的DLL檔案,叫做ManagedLibrary.dll。

接下來,我們完成C++部分的代碼。嵌入Mono的運作時,同時加載剛剛生成ManagedLibrary.dll檔案,并且執行其中的Main方法用來輸出Hello World。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/mono-config.h>
MonoDomain *domain;
int main()
{
    const char* managed_binary_path = "./ManagedLibrary.dll";
    //擷取應用域
    domain = mono_jit_init (managed_binary_path);
    //mono運作時的配置
    mono_set_dirs("/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc");
    mono_config_parse(NULL);
    //加載程式集ManagedLibrary.dll
    MonoAssembly* assembly = mono_domain_assembly_open(domain, managed_binary_path);
    MonoImage* image = mono_assembly_get_image(assembly);
    //擷取MonoClass
    MonoClass* main_class = mono_class_from_name(image, "ManagedLibrary", "MainTest");
    //擷取要調用的MonoMethodDesc
    MonoMethodDesc* entry_point_method_desc = mono_method_desc_new("ManagedLibrary.MainTest:Main()", true);
    MonoMethod* entry_point_method = mono_method_desc_search_in_class(entry_point_method_desc, main_class);
    mono_method_desc_free(entry_point_method_desc);
    //調用方法
    mono_runtime_invoke(entry_point_method, NULL, NULL, NULL);
    //釋放應用域
    mono_jit_cleanup(domain);
    return 0;
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

之後編譯運作,可以看到螢幕上輸出的Hello World。

但是既然要提供腳本功能,将Mono運作時嵌入C/C++程式之後,隻是在C/C++程式中調用C#中定義的方法顯然還是不夠的。腳本機制的最終目的還是希望能夠在腳本語言中使用原生的代碼,是以下面我将站在Unity3D遊戲引擎開發者的角度,繼續探索一下如何在C#檔案(腳本檔案)中調用C/C++程式中的代碼(遊戲引擎)。

回到目錄

0x03 How?如何模拟Unity3D中的腳本機制

首先,假設我們要實作的是Unity3D的元件系統。為了友善遊戲開發者能夠在腳本中使用元件,那麼我們首先要在C#檔案中定義一個Component類。

//腳本中的元件Component
public class Component
{
   public int ID { get; }
   private IntPtr native_handle;
}      

與此同時,在Unity3D遊戲引擎(C/C++)中,則必然有和腳本中的Component相對應的結構。

//遊戲引擎中的元件Component
struct Component
{
   int id;
}      

托管代碼(C#)中的接口

可以看到此時元件類Component隻有一個屬性,即ID。我們再為元件類增加一個屬性,Tag。

之後,為了使托管代碼能夠和非托管代碼互動,我們需要在C#檔案中引入命名空間System.Runtime.CompilerServices,同時需要提供一個IntPtr類型的句柄以便于托管代碼和非托管代碼之間引用資料。(IntPtr 類型被設計成整數,其大小适用于特定平台。 即是說,此類型的執行個體在 32 位硬體和作業系統中将是 32 位,在 64 位硬體和作業系統上将是 64 位。IntPtr 對象常可用于保持句柄。 例如,IntPtr 的執行個體廣泛地用在 System.IO.FileStream 類中來保持檔案句柄。)

最後,我們将Component對象的建構工作由托管代碼C#移交給非托管代碼C/C++,這樣遊戲開發者隻需要專注于遊戲腳本即可,無需去關注C/C++層面即遊戲引擎層面的具體實作邏輯了,是以我在此提供兩個方法即用來建立Component執行個體的方法:GetComponents,以及擷取ID的get_id_Internal方法。

這樣在C#端,我們定義了一個Component類,主要目的是為遊戲腳本提供相應的接口,而非具體邏輯的實作。下面便是在C#代碼中定義的Component類。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
using System;
using System.Runtime.CompilerServices;
namespace ManagedLibrary
{
   public class Component
   {
      //字段
      private IntPtr native_handle = (IntPtr)0;
      //方法
      [MethodImpl(MethodImplOptions.InternalCall)]
      public extern static Component[] GetComponents();
      [MethodImpl(MethodImplOptions.InternalCall)]
      public extern static int get_id_Internal(IntPtr native_handle);
      //屬性
      public int ID
      {
         get 
         {
            return get_id_Internal(this.native_handle);
         }
      }
      public int Tag {
         [MethodImpl(MethodImplOptions.InternalCall)]
         get;
      }
   }
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

之後,我們還需要建立這個類的執行個體并且通路它的兩個屬性,是以我們再定義另一個類Main,來完成這項工作。

Main的實作如下:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
// Main.cs
namespace ManagedLibrary
{
   public static class Main
   {
      public static void TestComponent ()
      {
         Component[] components = Component.GetComponents();
         foreach(Component com in components)
         {
            Console.WriteLine("component id is " + com.ID);
            Console.WriteLine("component tag is " + com.Tag);
         }
      }
   }
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

非托管代碼(C/C++)的邏輯實作

完成了C#部分的代碼之後,我們需要将具體的邏輯在非托管代碼端實作。而我上文之是以要在Component類中定義兩個屬性:ID和Tag,是為了使用兩種不同的方式通路這兩個屬性,其中之一就是直接将句柄作為參數傳入到C/C++中,例如上文我提供的get_id_Internal這個方法,它的參數便是句柄。第二種方法則是在C/C++代碼中通過Mono提供的mono_field_get_value方法直接擷取對應的元件類型的執行個體。

是以元件Component類中的屬性擷取有兩種不同的方法:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
//擷取屬性
int ManagedLibrary_Component_get_id_Internal(const Component* component)
{
    return component->id;
}
 
int ManagedLibrary_Component_get_tag(MonoObject* this_ptr)
{
    Component* component;
    mono_field_get_value(this_ptr, native_handle_field, reinterpret_cast<void*>(&Component));
    return component->tag;
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

之後,由于我在C#代碼中基本隻提供接口,而不提供具體邏輯實作。是以我還需要在C/C++代碼中實作擷取Component元件的具體邏輯,之後再以在C/C++代碼中建立的執行個體為樣本,調用Mono提供的方法在托管環境中建立相同的類型執行個體并且初始化。

由于C#中的GetComponents方法傳回的是一個數組,是以對應的,我們需要使用MonoArray從C/C++中傳回一個數組。是以C#代碼中GetComponents方法在C/C++中對應的具體邏輯如下:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
MonoArray* ManagedLibrary_Component_GetComponents()
{
    MonoArray* array = mono_array_new(domain, Component_class, num_Components);
    
    for(uint32_t i = 0; i < num_Components; ++i)
    {
        MonoObject* obj = mono_object_new(domain, Component_class);
        mono_runtime_object_init(obj);
        void* native_handle_value = &Components[i];
        mono_field_set_value(obj, native_handle_field, &native_handle_value);
        mono_array_set(array, MonoObject*, i, obj);
    }
    
    return array;
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

其中num_Components是uint32_t類型的字段,用來表示數組中元件的數量,下面我會為它指派為5。之後通過Mono提供的mono_object_new方法來建立MonoObject的執行個體。而需要注意的是代碼中的Components[i],Components便是在C/C++代碼中建立的Component執行個體,這裡用來給MonoObject的執行個體初始化指派。

建立Component執行個體的過程如下:

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
num_Components = 5;
    Components = new Component[5];
    for(uint32_t i = 0; i < num_Components; ++i)
    {
        Components[i].id = i;
        Components[i].tag = i * 4;
    }      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

C/C++代碼中建立的Component的執行個體的id為i,tag為i * 4。

最後我們還需要将C#中的接口和C/C++中的具體實作關聯起來。即通過Mono的mono_add_internal_call方法來實作,也即在Mono的運作時中注冊剛剛用C/C++實作的具體邏輯,以便将托管代碼(C#)和非托管代碼(C/C++)綁定。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
// get_id_Internal
mono_add_internal_call("ManagedLibrary.Component::get_id_Internal", reinterpret_cast<void*>(ManagedLibrary_Component_get_id_Internal));
//Tag get
mono_add_internal_call("ManagedLibrary.Component::get_Tag", reinterpret_cast<void*>(ManagedLibrary_Component_get_tag));
//GetComponents
mono_add_internal_call("ManagedLibrary.Component::GetComponents", reinterpret_cast<void*>(ManagedLibrary_Component_GetComponents));       
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

這樣,我們便使用非托管代碼(C/C++)實作了擷取元件、建立和初始化元件的具體功能,完整的代碼如下。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/mono-config.h>
 
struct Component
{
    int id;
    int tag;
};
 
Component* Components;
uint32_t num_Components;
MonoClassField* native_handle_field;
MonoDomain* domain;
MonoClass* Component_class;
 //擷取屬性
int ManagedLibrary_Component_get_id_Internal(const Component* component)
{
    return component->id;
}
 
int ManagedLibrary_Component_get_tag(MonoObject* this_ptr)
{
    Component* component;
    mono_field_get_value(this_ptr, native_handle_field, reinterpret_cast<void*>(&component));
    return component->tag;
}
//擷取元件
MonoArray* ManagedLibrary_Component_GetComponents()
{
    MonoArray* array = mono_array_new(domain, Component_class, num_Components);
    
    for(uint32_t i = 0; i < num_Components; ++i)
    {
        MonoObject* obj = mono_object_new(domain, Component_class);
        mono_runtime_object_init(obj);
        void* native_handle_value = &Components[i];
        mono_field_set_value(obj, native_handle_field, &native_handle_value);
        mono_array_set(array, MonoObject*, i, obj);
    }
    
    return array;
}
 
int main(int argc, const char * argv[])
{
    mono_set_dirs("/Library/Frameworks/Mono.framework/Versions/3.12.0/lib/", "/Library/Frameworks/Mono.framework/Home/etc");
    
    mono_config_parse(NULL);
    
    const char* managed_binary_path = "./ManagedLibrary.dll";
    
    domain = mono_jit_init(managed_binary_path);
    MonoAssembly* assembly = mono_domain_assembly_open(domain, managed_binary_path);
    MonoImage* image = mono_assembly_get_image(assembly);
    
    mono_add_internal_call("ManagedLibrary.Component::get_id_Internal", reinterpret_cast<void*>(ManagedLibrary_Component_get_id_Internal));
    mono_add_internal_call("ManagedLibrary.Component::get_Tag", reinterpret_cast<void*>(ManagedLibrary_Component_get_tag));
    mono_add_internal_call("ManagedLibrary.Component::GetComponents", reinterpret_cast<void*>(ManagedLibrary_Component_GetComponents));   
    Component_class = mono_class_from_name(image, "ManagedLibrary", "Component");
    native_handle_field = mono_class_get_field_from_name(Component_class, "native_handle");
    
    num_Components = 5;
    Components = new Component[5];
    for(uint32_t i = 0; i < num_Components; ++i)
    {
        Components[i].id = i;
        Components[i].tag = i * 4;
    }
    
    MonoClass* main_class = mono_class_from_name(image, "ManagedLibrary", "Main");
    
    const bool include_namespace = true;
    MonoMethodDesc* managed_method_desc = mono_method_desc_new("ManagedLibrary.Main:TestComponent()", include_namespace);
    MonoMethod* managed_method = mono_method_desc_search_in_class(managed_method_desc, main_class);
    mono_method_desc_free(managed_method_desc);
    
    mono_runtime_invoke(managed_method, NULL, NULL, NULL);
    
    mono_jit_cleanup(domain);
    
    delete[] Components;
    
    return 0;
}      
遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

接下來為了驗證我們是否成功的模拟了将Mono運作時嵌入“Unity3D遊戲引擎”中,我們需要将代碼編譯并且檢視輸出是否正确。

首先将C#代碼編譯為DLL檔案。我們在終端直接使用Mono的mcs編譯器來完成這個工作。

運作後生成了ManagedLibrary.dll檔案。

之後将unity.cpp和Mono運作時連結、編譯,會生成一個a.out檔案(在Mac上)。執行a.out,可以看到在終端上輸出了建立出的元件的ID和Tag的資訊。

遊戲中的腳本語言原理與發展 作者:陳嘉棟(慕容小匹夫)     源位址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 從遊戲腳本語言說起,剖析Mono所搭建的腳本基礎

回到目錄

0x04 後記

通過本文,我們可以看到遊戲腳本語言出現的必然性。同時也應該了解Unity3D的底層是C/C++實作的,但是它通過Mono提供了一套腳本機制,以友善遊戲開發者快速的開發遊戲同時也降低了遊戲開發的門檻。

繼續閱讀