环境
- Unity5.6.0f3个人免费版。
- Xcode8.3.2。
参考
- the_nerd.be上的这篇文章,还带视频。
- Unity官方参考文档的iOS部分,这里有很多资料,包括Unity导出Xcode工程的目录结构以及在Unity和iOS交互问题等。
需求
- Unity需求较多,Native需求较少:直接在Unity导出的Xcode工程中开发。
- Unity需求较多,Native需求只有一两个页面:可以直接将写好的OC代码文件放到Unity的
Assets/Plugins/
文件夹里。 - Unity需求较少,Native需求较多:需要将Unity导出的Xcode工程整合入原生的Xcode工程,也是本文接下来的内容。
实战
导出Unity工程
File
->Build & Run
在这里添加场景,然后选择Player Settings
进入设置。主要设置以下三项,其他按需求来。
导出后的位置如下图,我把两个工程放在同一个根目录下,这样对后期比较方便。
配置Native工程
复制文件
这一步最复杂,不过可以参考上面的视频教程,有些地方可能由于Unity和Xcode的版本需要变动一下。
首先拖入Unity工程的Classes
和Libraries
,为了确保以后维护起来方便,请不要勾选copy
,如下图。
这样做的好处是,只保留文件的引用而不复制文件,减少依赖关系。
接下来修改一些文件:
-
把
Classes/main.mm
文件的内容全部复制到你的main.m
文件里,并且把main.m
改名为main.mm
,然后把里面的UnityAppController
改成你的AppDelegate
。
-
新建
PrefixHeader
,把Classes/Prefix.pch文件的内容全部复制到新建的refixHeader
里。
接下来删除一些没用的文件,这里的所有删除都只是删除引用。
要删除的内容如下: -
Classes/main.mm。
-
Classes/Prefix.pch。
-
Classes/Native下的所有.h文件,可以在下方的Filter过滤器里输入
.h
来过滤。 -
Libraries/libil2cpp文件夹
Build Setting
- Build Setting里的
Linking
,Apple LLVM
都按照Unity导出的工程来设置。 - 添加
User-Defined
字段,也和Unity导出的工程一致。(在最上面有个+号)
Prefix Header
如下设置。
如果有自己的头文件需要包含,需要放在如下位置:
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#endif
Header Search Paths
添加到Unity工程的引用。
Library Search Paths
只需要添加一行${SRCROOT}/../Unity2iOS/Libraries
指向Unity工程的Libraries目录。
Build Phase
- 添加2行
Run Script
rm -rf "$TARGET_BUILD_DIR/$PRODUCT_NAME.app/DATA"
cp -Rf "$PROJECT_DIR/../Unity2iOS/Data" "$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Data"
注意修改其中的目录到自己的Unity工程。
- Link Binary With Libraries按Unity工程一个个添加,其中
Libiconv.2.dylib
,在Xcode8中已经找不到,从/usr/lib
中找到然后拖进去,注意optional的设置。 - 如果你是参考视频教程的话,视频中还要添加
-fn-objc-arc
,这一步千万不要添加,不然Build成功以后运行也会失败。可能是由于Unity版本导致的。
开始Build
到现在为止如果配置完全正确的话。是这个Build成功的,注意如果Unity导出的时候选择DeviceSDK的话,只能在真机上Build,选择模拟器就只能在模拟器上Build。
可能遇到的错误
有很多啦,比较Dirty的一个就是libiPhone-lib.a not found
,在Build Phase里的Link Binary With Libraries
把libiPhone-lib.a
移除再添加,然后clean一下就好了,视频里有说。
如果还有其他问题也都可以一个个解决,千万不要放弃。
运行Unity界面
Unity界面存在于UnityAppController.window
里,因此只需要控制AppDelegate.window
和UnityAppController.window
的显示顺序就行,UIWindow本质就是UIView,因此可以直接使用hide
等方法,只是要加上[window makeKeyAndVisable]
方法。
另外要在App的生命周期方法里调用UnityAppController对应的周期方法。
- (void)applicationWillResignActive:(UIApplication *)application {
[self.unityController applicationWillResignActive:application];
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
[self.unityController applicationDidEnterBackground:application];
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
[self.unityController applicationWillEnterForeground:application];
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
[self.unityController applicationDidBecomeActive:application];
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application {
[self.unityController applicationWillTerminate:application];
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
别忘了在DidFinishLaunch
里给self.unityController
赋值。
运行会发现有运行期错误,UnityAppController
的GetAppController()
方法得到了nil,修改如下。
inline UnityAppController* GetAppController()
{
return [(AppDelegate*)[UIApplication sharedApplication].delegate unityController];
}
显示Unity和隐藏Unity界面最简单的方法。
//显示
ApplicationDelegate.unityWindow.hidden = NO;
[ApplicationDelegate.unityWindow makeKeyAndVisible];
//隐藏
[ApplicationDelegate.window makeKeyAndVisible];
ApplicationDelegate.unityWindow.hidden = YES;
Unity和Native交互
Unity调用iOS方法
- C#中
[DllImport ("__Internal")]
private static extern void sim_showSelectTitleDialog();
- iOS中,文件名:
UnityFunctionManager.mm
,注意是.mm,该文件需要放到unity的Plugins目录下,这样打包时会被自动打包到Xcode工程里。
extern "C" void sim_showSelectTitleDialog(char* title,char* msg){
SIMUnityDialogManager* dialogManager = [SIMUnityDialogManager shareManager];
[dialogManager vrb_showSelectTitleDialogWithTitle:title Message:msg];
}
这里建议在Native工程里实现一个单例SIMUnityDialogManager
来实现该文件中的方法,这样就实现了具体的代码和接口分离,UnityFunctionManager.mm
这个文件可以由Unity的同学负责,iOS同学只需要负责SIMUnityDialogManager
里具体的方法实现。
iOS调Unity方法
- iOS里,任意文件都可以
UnitySendMessage("GameObject", "Function",[sendMsg UTF8String]);
第一个参数是GameObject,第二个参数是方法名,第三个参数是传输的数据。
- Unity里
void Function(string message)
{
//挂载在相应GameObject上的脚本
}
代码更新方案
由于Unity代码里需要更新维护,这样每次重新合并工程就很繁琐,并且不易做CI。
但是如果是通过以上教程实现工程合并,就会发现Unity工程和Native工程实际上并没有文件的关联,只存在文件的引用关系。每次Unity更新直接重新打包覆盖原来的工程就可以了。但是存在以下问题。
- C#文件的增删
文件增删会导致导出的Classes
文件夹中的文件的增删,因此在做CI的时候,可以考虑每次Unity工程更新都重新添加引用,但是要记得删除Classes/Native
里的头文件。 - UnityAppController被覆盖
每一次导出会重新生成UnityAppController文件,但是这个文件我们改了其中的GetAppController()
方法,虽然只改了一行,但是每次这个文件都被覆盖。这里我的做法是,把该文件复制到Native工程目录,然后删除Classes里面该文件,这样每次重新打包Unity工程的时候只要多执行一行rm /Classes/UnityAppController
就可以了。