iOS使用Unity容器动态加载3D模型
项目背景
我们的APP是一个数字藏品平台,里面的很多藏品需要展示3D模型,3D模型里面可能会包含场景,动画,交互。而对应3D场景来说,考虑到要同时支持iOS端,安卓端,Unity是个天然的优秀方案。
对于Unity容器来说,需要满足如下的功能:
1.在APP启动时,需要满足动态下载最新的模型文件。
2.在点击藏品查看模型时,需要根据不同的参数展示不同的模型,并且在页面消失后,自动卸载对应的模型。
如果要实现上面说的功能则是需要使用Unity的打包功能,将资源打包成AssetBundle资源包,然后把ab包进行上传到后台服务器,然后在APP启动时从服务器动态下载,然后解压到指定的目录中。
当用户点击藏品进入到Unity容器展示3D模型时,则可以根据传递的模型名称和ab包名,从本地的解压目录中加载对应的3D模型。
AssetBundle打包流程
创建AB打包脚本
AB包打包是在Editer阶段里。
首先要创建一个Editer目录并把脚本放置到这个目录下面,注意它们的层级关系:Assert/Editor/CS脚本,这个层级关系是固定的,不然会报错。

脚本实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using UnityEditor; using System.IO; /// <summary> /// /// </summary> public class AssetBundleEditor { //1.编译阶段插件声明 [MenuItem( "Assets/Build AssetBundles" )] static void BuildAssetBundles() { string dir = "AssetBundles" ; if (!Directory.Exists(dir)) { //2.在工程根目录下创建dir目录 Directory.CreateDirectory(dir); } //3.构建AssetBundle资源,AB资源包是一个压缩文件,可以把它看成是一个压缩的文件夹,里面 //可能包含多个文件,预制件,材质,贴图,声音。 BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, BuildTarget.iOS); } } |
设置需要打包的资源
可以在Project选中一个资源(预制件,材质,贴图,声音等),然后在Inspector下面的AssetBundle设置打包成的名称和后缀。如果名称带层级的如:scene/cube,那么打出来的AB包会自己添加一个scene目录,然后在目录下存在了cube资源包。
AB包可以存在依赖关系,比如GameObjectA和GameObjectB共同使用了Material3, 然后它们对应的AssetBundle名称和后缀分别为cube.ab, capsule.ab, share.ab。
虽然GameObjectA中包含了Material3资源,但是 AssetBundle在打包时如果发现Material3已经被打包成了share.ab, 那么就会只打GameObjectA,并在里面设置依赖关系就可以了。

使用插件工具进行打包
1.从gitHub上下载源码,然后将代码库中的Editor目录下的文件复制一份,放到工程Target的Assets/Editor目录下。打开的方式是通过点击Window->AssetBundle Browser进行打开

2.打包时,可以选择将打出的ab包内置到项目中,勾选Copy StreamingAssets ,让打出的内容放置在StreamingAssets目录下,这样可以将ab资源内置到Unity项目中。
3.通过上面的操作会完成资源打包,然后将打包的产物压缩上传到后台。

