Skip to content

Latest commit

 

History

History
265 lines (203 loc) · 12.4 KB

2017-03-23-Unity平台插件开发(iOS篇).md

File metadata and controls

265 lines (203 loc) · 12.4 KB
layout title categories tags
post
Unity平台插件开发实践(iOS)篇
Tech
Unity
iOS
C#

为什么我们需要开发Unity平台插件?

我们在游戏开发中使用Unity的一个重要原因是它可以让我们免去很大一部分与平台相关的开发工作,让我们将精力集中在游戏逻辑的编码上。开发者可以在不熟悉各个平台的情况下开发出在各个平台上运行的游戏。但是不可避免的,既然各个平台之间有差别,就肯定会遇到不得不写平台相关的代码的情况。在Unity游戏开发的过程中,你可能会遇到:

  • 想使用某个平台的某个酷炫的特性,但是Unity本身并没有提供对应的API。
  • 由于目标平台系统的更新快于Unity的版本更新,一些Unity提供的API可能已经不再适用。
  • ...

在遇到这些情况时,你都可以通过编写Unity插件来解决。

Unity平台插件是个什么?

本质上来说,Unity的插件就是每个平台的原生代码或者第三方库,通过自己编写原生代码或者添加库来实现Unity未提供的功能。

Unity插件开发需要掌握哪些技能?

在手游中,我们常见的两个平台就是iOS和Android,对应的编程语言也就是Objective-C/SwiftJava。同时,还需要对平台应用的开发有一定程度的了解。

Unity插件开发实践

这篇文章主要讲解iOS平台的插件开发,所以会牵涉到部分iOS开发的知识。好了,现在让我们开始来编写我们的第一个Unity插件吧。

准备工作

Unity将所有的平台插件都放在[Your Project]/Assets/Plugins目录下,如果你的工程下没有Plugins目录,你得手动新建一个。同时,每个平台的插件都有自己的目录,比如iOSPlugins/iOSAndroidPlugins/Android,放错位置的插件是不能被正确打包运行的,而且,不同平台插件的目录下有着不同的组织结构要求,具体可以参考Unity的文档。注意Plugins/iOS下是不能够有任何子目录的!

编写原生代码

让我们从最一个最简单需求开始:某天,策划爸爸突然跑到你的位置上说,我需要一个警示的弹框功能!样式必须得和系统默认的一样! 那么你首先必须知道,在iOS如何弹出一个警示框,还好这并不是很难。 先来建一个文件AlerViewManager.mm.mm后缀表示这是一个C++Objective-C混编的文件。然后输入下面的代码:

#import <UIKit/UIKit.h>
#ifdef __cplusplus
extern "C"{
#endif
    void showDialog()
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""
                                                        message:@"Message"
                                                       delegate:nil
                                              cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
        [alert show];
    }
#ifdef __cplusplus
}
#endif

extern "C"告诉编译器在翻译这个函数名时按C的规则去翻译相应的函数名。

这段代码的功能就是弹出一个警示框,然后显示“Message”文字,同时带有一个“OK”确认按钮。

如果你在一个Xcode工程中编辑此段代码,可能会收到一个警告,因为UIAlerView已经在iOS 9.0中被弃用了,不过这并不影响我们使用。

编写托管代码

现在你已经有一个原生的函数为你提供想要的功能,让我们来考虑如何在你的Unity工程中调用这个函数,也就是如何在C#中调用这个函数。 在你的工程中新建一个名为Brigde.cs的C#文件,输入下面的代码。

using UnityEngine;
using System.Runtime.InteropServices;

public class Bridge {
	[DllImport("__Internal")]
	private static extern void showDialog();

	public static void ShowDialog() {
#if UNITY_IOS
		showDialog ();
#endif
	}
}

其中关键的代码是

[DllImport("__Internal")]
private static extern void showDialog();

