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手冊