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类。
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; } } }之后,我们还需要创建这个类的实例并且访问它的两个属性,所以还要再定义另一个类Main,来完成这项工作。
Main的实现如下:
// 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); } } } } 非托管代码(C/C++)的逻辑实现完成了C#部分的代码之后,我们需要将具体的逻辑在非托管代码端实现。而我上文之所以要在Component类中定义两个属性:ID和Tag,是为了使用两种不同的方式访问这两个属性,其中之一就是直接将句柄作为参数传入到C/C++中,例如上文我提供的get_id_Internal这个方法,它的参数便是句柄。第二种方法则是在C/C++代码中通过Mono提供的mono_field_get_value方法直接获取对应的组件类型的实例。
获取组件Component类中的属性有两种不同的方法:
//获取属性 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; }之后,由于我在C#代码中基本只提供接口,而不提供具体逻辑实现。所以还需要在C/C++代码中实现获取Component组件的具体逻辑,之后再以在C/C++代码中创建的实例为样本,调用Mono提供的方法在托管环境中创建相同的类型实例并且初始化。
由于C#中的GetComponents方法返回的是一个数组,所以对应的需要使用MonoArray从C/C++中返回一个数组。C#代码中GetComponents方法在C/C++中对应的具体逻辑如下:
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; }其中num_Components是uint32_t类型的字段,用来表示数组中组件的数量,下面我会为它赋值为5。之后通过Mono提供的mono_object_new方法创建MonoObject的实例。而需要注意的是代码中的Components[i],Components便是在C/C++代码中创建的Component实例,这里用来给MonoObject的实例初始化赋值。
创建Component实例的过程如下:
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; }C/C++代码中创建的Component的实例的id为i,tag为i * 4。
最后将C#中的接口和C/C++中的具体实现关联起来。即通过Mono的mono_add_internal_call方法来实现,也即在Mono的运行时中注册刚刚用C/C++实现的具体逻辑,以便将托管代码(C#)和非托管代码(C/C++)绑定。
// 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));这样,便使用非托管代码(C/C++)实现了获取组件、创建和初始化组件的具体功能,接下来为了验证是否成功地模拟了将Mono运行时嵌入“Unity3D游戏引擎”中,我们需要编译代码并且查看输出是否正确。
首先将C#代码编译为DLL文件。在终端直接使用Mono的mcs编译器来完成这个工作。
运行后生成了ManagedLibrary.dll文件。
之后将unity.cpp和Mono运行时链接、编译,会生成一个a.out文件(在Mac上)。执行a.out,可以看到在终端上输出了创建的组件的ID和Tag的信息。
后记通过本文,我们可以看到游戏脚本语言出现的必然性。同时也应该了解Unity3D是C/C++实现的,但是它通过Mono提供了一套脚本机制,在方便游戏开发者快速开发游戏的同时也降低了游戏开发的门槛。