文章申明:本文来自JacksonDunstan的博客系列文章内容摘取和翻译,版权归其所有,附上原文的链接,大家可以有空阅读原文:C++ Scripting( in Unity)
上一篇文章写完,有同学觉得有点晦涩,其实可以多认真看两遍源码,仔细琢磨一下,就会有一种茅塞顿开的感觉:D。今天继续上文,深入讨论一下C++作为游戏脚本的研究,本文会较长,需要写一些示例代码做讲解。
一、对C#指针(引用)的封装
在上文,我们提到,C++对C#的调用,是基于C#的函数指针(引用)而来的,比如在C++中:
//return transform handle || function pointer name || take a handle to the go
int32_t (*GameObjectGetTransform) (int32_t thiz);
为了拓展性,我们都会倾向于对于这种int32_t类型的数据做一个封装,自然容易想到用一个结构体(结构体默认为public)
namespace System { struct Object { int32_t Handle; } }
利用继承的特点,我们可以延伸出其他类型的结构体定义:
namespace UnityEngine { struct Vector3 {float x; float y; float z;}; struct Transform:System::Object { void SetPosition(Vector3 val) { TransformSetPosition(Handle, val); } } struct GameObject:System::Object { GameObject() { Handle = GameObjectNew(); } Transform GetPosition() { Transform transform; transform.Handle = GameObjectGetTransform(Handle); return transform; } } }
二、对内存管理的控制
在C#部分,对于托管部分,是基于垃圾自动回收机制的,对于C++部分,相对较为简单的回收,可以基于计数的回收机制,当对象的引用计数为零的时候执行垃圾回收,那么对于我们可以定义两个全局变量来做相关的计数统计:
//global
int32_t managedObjectsRefCountLen; int32_t *managedObjectsRefCounts; //..... //init
managedObjectsRefCountLen = maxManagedObjects;//c#会传入该数据
managedObjectsRefCounts = (int32_t*)calloc(maxManagedObjects, sizeof(int32_t));
这样在GameObject的初始化和解析的时候可以执行相关的内存管理操作:
GameObject() { Handle = GameObjectNew(); managedObjectsRefCounts[Handle]++; } ~GameObject() { if(--managedObjectsRefCounts[Handle] == 0) { ReleaseObject(Handle); } }
对于其他的结构体,可以利用宏定义来实现类似的结构体定义中的操作。综上,可以实现在传递的时候对int32_t类型数据的封装,其次可以内嵌内存操作。整体代码对于c#的修改不多,对于C++的修改较多。
三、代码部分
对于c#部分的代码,基本不修改,只是修改一下Init函数,添加内存管理相关的数据和函数,具体代码如下:
.... //初始化函数及相关委托的修改
public delegate void InitDelegate(int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); .... ... #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX ... #elif UNITY_EDITOR_WIN ... #else [DllImport("NativeScript")] static extern void Init(int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); ... #endif
//新增释放
delegate void ReleaseObjectDelegate(int handle); ... //修改Awake函数中对于初始化的操作
void Awake() { ... //init c++ libraray
const int maxManagedObjects = 1024; ObjectStore.Init(maxManagedObjects); Init(maxManagedObjects, Marshal.GetFunctionPointerForDelegate(new ReleaseObjectDelegate(ReleaseObject)), Marshal.GetFunctionPointerForDelegate(new GameObjectNewDelegate(GameObjectNew)), Marshal.GetFunctionPointerForDelegate(new GameObjectGetTransformDelegate(GameObjectGetTransform)), Marshal.GetFunctionPointerForDelegate(new TransformSetPositionDelegate(TransformSetPosition))); } ... //c# function for c++ to call
static void ReleaseObject(int handle) { ObjectStore.Remove(handle); } ...
C++部分的代码修改较多,我就copy一下作者的工程源码吧 :D
// For assert()
#include <assert.h>
// For int32_t, etc.
#include <stdint.h>
// For malloc(), etc.
#include <stdlib.h>
// For std::forward
#include <utility>
// Macro to put before functions that need to be exposed to C#
#ifdef _WIN32 #define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT extern "C"
#endif
////////////////////////////////////////////////////////////////
// C# struct types
////////////////////////////////////////////////////////////////
namespace UnityEngine { struct Vector3 { float x; float y; float z; Vector3() : x(0.0f) , y(0.0f) , z(0.0f) { } Vector3( float x, float y, float z) : x(x) , y(y) , z(z) { } }; } ////////////////////////////////////////////////////////////////
// C# functions for C++ to call
////////////////////////////////////////////////////////////////
namespace Plugin { using namespace UnityEngine; void (*ReleaseObject)( int32_t handle); int32_t (*GameObjectNew)(); int32_t (*GameObjectGetTransform)( int32_t thiz); void (*TransformSetPosition)( int32_t thiz, Vector3 val); } ////////////////////////////////////////////////////////////////
// Reference counting of managed objects
////////////////////////////////////////////////////////////////
namespace Plugin { int32_t managedObjectsRefCountLen; int32_t* managedObjectRefCounts; void ReferenceManagedObject(int32_t handle) { assert(handle >= 0 && handle < managedObjectsRefCountLen); if (handle != 0) { managedObjectRefCounts[handle]++; } } void DereferenceManagedObject(int32_t handle) { assert(handle >= 0 && handle < managedObjectsRefCountLen); if (handle != 0) { int32_t numRemain = --managedObjectRefCounts[handle]; if (numRemain == 0) { ReleaseObject(handle); } } } } ////////////////////////////////////////////////////////////////
// Mirrors of C# types. These wrap the C# functions to present // a similiar API as in C#.
////////////////////////////////////////////////////////////////
namespace System { struct Object { int32_t Handle; Object(int32_t handle) { Handle = handle; Plugin::ReferenceManagedObject(handle); } Object(const Object& other) { Handle = other.Handle; Plugin::ReferenceManagedObject(Handle); } Object(Object&& other) { Handle = other.Handle; other.Handle = 0; } }; //宏定义操作
#define SYSTEM_OBJECT_LIFECYCLE(ClassName, BaseClassName) \ ClassName(int32_t handle) \ : BaseClassName(handle) \ { \ } \ \ ClassName(const ClassName& other) \ : BaseClassName(other) \ { \ } \ \ ClassName(ClassName&& other) \ : BaseClassName(std::forward<ClassName>(other)) \ { \ } \ \ ~ClassName() \ { \ DereferenceManagedObject(Handle); \ } \ \ ClassName& operator=(const ClassName& other) \ { \ DereferenceManagedObject(Handle); \ Handle = other.Handle; \ ReferenceManagedObject(Handle); \ return *this; \ } \ \ ClassName& operator=(ClassName&& other) \ { \ DereferenceManagedObject(Handle); \ Handle = other.Handle; \ other.Handle = 0; \ return *this; \ } } namespace UnityEngine { using namespace System; using namespace Plugin; struct GameObject; struct Component; struct Transform; struct GameObject : Object { SYSTEM_OBJECT_LIFECYCLE(GameObject, Object) GameObject(); Transform GetTransform(); }; struct Component : Object { SYSTEM_OBJECT_LIFECYCLE(Component, Object) }; struct Transform : Component { SYSTEM_OBJECT_LIFECYCLE(Transform, Component) void SetPosition(Vector3 val); }; GameObject::GameObject() : GameObject(GameObjectNew()) { } Transform GameObject::GetTransform() { return Transform(GameObjectGetTransform(Handle)); } void Transform::SetPosition(Vector3 val) { TransformSetPosition(Handle, val); } } ////////////////////////////////////////////////////////////////
// C++ functions for C# to call
////////////////////////////////////////////////////////////////
// Init the plugin
DLLEXPORT void Init( int32_t maxManagedObjects, void (*releaseObject)(int32_t), int32_t (*gameObjectNew)(), int32_t (*gameObjectGetTransform)(int32_t), void (*transformSetPosition)(int32_t, UnityEngine::Vector3)) { using namespace Plugin; // Init managed object ref counting
managedObjectsRefCountLen = maxManagedObjects; managedObjectRefCounts = (int32_t*)calloc( maxManagedObjects, sizeof(int32_t)); // Init pointers to C# functions
ReleaseObject = releaseObject; GameObjectNew = gameObjectNew; GameObjectGetTransform = gameObjectGetTransform; TransformSetPosition = transformSetPosition; } // Called by MonoBehaviour.Update
DLLEXPORT void MonoBehaviourUpdate() { using namespace UnityEngine; static int32_t numCreated = 0; if (numCreated < 10) { GameObject go; Transform transform = go.GetTransform(); float comp = (float)numCreated; Vector3 position(comp, comp, comp); transform.SetPosition(position); numCreated++; } }
四、c#和Unity API 的导出
写到上面部分,基本对于c#和c++之间的操作有一个整体的较为完整的讲解,还有一个没有提起,那就是,怎么将 unity 的API导出给C++使用呢?作者给出了一个导出方式:JSON导出这让熟悉c#导出到lua的同学可以发现异曲同工之妙,其基本的导出设计为:
{
"Assemblies": [
{
"Path": "/Applications/Unity/Unity.app/Contents/Managed/UnityEngine.dll",
"Types": [
{
"Name": "UnityEngine.Object",
"Constructors": [],
"Methods": [],
"Properties": [],
"Fields": []
},
{
"Name": "UnityEngine.GameObject",
"Constructors": [
{
"Types": []
}
],
"Properties": [ "transform" ],
"Fields": []
},
{
"Name": "UnityEngine.Component",
"Constructors": [],
"Methods": [],
"Properties": [],
"Fields": []
},
{
"Name": "UnityEngine.Transform",
"Constructors": [],
"Methods": [],
"Properties": [ "position" ],
"Fields": []
}
]
}
]
}
整体设计简介易懂,当然,并不是所有的c#特性都可以被导出,json的导出不支持:Array/out and ref/ delegate/ generic functions and types/ struct types,不知后期作者是否考虑扩展对这些不兼容的特效的导出。
使用json导出,整体的修改和使用非常简单,比如对Component,需要添加对其transform特性的导出,那么只需要修改为:
"Properties":["transform"]
那么,保存后重新导出,就可以得到transform特性。
此外,对于.Net的一些API, 也可以使用JSON导出的方式:
{
"Path": "/Applications/Unity/Unity.app/Contents/Mono/lib/mono/unity/System.dll",
"Types": [
{
"Name": "System.Diagnostics.Stopwatch",
"Constructors": [
{
"Types": []
}
],
"Methods": [
{
"Name": "Start",
"Types": []
},
{
"Name": "Reset",
"Types": []
}
],
"Properties": [ "ElapsedMilliseconds" ],
"Fields": []
}
]
}
基于上面的各个部分,整体的游戏工程,可以分为2个部分:逻辑代码部分和binding相关的部分,作者给出的工程规划:
Assets
|- Game.cpp // Game-specific code. Can rename this file, add headers, etc.
|- NativeScriptTypes.json // JSON describing which .NET types the game wants to expose to C++
|- NativeScriptConstants.cs // Game-specific constants such as plugin names and paths
|- NativeScript/ // C++ scripting system. Drop this into your project.
|- Editor/
|- GenerateBindings.cs // Code generator
|- Bindings.cs // C# code to expose functionality to C++
|- ObjectStore.cs // Object handles system
|- Bindings.h // C++ wrapper types for C# (declaration)
|- Bindings.cpp // C++ wrapper types for C# (definition)
|- BootScript.cs // MonoBehaviour to boot up the C++ plugin
|- BootScene.unity // Scene with just BootScript on an empty GameObject
对于NativeScript来说,相当于基本的binding相关的东西,对于任何工程都适用,对于其他部分,则根据具体的工程来设计。基于这样的设计,需要做到三个基本规范:
1、需要定义一个全局的类:
public static class NativeScriptConstants { /// <summary>
/// Name of the plugin used by [DllImport] when running outside the editor /// </summary>
public const string PluginName = "NativeScript"; /// <summary>
/// Path to load the plugin from when running inside the editor /// </summary>
#if UNITY_EDITOR_OSX
public const string PluginPath = "/NativeScript.bundle/Contents/MacOS/NativeScript"; #elif UNITY_EDITOR_LINUX
public const string PluginPath = "/NativeScript.so"; #elif UNITY_EDITOR_WIN
public const string PluginPath = "/NativeScript.dll"; #endif
/// <summary>
/// Maximum number of simultaneous managed objects that the C++ plugin uses /// </summary>
public const int MaxManagedObjects = 1024; /// <summary>
/// Path within the Unity project to the exposed types JSON file /// </summary>
public const string ExposedTypesJsonPath = "NativeScriptTypes.json"; }
2、NativeScriptConstants.ExposedTypesJsonPath需要指向前面所提到的json导出文件;
3、在C++代码部分,需要定义2个函数用来执行相关的更新
// Called when the plugin is initialized
void PluginMain()
{
}
// Called for MonoBehaviour.Update
void PluginUpdate()
{
}
最后,整体的工程可以在github上找到,给出工程的链接:
jacksondunstan/UnityNativeScripting
Over!