[转]Unity与iOS交互

需要注意的是,Unity一旦初始化,是不能关闭的,否则App直接就会被关闭。所以,一旦调起Unity,内存就不会降下来了。第一次启动会比较慢,之后就很快了。另外,集成Unity之后,就只能真机运行了,所以,要准备好证书,以免不必要的麻烦。

很多文章是用了两个UIWindow来回切换,而我并不推荐使用这种方式。另外,屏幕旋转问题,后面我会提到。

1、之前我们已经在pch中import了UnityAppController,所以其他地方不用再import了。所有的接口建议写在AppDelegate中。

首先,将AppDelegate.m改名为AppDelegate.mm
然后,在AppDelegate.h中,如下:

#import 

@interface AppDelegate : UIResponder 

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UnityAppController *unityController;

- (void)showUnityWindow;

- (void)hideUnityWindow;

- (void)shouldAttachRenderDelegate;

@end

接下来,修改AppDelegate.mm,如下:
如有报错,先暂时忽略。之后会填坑。

#import "AppDelegate.h"

//这里就说明,我们必须改成.mm的了~
extern "C" void VuforiaRenderEvent(int marker);
extern "C" void VuforiaSetGraphicsDevice(void* device, int deviceType, int eventType);


@interface AppDelegate ()
{
    //这里的两个BOOL,是用来区分,是否第一次加载Unity,以及Unity视图是否出现
    BOOL _notFirstShow;
    BOOL _isShowing;
}
@property (nonatomic, weak) UIImageView *ARLaunchView;

@end

@implementation AppDelegate

//这个方法应该是使用了高通就必须这样来渲染。如果不用高通,这个方法可以空实现(不确定,遇到问题了再反馈吧,后续会更新)
- (void)shouldAttachRenderDelegate {
    //如果报错,删掉上面的extern "C" void VuforiaSetGraphicsDevice(void* device, int deviceType, int eventType);
    UnityRegisterRenderingPlugin(&VuforiaSetGraphicsDevice, &VuforiaRenderEvent);
    //下面这一行先不写,如果上面的报错,就删掉上面那一行,调用下面这一行的
    UnityRegisterRenderingPlugin(NULL, &VuforiaRenderEvent);
    //如果两个都报错,那就都删掉,这个方法就做空实现。上面那两个extern "C"都可以删掉了
}

- (void)showUnityWindow {
    //这里的图片就是自定义的Unity启动图,因为第一次启动会很慢,有个启动图会好一点
    UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.window.height, self.window.width)];
    imgView.image = [UIImage imageNamed:@"AR_launch"];
    [self.window.rootViewController.view addSubview:imgView];
    //因为Unity是强制横屏的,所以这里要把imageView旋转
    imgView.transform = CGAffineTransformMakeRotation(M_PI_2);
    imgView.center = CGPointMake(self.window.center.x, self.window.center.y);
    self.ARLaunchView = imgView;
    //这里必须延迟执行,否则图片不会出现
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (!_notFirstShow) {
            //第一次启动
            [self.unityController startUnityFirstTime];
            _notFirstShow = YES;
        } else {
            //已经初始化
            [self.unityController startUnityOtherTime];
        }
        _isShowing = YES;
    });
}

