天天看點

Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值

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

輸出結果:

  1. 首先建立

    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]);
}
           
  1. 然後将上面的兩個檔案放到U3D項目的

    Assets

    目錄中。如圖:
Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值
  1. 分别選擇

    test.h

    test.m

    檔案,在Inspector面闆中去掉Any Platforms的勾選,然後保留iOS這一項選中。如圖:
Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值
  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對象中,讓腳本進行挂載。
Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值
  1. 使用快捷鍵Command+Shift+B(或者點選菜單File -> Build Settings)調出Build Settings視窗,将項目導出為iOS項目。如圖:
Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值
  1. 打開導出的iOS項目,先檢查之前建立的

    test.m

    test.h

    是否已經導出到項目中。如圖:
Unity3D與iOS的互動1. 關于Unity3D2. From U3D to iOS3. From iOS to U3D4. 類型傳遞5. 傳回值
  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);
           

上面代碼使用

Marshal

GetFunctionPointerForDelegate

來擷取

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的。具體對應關系如下表所示:

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類型。

  1. 首先在C#中聲明一個類型

    Person

class Person
{
	public string name;
	public int age;
}
           
  1. 在C中聲明一個接口

    printPersonInfo

    用于列印傳遞過來的Person資訊,如:
  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);
}