Unity3D與iOS的互動
- 1. 關于Unity3D
- 2. From U3D to iOS
-
-
- 2.1 實作原理
- 2.2 實作步驟
-
- 3. From iOS to U3D
-
-
- 3.1 UnitySendMessage
- 3.2 非托管方法
-
- 4. 類型傳遞
-
-
- 4.1 關于Marshal
-
- 5. 傳回值
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
輸出結果:
- 首先建立
和test.m
兩個檔案。分别寫入如下内容:test.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]);
}
- 然後将上面的兩個檔案放到U3D項目的
目錄中。如圖:Assets
- 分别選擇
和test.h
檔案,在Inspector面闆中去掉Any Platforms的勾選,然後保留iOS這一項選中。如圖:test.m
- 建立一個叫Sample的C#腳本檔案,并在這個檔案中寫入c接口的聲明,如:
public class Sample : MonoBehaviour
{
//引入聲明
[DllImport("__Internal")]
static extern void outputAppendString (string str1, string str2);
}
- 在Start方法中調用該方法,如:
void Start ()
{
#if UNITY_IPHONE
outputAppendString("Hello", "World");
#endif
}
注意:對于指定平台的方法,一定要使用預處理指令#if來包括起來。否則在其他平台下面執行會導緻異常。
- 拖動Sample腳本到場景的Main Camera對象中,讓腳本進行挂載。
- 使用快捷鍵Command+Shift+B(或者點選菜單File -> Build Settings)調出Build Settings視窗,将項目導出為iOS項目。如圖:
- 打開導出的iOS項目,先檢查之前建立的
和test.m
是否已經導出到項目中。如圖:test.h
- 編譯運作應用,可以看到控制台中會輸出合并後的字元串資訊,如:
2018-01-22 16:17:15.143166+0800 ProductName[29211:4392515] ###Hello World
3. From iOS to U3D
對于如何從iOS中調用U3D的接口,分為兩種辦法:一種是通過
UnitySendMessage
方法來調用Unity所定義的方法。另一種方法則是通過入口參數,傳入一個U3D的非托管方法,然後調用該方法即可。兩種方式的對比如下:
UnitySendMessage方式 | 非托管方法方式 |
---|---|
接口聲明固定,隻能是 。 | 接口靈活,可以為任意接口。 |
不能帶有傳回值 | 可以帶傳回值 |
必須要挂載到對象後才能調用。 | 可以不用挂載對象,但需要通過接口傳入該調用方法 |
下面将一一講述兩種方式的實作。
3.1 UnitySendMessage
- 基于上面調用iOS接口的例子,在
中增加一個Sample.cs
方法。如:callback
void callback (string resultStr)
{
Debug.LogFormat ("result string = {0}", resultStr);
}
- 由于項目已經挂載
到Main Camera中,這就不用再進行挂載。然後打開Sample.cs
檔案,在test.m
方法中調用outputAppendString
方法,并将組合字元串傳回給U3D。如:callback
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);
}
- 導出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 非托管方法
- 在
中建立一個delegate聲明,并使用Sample.cs
特性來辨別該delegate是非托管方法。代碼如下:UnmanagedFunctionPointer
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ResultHandler(string resultString);
其中的
CallingConvention.Cdel
為調用時轉換為C聲明接口。
- 然後聲明一個靜态方法,并使用
特性來标記為回調方法,目的是讓iOS中調用該方法時可以轉換為對應的托管方法。如:MonoPInvokeCallback
[MonoPInvokeCallback(typeof(ResultHandler))]
static void resultHandler (string resultStr)
{
}
注意:
MonoPInvokeCallback
特性參數是上一步中定義的非托管delegate。方法的聲明一定要與delegate定義一緻,并且必須為static進行修飾(iOS不支援非靜态方法回調),否則會導緻異常。
- 打開
檔案,定義一個新的接口,如: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#傳入的方法進行調用了。
- 回到
檔案,定義Sample.cs
的聲明。outputAppendString2
[DllImport("__Internal")]
static extern void outputAppendString2 (string str1, string str2, IntPtr resultHandler);
注意:回調方法的參數必須是IntPtr類型,表示一個函數指針。
- 在
方法中調用Start
,并将回調方法轉換為IntPtr類型傳給方法。如:outputAppendString2
ResultHandler handler = new ResultHandler(resultHandler);
IntPtr fp = Marshal.GetFunctionPointerForDelegate(handler);
outputAppendString2 ("Hello", "World", fp);
上面代碼使用
Marshal
的
GetFunctionPointerForDelegate
來擷取
resultHandler
的指針。
- 導出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的。具體對應關系如下表所示:
U3D | iOS |
---|---|
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類型。
- 首先在C#中聲明一個類型
Person
class Person
{
public string name;
public int age;
}
- 在C中聲明一個接口
用于列印傳遞過來的Person資訊,如:printPersonInfo
- 在C#中聲明此接口
[DllImport("__Internal")]
static extern void printPersonInfo (IntPtr personData);
- 建立一個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();
- 将byte數組通過
類轉換為Marshal
類型,并傳入給C接口。IntPtr
//轉換成功IntPtr
IntPtr personData = Marshal.AllocHGlobal(bufBytes.Length);
Marshal.Copy(bufBytes, 0, personData, bufBytes.Length);
printPersonInfo(personData);
Marshal.FreeHGlobal(personData);
注意:
Marshal
申請的記憶體不是自動回收的,是以調用後需要通過顯示方法
FreeHGlobal
調用釋放。
- 回到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);
}
- 導出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
類型配合序列化的方式來進行傳回值的傳回:
- 先定義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;
}
- 在C#中聲明該接口
[DllImport("__Internal")]
static extern IntPtr returnString (out int len);
- 調用該接口,并解析傳回參數值
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);
}