- (void)hideUnityWindow {
    [self.unityController doExitSelector];
    [self.ARLaunchView removeFromSuperview];
    //看到这里就明白了,我用的是模态的方式展示的~哈哈哈
    [self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
    _isShowing = NO;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //这里我是用代码加载的Window,没有用StoryBoard    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];

    self.unityController = [[UnityAppController alloc] init];
    [self.unityController application:application didFinishLaunchingWithOptions:launchOptions];

    //这里+1是因为我工程中的TextField长按的时候那个放大镜的问题。
    self.window.windowLevel = UIWindowLevelNormal + 1;
    //这里设置你们自己的根控制器
    self.window.rootViewController = [HRAccountTool chooseRootViewController];
    [self.window makeKeyAndVisible];

    return YES;
}

#pragma mark - UIApplicationDelegate
- (void)applicationWillResignActive:(UIApplication *)application {
    [self.unityController applicationWillResignActive:application];
}


- (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.
    if (_notFirstShow && !_isShowing) {
        //如果unity处于暂停状态,从后台唤醒时也要保持暂停状态
        [self.unityController doExitSelector];
    }
}


- (void)applicationWillTerminate:(UIApplication *)application {
    [self.unityController applicationWillTerminate:application];
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end

哗…AppDelegate终于改完了!然而我们只完成了三分之一的工作。继续吧…耐心点。

2、现在来修改UnityAppController

在Class目录下,修改UnityAppController.h,在- (void)startUnity:(UIApplication*)application; 方法下面,声明3个方法,是我们自定义的。
看到这里大概你们就明白了。我们要懒加载Unity组件,App启动的时候并不去加载,而是等到需要跳转的时候才加载。所以要把第一次跳转和其他分开。

//自定义开启关闭Unity
- (void)startUnityFirstTime;
- (void)startUnityOtherTime;
- (void)doExitSelector;

找到如下方法:

inline UnityAppController*  GetAppController()
{
    return (UnityAppController*)[UIApplication sharedApplication].delegate;
}

替换为:

inline UnityAppController*  GetAppController()
{
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    return delegate.unityController;
}

所以我们在AppDelegate.h中声明了这个属性~

重点来了!接下来修改UnityAppController.mm
在它引用头文件的最后一行,引入自定义控制器,我们暂且叫“ARViewController”,什么?没有?你不会创建啊!

#include "PluginBase/AppDelegateListener.h"
//我是放在这一行下面的
#import "ARViewController.h"

接下来,找到

- (void)shouldAttachRenderDelegate  {}

我们要实现它

- (void)shouldAttachRenderDelegate  {
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    [delegate shouldAttachRenderDelegate];
}

所以我们在AppDelegate.h中声明了这个方法,并在.mm中实现了它。
找到这个方法,默认实现是这样的:

- (void)applicationDidBecomeActive:(UIApplication*)application
{
    ::printf("-> applicationDidBecomeActive()\n");

    [self removeSnapshotView];

    if (_unityAppReady)
    {
        if (UnityIsPaused() && _wasPausedExternal == false)
        {
            UnityWillResume();
            UnityPause(0);
        }
        UnitySetPlayerFocus(1);
    }
    else if (!_startUnityScheduled)
    {
        _startUnityScheduled = true;
        [self performSelector: @selector(startUnity:) withObject: application afterDelay: 0];
    }

    _didResignActive = false;
}

我们要修改这个方法!

//首先在这个方法上面声明一个bool变量
bool homePageEnable = true;
//修改这个方法
- (void)applicationDidBecomeActive:(UIApplication*)application
{
    ::printf("-> applicationDidBecomeActive()\n");

    if(_snapshotView)
    {
        [_snapshotView removeFromSuperview];
        _snapshotView = nil;
    }
    if (homePageEnable) {
        homePageEnable = false;
        [self performSelector:@selector(startHomePage:) withObject:application afterDelay:0];
    }
    if(_unityAppReady)
    {
        if(UnityIsPaused())
        {
            UnityPause(0);
            UnityWillResume();
        }
        UnitySetPlayerFocus(1);
    }
    else if(!_startUnityScheduled)
    {
        _startUnityScheduled = true;
    }

    _didResignActive = false;
}

- (void)startHomePage:(UIApplication *)application {
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    [delegate.window makeKeyAndVisible];
}

- (void)startUnityFirstTime {
    [self startUnity:[UIApplication sharedApplication]];
     [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:[[ARViewController alloc] init] animated:YES completion:nil];
}

- (void)startUnityOtherTime {
     [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:[[ARViewController alloc] init] animated:YES completion:nil];
    if (_didResignActive) {
        UnityPause(false);
    }
    _didResignActive = false;
}

- (void)doExitSelector {
    UnityPause(true);
    _didResignActive = true;
    Profiler_UninitProfiler();
}

终于改完啦!已经完成了三分之二。

3、这里先说一个问题,如果你的工程只支持竖屏,需要在你的window的根控制器,实现以下三个方法:
注:如果只支持竖屏,才改这个。其他情况请跳过第3步。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

- (BOOL)shouldAutorotate {
    //如果设置了只支持竖屏,一定要return NO
    return NO;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}

之后,找到UnityViewControllerBase.mm文件,找到这个方法,改为return NO

- (BOOL)shouldAutorotate
{
    return NO;
}

4、修改ARViewController.m
实现

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self.view addSubview:UnityGetMainWindow().rootViewController.view];
    [self addChildViewController:UnityGetMainWindow().rootViewController];
}