[DllImport("__Internal")]是一个属性标签,它告诉Mono在自己的程序(编译完成的二进制文件)中寻找showDialog()函数(因为在iOS上,应用的所有代码最后都会被编译进一个二进制文件中)。 在链接时,托管代码中的showDialog函数就回指向原生代码中的showDialog函数,从而实现在C#中调用你所编写的原生代码。

在编写平台插件代码的托管代码时,一个好的做法是单独使用一个类来封装它,使用Unity提供的宏定义Unity_IOSUnity_ANDROID等来对不同的平台进行不同的处理,而使调用者无需关心所处的平台。

使用平台插件

新建一个场景,在场景的Camera下挂上一个Startup.cs的脚本,脚本内容如下:

using UnityEngine;
using System.Collections;

public class Startup : MonoBehaviour {
	void Start () {
		Bridge.ShowDialog ();
	}
}

然后Build出一个Xcode工程,运行我们的工程。 效果图 可以看到我们在Unity中成功调用了原生代码。

Objective-C -> C# ?

上面我们已经实现了在C#中调用原生Objective-C代码的功能。随之而来的问题是,我们怎么在原生代码中调用托管代码呢?Unity当然为我们提供了API来实现这个功能。 让我们来考虑这样一个场景:弹出警示框之后用户有两种选择,“确认”和“取消”,点击“确认”的话我们需要干一些额外的事情,而点击“取消”则是简单的隐藏掉警示框。 为了实现这个功能,修改之前的AlerViewManager.mm文件如下:

#import <UIKit/UIKit.h>

extern void UnitySendMessage(const char* obj, const char* method, const char* msg);

@interface AlertViewManager : NSObject <UIAlertViewDelegate>
+ (instancetype)sharedInstance;
- (void)showDialog;
@end

@implementation AlertViewManager

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static AlertViewManager *_instance = nil;
    dispatch_once(&onceToken, ^{
        _instance = [[AlertViewManager alloc] init];
    });
    
    return _instance;
}

- (void)showDialog {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""
                                                    message:@"Message"
                                                   delegate:self
                                          cancelButtonTitle:@"取消"
                                          otherButtonTitles:@"确认", nil];
    [alert show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    NSLog(@"button %@ clicked", @(buttonIndex));
    NSString *strIdx = [NSString stringWithFormat:@"%@", @(buttonIndex)];
    UnitySendMessage("AlertViewCallbackGameObject", "AlertViewCallback", [strIdx UTF8String]);
}

@end

#ifdef __cplusplus
extern "C"{
#endif
    void showDialog()
    {
        [[AlertViewManager sharedInstance] showDialog];
    }
#ifdef __cplusplus
}
#endif

这里面一个关键的函数就是UnitySendMessage,这个函数的原型在UnityInterface.h中有定义,在这个头文件中,还可以看见其他一些可用方法。该方法的三个参数分别表示场景中接收回调的GameObject的名称用于处理回调的挂在这个GameObject上的脚本中的方法该方法的参数

知道了这些,我们在场景中添加一个名字为AlertViewCallbackGameObject的GameObject,同时在这个GameObject上挂载一个脚本,脚本中需要有这样一个方法:

public void AlertViewCallback(string param) {
	// do whatever you want
	Debug.Log ("Users Choose: " + param);
}

这里我们仅仅是打印了用户的选择。 构建工程->运行Xcode工程,选择一个按钮按下,我们就可以在控制台中看见类似的Log

Users Choose: 1
...

至此,已经介绍完了iOS上Unity插件开发的基本流程。

一点代码背后的事

在Unity的官方文档上,我们可以看见这样一个Tip:

Managed-to-unmanaged calls are quite processor intensive on iOS. Try to avoid calling multiple native methods per frame.

它告诉我们,在iOS上原生代码和托管代码的交互是一项十分耗费CPU的工作,应该避免每帧频繁的调用原生代码。那么为什么这个操作会给CPU带来高额的开销呢?让我们来看看Unity究竟做了哪些事。

