iOS嵌入虚拟引擎unity3d

5,649 阅读5分钟

我正在参加「掘金·启航计划」

前言

最近虚拟引擎还是很火的,QQ超级秀,淘宝人生,抖音仔仔,玩的都是虚拟偶像,那如果我们 APP 如果也想做类似的功能,那我们好做吗,有没有什么优缺点,用什么方案比较好,这些都值得我们去探讨一下。因为没有接触过 UE4 ,本文仅讨论 unity 方案如何嵌入使用,如何协议交互,以及带来的问题。

Unity导入iOS工程

其实 unity 导出的包,也是一个 Target,那我们这里采用的方案是把 Target 接入到我们项目工程,具体看业务决定,有些是用iOS SDK 嵌入到 unity 的 iOS 包,这个不在本文讨论范围内。本来是一个 Target ,我们工程也是一个 Target ,这时候我们就可以通过 workspace 来添加到一起。

首先我们把 unity 包放到我们工程下面。

screenshot-20221013-173127.png

然后,我们在项目工程中添加 Unity-iPhone.xcodepro

screenshot-20221013-145359.png

把 unity 工程导入到项目中。

screenshot-20221013-145547.png

我们还需要更改一些项目配置。

首先,我们需要对 unity 工程的 bitcode 设置为 NO。 然后 Data 文件夹勾上 UnityFramework 。

screenshot-20221013-145909.png

最后,我们需要把 NativeCallProxy.h文件更改unityFramework权限为Public

screenshot-20221013-174814.png

这样,我们项目配置就完成了。

配置Unity

导入工程成功后,我们就要对 unity 进行代码配置使用。

首先,我们创建一个叫UnityManager类的单例工具,专门来处理 unity 配置信息,以及交互使用。

我们优先导入头文件#include <UnityFramework/NativeCallProxy.h>,配置一下信息参数,如下:

 @property (nonatomic, assign) int gArgc; 
 @property (nonatomic, assign) char** gArgv;
 @property (nonatomic, strong) UnityFramework *unityFramework; 
 @property (nonatomic, strong, readonly ) UIView *unityView; 

然后我们在 main 赋值一下gArgcgArgv

int main(int argc, char * argv[]) {
    @autoreleasepool {
        UnityManager.shareInstance.gArgc = argc;
        UnityManager.shareInstance.gArgv = argv;
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

接着我们在 UnityManager 添加一个初始化 Unity 加载方法。

- (UnityFramework *)loadUnityFramework {
    NSString* bundlePath = nil;
    bundlePath = [[NSBundle mainBundle] bundlePath];
    bundlePath = [bundlePath stringByAppendingString: @"/Frameworks/UnityFramework.framework"];
    NSBundle* bundle = [NSBundle bundleWithPath: bundlePath];
    if ([bundle isLoaded] == false) {
        [bundle load];
    }
    UnityFramework* ufw = [bundle.principalClass getInstance];
    if (![ufw appController]) {
        // unity is not initialized
        [ufw setExecuteHeader: &_mh_execute_header];
    }
    return ufw;
}

然后我们添加一个启动引擎的方法。

- (void)loadUnityWithComplete:(void(^)(void))complete {
    if (!self.unityView) {
        [self setUnityFramework: [self loadUnityFramework]];
        [[self unityFramework] setDataBundleId: "com.unity3d.framework"];
        [[self unityFramework] registerFrameworkListener: self];
        // 用于桥接使用
        [NSClassFromString(@"FrameworkLibAPI") registerAPIforNativeCalls:self];
        [[self unityFramework] runEmbeddedWithArgc:self.gArgc argv: self.gArgv appLaunchOpts: self.launchOptions];
        self.unityView = [[[self unityFramework] appController] rootView];
        self.unityFramework.appController.window.hidden = YES;
    }
    // 等unity回调信息用到
    self.loadUnityComplete = complete;
}

最后我们把 unity 加入到我们想要展示的视图当中即可。

[self.view addSubview:OPRUnityManager.shareInstance.unityView];

视图的大小可以自定义哦。

unity 协议对接

NativeCallProxy.h文件里面包含了我们获取 unity 信息的桥接协议。

screenshot-20221013-180449.png

所以我们的 UnityManager 需要遵守NativeCallsProtocol。这样我们就可以接收到 unity 的信息。

另外我们着重关注UnityFramework,这里面赋予了我们好多可以已使用功能。

screenshot-20221013-154522.png

那这里我们想发消息给 unity,就可以利用下面的方法,名字和 unity 一起命令即可。

- (void)sendMessageToGOWithName:(const char*)goName functionName:(const char*)name message:(const char*)msg
{
    UnitySendMessage(goName, name, msg);
}

就这样,我们就完成了双方的通信功能了。

unity遇到的问题

问题1:unityFramework 的 rootView 问题

我们获取unityView视图是通过 unityFramework 的 rootView获取的,它本身就有自己的 window。

self.unityView = [[[self unityFramework] appController] rootView];

如果我们一个A视图添加了 unityView ,然后去到另外一个B视图,也添加一个 unityView,这时候之前的A视图就不会有unityView,我们只能回到A视图的时候,再重新布局一次。

问题2:unityView 手势问题

首先我们得保证,unityView这个视图层级,没有被其它view挡住,就算这个view设置了clearColor,也会影响unityView的触摸手势问题。

第二种就是滑动,刚好我们unityView加入到我们的 scrollView 里面,这时候左右触摸 unityView,也会影响我们 scrollView 的抖动。

这时候我们需要加入一个手势判断,通过45度来决定,现在触发的是 unityView 的事件,还是 scrollView 的手势事件。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
        UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint translation = [pan translationInView:self];
        CGFloat absX = fabs(translation.x);
        CGFloat absY = fabs(translation.y);
        if (absX > absY ) {
            return NO;
        } else if (absY > absX) {
            return YES;
        }
    }
    return YES;
}

