Unity3D與iOS的交互

1. 關(guān)于Unity3D

Unity3D(以下簡(jiǎn)稱U3D)是由Unity Technologies開發(fā)的一個(gè)讓玩家輕松創(chuàng)建諸如三維視頻游戲、建筑可視化、實(shí)時(shí)三維動(dòng)畫等類型互動(dòng)內(nèi)容的多平臺(tái)的綜合型游戲開發(fā)工具魔眨,是一個(gè)全面整合的專業(yè)游戲引擎程腹。

作為一款跨平臺(tái)開發(fā)工具,難免會(huì)與原生平臺(tái)進(jìn)行一些交互操作來完成一些特定的平臺(tái)功能匈睁。例如:你需要直接操作iOS的IAP來實(shí)現(xiàn)游戲中的內(nèi)付費(fèi)功能卷哩;甚至一些第三方SDK沒有提供U3D版本的情況下蛋辈,你會(huì)直接在原生系統(tǒng)平臺(tái)調(diào)用其提供接口等等。

下面將為大家介紹将谊,在U3D下如何實(shí)現(xiàn)與iOS系統(tǒng)的交互工作冷溶,來滿足一些需要借助原生系統(tǒng)的功能需求。

2. From U3D to iOS

2.1 實(shí)現(xiàn)原理

由于U3D無法直接調(diào)用Objc或者Swift語言聲明的接口尊浓,幸好U3D的主要語言是C#逞频,因此可以利用C#的特性來訪問C語言所定義的接口,然后再通過C接口再調(diào)用ObjC的代碼(對(duì)于Swift代碼則還需要使用OC橋接)眠砾。例如虏劲,有如下C語言方法:

void nativeMethod ()
{
  NSLog(@"------- objc method call...\n");
}

在C#中則可以像下面代碼一樣進(jìn)行引入和調(diào)用:

using System.Runtime.InteropServices;

[DllImport("__Internal")]
internal extern static void nativeMethod();

其中DllImport為一個(gè)Attribute托酸,目的是通過非托管方式將庫中的方法導(dǎo)出到C#中進(jìn)行使用褒颈。而傳入"__Internal"則是表示這個(gè)是一個(gè)靜態(tài)庫或者是一個(gè)內(nèi)部方法。通過上面的聲明励堡,這個(gè)方法就可以在C#里面進(jìn)行調(diào)用了谷丸。如:

public class Sample
{
  public void test ()
  {
    nativeMethod();
  }
}

2.2 實(shí)現(xiàn)步驟

下面通過一個(gè)拼接字符串的例子來說明怎么樣從U3D中傳入兩個(gè)字符串到iOS中,然后由iOS拼接后通過NSLog輸出結(jié)果:

  1. 首先新建test.mtest.h兩個(gè)文件应结。分別寫入如下內(nèi)容:
/// test.h

extern "C"
{
  extern void outputAppendString (char *str1, char *str2);
}
/// test.m
#import <Foundation/Foundation.h>

void outputAppendString (char *str1, char *str2)
{
  NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
  NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
  
  NSLog(@"###%@", [NSString stringWithFormat:@"%@ %@", string1, string2]);
}
  1. 然后將上面的兩個(gè)文件放到U3D項(xiàng)目的Assets目錄中刨疼。如圖:
放入U(xiǎn)3D項(xiàng)目
  1. 分別選擇test.htest.m文件,在Inspector面板中去掉Any Platforms的勾選鹅龄,然后保留iOS這一項(xiàng)選中揩慕。如圖:
設(shè)置平臺(tái)插件
  1. 新建一個(gè)叫Sample的C#腳本文件,并在這個(gè)文件中寫入c接口的聲明扮休,如:
public class Sample : MonoBehaviour 
{
    //引入聲明
    [DllImport("__Internal")]
    static extern void outputAppendString (string str1, string str2);
}
  1. 在Start方法中調(diào)用該方法迎卤,如:
void Start () 
{
    #if UNITY_IPHONE    
    outputAppendString("Hello", "World");
    #endif
}

注意:對(duì)于指定平臺(tái)的方法,一定要使用預(yù)處理指令#if來包括起來玷坠。否則在其他平臺(tái)下面執(zhí)行會(huì)導(dǎo)致異常蜗搔。

  1. 拖動(dòng)Sample腳本到場(chǎng)景的Main Camera對(duì)象中劲藐,讓腳本進(jìn)行掛載。
掛載腳本
  1. 使用快捷鍵Command+Shift+B(或者點(diǎn)擊菜單File -> Build Settings)調(diào)出Build Settings窗口樟凄,將項(xiàng)目導(dǎo)出為iOS項(xiàng)目聘芜。如圖:
