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
输出结果:
- 首先新建
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
和test.m
文件,在Inspector面板中去掉Any Platforms的勾选,然后保留iOS这一项选中。如图:
- 新建一个叫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方式 | 非托管方法方式 |
---|---|
接口声明固定,只能是void method(string message) 。 |
接口灵活,可以为任意接口。 |
不能带有返回值 | 可以带返回值 |
必须要挂载到对象后才能调用。 | 可以不用挂载对象,但需要通过接口传入该调用方法 |
下面将一一讲述两种方式的实现。
3.1 UnitySendMessage
- 基于上面调用iOS接口的例子,在
Sample.cs
中增加一个callback
方法。如:
void callback (string resultStr)
{
Debug.LogFormat ("result string = {0}", resultStr);
}
- 由于项目已经挂载
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);
}
- 导出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 非托管方法
- 在
Sample.cs
中建立一个delegate声明,并使用UnmanagedFunctionPointer
特性来标识该delegate是非托管方法。代码如下:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ResultHandler(string resultString);
其中的CallingConvention.Cdel
为调用时转换为C声明接口。
- 然后声明一个静态方法,并使用
MonoPInvokeCallback
特性来标记为回调方法,目的是让iOS中调用该方法时可以转换为对应的托管方法。如:
[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
方法中调用outputAppendString2
,并将回调方法转换为IntPtr类型传给方法。如:
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中声明一个接口
printPersonInfo
用于打印传递过来的Person信息,如:
void printPersonInfo(void *personData);
- 在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
类转换为IntPtr
类型,并传入给C接口。
//转换成功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);
}
作者:杰嗒嗒的阿杰
链接:https://www.jianshu.com/p/1ab65bee6692
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。