首先我们必须知道,在iOS平台上,存在着一个叫做IL2CPP的东西,简单来说,它负责将我们的C#代码转成C++代码(其实准备来说是将IL代码转成C++代码),因为在iOS系统上是不能直接运行C#代码的。

打开之前Unity生成的Xcode工程,搜索showDialog关键字,可以看见这么一段代码:

extern "C" {void DEFAULT_CALL showDialog();}
extern "C" void Bridge_showDialog_m1 (Object_t * __this /* static, unused */, const MethodInfo* method)
{
	typedef void (DEFAULT_CALL *PInvokeFunc) ();
	static PInvokeFunc _il2cpp_pinvoke_func;
	if (!_il2cpp_pinvoke_func)
	{
		_il2cpp_pinvoke_func = (PInvokeFunc)showDialog;

		if (_il2cpp_pinvoke_func == NULL)
		{
			il2cpp_codegen_raise_exception((Il2CppCodeGenException*)il2cpp_codegen_get_not_supported_exception("Unable to find method for p/invoke: 'showDialog'"));
		}
	}

	// Native function invocation
	_il2cpp_pinvoke_func();

}

可以发现,这个Bridge_showDialog_m1方法,就是我们在C#中对showDialog调用的实现,但是一切看起来好像很正常,并没有做其他多余的事。

现在让我们做一些修改,为showDialog添加两个参数,一个为int,一个为string,这需要修改你的原生代码和托管代码,然后重新生成一个Xcode工程。在新的代码中Bridge_showDialog_m1长这个样:

extern "C" {void DEFAULT_CALL showDialog(int32_t, char*);}
extern "C" void Bridge_showDialog_m1 (Object_t * __this /* static, unused */, int32_t ___type, String_t* ___message, const MethodInfo* method)
{
	typedef void (DEFAULT_CALL *PInvokeFunc) (int32_t, char*);
	static PInvokeFunc _il2cpp_pinvoke_func;
	if (!_il2cpp_pinvoke_func)
	{
		_il2cpp_pinvoke_func = (PInvokeFunc)showDialog;

		if (_il2cpp_pinvoke_func == NULL)
		{
			il2cpp_codegen_raise_exception((Il2CppCodeGenException*)il2cpp_codegen_get_not_supported_exception("Unable to find method for p/invoke: 'showDialog'"));
		}
	}

	// Marshaling of parameter '___type' to native representation

	// Marshaling of parameter '___message' to native representation
	char* ____message_marshaled = { 0 };
	____message_marshaled = il2cpp_codegen_marshal_string(___message);

	// Native function invocation
	_il2cpp_pinvoke_func(___type, ____message_marshaled);

	// Marshaling cleanup of parameter '___type' native representation

	// Marshaling cleanup of parameter '___message' native representation
	il2cpp_codegen_marshal_free(____message_marshaled);
	____message_marshaled = NULL;

}

可以看见它对我们传入的参数做了一些事情。int类型的参数直接被传给了_il2cpp_pinvoke_func,而string类型的参数则被转成了char *类型,然后被传给了_il2cpp_pinvoke_func,之后被释放掉。有对应il2cpp_codegen_marshal_stringil2cpp_codegen_marshal_free一组函数来操作。这是因为在C#中和在Objective-C,字符串类型的表现形式是不同的,所以IL2CPP必须在中间做一个转换工作,很显然,这需要对内存进行分配,转换,使用完后再进行释放,这部分就是额外的开销了。而int类型在两边的表现形式是相同的,所以不需要进行转换。

在托管代码层面,像intstring两种类型分别被归为blittablenon-blittableblittable在托管代码和非托管代码中有着同样的表现形式,而non-blittable则不同,需要进行转换。像intfloatbyte这些类型都是blittable类型,而像boolstring等类型是non-blittable类型。

blittable类型数据能够直接传给原生代码,而non-blittable则必须经过转换才能传给原生代码,如果参数是一个数组,那么每个数组中的每个变量都需要进行转换,这个开销也是十分巨大的。