项目背景
我们的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脚本,这个层级关系是固定的,不然会报错。
脚本实现如下:
- 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页面展示。
- /// <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);
- }
-
- }
模型的相关设置
手势支持
对于加载完成后的模型需要添加手势支持,允许用户旋转,缩放查看,不能说只能静止观看。这里添加手势控制脚本用于支持手势功能。
模型实现成功后,把实例对象设置到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动态库
此时将得到一个iOS 工程。
原生与Unity通信
创建原生与Unity通信接口,并放置到Unity项目中。
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);
- }
然后其他需要拥有Unity能力的APP就可以集成此动态库,展示Unity视图。
原生与Unity通信交互
首先定义一套接口,用于规定原生到Unity发送消息时,参数对应的意义。
然后在场景中添加DispatchGO游戏对象,在此对象上面添加DispatchGO组件,DispatchGO组件用于接收原生发送过来的消息,并进行逻辑处理。
在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>
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];
- }