关于 Unity Native 插件的开发

本贴最后更新于 1285 天前,其中的信息可能已经时异事殊

构建 native 库的意义

  1. 核心算法保密(例如随机算法,寻路算法等)
  2. 效率考量(c++ 库效率比 C#要高一个数量级)
  3. 跨端需求(客户端,服务端)

目标

  • 可以被用作 Unity Plugin,供 Unity Editor(Windows + Mac )使用
  • 也可以编译为库文件,供移动端使用(Android + iOS)

准备工作

  1. 本文以 Mac 系统为例
  2. 安装 Homebrew
  3. 安装 CMake,一个跨平台的安装和编译工具,可以用简单的语句来描述所有平台的安装或编译过程
  4. JDK SDK NDK 这些可以使用 Unity 安装包内附带的
  5. XCode
  6. Visual Studio

native 插件的基本要素

1. 全局头文件 Define.h,定义 API,为了兼顾做 Unity-Plugin 和普通 dll 库,这里做一个宏定义判断(标准写法,全网通用)

#pragma once
// Unity native plugin API
// Compatible with C99

#if defined(__CYGWIN32__)
    #define API __declspec(dllexport) __stdcall
#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(_WIN64) || defined(WINAPI_FAMILY)
    #define API __declspec(dllexport) __stdcall
#elif defined(__MACH__) || defined(__ANDROID__) || defined(__linux__) || defined(__QNX__)
    #define API
#else
    #define API
#endif

2.具体暴露给 Unity 的方法如何定义?例如 Encrypt.h,标准写法如下

#include "Define.h"
#ifdef __cplusplus
extern "C"
{
#endif

    //Encrypt
    void API EncodeNoGC(char* params, int paramsLength);
    //Decrypt
    void API DecodeNoGC(char* params, int paramsLength);
    //The key
    char KEY[] = "abcdefg123456";
#ifdef __cplusplus
}
#endif

这样的代码到底是什么意思呢?首先,__cplusplus 是 cpp 中的自定义宏,那么定义了这个宏的话表示这是一段 cpp 的代码,也就是说,上面的代码的含义是:如果这是一段 cpp 的代码,那么加入 extern "C"{和}处理其中的代码。

  要明白为何使用 extern "C",还得从 cpp 中对函数的重载处理开始说起。在 c++ 中,为了支持重载机制,在编译生成的汇编码中,要对函数的名字进行一些处理,加入比如函数的返回类型等等.而在 C 中,只是简单的函数名字而已,不会加入其他的信息.也就是说:C++ 和 C 对产生的函数名字的处理是不一样的.

3.接下来具体逻辑怎么写?例如 Encrypt.cpp,标准写法如下

#include "Encrypt.h"

#ifdef __cplusplus
extern "C"
{
#endif
    void API EncodeNoGC(char* params, int paramsLength)
    {
        //加密逻辑
    }

    void API DecodeNoGC(char* params, int paramsLength)
    {
        //解密逻辑
    }
#ifdef __cplusplus
}
#endif

4.这样一个最基本的 native 就写完里,下一步是编译,我们使用 cmake,这里不做 cmake 科普,一个 CMakeLists.txt 如下

CMAKE_MINIMUM_REQUIRED(VERSION 2.6)

PROJECT(kernal)

AUX_SOURCE_DIRECTORY(src/. SRC_LIST)

FILE(GLOB_RECURSE HEADER_LIST  src/*.h )
source_group("Header Files" FILES ${HEADER_LIST}) 

if ( WIN32 AND NOT CYGWIN AND NOT ( CMAKE_SYSTEM_NAME STREQUAL "WindowsStore" ) )
    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT" CACHE STRING "")
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd" CACHE STRING "")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT" CACHE STRING "")
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd" CACHE STRING "")
    set(CompilerFlags
            CMAKE_CXX_FLAGS_DEBUG
            CMAKE_CXX_FLAGS_RELEASE
            CMAKE_C_FLAGS_DEBUG
            CMAKE_C_FLAGS_RELEASE
            )
    foreach(CompilerFlag ${CompilerFlags})
        string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}")
    endforeach()
endif ()

if (APPLE)
    if (IOS)
    	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fembed-bitcode")
        set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fembed-bitcode")
    	ADD_Library(kernal  ${HEADER_LIST} ${SRC_LIST} )
		set_xcode_property (kernal IPHONEOS_DEPLOYMENT_TARGET "7.0" "all")
    else ()
		ADD_Library(kernal  MODULE ${HEADER_LIST} ${SRC_LIST} )
		set_target_properties(kernal PROPERTIES BUNDLE TRUE)
	endif ()
elseif (ANDROID)
	ADD_Library(kernal SHARED ${HEADER_LIST} ${SRC_LIST} )
else ()
	ADD_Library(kernal MODULE ${HEADER_LIST} ${SRC_LIST} )
	set_target_properties(kernal PROPERTIES BUNDLE TRUE)
endif ()

这个 cmake 是包含各个平台的编译的,其中特别的

  • Apple 平台下,Mac 出.boudle 库
  • Apple 平台下,iOS 出.a 静态库
  • Android 是要出 SHARED 包,也就是.so,静态库是不能用的
  • Windows 部分没什么特别(个别代码摘自 xLua 的编译文件)

5.有了 CMakeLists.txt,各个平台执行 shell 或者 bat 包即可,仅举一个例子 Android

if [ -n "$ANDROID_NDK" ]; then
    export NDK=${ANDROID_NDK}
elif [ -n "$ANDROID_NDK_HOME" ]; then
    export NDK=${ANDROID_NDK_HOME}
elif [ -n "$ANDROID_NDK_HOME" ]; then
    export NDK=${ANDROID_NDK_HOME}
else
    export NDK=/Applications/Unity/Hub/Editor/2019.4.0f1/PlaybackEngines/AndroidPlayer/NDK
fi

if [ ! -d "$NDK" ]; then
    echo "Please set ANDROID_NDK environment to the root of NDK."
    exit 1
fi

function build() {
    API=$1
    ABI=$2
    TOOLCHAIN_ANME=$3
    BUILD_PATH=build_android_${ABI}
    cmake -H. -B${BUILD_PATH} -DANDROID_ABI=${ABI} -DCMAKE_TOOLCHAIN_FILE=${NDK}/build/cmake/android.toolchain.cmake -DANDROID_NATIVE_API_LEVEL=${API} -DANDROID_TOOLCHAIN=clang -DANDROID_TOOLCHAIN_NAME=${TOOLCHAIN_ANME}
    cmake --build ${BUILD_PATH} --config Release
    cp ${BUILD_PATH}/libkernal.so output/android/libs/${ABI}/libkernal.so
#    cp ${BUILD_PATH}/libkernal.so ../unity/Assets/Plugins/kernal/Android/libs/${ABI}/libkernal.so
}

build android-16 armeabi-v7a arm-linux-androideabi-4.9
build android-16 arm64-v8a  arm-linux-androideabi-clang
build android-16 x86 x86-4.9

最关键的先要指定 NDK 路径,然后就是按部就班的执行 build 了,所有的脚本执行完,得到如下 output 结果
image

接下来就是原封不动的扔到 Unity 的 Plugins 里去

image

6.在 Unity 中如何调用呢,废话不多说,直接上代码 Encrypt.cs

public class Encrypt
{
#if UNITY_IOS && !UNITY_EDITOR
	const string ENCRYPT_DLL = "__Internal";
#else
    const string ENCRYPT_DLL = "kernal";
#endif
    [DllImport(ENCRYPT_DLL, EntryPoint = "EncodeNoGC", CallingConvention = CallingConvention.Cdecl)]
    public static extern void EncodeNoGC(byte[] aData, int aLength);
  
    [DllImport(ENCRYPT_DLL, EntryPoint = "DecodeNoGC", CallingConvention = CallingConvention.Cdecl)]
    public static extern void DecodeNoGC(byte[] aData, int aLength);
}

注意这里的 CallingConvention 必须是 CallingConvention.Cdecl,Standard 调用在移动端是不被支持的

Demo

上面的代码仅仅是核心代码,一些 include 没有放进来,完整 Demo 可以看如下示例

https://github.com/kyochow/xor_unity_native.git

  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 250 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • kyochow
    作者

    写的也是挺凌乱的,见谅见谅