问题3:unity 排查问题

iOS 和 unity 交互方面,如果只是提供一个入口,那里面的排查处理就比较简单,那如果很多页面都可能用到unity,而且又有原生的,那交互起来就比较多了,如果出了问题,那该如何排查呢?

  1. 首先 unity 尽量写全一些日志信息,这样我们可以通过 Xcode 的控制台去看有没有报错信息。
  2. 我们可以让 unity 开启本地服务器,在我们交互协议的时候,把信息发送出去,这样只要有手机就可以看调用流程。

问题4:unity 内存暴增问题

unity 引擎加进来,自然会增加内存,而且要渲染各种资源,绘制各种东西,这时候如果想排查为什么会增量很多,就可以通过 xcode -> Debug -> Capture GPU Workload 来查看内存问题。

screenshot-20221014-113807.png

问题5: 电量消耗问题

自从引入了 unity 引擎后,电量消耗也明显加快了,这块的问题也是 unity 团队非常关注的问题。严重的时候,电量消耗方面,GPU占用 45%。

我们 APP 端能做了,就是及时给数据进行反馈。所以我们在每个页面都给一个数据反馈,显示实时 CPU 使用率,内存大小。

内存大小:

+ (int64_t)memoryUsage {
    int64_t memoryUsageInByte = 0;
        task_vm_info_data_t vmInfo;
        mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
        kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
        if(kernelReturn == KERN_SUCCESS) {
            memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
        } else {
        }
        return memoryUsageInByte;
}

CPU 使用率:

+ (double)getCpuUsage {
    kern_return_t           kr;
    thread_array_t          threadList;         
    mach_msg_type_number_t  threadCount;        
    thread_info_data_t      threadInfo;         
    mach_msg_type_number_t  threadInfoCount;    
    thread_basic_info_t     threadBasicInfo;   
    
    kr = task_threads(mach_task_self(), &threadList, &threadCount);
    if (kr != KERN_SUCCESS) {
        return -1;
    }
    double cpuUsage = 0;
    for (int i = 0; i < threadCount; i++) {
        threadInfoCount = THREAD_INFO_MAX;
        kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);
        if (kr != KERN_SUCCESS) {
            return -1;
        }
        
        threadBasicInfo = (thread_basic_info_t)threadInfo;
        if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
            cpuUsage += threadBasicInfo->cpu_usage;
        }
    }
    
    // 回收内存,防止内存泄漏
    vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t));

    return cpuUsage / (double)TH_USAGE_SCALE * 100.0;
}

最后

说实话,一路走来也遇到各种各样的坑,很多时候拿出来的方案也不一定是最优方案,后面会在写一篇关于 unity3d 联调之间产生的有趣事情。秉着一起学习的心态,也希望有专业的同学能提出更好的意见,万分感谢!!!

参考

iOS开发入门-unity手册