Unity 和 iOS 交互(通用數(shù)據(jù))

背景

依稀記得3年前第一次接觸到Unity,驚嘆他的強大能力,也初次接觸到音視頻領(lǐng)域,還接觸了一些攝影相關(guān)能力(感謝@老晉哥)。
轉(zhuǎn)眼至今公司很重視音視頻新產(chǎn)品,逐漸的公司也把一些核心能力放到了Unity這個平臺上。
今天要分享的交互方式也很好支持著沙拉視頻Blurrr等新產(chǎn)品,相信不久還會支持更多的新產(chǎn)品。
那么Unity和iOS數(shù)據(jù)交互效率就至關(guān)重要了,如何打造一種高效的交互方式,成了這2年來很重要的事情。

過程&實現(xiàn)

iOS 向 Unity 發(fā)送消息

這種情況下一般使用UnitySendMessage的方法來實現(xiàn),看看官方文檔怎么說

使用 UnitySendMessage 時具有以下限制:

1.通過原生代碼,只能調(diào)用與以下簽名對應(yīng)的腳本方法:void MethodName(string message);。 2.對 UnitySendMessage 的調(diào)用是異步的,并有一幀延遲。 3.如果兩個或多個游戲?qū)ο缶哂邢嗤拿Q,則在使用 UnitySendMessage 時可能導(dǎo)致沖突。

無論使用什么方式調(diào)用UnitySendMessage方法,方法最終會使用主線程運行。

假如客戶端有一個渲染需求,然后呢,Unity這邊正好有一個接口滿足,叫 LoadRender,代碼如下

iOS 這邊需要簡單封裝一下方便調(diào)用

/*
iOS
Objc++
*/
- (void)api_LoadRender:(NSString *)json
{
  // RenderController 表示類名
  //LoadRender 表示方法名
  // [json UTF8String] 參數(shù)
  UnitySendMessage("RenderController", "LoadRender", [json UTF8String]);
}

Unity這邊也需要有對應(yīng)的方法實現(xiàn)

/*
Unity
C#
*/ 
public class RenderController
{
  public void LoadRender(string json)
  {
    // 處理這次調(diào)用
    // Unity 向 iOS 發(fā)送消息 告訴是成功或者失敗
    ...
  }
}

Unity 向 iOS 發(fā)送消息

先看一個最簡單的示例
Unity當(dāng)中聲明一個方法

/*
Unity
C#
*/
[DllImport("__Internal")]
internal extern static void calliOS_method1();
/*
iOS
Objc++
UNITY_INTERFACE_EXPORT 和 UNITY_INTERFACE_API 在 Unity的IUnityInterface.h 中聲明
*/
extern "C"
{
  //實現(xiàn)方法
  void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API calliOS_method1()
  {
    // 運行邏輯1
  }
}

這樣調(diào)用calliOS_method1時,客戶端就及時收到并運行了代碼,注意哦!這里是及時響應(yīng)的。
當(dāng)然業(yè)務(wù)多了之后,cs當(dāng)中定義方法也會增多,維護成本也開始變高。
到目前為止,客戶端想要回傳數(shù)據(jù)給Unity端還不能支持。
所以我們得改造一下,支持返回值的方法來試試。

1. 重新定義下接口,使之能通用起來

cs 代碼中聲明一個通用方法,有一個指針輸入,和一個指針返回

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

這樣的話就可以根據(jù)業(yè)務(wù)需求再定義輸入結(jié)構(gòu)
這時,上面的LoadRender方法就可以加入回調(diào)能力,
當(dāng)客戶端通過api_LoadRender發(fā)起API調(diào)用后,Unity下一幀運行好后就調(diào)用通用接口回調(diào)給iOS端

2. 雙邊定義相同結(jié)構(gòu)

/*
Unity
C#
*/
public struct ETPlatformAPICallBack
{
  public int type;         //結(jié)
  public int componentType;//構(gòu)
  public int status;       //一
  public double re_status; //致
}

