美文网首页
Unity 和 iOS 交互(通用数据)

Unity 和 iOS 交互(通用数据)

作者: Cc_Seya | 来源:发表于2022-03-02 20:50 被阅读0次

    背景

    依稀记得3年前第一次接触到Unity,惊叹他的强大能力,也初次接触到音视频领域,还接触了一些摄影相关能力(感谢@老晋哥)。
    转眼至今公司很重视音视频新产品,逐渐的公司也把一些核心能力放到了Unity这个平台上。
    今天要分享的交互方式也很好支持着沙拉视频Blurrr等新产品,相信不久还会支持更多的新产品。
    那么Unity和iOS数据交互效率就至关重要了,如何打造一种高效的交互方式,成了这2年来很重要的事情。

    过程&实现

    iOS 向 Unity 发送消息

    这种情况下一般使用UnitySendMessage的方法来实现,看看官方文档怎么说

    使用 UnitySendMessage 时具有以下限制:

    1.通过原生代码,只能调用与以下签名对应的脚本方法:void MethodName(string message);。 2.对 UnitySendMessage 的调用是异步的,并有一帧延迟。 3.如果两个或多个游戏对象具有相同的名称,则在使用 UnitySendMessage 时可能导致冲突。

    无论使用什么方式调用UnitySendMessage方法,方法最终会使用主线程运行。

    假如客户端有一个渲染需求,然后呢,Unity这边正好有一个接口满足,叫 LoadRender,代码如下

    iOS 这边需要简单封装一下方便调用

    /*
    iOS
    Objc++
    */
    - (void)api_LoadRender:(NSString *)json
    {
      // RenderController 表示类名
      //LoadRender 表示方法名
      // [json UTF8String] 参数
      UnitySendMessage("RenderController", "LoadRender", [json UTF8String]);
    }
    

    Unity这边也需要有对应的方法实现

    /*
    Unity
    C#
    */ 
    public class RenderController
    {
      public void LoadRender(string json)
      {
        // 处理这次调用
        // Unity 向 iOS 发送消息 告诉是成功或者失败
        ...
      }
    }
    

    Unity 向 iOS 发送消息

    先看一个最简单的示例
    Unity当中声明一个方法

    /*
    Unity
    C#
    */
    [DllImport("__Internal")]
    internal extern static void calliOS_method1();
    
    /*
    iOS
    Objc++
    UNITY_INTERFACE_EXPORT 和 UNITY_INTERFACE_API 在 Unity的IUnityInterface.h 中声明
    */
    extern "C"
    {
      //实现方法
      void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API calliOS_method1()
      {
        // 运行逻辑1
      }
    }
    

    这样调用calliOS_method1时,客户端就及时收到并运行了代码,注意哦!这里是及时响应的。
    当然业务多了之后,cs当中定义方法也会增多,维护成本也开始变高。
    到目前为止,客户端想要回传数据给Unity端还不能支持。
    所以我们得改造一下,支持返回值的方法来试试。

    1. 重新定义下接口,使之能通用起来

    cs 代码中声明一个通用方法,有一个指针输入,和一个指针返回

    /*
    iOS
    Objc++
    */
    [DllImport("__Internal")]
    internal extern static IntPtr onETPlatform(IntPtr entity);
    

    这样的话就可以根据业务需求再定义输入结构
    这时,上面的LoadRender方法就可以加入回调能力,
    当客户端通过api_LoadRender发起API调用后,Unity下一帧运行好后就调用通用接口回调给iOS端

    2. 双边定义相同结构

    /*
    Unity
    C#
    */
    public struct ETPlatformAPICallBack
    {
      public int type;         //结
      public int componentType;//构
      public int status;       //一
      public double re_status; //致
    }
    
    /*
    iOS
    Objc++
    */
    extern "C"
    {
      typedef struct _ETPlatformAPICallBack
      {
        int type;         //结
        int componentType;//构
        int status;       //一
        double re_status; //致
      } ETPlatformAPICallBack;
    }
    

    Unity端的LoadRender方法做完后,需要创建一个 ETPlatformAPICallBack 对象,并且赋值(表示状态),再通过一个简单方法转换成 一个IntPtr 对象,如下

    /*
    Unity
    C#
    */
    // T是范型
    private static IntPtr StructToIntPtr<T>(T a)
    {
      IntPtr sp = Marshal.AllocCoTaskMem(Marshal.SizeOf(a));
      Marshal.StructureToPtr(a, sp, false);
      return sp;
    }
    

    转换了对象后,调用onETPlatform 通用接口,传入刚才转义好的结构体,iOS端就可以及时收到消息

    3.在iOS端实现onETPlatform方法

    C# 的 IntPtr 对应 void*

    /*
    iOS
    Objc++
    */
    extern "C"
    {
      void* UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API onETPlatform(void* entity)
      {
        //由于所有的struct 都必须是 int type 开头,所以我拿到指针的前面位置转成int,然后判定是什么类型
        int32_t *p_int = static_cast<int32_t *>(entity);
        int type = p_int[0];
        switch(type) {
          case ETUnityPlatformStructType_LoadRenderCallBack: // 调用LoadRender 接口后回调
          {
             ETPlatformAPICallBack *cety = static_cast<ETPlatformAPICallBack *>(entity);
             int status = cety.status; //使用 Unity 传过来的值 定义0代表成功 -1表示失败
             cety.re_status = 1.2; //把新值传入到 Unity 端
          } break;
          default:
            assert(false); // 不支持
            break;
        }
        return entity;
      }
    }
    

    稍微解释一下
    onETPlatform方法接收到一个entity,然后把entity强转成int指针,获取指针位置0的int值
    通过type值确定它的所属struct类型,所以每一个struct的第一个字段都是int类型
    然后再static_cast转换成对应的struct,然后根据业务使用字段或者修改字段
    最后直接返回entity即可(因为是指针操作)。

    4.Unity获取返回值

    完成调用后会收到IntPtr对象,再把这个对象转换成strcut,如下

    /*
    Unity
    C#
    */
    private static T IntPtrToStruct<T>(IntPtr ptr)
    {
      return (T)Marshal.PtrToStructure(ptr, typeof(T));
    }
    

    最后拿到iOS的返回数据

    关键代码

    // Unity C#
    public class RenderController
    {
      private T InvokePlatform_iOS<T>(T entity)
      {
        IntPtr sp = StructToIntPtr(entity); // 1 把要给iOS的 C# struct 转成 指针
        var ptr = onETPlatform(sp);// 把数据传入到iOS端
        var ver = IntPtrToStruct<T>(ptr);// 把iOS端的返回数据转换成 C# 的struct 对象
        Marshal.FreeCoTaskMem(sp);// 释放
        return ver;
      }
      // 第一节讲过
      public void LoadRender(string json)
      {
        // 处理这次调用
        // Unity 向 iOS 发送消息 告诉是成功或者失败 的伪代码
        ...
        var cb = new ETPlatformAPICallBack(); // 创建对象
        cb.type = ETUnityPlatformStructType_LoadRenderCallBack;// 这里要保证和iOS的枚举一致
        cb.status = 0;// 0=成功  -1=失败(-1失败码)
        var ety = InvokePlatform_iOS(cb); // 请求客户端,并获取客户端返回的数据
        // ety.re_status 值为客户端写入值 1.2
      }
    }
    

    至此,交互方式分享就到这里结束了。

    相关注意:

    1.客户端也分为iOS和MacOS,之所以有2个平台是因为MacOS上方便测试,还有一些效果打包需要在MacOS上去做,所以这2个平台需要解耦,做法是专门写了基类,iOS和MacOS分别有继承它。

    2.如果struct没有统一,会出现比较严重的问题,这个问题是内存被改写引发的不可知问题类型,可能就崩溃,可能是值改变引起的连锁反应吧,所以这个strcut 一定一定一定要保持参数结构统一。

    3.因为这些接口调用比较频繁(一帧里好几次,甚至几十次,取决于需要交互的数据量)所以建议entity new出来后就不要销毁了(多线程就new吧)。

    参考资料

    构建适用于 iOS 的插件

    相关文章

      网友评论

          本文标题:Unity 和 iOS 交互(通用数据)

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