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
腳本實現如下:
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頁面展示。
   /// <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包

   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資源包的腳本實現:
   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場景切換的腳本實現:

    //接收原生事件:切換場景
    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文件創建通信協議
#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文件實現如下:

#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的實現

#pragma mark - NativeCallsProtocol
- (void)callNative:(NSString *)params {
    NSLog(@"收到Unity的調用:%@",params);
}

 

 Unity調用原生
   //重要聲明,聲明在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
 
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發送消息。

        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); 進行調用。

    //重要聲明,聲明在iOS原生中存在下面的方法,然後C#中可以直接進行調用
    [DllImport("__Internal")]
    static extern void callNative(string value);

    public void btnClick() {
        Debug.Log(tmpInput.text);
        callNative(tmpInput.text);
    }

 

原生端創建Unity容器

在APP啓動時,對UnityFramework進行初始化。
@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>

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。
#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];
}

 

 

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章