1. 程式人生 > 實用技巧 >Unity3D與iOS的互動

Unity3D與iOS的互動

1. 關於Unity3D

Unity3D(以下簡稱U3D)是由Unity Technologies開發的一個讓玩家輕鬆建立諸如三維視訊遊戲、建築視覺化、實時三維動畫等型別互動內容的多平臺的綜合型遊戲開發工具,是一個全面整合的專業遊戲引擎。

作為一款跨平臺開發工具,難免會與原生平臺進行一些互動操作來完成一些特定的平臺功能。例如:你需要直接操作iOS的IAP來實現遊戲中的內付費功能;甚至一些第三方SDK沒有提供U3D版本的情況下,你會直接在原生系統平臺呼叫其提供介面等等。

下面將為大家介紹,在U3D下如何實現與iOS系統的互動工作,來滿足一些需要藉助原生系統的功能需求。

2. From U3D to iOS

2.1 實現原理

由於U3D無法直接呼叫Objc或者Swift語言宣告的介面,幸好U3D的主要語言是C#,因此可以利用C#的特性來訪問C語言所定義的介面,然後再通過C介面再呼叫ObjC的程式碼(對於Swift程式碼則還需要使用OC橋接)。例如,有如下C語言方法:

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

在C#中則可以像下面程式碼一樣進行引入和呼叫:

using System.Runtime.InteropServices;

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

其中DllImport為一個Attribute,目的是通過非託管方式將庫中的方法匯出到C#中進行使用。而傳入"__Internal"則是表示這個是一個靜態庫或者是一個內部方法。通過上面的宣告,這個方法就可以在C#裡面進行呼叫了。如:

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

2.2 實現步驟

下面通過一個拼接字串的例子來說明怎麼樣從U3D中傳入兩個字串到iOS中,然後由iOS拼接後通過NSLog輸出結果:

  1. 首先新建test.mtest.h兩個檔案。分別寫入如下內容:
/// 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. 然後將上面的兩個檔案放到U3D專案的Assets目錄中。如圖:
放入U3D專案
  1. 分別選擇test.htest.m檔案,在Inspector面板中去掉Any Platforms的勾選,然後保留iOS這一項選中。如圖:
設定平臺外掛
  1. 新建一個叫Sample的C#指令碼檔案,並在這個檔案中寫入c介面的宣告,如:
public class Sample : MonoBehaviour 
{
    //引入宣告
    [DllImport("__Internal")]
    static extern void outputAppendString (string str1, string str2);
}
  1. 在Start方法中呼叫該方法,如:
void Start () 
{
    #if UNITY_IPHONE    
    outputAppendString("Hello", "World");
    #endif
}

注意:對於指定平臺的方法,一定要使用預處理指令#if來包括起來。否則在其他平臺下面執行會導致異常。

  1. 拖動Sample指令碼到場景的Main Camera物件中,讓指令碼進行掛載。
掛載指令碼
  1. 使用快捷鍵Command+Shift+B(或者點選選單File -> Build Settings)調出Build Settings視窗,將專案匯出為iOS專案。如圖:
匯出iOS專案
  1. 開啟匯出的iOS專案,先檢查之前建立的test.mtest.h是否已經匯出到專案中。如圖:
檢查檔案
  1. 編譯執行應用,可以看到控制檯中會輸出合併後的字串資訊,如:
2018-01-22 16:17:15.143166+0800 ProductName[29211:4392515] ###Hello World

3. From iOS to U3D

對於如何從iOS中呼叫U3D的介面,分為兩種辦法:一種是通過UnitySendMessage方法來呼叫Unity所定義的方法。另一種方法則是通過入口引數,傳入一個U3D的非託管方法,然後呼叫該方法即可。兩種方式的對比如下:

UnitySendMessage方式非託管方法方式
介面宣告固定,只能是void method(string message) 介面靈活,可以為任意介面。
不能帶有返回值 可以帶返回值
必須要掛載到物件後才能呼叫。 可以不用掛載物件,但需要通過介面傳入該呼叫方法

下面將一一講述兩種方式的實現。

3.1 UnitySendMessage

  1. 基於上面呼叫iOS介面的例子,在Sample.cs中增加一個callback方法。如:
void callback (string resultStr)
{
    Debug.LogFormat ("result string = {0}", resultStr);
}
  1. 由於專案已經掛載Sample.cs到Main Camera中,這就不用再進行掛載。然後開啟test.m檔案,在outputAppendString方法中呼叫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. 匯出iOS專案,編譯執行看執行結果。
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中建立一個delegate宣告,並使用UnmanagedFunctionPointer特性來標識該delegate是非託管方法。程式碼如下:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ResultHandler(string resultString);

