• 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;
     
     
    ///
    ///
    ///
     
    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
    ///
     ///读取原生沙盒Documents/AssetsBundle目录下的文件,Documents/AssetsBundle下的文件通过Native原生下载的资源
     ///
     /// Documents/AssetsBundle下的ab文件
     /// 读取到的字符串
     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(gameObjectName);
         profab.tag = tagName;
         Instantiate(profab);
     
     
         GestureController gc = GameObject.FindObjectOfType();
         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
     
    @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) 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 api = NULL;
    + (void)registerAPIforNativeCalls:(id) 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();
            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();
                laUnity.LoadStreamingAssetBundleWithABName(res.packageName, res.name, res.tag);
            }
            else {
                LoadAssetUtility laUnity = GameObject.FindObjectOfType();
                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();
            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

    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()
     
    @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];
    }

     

     

     

     
  • 相关阅读:
    Pytorch的variable和tensor区别
    图解LeetCode——792. 匹配子序列的单词数(难度:中等)
    判断船进去倾倒区次数
    给Series、DataFrame的索引增加前缀:add_prefix()函数
    javaScript中的函数防抖和节流
    高效节能双冷源空调架构在某新建数据中心项目中的应用
    加密算法笔记
    数据结构和算法之归并排序
    量化金融-分类数据的检验
    conan 报错 HTTPSConnectionPool(host=‘center.conan.io‘, port=443):
  • 原文地址:https://www.cnblogs.com/zhou--fei/p/18057212