概要
Unity的代码其实是分为三层的,最上层供用户开发的C#层,中间封装了各种接口的C#层,最后就是C++层面的引擎底层具体函数的实现了。
到这里有些小伙伴可能就要问了,为啥Unity要搞这么复杂,代码结构还要分为三层。这是因为作为一款强大的商业引擎,他不仅要考虑引擎的性能问题,也同时要考虑对于开发人员的友好程度。那怎么才算友好呢,很简单,代码易于编写,支持跨平台(一套代码多平台可以运行),那怎么才算运行效率高呢,就是尽量使用低级语言。
跨平台方案的话我们可以先看看Java的。Java 跨平台通过提供一个中间层来解决跨平台问题。JRE(Java 运行时)就是这里的中间层。通过了解 Java 代码编译过程,这一点就比较容易理解。JRE 中的 JVM(Java 虚拟机)并不能直接运行 Java 代码,而是运行编译之后的 .class 文件内容,该文件中其实是 Java 字节码(bytecode)。换句话说,Java 代码是先编译成字节码,然后运行在 JVM 之上的,JVM 负责将字节码编译为运行在 CPU 之上的机器码。这里的 JRE 帮我们屏蔽了硬件层面的区别,我们只需要在 macOS、 Windows 甚至塞班系统中嵌入 Java 运行时,我们的 Java 代码就可以跑在这些平台上了。
其实 Unity3D 的解决方案是相同的:增加一个中间层来负责消除硬件的差别。我们知道 Unity3D 使用的语言是 C#,提到 C# 第一个想到的就是 .NET。.NET 相当于 C# 的运行时,C# 编译生成 .dll 或 .exe 中间文件,而这些中间文件交由 .NET 来进而编译成机器码。但这里的和 JRE 不同的地方是,.NET 只能运行在 Windows 平台。想要实现跨平台特性,Unity3D 是不能使用 .NET 作为 C# 运行时的。这里就要提到 Mono。
Mono
如果要了解Unity中代码的架构,那么你一定要对Mono有所了解,起码得知道这是什么,有什么组成,而Unity又利用它做了些什么。本章节就初步对Mono进行一下介绍,揭开它神秘的面纱。
Mono是什么
Mono是一个免费的开源项目。由微软的子公司Xamarin(前身为Novell,最初由Ximian)和.NET基金会领导。旨在构建符合ECMA(欧洲计算机制造商协会)标准的.NET Framework兼容工具集。包括 C#编译器和带有实时(JIT)编译的公共语言运行时。
简而言之,Mono是Unity3D的一个运行时,负责C/C++和C#/CIL的交互。
组成
Mono主要由三部分组成,即C#编译器(mcs.exe)、Mono运行时(mono.exe)和基础类库。
- C#编译器。mcs就是Mono所用的C#编译器,和.Net用的csc编译器不同。
- Mono运行时。Mono运行时提供了三种编译器,JIT、AOT和Full-AOT。同时还有类库加载器、垃圾回收器和线程系统。
- 基础类库。Mono平台提供了非常广泛的基础类,这些类库与.NET框架也相兼容。
C++层与C#层的交互原理
首先你得下载Mono运行时。下载完成后,设置环境变量,将添加以下变量至Path路径下面:C:\Program Files\Mono\bin
接下来就创建VS工程,这里要创建两个工程,一个C++工程还有一个就是C#工程,因为我们要完成两个工程间代码的相互访问。在C++工程中你还要设置一下依赖项,下面会具体说明。
C++层调用C#层代码
首先,你得设置一下VS中的包含目录、库目录、附加依赖项,具体位置在 项目->属性->配置属性->VC++目录和链接器。
包含目录:里面一般放的都是.h头文件,也就是函数和类的声明。 库目录:lib库的集合。 附加依赖项:一般都是lib库,是对.h文件或者说是对函数和类的具体实现。
include:C:\Program Files\Mono\include\mono-2.0
lib:C:\Program Files\Mono\lib
附加依赖项:mono-2.0-sgen.lib
注意:因为我下载的mono是x64的,所以vs中也要选择x64平台,不然会出问题。
编写C#代码
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CsProject
{
class Program
{
static void Main(string[] args)
{
}
static void SayHello()
{
System.Console.WriteLine("Cs Project, Hello World!");
System.Console.Read();
}
}
}
编写完之后用mono的编译器对其进行编译,编译过后就会生成对应的.dll文件。
注意,如果命令中提示:不是内部或外部命令,也不是可运行的程序。这肯定是你的环境变量没配置成功,重新配置下然后重启下cmd就可以了。
编写C++代码
#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 = "E:\\vs_projects\\mono\\CsProject\\CsProject\\Program.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);
MonoClass* main_class = mono_class_from_name(image, "CsProject", "Program");
MonoMethodDesc* entry_point_method_desc = mono_method_desc_new("CsProject.Program:SayHello", 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;
}
在C++工程中点击运行,就可以看到我们成功在C++层调用到了C#层的代码啦~
C#层调用C++层代码
这里就做一个最简单的函数作为展示,复杂的例如Unity的Component组件大家可以自行研究,其实都大同小异。
C++层代码
int main(){
...
MonoImage* image = mono_assembly_get_image(assembly);
register_method();//放在image之后,前面的代码和之前一样
...
return 0;
}
void register_method() {
mono_add_internal_call("CsProject.Program::get_id", reinterpret_cast<void*>(get_id));
}
C#层代码
namespace CsProject
{
class Program
{
private static IntPtr native_handle = (IntPtr)0;
[MethodImpl(MethodImplOptions.InternalCall)]
public extern static int get_id(IntPtr native_handle);
static void CallCppMethod()
{
Console.WriteLine("Call Cpp Project, get_id:" + get_id(native_handle));
Console.Read();
}
}
}
在C++工程中点击运行,就可以看到我们成功在C#层调用到了C++层的代码啦~
参考文献
- 《Unity3D 脚本编程 - 使用C#语言开发跨平台游戏》