其中的CallingConvention.Cdel為呼叫時轉換為C宣告介面。

  1. 然後宣告一個靜態方法,並使用MonoPInvokeCallback特性來標記為回撥方法,目的是讓iOS中呼叫該方法時可以轉換為對應的託管方法。如:
[MonoPInvokeCallback(typeof(ResultHandler))]
static void resultHandler (string resultStr)
{
    
}

注意:MonoPInvokeCallback特性引數是上一步中定義的非託管delegate。方法的宣告一定要與delegate定義一致,並且必須為static進行修飾(iOS不支援非靜態方法回撥),否則會導致異常。

  1. 開啟test.m檔案,定義一個新的介面,如:
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中需要定義一個與C#的delgate相同的函式指標ResultHandler。然後新增的outputAppendString2方法中多了一個回撥引數resultHandler。這樣就能夠把C#傳入的方法進行呼叫了。

  1. 回到Sample.cs檔案,定義outputAppendString2的宣告。
[DllImport("__Internal")]
static extern void outputAppendString2 (string str1, string str2, IntPtr resultHandler);

注意:回撥方法的引數必須是IntPtr型別,表示一個函式指標。

  1. Start方法中呼叫outputAppendString2,並將回撥方法轉換為IntPtr型別傳給方法。如:
ResultHandler handler = new ResultHandler(resultHandler);
IntPtr fp = Marshal.GetFunctionPointerForDelegate(handler);
outputAppendString2 ("Hello", "World", fp);

上面程式碼使用MarshalGetFunctionPointerForDelegate來獲取resultHandler的指標。

  1. 匯出iOS專案,編譯執行。
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. 型別傳遞

對於基礎型別資料(如:int、double、string等)是可以直接從U3D中傳遞給iOS的。具體對應關係如下表所示:

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

注意

  • 引用型資料不能直接從U3D傳給iOS。如果需要傳遞這樣的型別,可以考慮將物件序列化成byte陣列,然後在iOS中進行反序列化將其還原回來。
  • 對於string型別,會自動轉換為c語言中的char *。但是由於C#中的string是託管型別,因此char *是無法直接轉換為string的,所以不要直接在返回值中返回char *型別。下一節會針對返回值進行詳細的說明。
  • struct型別資料中不能包含引用型資料,否則在呼叫介面時會報告類似下面的提示:
MarshalDirectiveException: Cannot marshal field 't' of type 'TestStructType': Reference type field marshaling is not supported.

4.1 關於Marshal

Marshal型別主要是用於將C#中託管和非託管型別進行一個轉換的橋樑。其提供了一系列的方法,這些方法包括用於分配非託管記憶體、複製非託管記憶體塊、將託管型別轉換為非託管型別,此外還提供了在與非託管程式碼互動時使用的其他雜項方法等。

本質上U3D與iOS的互動過程就是C#與C的互動過程,所以Marshal就成了互動的關鍵,因為C#與C的互動正正涉及到託管與非託管程式碼的轉換。下面將舉例說明,如何將一個C#的引用型別轉換到對應的OC型別。

  1. 首先在C#中宣告一個型別Person
class Person
{
    public string name;
    public int age;
}
  1. 在C中宣告一個介面printPersonInfo用於列印傳遞過來的Person資訊,如:
void printPersonInfo(void *personData);
  1. 在C#中宣告此介面
[DllImport("__Internal")]
static extern void printPersonInfo (IntPtr personData);
  1. 建立一個Person的例項,然後將其序列化成byte陣列,這裡使用到物件序列化的一些知識。
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陣列通過Marshal類轉換為IntPtr型別,並傳入給C介面。
//轉換成功IntPtr
IntPtr personData = Marshal.AllocHGlobal(bufBytes.Length);
Marshal.Copy(bufBytes, 0, personData, bufBytes.Length);

printPersonInfo(personData);

Marshal.FreeHGlobal(personData);

注意:Marshal申請的記憶體不是自動回收的,因此呼叫後需要通過顯示方法FreeHGlobal呼叫釋放。

  1. 回到C程式碼中,並實現其內部處理邏輯,如:
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. 匯出iOS專案,編譯執行可以看到日誌裡面的輸出結果
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. 返回值

除了基礎型別中的數值型別可以直接從iOS中返回給U3D外,其他的型別是不能直接進行返回的,其中理由也很簡單,因為非託管型別不能直接轉換成託管型別。如果你想直接返回一個字串給U3D,那麼在執行時就會產生異常,因為轉換成託管型別後他的記憶體由系統管理,一旦物件銷燬他就會被釋放記憶體,但它並不知道非託管模式下它是否被釋放。

為了解決返回值的問題,其實可以藉助上面提到的Marshal型別配合序列化的方式來進行返回值的返回:

  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. 呼叫該介面,並解析返回引數值
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);
}


作者:傑嗒嗒的阿杰
連結:https://www.jianshu.com/p/1ab65bee6692
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。