背景
依稀記得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吧)。