Research on Unity's use of C++ as a game logic script (2)

Article statement: This article is extracted and translated from JacksonDunstan's blog series. The copyright belongs to it. A link to the original text is attached. You can read the original text when you have time: C++ Scripting( in Unity)

After writing the last article, some classmates thought it was a bit obscure. In fact, you can read the source code twice more carefully and think about it carefully, and you will have a feeling of sudden enlightenment :D. Continuing the above today, I will discuss the research of C++ as a game script in depth. This article will be longer and need to write some sample codes to explain.

 

1. Encapsulation of C# pointers (references)

In the above, we mentioned that the call of C++ to C# is based on the function pointer (reference) of C#, such as in C++:

//return transform handle  || function pointer name  || take a handle to the go
int32_t                    (*GameObjectGetTransform)   (int32_t thiz);

For scalability, we tend to encapsulate this int32_t type of data, and naturally it is easy to think of using a structure (the structure defaults to public)

namespace System
{
     struct Object
     {
         int32_t Handle;
     }
}

Using the characteristics of inheritance, we can extend the definition of other types of structures:

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;
          }
     }
}

 

Second, the control of memory management

In the C# part, for the managed part, it is based on the automatic garbage collection mechanism. For the C++ part, the relatively simple collection can be based on the counting collection mechanism. When the reference count of the object is zero, garbage collection is performed, then for us we can Define two global variables to do related count statistics:

//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!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325255651&siteId=291194637
Recommended