Unity使用C++作為游戲邏輯腳本的研究(二)


文章申明:本文來自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!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM