美文网首页
iOS 与 Unity 消息传递 (Swift 与 C#)

iOS 与 Unity 消息传递 (Swift 与 C#)

作者: dangbo | 来源:发表于2019-04-10 16:59 被阅读0次
    image

    背景知识

    Unity 在导出 iOS 工程的时候支持两种模式:mono 和 IL2CPP。

    mono 就是传统的虚拟机模式。这种模式只支持 32 位的 cpu,根据官方的答复,以后也不会有更多的支持了。现在 iOS 上架 AppStore 必须要支持 arm64,所以 mono 模式就是看看就好了。

    IL2CPP IL(中间语言)转 C++,也就是把 C# 编译成的 IL,再从 IL 转成 C++,然后跟 iOS 代码一起混合编译。所以基于 IL2CPP 的Unity 与 iOS 原生接口的相互调用,本质上就是 C++ 和 Objective C 的相互调用,理解了这个,我们做互相调用的开发就很容易了。

    Swift 参数与 C# 转换

    函数参数

    例如 void UnitySendMessage(const char* obj, const char* method, const char* msg);

    String 转 const char* => url.cString(using: String.defaultCStringEncoding)

    函数返回值

    我们不能在函数内存申请局部变量去返回给 C# 用,因为函数退出的时候,内存就释放了,那么我们需要用 malloc 去分配内存。malloc 分配的内存不需要调用 free,也不用担心会内存泄露,因为合成的代码会自动帮忙处理的。

    int 数组

    static int *intArr = NULL;
    int* getAllBotId() {
        NSArray *array = [SwiftCommon getAllBotId];
        intArr = malloc(array.count);
        for (int i = 0; i < array.count; i++) {
            intArr[i] = [[array objectAtIndex:i] intValue];
        }
        return intArr;
    }
    

    char 数组

    char* getRobotCardsInfo() {
        NSString *string = [SwiftCommon getRobotCardsInfo];
        char* cStringCopy(const char* string);
        return cStringCopy([string UTF8String]);
    }
    
    char* cStringCopy(const char* string){
        if (string == NULL){
            return NULL;
        }
        char* res = (char*)malloc(strlen(string)+1);
        strcpy(res, string);
        return res;
    }
    

    通信一:Unity 向 iOS 发消息

    Unity 声明一个 [DllImport( "__Internal" )] 函数

    iOS .m 文件写函数实现

    通信二:iOS 向 Unity 发消息

    方法一:UnitySendMessage

    Unity 自身提供了一个函数,可以将信息通过字符串或 json 字符串传入

    /**
      * iOS 主动发消息给 Unity
      * @param obj     Unity GameObject名称
      * @param method  GameObject绑定脚本的方法
      * @param msg     要传递的消息
      */
    void UnitySendMessage(const char* obj, const char* method, const char* msg);
    

    方法二: 指针

    原理:C++ 里面有函数指针的概念,我们可以在 iOS 端实现一个函数,接收函数指针,Unity 侧调用,把 Unity 的函数当做指针传过去,在 iOS 端把它存下来,后续使用。

    示例:将一个 string 传到 unity, 代码量比 UnitySendMessage 多,但这是为下一个例子做铺垫,通过指针回调以一种更优雅的方式传回数据

    TtsPlayeriOSProxy.cs
    
    [MonoPInvokeCallback]
    public static int playTtsUrl(string url) {
        // 收到 iOS 传来的 url
    }
    
    [DllImport("__Internal")]
    private static extern void setTtsFunction(FunctionPlayer urlPlayer);
    
    // ios call c# delegate
    public delegate int FunctionPlayer(string url);
    
    ...
    
    // 单例构造函数
    private TtsPlayeriOSProxy ()
    {
        setTtsFunction (playTtsUrl);
    }
            
    
    UnityPlugin.m 
    
    // C#设置过来的函数指针类型
    typedef int (*PlayTtsUrlFunction)(const char *url);
    
    static PlayTtsUrlFunction playTtsUrlFunc;
    
    void setTtsFucntion(PlayTtsUrlFunction urlFunction) {
        playTtsUrlFunc = urlFunction;
    }
    
    void playTtsUrl(const char* url) {
        if (playTtsUrlFunc != nil) {
            playTtsUrlFunc(url);
        }
    }
    
    

    在 .swift 文件中可直接调用 playTtsUrl 函数将 url 传递到 Unity

    知识补充:

    1. C# delegate

    C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。
    例如,假设有一个委托:

    public delegate int MyDelegate (string s);

    上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量。

    2. [DllImport( "__Internal" )]
    private static extern void sendRequest(int requestId, byte[] data, int dataLength);
    

    [DllImport( "__Internal" )] 表示这个函数位于Dll中,DLL名字是 __Internal,这是固定语法,意思是这个函数是静态链接在 iOS 的 App 中的。extern 表示是一个外部的函数。

    3. [MonoPInvokeCallback]

    用来标记这个函数会被iOS侧反向调用

    通信三:Unity 将任务交付 iOS 处理,结果回调 Unity

    示例:Unity 组装数据 encode 成 byte 数组交付 iOS 发起网络请求

    image

    Unity

    NetworkManageriOSProxy.cs 单例类
    
    [DllImport( "__Internal" )]
    private static extern void sendRequest(int requestId, byte[] data, int dataLength);
    [DllImport("__Internal")]
    private static extern int setFunctionPointerOnResponse(FunctionPointerOnResponse pointer);
    
    // ios call c# delegate
    public delegate void FunctionPointerOnResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength);
        
    [MonoPInvokeCallback]
    // 需要 length 重组数据
    static public void onResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength)
    {            
        if (errorCode == 0)
        {
            byte[] buffer = new byte[responseDataLength];         
            Marshal.Copy(responseData, buffer, 0, responseDataLength);
           NetworkManagerProxy.onResponse(reqeustId, errorCode, buffer);    
        }
    }
    
    ...
        
    private static bool isCallbackSeted = false;
        
    public static void sendRequestForiOS(int requestId, byte[] data)
    {
        if (!isCallbackSeted) {
            setFunctionPointerOnResponse(onRespone);
            isCallbackSeted = true;
        }
        sendRequest(requestId, data, data.Length);
    }
    
    
    

    针对 onResponse 函数的 responseData 参数做一个解释

    iOS c++: void onRespone(int reqeustId, int errorCode, const void responseData*, int responseDataLength);
    Unity c#: static public void onResponse(int reqeustId, int errorCode, IntPtr responseData, int responseDataLength);

    iOS侧 调用 C# 的时候,基本类型是可以直接映射的,要注意的是,如果是数组之类的参数,在 iOS 侧用 C++ 的表现是指针,这个其实是内存里面的 rawdata,会以一个 IntPtr 的参数传递到 Unity 侧,我们知道在 C# 中数组是个对象,我们要把它转为“托管的对象”。上面的代码有示例如何转换。

    iOS

    .m文件
    
    // C#设置过来的函数指针类型
    typedef void (*FunctionPointerOnResponse)(int reqeustId, int errorCode, void* responseData, int responseDataLength);
    // 用于保存回调指针的
    static FunctionPointerOnResponse callbackFunction;
    
    void setFunctionPointerOnResponse(FunctionPointerOnResponse pointer) {
        callbackFunction = pointer;
    }
    
    // 需要 length 重组数据
    void sendRequest(int requestId, Byte* data, int dataLength) {
        NSData *body = [[NSData alloc] initWithBytes:data length:dataLength];
        [SwiftCommon sendUnityRequest:requestId body:body];
    }
    
    
    // 收到数据的时候回调此接口
    void onRespone(int reqeustId, int errorCode, const void* responseData, int responseDataLength) {
        if (callbackFunction) {
            callbackFunction(reqeustId, errorCode, (void *)(responseData), responseDataLength);
        }
    }
    
    
    SwiftCommon.swift
    
    /**
        OC调用swift中不支持的类型的方法
     */
    class SwiftCommon: NSObject {
        // 伪代码
        @objc static func sendUnityRequest(_ requestId: Int, body: NSData) {
            let request = xxx
            NetworkEngine.shared.sendRequest(request, success: { response in
                guard var data = response.data else {
                    return
                }
                let dataLength = Int32(data.count)
                data.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) -> Void in
                    // 回传给 unity
                    onRespone(Int32(requestId), response.ret, bytes, dataLength)
                })
            }) { error in
            
            }
        }
    }
    

    通信四:iOS 将任务交给 Unity 处理,结果回调 iOS

    image

    Unity

    通信三中写了如何将指针传入iOS, 这里不再赘述传递 start 和 stop 指针的 c# 代码 
    
    iOSASRListener.cs
    
    [DllImport("__Internal")]
    private static extern void _HandleASRSuccessed(string requestId, bool isFinish, string msgText, string rspMsg, byte[] msgResponse, int msgResponeLength);
    [DllImport("__Internal")]
    private static extern void _HandleASRFailed(string requestId, int errorCode);
    
    

    iOS

    AsrProxy.m
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    // C#设置过来的函数指针类型
    typedef void (*FunctionPointerForAsr)(void);
        
    static FunctionPointerForAsr start;
    static FunctionPointerForAsr stop;
    
    void setFunctionPointerForAsr(FunctionPointerForAsr startFunc, FunctionPointerForAsr stopFunc)
    {
        start = startFunc;
        stop = stopFunc;
    }
    
    void StartAsr() 
    {
        if (start)
        {
            start();
        }
    }
    
    void StopAsr() 
    {
        if (stop)
        {
            stop();
        }
    }
    
    #ifdef __cplusplus
    }
    #endif
    
    AsrListenerProxy.m
    
    extern void (^ __nonnull HandleASRSuccessed_SwiftCallBack)(const char* __nonnull requestId, bool isFinish, const char* __nonnull msgText, const char* __nonnull rspMsg, NSData* msgResponseData) = NULL;
    extern void (^ __nonnull HandleASRFailed_SwiftCallBack)(const char* __nonnull requestId, int errorCode) = NULL;
    
    void _HandleASRSuccessed(const char* requestId, bool isFinish, const char* msgText, const char* rspMsg, Byte* msgResponse, int msgResponeLength)
    {
        if (HandleASRSuccessed_SwiftCallBack != NULL ){
            NSData * data = [[NSData alloc]initWithBytes:msgResponse length:msgResponeLength];
            HandleASRSuccessed_SwiftCallBack(requestId,isFinish,msgText,rspMsg,data);
        }
    }
    
    void _HandleASRFailed(const char* requestId, int errorCode)
    {
        if (HandleASRFailed_SwiftCallBack != NULL ){
            HandleASRFailed_SwiftCallBack(requestId,errorCode);
        }
    }
    
    AsrService.swift
    
    @objc protocol AsrServiceDelegate {
        
        func asrSuccessed(requestId: String, isFinish: Bool, msgText: String, rspMsg: String, response: UniSendMsgResponse?)
        
        func asrFailed(requestId: String, errorCode: Int32)
    }
    
    class AsrService {
        
        weak var delegate: AsrServiceDelegate?
        
        init() {
            bridgingCFunction()
        }
    
        convenience init(delegate: AsrServiceDelegate) {
            self.init()
            self.delegate = delegate
        }
        
        fileprivate func bridgingCFunction() {
            HandleASRSuccessed_SwiftCallBack     = handleASRSuccessed
            HandleASRFailed_SwiftCallBack        = handleASRFailed
        }
        
        func startAsr() {
            StartAsr()
        }
        func stopAsr() {
            StopAsr()
        }
    
        fileprivate func handleASRSuccessed(requestId: UnsafePointer<Int8> , isFinish: Bool, msgText: UnsafePointer<Int8>, rspMsg: UnsafePointer<Int8>, msgResponseData: Data?) {
        
            var response: UniSendMsgResponse?
            if let `msgResponseData` = msgResponseData {
                 response = UniSendMsgResponse.fromData(msgResponseData) as? UniSendMsgResponse
            }
            
            self.delegate?.asrSuccessed(requestId: requestId.stingValue, isFinish: isFinish, msgText: msgText.stingValue, rspMsg: rspMsg.stingValue, response: response)
        }
        
        fileprivate func handleASRFailed(requestId: UnsafePointer<Int8> , errorCode: Int32) {
            self.delegate?.asrFailed(requestId: requestId.stingValue, errorCode: errorCode)
        }
      
    }
    

    踩坑

    MonoPInvokeCallback属性无法找到

    这个是旧版本 Unity 才有的一个属性类,其实就是用来标记这个函数会被 iOS 侧反向调用的,也没有什么实质性的意义。但是新版本去掉了导致编译不过。所以就在工程里面添加一个就行了。

    // 参考这个帖子的说明 https://garry.tv/2018/02/15/steamworks-and-il2cpp/
    internal class MonoPInvokeCallbackAttribute : Attribute
    {
        public MonoPInvokeCallbackAttribute() { }
    }
    

    参考

    Unity C#和iOS函数互相调用的方法

    C# 委托

    相关文章

      网友评论

          本文标题:iOS 与 Unity 消息传递 (Swift 与 C#)

          本文链接:https://www.haomeiwen.com/subject/mzciiqtx.html