導(dǎo)出iOS項(xiàng)目
  1. 打開導(dǎo)出的iOS項(xiàng)目,先檢查之前創(chuàng)建的test.mtest.h是否已經(jīng)導(dǎo)出到項(xiàng)目中缝龄。如圖:
檢查文件
  1. 編譯運(yùn)行應(yīng)用汰现,可以看到控制臺(tái)中會(huì)輸出合并后的字符串信息,如:
2018-01-22 16:17:15.143166+0800 ProductName[29211:4392515] ###Hello World

3. From iOS to U3D

對(duì)于如何從iOS中調(diào)用U3D的接口二拐,分為兩種辦法:一種是通過UnitySendMessage方法來調(diào)用Unity所定義的方法服鹅。另一種方法則是通過入口參數(shù),傳入一個(gè)U3D的非托管方法百新,然后調(diào)用該方法即可企软。兩種方式的對(duì)比如下:

UnitySendMessage方式 非托管方法方式
接口聲明固定,只能是void method(string message)饭望。 接口靈活仗哨,可以為任意接口。
不能帶有返回值 可以帶返回值
必須要掛載到對(duì)象后才能調(diào)用铅辞。 可以不用掛載對(duì)象厌漂,但需要通過接口傳入該調(diào)用方法

下面將一一講述兩種方式的實(shí)現(xiàn)。

3.1 UnitySendMessage

  1. 基于上面調(diào)用iOS接口的例子斟珊,在Sample.cs中增加一個(gè)callback方法苇倡。如:
void callback (string resultStr)
{
    Debug.LogFormat ("result string = {0}", resultStr);
}
  1. 由于項(xiàng)目已經(jīng)掛載Sample.cs到Main Camera中,這就不用再進(jìn)行掛載囤踩。然后打開test.m文件旨椒,在outputAppendString方法中調(diào)用callback方法,并將組合字符串返回給U3D堵漱。如:
void outputAppendString (char *str1, char *str2)
{
    NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
    NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
    
    NSString *resultStr = [NSString stringWithFormat:@"%@ %@", string1, string2];
    NSLog(@"###%@", resultStr);
    
    UnitySendMessage("Main Camera", "callback", resultStr.UTF8String);
}
  1. 導(dǎo)出iOS項(xiàng)目综慎,編譯運(yùn)行看執(zhí)行結(jié)果。
2018-01-22 17:47:00.137259+0800 ProductName[29561:4429040] ###Hello World
Setting up 1 worker threads for Enlighten.
  Thread -> id: 170cb3000 -> priority: 1 
result string = Hello World
 
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

3.2 非托管方法

  1. Sample.cs中建立一個(gè)delegate聲明勤庐,并使用UnmanagedFunctionPointer特性來標(biāo)識(shí)該delegate是非托管方法示惊。代碼如下:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ResultHandler(string resultString);

其中的CallingConvention.Cdel為調(diào)用時(shí)轉(zhuǎn)換為C聲明接口。

  1. 然后聲明一個(gè)靜態(tài)方法愉镰,并使用MonoPInvokeCallback特性來標(biāo)記為回調(diào)方法米罚,目的是讓iOS中調(diào)用該方法時(shí)可以轉(zhuǎn)換為對(duì)應(yīng)的托管方法。如:
[MonoPInvokeCallback(typeof(ResultHandler))]
static void resultHandler (string resultStr)
{
    
}

注意:MonoPInvokeCallback特性參數(shù)是上一步中定義的非托管delegate丈探。方法的聲明一定要與delegate定義一致录择,并且必須為static進(jìn)行修飾(iOS不支持非靜態(tài)方法回調(diào)),否則會(huì)導(dǎo)致異常。

  1. 打開test.m文件糊肠,定義一個(gè)新的接口辨宠,如:
typedef void (*ResultHandler) (const char *object);

void outputAppendString2 (char *str1, char *str2, ResultHandler resultHandler)
{
    NSString *string1 = [[NSString alloc] initWithUTF8String:str1];
    NSString *string2 = [[NSString alloc] initWithUTF8String:str2];
    
    NSString *resultStr = [NSString stringWithFormat:@"%@ %@", string1, string2];
    NSLog(@"###%@", resultStr);
    
    resultHandler (resultStr.UTF8String);
}

上面代碼可見,在C中需要定義一個(gè)與C#的delgate相同的函數(shù)指針ResultHandler货裹。然后新增的outputAppendString2方法中多了一個(gè)回調(diào)參數(shù)resultHandler嗤形。這樣就能夠把C#傳入的方法進(jìn)行調(diào)用了。

  1. 回到Sample.cs文件弧圆,定義outputAppendString2的聲明赋兵。
[DllImport("__Internal")]
static extern void outputAppendString2 (string str1, string str2, IntPtr resultHandler);