AssetsBundle资源包的使用
APP启动时,下载AssetBundle压缩包, 然后解压放置在沙盒Documents/AssetsBundle目录下,当点击APP中的按钮进入到Unity容器页面时,通过包名加载对应的ab包进行Unity页面展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | /// <summary> ///读取原生沙盒Documents/AssetsBundle目录下的文件,Documents/AssetsBundle下的文件通过Native原生下载的资源 /// </summary> /// <param name="abName">Documents/AssetsBundle下的ab文件</param> /// <returns>读取到的字符串</returns> public static AssetBundle GetNativeAssetFromDocumentsOnProDownLoad( string abName) { string localPath = "" ; if (Application.platform == RuntimePlatform.Android) { localPath = "jar:file://" + Application.persistentDataPath + "/AssetsBundle/" + abName; } else { localPath = "file://" + Application.persistentDataPath + "/AssetsBundle/" + abName; } UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(localPath); var operation = request.SendWebRequest(); while (!operation.isDone) { } if (request.result == UnityWebRequest.Result.ConnectionError) { Debug.Log(request.error); return null ; } else { AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request); return assetBundle; } //UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(localPath); //yield return request.Send(); //AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request); //return assetBundle; } |
注意:当离开Unity容器时需要卸载里面加载的ab包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public void TestUnLoadGameObject() { UnLoadGameObjectWithTag( "NFT" ); } public void UnLoadGameObjectWithTag( string tagName) { GameObject go = GameObject.FindWithTag(tagName); if (go) { Destroy(go, 0.5f); } else { Debug.Log(go); } } public void UnLoadAllGameObjectWithTag( string tagName) { GameObject[] gos = GameObject.FindGameObjectsWithTag(tagName); foreach (GameObject go in gos) { Destroy(go, 0.5f); } } |
模型的相关设置
手势支持
对于加载完成后的模型需要添加手势支持,允许用户旋转,缩放查看,不能说只能静止观看。这里添加手势控制脚本用于支持手势功能。

模型实现成功后,把实例对象设置到GestureController组件的Target上面,实现模型的手势支持。
加载Unity内置ab资源包的脚本实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public void TestLoadStreamingAssetBundle() { LoadStreamingAssetBundleWithABName( "cube.ab" , "Cube" , "NFT" ); } public void LoadStreamingAssetBundleWithABName( string abName, string gameObjectName, string tagName) { AssetBundle ab = FileUtility.GetNativeAssetFromStreamingAssets(abName); GameObject profab = ab.LoadAsset<GameObject>(gameObjectName); profab.tag = tagName; Instantiate(profab); GestureController gc = GameObject.FindObjectOfType<GestureController>(); gc.target = profab.transform; ab.Unload( false ); } |
Unity场景切换的脚本实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //接收原生事件:切换场景 public void SwitchScene( string parmas) { Debug.Log(parmas); Param param = new Param(); Param res = JsonDataContractJsonSerializer.JsonToObject(parmas, param) as Param; Debug.Log(res.name); Debug.Log( "------------" ); for ( int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); Debug.Log(scene.name); } SceneManager.LoadScene(res.name, LoadSceneMode.Single); Debug.Log( "------------" ); for ( int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); Debug.Log(scene.name); } } |
Unity导出iOS项目
构建UnityFramework动态库


此时将得到一个iOS 工程。
原生与Unity通信
创建原生与Unity通信接口,并放置到Unity项目中。

NativeCallProxy.h文件创建通信协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #import <Foundation/Foundation.h> @protocol NativeCallsProtocol @required /// Unity调用原生 /// - Parameter params: {"FeatureName":"下载资源", "params": "参数"} - ( void )callNative:(NSString *) params ; @end __attribute__ ((visibility( "default" ))) @ interface NativeCallProxy : NSObject // call it any time after UnityFrameworkLoad to set object implementing NativeCallsProtocol methods + ( void )registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi; @end |
NativeCallProxy.mm文件实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #import "NativeCallProxy.h" @implementation NativeCallProxy id<NativeCallsProtocol> api = NULL; + ( void )registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi { api = aApi; } @end extern "C" { void callNative( const char * value); } void callNative( const char * value){ return [api callNative:[NSString stringWithUTF8String:value]]; } |
原生的Delegate的实现
1 2 3 4 | #pragma mark - NativeCallsProtocol - ( void )callNative:(NSString *) params { NSLog( @"收到Unity的调用:%@" , params ); } |
Unity调用原生
1 2 3 4 5 6 7 8 9 10 11 12 13 | //重要声明,声明在iOS原生中存在下面的方法,然后C#中可以直接进行调用 [DllImport( "__Internal" )] static extern void callNative( string value); public void changeLabel( string textString) { tmpText.text = textString; } public void btnClick() { Debug.Log(tmpInput.text); callNative(tmpInput.text); } |
然后根据工程设置,生成UnityFramework。创建UnityFramework的详细流程可以参考文章:https://www.cnblogs.com/zhou--fei/p/17622488.html。
然后其他需要拥有Unity能力的APP就可以集成此动态库,展示Unity视图。
原生与Unity通信交互
首先定义一套接口,用于规定原生到Unity发送消息时,参数对应的意义。