/*
iOS
Objc++
*/
extern "C"
{
  typedef struct _ETPlatformAPICallBack
  {
    int type;         //結(jié)
    int componentType;//構(gòu)
    int status;       //一
    double re_status; //致
  } ETPlatformAPICallBack;
}

Unity端的LoadRender方法做完后,需要創(chuàng)建一個 ETPlatformAPICallBack 對象,并且賦值(表示狀態(tài)),再通過一個簡單方法轉(zhuǎn)換成 一個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;
}

轉(zhuǎn)換了對象后,調(diào)用onETPlatform 通用接口,傳入剛才轉(zhuǎn)義好的結(jié)構(gòu)體,iOS端就可以及時收到消息

3.在iOS端實現(xiàn)onETPlatform方法

C# 的 IntPtr 對應(yīng) void*

/*
iOS
Objc++
*/
extern "C"
{
  void* UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API onETPlatform(void* entity)
  {
    //由于所有的struct 都必須是 int type 開頭,所以我拿到指針的前面位置轉(zhuǎn)成int,然后判定是什么類型
    int32_t *p_int = static_cast<int32_t *>(entity);
    int type = p_int[0];
    switch(type) {
      case ETUnityPlatformStructType_LoadRenderCallBack: // 調(diào)用LoadRender 接口后回調(diào)
      {
         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強轉(zhuǎn)成int指針,獲取指針位置0的int值
通過type值確定它的所屬struct類型,所以每一個struct的第一個字段都是int類型
然后再static_cast轉(zhuǎn)換成對應(yīng)的struct,然后根據(jù)業(yè)務(wù)使用字段或者修改字段
最后直接返回entity即可(因為是指針操作)。

4.Unity獲取返回值

完成調(diào)用后會收到IntPtr對象,再把這個對象轉(zhuǎn)換成strcut,如下

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

最后拿到iOS的返回數(shù)據(jù)

關(guān)鍵代碼

// Unity C#
public class RenderController
{
  private T InvokePlatform_iOS<T>(T entity)
  {
    IntPtr sp = StructToIntPtr(entity); // 1 把要給iOS的 C# struct 轉(zhuǎn)成 指針
    var ptr = onETPlatform(sp);// 把數(shù)據(jù)傳入到iOS端
    var ver = IntPtrToStruct<T>(ptr);// 把iOS端的返回數(shù)據(jù)轉(zhuǎn)換成 C# 的struct 對象
    Marshal.FreeCoTaskMem(sp);// 釋放
    return ver;
  }
  // 第一節(jié)講過
  public void LoadRender(string json)
  {
    // 處理這次調(diào)用
    // Unity 向 iOS 發(fā)送消息 告訴是成功或者失敗 的偽代碼
    ...
    var cb = new ETPlatformAPICallBack(); // 創(chuàng)建對象
    cb.type = ETUnityPlatformStructType_LoadRenderCallBack;// 這里要保證和iOS的枚舉一致
    cb.status = 0;// 0=成功  -1=失?。?1失敗碼)
    var ety = InvokePlatform_iOS(cb); // 請求客戶端,并獲取客戶端返回的數(shù)據(jù)
    // ety.re_status 值為客戶端寫入值 1.2
  }
}

至此,交互方式分享就到這里結(jié)束了。

相關(guān)注意:

1.客戶端也分為iOS和MacOS,之所以有2個平臺是因為MacOS上方便測試,還有一些效果打包需要在MacOS上去做,所以這2個平臺需要解耦,做法是專門寫了基類,iOS和MacOS分別有繼承它。

2.如果struct沒有統(tǒng)一,會出現(xiàn)比較嚴重的問題,這個問題是內(nèi)存被改寫引發(fā)的不可知問題類型,可能就崩潰,可能是值改變引起的連鎖反應(yīng)吧,所以這個strcut 一定一定一定要保持參數(shù)結(jié)構(gòu)統(tǒng)一。

3.因為這些接口調(diào)用比較頻繁(一幀里好幾次,甚至幾十次,取決于需要交互的數(shù)據(jù)量)所以建議entity new出來后就不要銷毀了(多線程就new吧)。

參考資料

構(gòu)建適用于 iOS 的插件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容