iOS嵌入虚拟引擎unity3d

语言: CN / TW / HK

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

前言

最近虚拟引擎还是很火的,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手册