然后在场景中添加DispatchGO游戏对象,在此对象上面添加DispatchGO组件,DispatchGO组件用于接收原生发送过来的消息,并进行逻辑处理。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class Param { public string packageName { get ; set ; } public string name { get ; set ; } public string tag { get ; set ; } public string type { get ; set ; } public string isAll { get ; set ; } } public class DispatchGO : MonoBehaviour { //接收原生事件 public void DispatchEvent( string parmas) { Debug.Log(parmas); //事件分发 ChangeLabel cl = GameObject.FindObjectOfType<ChangeLabel>(); cl.changeLabel(parmas); } //接收原生事件:加载模型 public void LoadModel( string parmas) { Debug.Log(parmas); Param param = new Param(); Param res = JsonDataContractJsonSerializer.JsonToObject(parmas, param) as Param; Debug.Log(res.packageName); Debug.Log(res.name); Debug.Log(res.tag); Debug.Log(res.type); if (res.type == "0" ) { LoadAssetUtility laUnity = GameObject.FindObjectOfType<LoadAssetUtility>(); laUnity.LoadStreamingAssetBundleWithABName(res.packageName, res.name, res.tag); } else { LoadAssetUtility laUnity = GameObject.FindObjectOfType<LoadAssetUtility>(); laUnity.LoadNativeAssetBundleWithABName(res.packageName, res.name, res.tag); } } //接收原生事件:卸载模型 public void UnLoadModel( string parmas) { Debug.Log(parmas); Param param = new Param(); Param res = JsonDataContractJsonSerializer.JsonToObject(parmas, param) as Param; UnLoadAssetUtility unLAUnity = GameObject.FindObjectOfType<UnLoadAssetUtility>(); if (res.isAll == "1" ) { unLAUnity.UnLoadAllGameObjectWithTag(res.tag); } else { unLAUnity.UnLoadGameObjectWithTag(res.tag); } } //接收原生事件:切换场景 public void SwitchScene( string parmas) { Debug.Log(parmas); Param param = new Param(); Param res = JsonDataContractJsonSerializer.JsonToObject(parmas, param) as Param; Debug.Log(res.name); Debug.Log( "------------" ); for ( int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); Debug.Log(scene.name); } SceneManager.LoadScene(res.name, LoadSceneMode.Single); Debug.Log( "------------" ); for ( int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); Debug.Log(scene.name); } } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } |
在iOS原生侧,本地通过使用unityFramework的sendMessageToGOWithName方法从原生想Unity发送消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | case 103: { NSDictionary * params = @{ @"tag" : @"NFT" , @"isAll" : @"1" }; [ad.unityFramework sendMessageToGOWithName: "DispatchGO" functionName: "UnLoadModel" message:[self serialJsonToStr: params ]]; } break ; case 104: { NSDictionary * params = @{ @"name" : @"DemoScene" }; [ad.unityFramework sendMessageToGOWithName: "DispatchGO" functionName: "SwitchScene" message:[self serialJsonToStr: params ]]; } break ; |
Unity通过调用iOS中协议声明的方法void callNative(string value); 进行调用。
1 2 3 4 5 6 7 8 | //重要声明,声明在iOS原生中存在下面的方法,然后C#中可以直接进行调用 [DllImport( "__Internal" )] static extern void callNative( string value); public void btnClick() { Debug.Log(tmpInput.text); callNative(tmpInput.text); } |
原生端创建Unity容器
在APP启动时,对UnityFramework进行初始化。
1 2 3 4 5 6 7 8 9 | @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [UnitySceneManager sharedInstance].launchOptions = launchOptions; [[UnitySceneManager sharedInstance] Init]; return YES; } |
UnitySceneManager的主要实现逻辑如下:#import "UnitySceneManager.h"#import <UnityFramework/NativeCallProxy.h>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | extern int argcApp; extern char ** argvApp; @ interface UnitySceneManager()<UnityFrameworkListener, NativeCallsProtocol> @end @implementation UnitySceneManager #pragma mark - Life Cycle + (instancetype)sharedInstance { static UnitySceneManager *shareObj; static dispatch_once_t onceKey; dispatch_once(&onceKey, ^{ shareObj = [[super allocWithZone:nil] init]; }); return shareObj; } + (instancetype)allocWithZone:( struct _NSZone *)zone { return [self sharedInstance]; } - (instancetype)copyWithZone:( struct _NSZone *)zone { return self; } #pragma mark - Private Method - ( void )Init { [self initUnityFramework]; [NativeCallProxy registerAPIforNativeCalls:self]; } - ( void )unloadUnityInternal { if (self.unityFramework) { [self.unityFramework unregisterFrameworkListener:self]; } self.unityFramework = nil; } - (BOOL)unityIsInitialized { return (self.unityFramework && self.unityFramework.appController); } // MARK: overwrite #pragma mark - Public Method - ( void )initUnityFramework { UnityFramework *unityFramework = [self getUnityFramework]; self.unityFramework = unityFramework; [unityFramework setDataBundleId: "com.zhfei.framework" ]; [unityFramework registerFrameworkListener:self]; [unityFramework runEmbeddedWithArgc:argcApp argv:argvApp appLaunchOpts:self.launchOptions]; } - (UnityFramework *)getUnityFramework { 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; } #pragma mark - Event #pragma mark - Delegate #pragma mark - UnityFrameworkListener - ( void )unityDidUnload:(NSNotification*)notification { } - ( void )unityDidQuit:(NSNotification*)notification { } #pragma mark - NativeCallsProtocol - ( void )callNative:(NSString *) params { NSLog( @"收到Unity的调用:%@" , params ); } #pragma mark - Getter, Setter #pragma mark - NSCopying #pragma mark - NSObject #pragma mark - AppDelegate生命周期绑定 - ( void )applicationWillResignActive { [[self.unityFramework appController] applicationWillResignActive: [UIApplication sharedApplication]]; } - ( void )applicationDidEnterBackground { [[self.unityFramework appController] applicationDidEnterBackground: [UIApplication sharedApplication]]; } - ( void )applicationWillEnterForeground { [[self.unityFramework appController] applicationWillEnterForeground: [UIApplication sharedApplication]]; } - ( void )applicationDidBecomeActive { [[self.unityFramework appController] applicationDidBecomeActive: [UIApplication sharedApplication]]; } - ( void )applicationWillTerminate { [[self.unityFramework appController] applicationWillTerminate: [UIApplication sharedApplication]]; } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | #import "UnityContainerViewController.h" #import "UnitySceneManager.h" @ interface UnityContainerViewController () @end @implementation UnityContainerViewController #pragma mark - Life Cycle - ( void )viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [self setupUI]; } - ( void )viewDidLayoutSubviews { [super viewDidLayoutSubviews]; UnitySceneManager *ad = [UnitySceneManager sharedInstance]; ad.unityFramework.appController.rootView.frame = self.view.bounds; } - ( void )viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; UnitySceneManager *ad = [UnitySceneManager sharedInstance]; [ad.unityFramework pause:NO]; } - ( void )viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; UnitySceneManager *ad = [UnitySceneManager sharedInstance]; [ad.unityFramework pause:YES]; } #pragma mark - Private Method - ( void )setupUI { self.view.backgroundColor = [UIColor whiteColor]; UnitySceneManager *ad = [UnitySceneManager sharedInstance]; UIView *rootView = ad.unityFramework.appController.rootView; rootView.frame = [UIScreen mainScreen].bounds; [self.view addSubview:rootView]; [self.view sendSubviewToBack:rootView]; } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器