Unity3D與iOS的交互

1. 關(guān)于Unity3D

Unity3D(以下簡稱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)行返回的,其中理由也很簡單,因?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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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