如果设置了只支持竖屏,才实现以下三个方法,否则请跳过下面这一步。

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationLandscapeRight;
}

- (BOOL)shouldAutorotate {
    return NO;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscapeRight;
}

5、调起Unity、关闭Unity、相互传值(交互)
调起Unity:
调起是很方便的,因为是从原生跳转过去嘛,比如点击某个按钮,那么只需在按钮的点击方法中调用即可:

AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[delegate showUnityWindow];

这样就会模态出来Unity的界面,而且是用根控制器模态的。当然你也可以用其他控制器。

关闭Unity:
接下来是关闭Unity,对于返回原生,我建议用Unity调Native,其实就是实现一个C语言方法,当然这个要在Unity导出工程之前,就写好。建议在Libraries/Plugins/iOS目录中,添加专门用于Native与Unity交互的.h和.mm,举个例子,在.mm中:

#import "NativeUnity.h"

#ifndef IS_iPhoneX_Device
#define IS_iPhone5_Device() ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO)
#define IS_iPhone6_Device() ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO)
#define IS_iPhone6P_Device() ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO)
#define IS_iPhoneX_Device() ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)
#endif

#if defined(__cplusplus)
extern "C"{
#endif
    void UnityCallIOS(){

    }

    void PauseUnity(){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"UnityWantToExit" object:nil];
    }

    // 点击模型播放音频视频
    void MsgModelClick (int btnType, const char* btnId, const char* url, float posX, float posY)
    {
    //code...
    }

    //更新地面状态
    void MsgUnityHideGroudState(bool IsGroundHide){
    //code...
    }

    /**
     是否时iphoneX

     @return 1表示是
     */
    int MsgGetIsIphoneX(){
        if (IS_iPhoneX_Device()) {
            return 1;
        }
        return 0;
    }

    /*
     * 获取缓存目录地址
     */
    const char *MsgGetCachesPath(){
        NSArray *Paths=NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *path=[Paths objectAtIndex:0];
        return [path UTF8String];
    }

#if defined(__cplusplus)
}
#endif

这里的两个函数,UnityCallIOS()和PauseUnity()都是Native和Unity约定好的。说通俗一点,就是Unity调用,Native实现。
这个地方就是典型的Unity调Native。
在上面的交互中,通过Unity调用OC,然后我发出了一条通知。
我是在根控制器监听通知的,为何要在根控制器,因为Unity被调起的时候,它一定存在。
这里我使用了RAC监听通知,当然可以换成普通的监听通知,不过别忘了移除通知。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"UnityWantToExit" object:nil] subscribeNext:^(NSNotification * _Nullable x) {
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    [delegate hideUnityWindow];
}];

那么Native调Unity呢?更简单,只需一句代码
这其实类似于iOS的performSelector:方法

//第一个参数:消息的接受者,或者说谁来执行方法
//第二个参数:函数名
//第三个参数:需要传的值

UnitySendMessage("SceneMain", "MethodName", "What the fuck?");

//注意,三个参数都是C语言字符串,没有'@'

转自:Unity与iOS相互调起、交互

你可能感兴趣的:([转]Unity与iOS交互)