注意:回調(diào)方法的參數(shù)必須是IntPtr類型,表示一個(gè)函數(shù)指針搔预。

  1. Start方法中調(diào)用outputAppendString2霹期,并將回調(diào)方法轉(zhuǎn)換為IntPtr類型傳給方法。如:
ResultHandler handler = new ResultHandler(resultHandler);
IntPtr fp = Marshal.GetFunctionPointerForDelegate(handler);
outputAppendString2 ("Hello", "World", fp);

上面代碼使用MarshalGetFunctionPointerForDelegate來獲取resultHandler的指針拯田。

  1. 導(dǎo)出iOS項(xiàng)目历造,編譯運(yùn)行。
2018-01-22 19:02:31.339317+0800 ProductName[29852:4459349] ###Hello World
result string = Hello World
Sample:outputAppendString2(String, String, IntPtr)
 
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/DebugBindings.gen.cpp Line: 51)

4. 類型傳遞

對(duì)于基礎(chǔ)類型數(shù)據(jù)(如:int船庇、double吭产、string等)是可以直接從U3D中傳遞給iOS的。具體對(duì)應(yīng)關(guān)系如下表所示:

U3D iOS
short short
int int
long long long
bool bool
char char
string char *
struct struct
byte[] void *
IntPtr void *

注意

  • 引用型數(shù)據(jù)不能直接從U3D傳給iOS鸭轮。如果需要傳遞這樣的類型臣淤,可以考慮將對(duì)象序列化成byte數(shù)組,然后在iOS中進(jìn)行反序列化將其還原回來窃爷。
  • 對(duì)于string類型邑蒋,會(huì)自動(dòng)轉(zhuǎn)換為c語言中的char *。但是由于C#中的string是托管類型按厘,因此char *是無法直接轉(zhuǎn)換為string的医吊,所以不要直接在返回值中返回char *類型。下一節(jié)會(huì)針對(duì)返回值進(jìn)行詳細(xì)的說明刻剥。
  • struct類型數(shù)據(jù)中不能包含引用型數(shù)據(jù)遮咖,否則在調(diào)用接口時(shí)會(huì)報(bào)告類似下面的提示:
MarshalDirectiveException: Cannot marshal field 't' of type 'TestStructType': Reference type field marshaling is not supported.

4.1 關(guān)于Marshal

Marshal類型主要是用于將C#中托管和非托管類型進(jìn)行一個(gè)轉(zhuǎn)換的橋梁滩字。其提供了一系列的方法造虏,這些方法包括用于分配非托管內(nèi)存、復(fù)制非托管內(nèi)存塊麦箍、將托管類型轉(zhuǎn)換為非托管類型漓藕,此外還提供了在與非托管代碼交互時(shí)使用的其他雜項(xiàng)方法等。

本質(zhì)上U3D與iOS的交互過程就是C#與C的交互過程挟裂,所以Marshal就成了交互的關(guān)鍵享钞,因?yàn)镃#與C的交互正正涉及到托管與非托管代碼的轉(zhuǎn)換。下面將舉例說明,如何將一個(gè)C#的引用類型轉(zhuǎn)換到對(duì)應(yīng)的OC類型栗竖。

  1. 首先在C#中聲明一個(gè)類型Person
class Person
{
    public string name;
    public int age;
}
  1. 在C中聲明一個(gè)接口printPersonInfo用于打印傳遞過來的Person信息暑脆,如:
void printPersonInfo(void *personData);
  1. 在C#中聲明此接口
[DllImport("__Internal")]
static extern void printPersonInfo (IntPtr personData);
  1. 創(chuàng)建一個(gè)Person的實(shí)例,然后將其序列化成byte數(shù)組狐肢,這里使用到對(duì)象序列化的一些知識(shí)添吗。
Person person = new Person();
person.name = "vimfung";
person.age = 18;

List<byte> buf = new List<byte>();

//寫入name
byte[] bytes = BitConverter.GetBytes (person.name.Length);
if (BitConverter.IsLittleEndian)
{
    Array.Reverse (bytes);
}
buf.AddRange (bytes);
buf.AddRange (Encoding.UTF8.GetBytes (person.name));

//寫入age
bytes = BitConverter.GetBytes (person.age);
if (BitConverter.IsLittleEndian)
{
    Array.Reverse (bytes);
}
buf.AddRange(bytes);

byte[] bufBytes = buf.ToArray();
  1. 將byte數(shù)組通過Marshal類轉(zhuǎn)換為IntPtr類型,并傳入給C接口份名。
//轉(zhuǎn)換成功IntPtr
IntPtr personData = Marshal.AllocHGlobal(bufBytes.Length);
Marshal.Copy(bufBytes, 0, personData, bufBytes.Length);

