前言
公司新项目,要和做C++算法的人一起合作开发,起初项目定于Windows平台,就看了一些C++和DLL交互的一些资料,做了一套生成DLL交互的接口,后来项目写方案由于设备又定到安卓平台,尝试过打包之后,DLL打包不到安卓平台,试过将dll改名打AB包然后用Assembly.Load的方式,但这种方式只适用于C#的DLL,后来经过多方调研,在某q群抛出这个问题才知道有和C++源码交互的方式,但自己几经尝试都调用失败,差点放弃想在安卓平台编译成so来交互,元旦过后,某天梦中想到这个问题,应该去q群继续抛出这个问题请教别人是怎么做的,最终经过两天的不懈努力,尝试编写了C接口和解决打包过程中的各种问题,最终总结了这几天来的成果。
需求分析
先上个图
这是一个基于Unity渲染的数智人项目,前端使用Unity来显示和交互,一些输入和数字人的表现都是C#层开发,比如动画系统、麦克风输入、语音数据同步嘴唇等等,后端算法主要采用C++开发,比如文本和语音互转、AI、算法模型训练等,这就涉及到了C#和C++之间的交互,也就是相互调用的问题。
所以我最终的流程就是
- Unity用麦克风监听玩家说话,并把说话内容发给C++算法
- C++得到文本,进行智能对话系统的训练
- 得到训练的对话文本后,文本转语音,发送到Unity
- Unity根据语音数据驱动说话,包括嘴唇变化、肢体动作的表现
- AI数智人回答完毕又开始循环到步骤1
具体实现
1.C#层接口定义
C#层代码:因为函数与生成的 C++ 代码链接在一起,所以没有单独的 DLL 可进行 _P/Invoke 调用。因此,可使用"__Internal"关键字代替 DLL 名称,从而使 C++ 链接器负责解析函数,而不是在运行时加载函数,如下例所示:
- 定义了初始化函数,给C++传递C#层的接口,并缓存在C++来使用C#的回调,比如Unity的日志输出、Unity内接收语音数据的方法,从而缓存后使得C++具有调用C#回调函数(委托)的能力。
[DllImport("__Internal")]
static extern int Init(
ULogCallback logCallback
,UReceivesAI_DialogueCallback diaogueCallback
);
-
- 两个初始化作为回调的参数,可以自行拓展,然后给C++缓存起来,其中的ULogCallback 为Unity的日志回调的委托,diaogueCallback为接收AI对话的委托,ULogCallback 的原型为:
/// <summary>
/// Unity日志调用的回调委托
/// </summary>
/// <param name="level"></param>
/// <param name="msg"></param>
public delegate void ULogCallback(LogLevel level, string msg);
public enum LogLevel
{
Info,
Warn,
Error
};
-
- 而diaogueCallback的原型为:
/// <summary>
/// Unity接收的AI对话语音的回调委托
/// </summary>
/// <param name="voiceDatas">语音数据</param>
public delegate void UReceivesAI_DialogueCallback(byte[] voiceDatas);
-
- 调用的时候我们默认发送Unity的日志委托,我们封装了InitDLL方法,然后回调参数可以自行拓展,但是要对应C++接口一起修改。
[MonoPInvokeCallback(typeof(void))]
/// <summary>
/// 输出日志
/// </summary>
/// <param name="level">等级</param>
/// <param name="msg">消息</param>
static void UnityLog(LogLevel level, string msg)
{
msg = $"Unity回调收到C++输出日志:{msg}";
if (level == LogLevel.Info)
{
Debug.Log(msg);
}
else if (level == LogLevel.Warn)
{
Debug.LogWarning(msg);
}
else
{
Debug.LogError(msg);
}
}
public static void InitDLL(UReceivesAI_DialogueCallback UDialogueCallback)
{
/*int init = Init(
Marshal.GetFunctionPointerForDelegate((Delegate)(ULogCallback)UnityLog),
Marshal.GetFunctionPointerForDelegate((Delegate)UDialogueCallback)
);*/
int init = Init(
UnityLog//这个方法回调基本不变,不做参数
,UDialogueCallback
);
}
注意事项:
这里要注意个问题,在使用DLL交互时没有问题,但是用源码交互时会出现报错:NotSupportedException: To marshal a managed method, please add an attribute named ‘MonoPInvokeCallback’ to the method definition. The method we’re attempting to marshal is…,查了下资料发现需要在传递的委托函数上加上[MonoPInvokeCallback(typeof(…))],里面的类型我目前填void,或者只有一个函数参数的情况下填那个参数的类型。
参考:
- 定义了给C++发送语音数据的接口,以下接口封装都比较简单,不做过多介绍
[DllImport("__Internal")]
public static extern void ReceivingMicrophoneSpeech(byte[] voiceDatas);
- 定义了给C++发送文本测试的接口
[DllImport("__Internal")]
public static extern void TEST_Call(string msg);
2.创建C/C++的动态链接库工程
由于之前交互C++DLL,创建过动态链接库工程,所以接下来Unity中的C++代码都从链接库工程里拷贝过去(后来NativeCode我改名为CInterface),关于如何新建链接库工程,可以参考文末链接:Unity 之 C#与C++/C交互指针函数指针结构体交互
3.C++层对应C#层定义接口
使用 IL2CPP 脚本后端时,可将 C++ (.cpp) 代码文件直接添加到 Unity 项目中。这些 C++ 文件将充当 Plugin Inspector 中的插件。如果将 C++ 文件配置为与 Windows 播放器兼容,则 Unity 会将这些文件与从托管程序集生成的 C++ 代码一起编译。单击 .cpp 文件,然后在 Inspector 窗口的 Platform settings 部分中选择平台设置。
在C#层定义了交互接口后,同时也要定义C/C++端的对应数据结构,要严格的和C#的定义一一对应,可以参考文末链接:C#与C++之间类型的对应
CInterface.h
#ifndef __NativeCode_H__
#define __NativeCode_H__
//#ifndef EXPORT_DLL
//#define EXPORT_DLL __declspec(dllexport) //导出dll声明
//#endif
enum class LogLevel {
Info,
Warn,
Error
};
//定义回调函数指针
typedef void(__stdcall* ULogCallback)(LogLevel level ,const char*);//Unity日志输出函数
typedef void(__stdcall* UReceivesAI_DialogueCallback)(unsigned char voiceDatas[]);//Unity需要播放的AI对话函数
extern "C" {
int Init(ULogCallback logCallback ,UReceivesAI_DialogueCallback diaogueCallback);//初始化注册Unity回调函数
void ReceivingMicrophoneSpeech(unsigned char voiceDatas[]);
void TEST_Call(char* char_Str);//测试方法
}
#endif//__NativeCode_H__
CInterface.cpp
//#include "pch.h"
#include "CInterface.h"
ULogCallback ULog;
UReceivesAI_DialogueCallback UReceivesAI_Dialogue;
extern "C" {
int Init(ULogCallback logCallback ,UReceivesAI_DialogueCallback diaogueCallback)
{
ULog = logCallback;
UReceivesAI_Dialogue = diaogueCallback;
//TODO:logCallback支持中文字符
ULog(LogLevel::Info, "12345");
ULog(LogLevel::Info, "abc");
return 0;
}
void TEST_Call(char * char_Str) {
ULog(LogLevel::Info, char_Str);
}
void ReceivingMicrophoneSpeech(unsigned char voiceDatas[]) {
ULog(LogLevel::Info, "Call ReceivingMicrophoneSpeech");
}
}
注意事项:
打包的时候,编译cpp时会出现以下报错:
- CInterface.h出现不支持__declspec,所以DLL交互时留下__declspec(dllexport) 关键字都去掉
- CInterface.cpp中的DLL交互留下头文件#include "pch.h"找不到,直接注释掉即可
- 所有的C++接口都需要extern “C”
- 集成cpp源码到Unity交互需要在ProjectSetting-Player-OtherSetting-Configuration-ScriptingBackend选择IL2CPP
总结
使用CPP源码可以在Unity引擎打包后编译成对应平台的代码,前提是脚本后端选择IL2CPP,但是在编辑器模式这套方法是用不了的,所以我打算后期在编辑器模式下使用dll交互,加个平台判断,等后期流程上成熟后将继续总结下。
参考资料
Windows 播放器:适用于 IL2CPP 的 C++ 源代码插件
Unity 之 C#与C++/C交互指针函数指针结构体交互文章来源:https://www.toymoban.com/news/detail-800993.html
C#与C++之间类型的对应文章来源地址https://www.toymoban.com/news/detail-800993.html
到了这里,关于Unity开发进行C、C++源码交互,支持跨平台的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!