【置顶】C#和C/C++混合编程系列1-概述
概述:
引入C#主要是为了降低开发难度,特别是战斗服部分,相比使用lua具有高性能,双端开发等优势。
引擎主体还是C/C++实现。这一点和Unity就很像了,引入C#主要应用场景是游戏逻辑开发(主要是战斗部分)。
C#和C/C++混合运行机制在该篇进行了基础概述 Http服务器-第十步加入基于Mono平台的C#脚本支持 http://dreamyouxi.com:7129/blog/1277
这里阐述在StickEngine中的机制和概念及实现。StickEngine简介详见 http://dreamyouxi.com:7129/blog/1203
设计主体思路是C/C++实现引擎部分和大部分GamePlay框架(网络同步等)。C#实现部分逻辑编写,实现难度降低和双端开发的可能性。在API理念设计上靠向于Unity,这样开发人员上手就比较快了。C#在这里扮演的角色更像是脚本语言了。
在组织结构上有2个C#编写的dll和一个runtime,一个是引擎层StickEngine.dll,另外一个是游戏逻辑Scripting.dll。
引擎层dll是随着C/C++版本紧密贴合的。GamePlay开发人员只需要编写Scripting.dll即可。 在客户端也是如此。对于GamePlay开发人员来说StickEngine.dll提供的API都是一样的。只不过runtime不一样,服务端是运行在C/C++编写的二进制上面,客户端运行在Unity环境下。当然Unreal Engine之上也可以进行StickEngine.dll的支持。
下面将通过 内存管理模型,C#和C/C++交互,序列化,物理引擎,并发模型,异步编程,性能差异分析,异常处理 等案例来阐述StickEngine中的混合编程范式。。
内存管理模型:
C/C++可以有许多内存管理机制,比如引用计数和一些标准库提供的智能指针。C#是带GC的语言,因此这里管理上的问题主要集中在C/C++和C#之间的引用问题。这里列出2个方案,他们可以相辅相成,也可以应用在不同的场景。
A:利用mono 提供的gc handle 机制。C/C++进行引用和释放,主动告知C#的GC收集器可以进行的动作。
B:C#层建立map的形式在C#代码和其域内的引用。C/C++析构时,再从map中remove掉以删除C#对象。
C#和C/C++交互:
在lua里面和C/C++交互通过lua C-API提供的”栈”进行,mono下一般可以通过P/Invoke进行。
C/C++侧
C#侧
物理引擎 :
引入物理引擎基本上有2种方式,一个是C/C++层实现然后提供hight-level API给C#使用,另外一个方式是吧Box2D PhysX等物理引擎通过PInvoke原始提供到C#使用。在这个StickEngine体系下,是走的方式1即 C/C++提供API给C#使用。下面给出一个案例来说明使用方法。
C/C++侧
C#侧
序列化:
在这里实现了类似于Unity Prefab的概念来进行序列化操作和存储,C/C++层的序列化在这里不阐述,这里讲解C#中的class的序列化问题。在这里利用反射和深拷贝进行反序列化和对象快速生成。c# class的字段,根据反射信息进行逐个解析,然后可以缓存在内存里面。以Prefab为原型生成对象时,直接进行深拷贝,这样就简单化了。当然在客户但内存可能不会有那么大因此可能不会全量缓存在内存。
C#使用形式:
并发模型:
在客户端的话,逻辑线程目前就一个因此不存在并发模型,这里指的是服务端的并发模型。就像lua,每一个线程甚至每一个房间单独开一个虚拟机来进行环境隔离和沙箱机制。在mono下也有类似的概念,在mono下称之为domain,这里有一些代价比如内存部分是重复的,就是不同的domain之间其实是存在很多重复的内存使用的。但是既然是沙盒环境又是服务器,问题也没那么严峻了。如果需要可以考虑优化mono的domain 可以添加share机制 来减少相关压力。
异步模型:
在C/C++里面最简单的异步编程模型是基于回调callback的,其他模型也可以有,比如协程。C#里面自带关键字await,yield等,就对GamePlay上面的异步编程进行了极大的加持,C/C++和C#之间,C#和C#之间进行异步编程成为了可能,比如网络通信逻辑流程上。当然在Unity里面提供了一个简易的协程机制(StartCoroutine),在这里将会另起炉灶。下面展示一个例子来说明基本用法和原理。
运行结果如下:
性能差异简要分析:
虽然C#是编译型语言,但是他的基本方式和java类似,生成中间语言IL,在Mono中提供了2种方式,一个是jit,另外一个是AOT方式。在这里还有一层性能方式是纯C/C++实现 GamePlay代码的区别。引入了C#和C/C++ 交互方式存在许多额外代价,比如内存至少双倍copy,box和unbox代价,托管和非托管代码的转换。下面做几个小测试来阐述C#量化的代价大约有多少。
下面是C/C++实现移动逻辑,代价:33 ms
下图是C#实现的位移逻辑 代价 36 ms
C/C++
C#
上述2个基本对比测试,用的是mono默认的方式(jit),这个案例中引入了C#的代价似乎很小,损耗大约是10%。改为AOT方式后理论上代价将会更少。上述案例是一个典型的GamePlay代码案例,在box unbox P/Invoke密集环境下损耗将会有更大的占比。因此在结构设计上要更加的合理才能得到更低的损耗。
异常处理:
在mono的C-API中,是没有异常这个概念的,是通过C/C++编程中常规的错误码来标识成功与否。在C/C++调用C#代码中的异常处理其实很简单。如下图的案例。
输出就会是异常信息了。但是异常栈没有显示源代码行数等信息。这里可能是未加载pdb信息。
总结:
有了上述基本操作,让客户端和服务端一样的编程范式下的双端开发,高性能,加上C#语言对编程上的赋能,成为了可能。