printPersonInfo(personData);

Marshal.FreeHGlobal(personData);

注意:Marshal申請(qǐng)的內(nèi)存不是自動(dòng)回收的碟联,因此調(diào)用后需要通過顯示方法FreeHGlobal調(diào)用釋放。

  1. 回到C代碼中僵腺,并實(shí)現(xiàn)其內(nèi)部處理邏輯鲤孵,如:
void printPersonInfo(void *personData)
{
    int offset = 0;
    
    //獲取name
    int nameLen = (((unsigned char *)personData) [offset] << 24)
    | (((unsigned char *)personData) [offset + 1] << 16)
    | (((unsigned char *)personData) [offset + 2] << 8)
    | (((unsigned char *)personData) [offset + 3]);
    offset += 4;
    
    char *nameBuf = malloc(sizeof(char) * (nameLen + 1));
    memset(nameBuf, 0, nameLen);
    memcpy(nameBuf, (char *)personData + offset, nameLen);
    offset += nameLen;
    NSLog(@"person name = %s", nameBuf);
    
    //獲取age
    int age = (((unsigned char *)personData) [offset] << 24)
    | (((unsigned char *)personData) [offset + 1] << 16)
    | (((unsigned char *)personData) [offset + 2] << 8)
    | (((unsigned char *)personData) [offset + 3]);
    NSLog(@"person age = %d", age);
}
  1. 導(dǎo)出iOS項(xiàng)目,編譯運(yùn)行可以看到日志里面的輸出結(jié)果
2018-01-29 14:38:56.378376+0800 ProductName[8584:1163121] person name = vimfung
2018-01-29 14:38:56.378509+0800 ProductName[8584:1163121] person age = 18

5. 返回值

除了基礎(chǔ)類型中的數(shù)值類型可以直接從iOS中返回給U3D外辰如,其他的類型是不能直接進(jìn)行返回的普监,其中理由也很簡(jiǎn)單,因?yàn)榉峭泄茴愋筒荒苤苯愚D(zhuǎn)換成托管類型琉兜。如果你想直接返回一個(gè)字符串給U3D鹰椒,那么在運(yùn)行時(shí)就會(huì)產(chǎn)生異常,因?yàn)檗D(zhuǎn)換成托管類型后他的內(nèi)存由系統(tǒng)管理呕童,一旦對(duì)象銷毀他就會(huì)被釋放內(nèi)存漆际,但它并不知道非托管模式下它是否被釋放。

為了解決返回值的問題夺饲,其實(shí)可以借助上面提到的Marshal類型配合序列化的方式來進(jìn)行返回值的返回:

  1. 先定義C代碼中的接口
void* returnString(int *len)
{
    NSString *retStr = @"Hello World";
    *len = (int)retStr.length;
    
    char *nameBuffer = malloc(sizeof(char) * (retStr.length + 1));
    memcpy(nameBuffer, retStr.UTF8String, retStr.length);
    return nameBuffer;
}
  1. 在C#中聲明該接口
[DllImport("__Internal")]
static extern IntPtr returnString (out int len);
  1. 調(diào)用該接口奸汇,并解析返回參數(shù)值
int strLen = 0;
IntPtr stringData = returnString(out strLen);
if (strLen > 0)
{
    byte[] buffer = new byte[strLen];
    Marshal.Copy(stringData, buffer, 0, strLen);
    Marshal.FreeHGlobal(stringData);

    string str = Encoding.UTF8.GetString(buffer);
    Debug.Log(str);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市往声,隨后出現(xiàn)的幾起案子擂找,更是在濱河造成了極大的恐慌,老刑警劉巖浩销,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贯涎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡慢洋,警方通過查閱死者的電腦和手機(jī)塘雳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來普筹,“玉大人败明,你說我怎么就攤上這事√溃” “怎么了妻顶?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我讳嘱,道長幔嗦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任沥潭,我火速辦了婚禮崭添,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘叛氨。我一直安慰自己呼渣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布寞埠。 她就那樣靜靜地躺著屁置,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仁连。 梳的紋絲不亂的頭發(fā)上蓝角,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音饭冬,去河邊找鬼使鹅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛昌抠,可吹牛的內(nèi)容都是我干的患朱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼炊苫,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼裁厅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侨艾,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤执虹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后唠梨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袋励,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年当叭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茬故。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡科展,死狀恐怖均牢,靈堂內(nèi)的尸體忽然破棺而出糠雨,到底是詐尸還是另有隱情才睹,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站琅攘,受9級(jí)特大地震影響垮庐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坞琴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一哨查、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剧辐,春花似錦寒亥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忍啤,卻和暖如春加勤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背同波。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工鳄梅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人未檩。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓戴尸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親冤狡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子校赤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354