前言
我的一个名为嘤嘤嘤的同事,做机器人仿真,让我给他写一个Unity的机械臂仿真程序,整个流程就是我的unity程序开一个TCP的服务器,通过接收他发来的指令,并且解析成对应的动作来操控Unity的机械臂.本身项目很简单,但是问题是我正忙于考研,我作出第一个版本后,他需要改动需求的话我还要改,他不会c#和Unity只会C语言,这种事情当然难不住我.于是就有了下面的奇葩操作.
我在Unity内部只实现操作机械臂的关键API接口,然后接收到通信数据后,将数据的处理以及执行API对应的动作全都放到一个C++编写的DLL里.Unity启动后去加载这个DLL,并且与之通信,这样的话只要用C/C++编写对应的Dll,就能被Unity程序调用并且与之交互,在我提供的API基础上他想怎么改就怎么改,非常的"方便" .就不用浪费我的时间了
需求分析
先上个图
最终要控制的就是这个玩意儿的运动
内容包括控制每个关节的旋转,以及设置一些物体的坐标和显示隐藏等,这对于Unity来说不要再简单,奈何他不会呢
所以我最终整理的流程就是
- Unity端启动TCP服务器
- 他通过TCP向Unity端发送数据
- Unity端接收到数据后不做任何处理,将数据传入到DLL的处理函数
- DLL内进行数据的解析,并且根据解析的内容调用我提供的API进行回调,控制Unity端的行为
具体实现
1.创建C/C++的动态链接库工程
这里使用的是CLion,这里命名为Cplugin
2.定义Unity和DLL交互的数据结构
C#端的代码
//用于枚举Unity端的每一个需要被控制的GameObject
public enum ObjectEnum
{
//对应六个旋转轴
J1,
J2,
J3,
J4,
J5,
J6,
//对应四个空间坐标系
Axis1,
Axis2,
Axis3,
Axis4
}
//用于枚举日志类型
public enum LogLevel
{
Info,
Warn,
Error
}
//用于传递Vector3数据
public struct Vec3
{
public float x;
public float y;
public float z;
}
同时也要定义C/C++端的对应数据结构,要严格的和C#的定义一一对应
enum ObjectEnum {
J1, J2, J3, J4, J5, J6, Axis1, Axis2, Axis3, Axis4
};
enum LogLevel {
Info,
Warn,
Error
};
struct Vec3 {
float x;
float y;
float z;
};
3.定义给DLL调用的C#的回调函数
函数的具体业务细节不重要,就是常规的unity操作,所以没有上传,主要是函数签名
/// <summary>
/// 输出日志
/// </summary>
/// <param name="level">等级</param>
/// <param name="msg">消息</param>
static void cb_unity_log(LogLevel level, [MarshalAs(UnmanagedType.LPStr)] string msg)
{
if (level == LogLevel.Info)
{
Debug.Log(msg);
}
else if (level == LogLevel.Warn)
{
Debug.LogWarning(msg);
}
else
{
Debug.LogError(msg);
}
}
/// <summary>
/// 设置物体位置
/// </summary>
/// <param name="target">目标物体</param>
/// <param name="pos">位置</param>
/// <returns></returns>
static int cb_set_position(ObjectEnum target, Vec3 pos)
{
//具体的设置位置的操作
return 0;
}
/// <summary>
/// 设置物体旋转
/// </summary>
/// <param name="target">目标物体</param>
/// <param name="rot">角度信息</param>
/// <returns></returns>
static int cb_set_rotation(ObjectEnum target, Vec3 rot)
{
//具体的设置旋转的操作 下同
return 0;
}
/// <summary>
/// 获取物体的位置
/// </summary>
/// <param name="target">目标物体</param>
/// <returns>位置信息</returns>
static Vec3 cb_get_position(ObjectEnum target)
{
//some code
}
static Vec3 cb_get_rotation(ObjectEnum target)
{
//some code
}
/// <summary>
/// 设置物体的现实和隐藏
/// </summary>
/// <param name="target"></param>
/// <param name="isShow"></param>
/// <returns></returns>
static int cb_show_hide(ObjectEnum target, bool isShow)
{
//some code
}
4.在DLL中也要定义对应的回调函数的签名
int _declspec(dllexport)
_stdcall cb_set_rotation(ObjectEnum target, Vec3 rotation); //对应C#的设置物体旋转函数
Vec3 _declspec(dllexport)
_stdcall cb_get_rotation(ObjectEnum target);
Vec3 _declspec(dllexport)
_stdcall cb_get_position(ObjectEnum target);
int _declspec(dllexport)
_stdcall cb_set_position(ObjectEnum target, Vec3 position);
int _declspec(dllexport)
_stdcall cb_show_hide(ObjectEnum target, bool isShow);
void _declspec(dllexport)
_stdcall cb_unity_log(LogLevel level, const char *);
5.定义DLL被C#调用的函数
这里一共定义两个函数,一个是初始化函数,一个数据处理函数
其种初始化函数用于将C#的对应的API通过委托的方式传入DLL,用于回调.数据处理函数则就是用于处理TCP数据的入口
声明宏
#define API __declspec(dllexport)
声明C#回调的函数指针
decltype(cb_unity_log) *unity_log = NULL;
decltype(cb_set_position) *set_position = NULL; //
decltype(cb_set_rotation) *set_rotation = NULL; //
decltype(cb_get_position) *get_position = NULL; //
decltype(cb_get_rotation) *get_rotation = NULL; //
decltype(cb_show_hide) *show_hide = NULL; //
声明函数c
//用于处理C#接收到的TCP发来的数据
extern "C" API void process_data(unsigned char data[], int len) {
std::ostringstream oss;
float b = 0;
// unity_log(LogLevel::Info, oss.str().c_str());
if (len == 24) { //2 3 5 x / 1 z / 4 6 y
for (int i = 0; i < 6; i++) {
float anInt = *(float *) &data[i * 4];
Vec3 v;
v.x = v.y = v.z = 0;
if (i == 1 || i == 2 || i == 4)
v.x = anInt;
else if (i == 0)
v.z = anInt;
else if (i == 3 || i == 5)
v.y = anInt;
set_rotation((ObjectEnum) i, v);//调用C#的设置旋转API
oss << "data:" << anInt;
b = anInt;
}
unity_log(LogLevel::Error, oss.str().c_str());//调用C#的日志API
}
}
//用于设置回调函数
extern "C" API int
init(intptr_t logHandler, intptr_t setPositionHandler, intptr_t setRotationHandler,
intptr_t getPositionHandler,
intptr_t getRotationHandler,
intptr_t showHideHandler
) {
unity_log = (decltype(cb_unity_log) *) logHandler;
set_position = (decltype(cb_set_position) *) setPositionHandler;
set_rotation = (decltype(cb_set_rotation) *) setRotationHandler;
get_position = (decltype(cb_get_position) *) getPositionHandler;
get_rotation = (decltype(cb_get_rotation) *) getRotationHandler;
show_hide = (decltype(cb_show_hide) *) showHideHandler;
unity_log(LogLevel::Info, "cPlugin initialize successfully");
return 0;
}
6.C#端定义回调API的函数委托以及导入DLL的函数
//定义与提供的api具有相同函数签名的委托
public delegate void UnityLogDelegate(LogLevel level, string msg);
public delegate int SetPositionDelegate(ObjectEnum t, Vec3 v);
public delegate int SetRotationDelegate(ObjectEnum t, Vec3 v);
public delegate Vec3 GetPositionDelegate(ObjectEnum t);
public delegate Vec3 GetRotationDelegate(ObjectEnum t);
public delegate int ShowHideDelegate(ObjectEnum t, bool isShow);
//对应Dll的init函数
[DllImport("cplugin", CallingConvention = CallingConvention.StdCall, EntryPoint = "init")]
public static extern int InitCPlugin(
IntPtr logHandler, IntPtr setPositionHandler, IntPtr setRotationHandler,
IntPtr getPositionHandler,
IntPtr getRotationHandler,
IntPtr showHideHandler
);
//对应Dll的process_data函数
[DllImport("cplugin", CallingConvention = CallingConvention.StdCall, EntryPoint = "process_data")]
public static extern int ProcessData(byte[] data, int len);
7.C#通过调用DLL的Init函数将回调函数注入
8.测试
将DLL编译好后直接放到Unity的对应目录下即可自动加载
可以看到DLL被成功加载,并且接收数据后,对场景进行了姿态的控制
注意事项
- 注意DLL和Unity的操作系统位数要相同
- 注意DLL的名称要和代码中一致
- C#中的DLL import中的DLL名称不带.dll后缀
总结
得不偿失,简单的功能搞得这么复杂,但是我的同事不会C#我也很无奈啊
完整代码
c++:
#ifndef CPLUGIN_LIBRARY_H
#define CPLUGIN_LIBRARY_H //防止重复包含
#define API __declspec(dllexport)
#include <cstdint>
#include <iostream>
#include <sstream>
enum ObjectEnum {
J1, J2, J3, J4, J5, J6, Axis1, Axis2, Axis3, Axis4
};
enum LogLevel {
Info,
Warn,
Error
};
struct Vec3 {
float x;
float y;
float z;
};
int _declspec(dllexport)
_stdcall cb_set_rotation(ObjectEnum target, Vec3 rotation);
Vec3 _declspec(dllexport)
_stdcall cb_get_rotation(ObjectEnum target);
Vec3 _declspec(dllexport)
_stdcall cb_get_position(ObjectEnum target);
int _declspec(dllexport)
_stdcall cb_set_position(ObjectEnum target, Vec3 position);
int _declspec(dllexport)
_stdcall cb_show_hide(ObjectEnum target, bool isShow);
void _declspec(dllexport)
_stdcall cb_unity_log(LogLevel level, const char *);
decltype(cb_unity_log) *unity_log = NULL;
decltype(cb_set_position) *set_position = NULL; //
decltype(cb_set_rotation) *set_rotation = NULL; //
decltype(cb_get_position) *get_position = NULL; //
decltype(cb_get_rotation) *get_rotation = NULL; //
decltype(cb_show_hide) *show_hide = NULL; //
extern "C" API void process_data(unsigned char data[], int len) {
std::ostringstream oss;
float b = 0;
// unity_log(LogLevel::Info, oss.str().c_str());
if (len == 24) { //2 3 5 x / 1 z / 4 6 y
for (int i = 0; i < 6; i++) {
float anInt = *(float *) &data[i * 4];
Vec3 v;
v.x = v.y = v.z = 0;
if (i == 1 || i == 2 || i == 4)
v.x = anInt;
else if (i == 0)
v.z = anInt;
else if (i == 3 || i == 5)
v.y = anInt;
set_rotation((ObjectEnum) i, v);
oss << "data:" << anInt;
b = anInt;
}
unity_log(LogLevel::Error, oss.str().c_str());
// if (b > 50) {
// show_hide(ObjectEnum::J1, true);
// } else {
// show_hide(ObjectEnum::J1, false);
// }
}
}
extern "C" API int
init(intptr_t logHandler, intptr_t setPositionHandler, intptr_t setRotationHandler,
intptr_t getPositionHandler,
intptr_t getRotationHandler,
intptr_t showHideHandler
) {
unity_log = (decltype(cb_unity_log) *) logHandler;
set_position = (decltype(cb_set_position) *) setPositionHandler;
set_rotation = (decltype(cb_set_rotation) *) setRotationHandler;
get_position = (decltype(cb_get_position) *) getPositionHandler;
get_rotation = (decltype(cb_get_rotation) *) getRotationHandler;
show_hide = (decltype(cb_show_hide) *) showHideHandler;
unity_log(LogLevel::Info, "cPlugin initialize successfully");
return 0;
}
#endif //CPLUGIN_LIBRARY_H
C#:
using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
public class CPluginApi
{
public delegate void UnityLogDelegate(LogLevel level, string msg);
public delegate int SetPositionDelegate(ObjectEnum t, Vec3 v);
public delegate int SetRotationDelegate(ObjectEnum t, Vec3 v);
public delegate Vec3 GetPositionDelegate(ObjectEnum t);
public delegate Vec3 GetRotationDelegate(ObjectEnum t);
public delegate int ShowHideDelegate(ObjectEnum t, bool isShow);
[DllImport("cplugin", CallingConvention = CallingConvention.StdCall, EntryPoint = "init")]
public static extern int InitCPlugin(
IntPtr logHandler, IntPtr setPositionHandler, IntPtr setRotationHandler,
IntPtr getPositionHandler,
IntPtr getRotationHandler,
IntPtr showHideHandler
);
[DllImport("cplugin", CallingConvention = CallingConvention.StdCall, EntryPoint = "process_data")]
public static extern int ProcessData(byte[] data, int len);
public static void Init()
{
int initCPlugin = InitCPlugin(
Marshal.GetFunctionPointerForDelegate((Delegate) (UnityLogDelegate) cb_unity_log),
Marshal.GetFunctionPointerForDelegate((Delegate) (SetPositionDelegate) cb_set_position),
Marshal.GetFunctionPointerForDelegate((Delegate) (SetRotationDelegate) cb_set_rotation),
Marshal.GetFunctionPointerForDelegate((Delegate) (GetPositionDelegate) cb_get_position),
Marshal.GetFunctionPointerForDelegate((Delegate) (GetRotationDelegate) cb_get_rotation),
Marshal.GetFunctionPointerForDelegate((Delegate) (ShowHideDelegate) cb_show_hide));
}
/// <summary>
/// 输出日志
/// </summary>
/// <param name="level">等级</param>
/// <param name="msg">消息</param>
static void cb_unity_log(LogLevel level, [MarshalAs(UnmanagedType.LPStr)] string msg)
{
if (level == LogLevel.Info)
{
Debug.Log(msg);
}
else if (level == LogLevel.Warn)
{
Debug.LogWarning(msg);
}
else
{
Debug.LogError(msg);
}
}
/// <summary>
/// 设置物体位置
/// </summary>
/// <param name="target">目标物体</param>
/// <param name="pos">位置</param>
/// <returns></returns>
static int cb_set_position(ObjectEnum target, Vec3 pos)
{
RobotController.Instance.SetPosition(target, MyUtils.Vec3ToUnityV3(pos));
return 0;
}
/// <summary>
/// 设置物体旋转
/// </summary>
/// <param name="target">目标物体</param>
/// <param name="rot">角度信息</param>
/// <returns></returns>
static int cb_set_rotation(ObjectEnum target, Vec3 rot)
{
RobotController.Instance.SetRotation(target, MyUtils.Vec3ToUnityV3(rot));
return 0;
}
/// <summary>
/// 获取物体的位置
/// </summary>
/// <param name="target">目标物体</param>
/// <returns>位置信息</returns>
static Vec3 cb_get_position(ObjectEnum target)
{
return MyUtils.UnityV3ToVec3(RobotController.Instance.GetPosition(target));
}
static Vec3 cb_get_rotation(ObjectEnum target)
{
return MyUtils.UnityV3ToVec3(RobotController.Instance.GetRotation(target));
}
/// <summary>
/// 设置物体的现实和隐藏
/// </summary>
/// <param name="target"></param>
/// <param name="isShow"></param>
/// <returns></returns>
static int cb_show_hide(ObjectEnum target, bool isShow)
{
RobotController.Instance.ShowAndHide(target, isShow);
return 0;
}
}