Unity 远程真机调试插件开发

项目目标:完成远程真机调试插件,实现对端侧 3D/XR 场景的 Unity 实时场景部署与调试;

具体内容

  1. 在端侧构建调试 SDK,用于接收 Unity 发送的命令,和向 Unity 同步状态;
  2. 在 Unity 中基于插件实现Debug Server,用于和端侧进行实时通信(Unity 场景快速导出并推送到端侧)和同步状态(Unity 场景双向状态同步调试);
  3. PC端:MacOS,移动端:iOS;

阶段任务概要:

  • 端侧 DebugSDK 技术选型 - Unity / C++,Unity 插件技术选型 - OC / C++
  • 通信协议选型:Socket Raw Packet / gRPC + Protobuf / Hybrid (Raw Packet + RPC)
  • 基础 Socket 通道搭建:Unity Socket Server (nc test)、Socket Client
  • RPC 通道搭建:Unity RPC、iOS gRPC、C/S 双端互调

Unity C++插件开发基础

参考:

.NET使用CIL作为高级语言与机器语言的中间层以实现跨平台,且使用公共语言运行时CLR(支持提前AOT和实时JIT类型),无论使用的是Mono、.NET Framework还是.NET Core。将执行过程交由运行时管理(如自动内存管理、安全边界、类型安全等)的代码称为托管代码(Managed Code);直接运行编译出的机器语言而不依赖运行时的代码称为原生代码(Native Code)。二者各有优势和局限性,具体见官网介绍。Unity最常用的托管代码形式插件使用C#语言,原生插件可以使用C/C++/Objective-C等。.NET中,C#与C++的互操作(Interop)可以通过P/Invoke实现(也可以使用C++/CLI作为中间层,但Mono不支持)。

Unity native 插件开发流程:C++ → C API → 打包 → C# 封装接口。

在 Safe Mode 下 C# 与 C++ 无法直接交互,C++ 需要包装成 C API,由 C# 调用 C API。 插件包:Android 打包成 so,IOS 打包成 framework 或者 .a,macOS 打包成 .bundle,Windows 打包成 dll。Windows 放在 Plugins/x86(x64) 目录下,Mac 直接放在 Plugins 目录下,iOS 放在 Plugins/iOS 目录下,Android 放在 Plugins/Android/libs/armeabi-v7a 目录下。如果修改了本地插件,需要将 dll 或者 bundle 覆盖了之后重启 Unity, 不然还是会使用老的 Native Plugin。

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
cmake_minimum_required(VERSION 3.5)
project(UnityPlugin)

aux_source_directory(. src) # 搜索当前目录下所有.cpp文件放入${src}
aux_source_directory(channel.cpp lib) # 需要编成.dylib的文件

# C++编译标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fms-extensions")

# 生成可执行文件

add_executable(main ${src})

# 生成.bundle文件

# add_library(project1 MODULE ${lib}) #编译为程序资源包 *.bundle
# set_target_properties(project1 PROPERTIES BUNDLE TRUE)

# 设置生成.bundle文件的相关参数
set(MACOSX_BUNDLE_BUNDLE_NAME "ChannelPlugin")
set(MACOSX_BUNDLE_BUNDLE_VERSION "1.0.0")

# 添加源文件生成共享库
add_library(ChannelPlugin SHARED channel.cpp)

# 设置.bundle文件的输出路径
set_target_properties(ChannelPlugin PROPERTIES
OUTPUT_NAME ChannelPlugin
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bundles"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bundles"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bundles"
BUNDLE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bundles"
)

# 设置.bundle文件的资源文件
set_source_files_properties(
"${CMAKE_CURRENT_BINARY_DIR}/bundles"
PROPERTIES MACOSX_PACKAGE_LOCATION "/"
)

channel.h头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef CHANNEL_H
#define CHANNEL_H

#ifdef __cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif

#ifndef EXPORT_API
#if UNITY_METRO
#define EXPORT_API EXTERN_C __declspec(dllexport) __stdcall
#elif UNITY_WIN
#define EXPORT_API EXTERN_C __declspec(dllexport)
#else
#define EXPORT_API EXTERN_C
#endif
#endif /* EXPORT_API*/

EXPORT_API int func(int x);

#endif /* CHANNEL_H*/

在所有出现 func 声明或定义的时候都要带 EXPORT_API ;将 /bundles 中的 .dylib 文件复制到Unity的Assets/Plugins下,将 C# 插件放入Assets/Editor下即可。

注意:Unity 改变C#脚本后会实时重编译,但在移入新.dylib文件并不会,需要退出重新进入 Unity 项目。

RPC 协议选型与通道搭建

参考:gRPC官方文档中文版gRPC官方文档Protocol Buffer官方文档

gRPC是一个高性能、通用的开源RPC框架,其由Google 2015年主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf序列化协议开发,且支持众多开发语言。

RPC 的主要目的是让客户端可以像调用函数一样直接与服务端通信,尽可能使网络交互过程变得透明。使用时,首先需要在 .proto 文件中定义各个函数原型与数据结构(将作为函数参数传递,也就是网络传输的数据),然后 Protobuf 会根据 .proto 生成桩程序 demo.pb.cc 和 demo.pb.h(这两个文件不应被用户修改),gRPC 再生成 demo.grpc.pb.cc 和 demo.grpc.pb.h(这两个文件也一般无需修改),最后用户在自定义程序 demo.client 和 demo.server 中自定义需要的操作和传递的信息。

具体安装使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
brew install autoconf automake libtool pkg-config # 安装依赖库
brew install protobuf grpc
export MY_INSTALL_DIR=$HOME/.local/share # 自定义安装路径
export PATH="$MY_INSTALL_DIR/bin:$PATH"
git clone --recurse-submodules -b v1.56.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc

cd grpc
mkdir -p cmake/build
pushd cmake/build
cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR ../..
make -j 4
make install
popd

重新生成gRPC代码:

1
2
3
4
5
6
7
8
9
10
cd examples/cpp/helloworld/cmake/build
# 重新生成 helloworld.pb.{h,cc} 和 helloworld.grpc.pb.{h,cc}
make helloworld.grpc.pb.o
# 实际运行的是以下两条命令:
protoc -I ../../protos --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../../protos/helloworld.proto
protoc -I ../../protos --cpp_out=. ../../protos/helloworld.proto
# 用户自行修改my_project/下的.cc代码
make -j 4
./greeter_server
./greeter_client

目录结构:

1
2
3
4
5
6
7
8
cpp/
my_project/
cmake/
build/
greeter_client.cc
greeter_server.cc
proto/
my_project.proto

调通后,同样依照官方手册调通 Objective-C 语言的 gRPC (仅支持客户端)。然后修改 CMakeLists.txt 文件以得到 .dylib 文件,从而导入到 Unity 中被插件调用。

至此,我们已经能够实现在 Unity 上通过 C# 脚本实时获取场景树信息(Unity 序列化方案目前采用JsonUtility),并通过跨语言互操作将信息传递给 C++ gRPC,再通过 gRPC 实时传输至客户端(可以由客户端每 0.1 秒发送一次请求),最后客户端拿到 Unity 场景树信息后在本地重建场景。