欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity使用C++作为游戏逻辑脚本的研究(二)

程序员文章站 2024-03-25 23:59:28
...

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