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脚本,这个层级关系是固定的,不然会报错。
0
脚本实现如下:
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,并在里面设置依赖关系就可以了。
0
 
使用插件工具进行打包
1.从gitHub上下载源码,然后将代码库中的Editor目录下的文件复制一份,放到工程Target的Assets/Editor目录下。打开的方式是通过点击Window->AssetBundle Browser进行打开
 
0
2.打包时,可以选择将打出的ab包内置到项目中,勾选Copy StreamingAssets ,让打出的内容放置在StreamingAssets目录下,这样可以将ab资源内置到Unity项目中。
 
3.通过上面的操作会完成资源打包,然后将打包的产物压缩上传到后台。
0
 
 
 
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);
     }
 
 }

 

模型的相关设置
手势支持
对于加载完成后的模型需要添加手势支持,允许用户旋转,缩放查看,不能说只能静止观看。这里添加手势控制脚本用于支持手势功能。
0
模型实现成功后,把实例对象设置到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动态库
 
0
 
0
此时将得到一个iOS 工程。
 
原生与Unity通信
创建原生与Unity通信接口,并放置到Unity项目中。
0
 
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发送消息时,参数对应的意义。
0
 
然后在场景中添加DispatchGO游戏对象,在此对象上面添加DispatchGO组件,DispatchGO组件用于接收原生发送过来的消息,并进行逻辑处理。
0
 
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
Unity容器的原生实现,其实也是在一个普通的ViewController里面包含了Unity视图的View。
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];
}

 

 

 

 
posted @   滴水微澜  阅读(412)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示