项目背景
我们的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);
}
然后根据工程设置,生成UnityFramework。创建UnityFramework的详细流程可以参考文章:https://www.cnblogs.com/zhou--fei/p/17622488.html
然后其他需要拥有Unity能力的APP就可以集成此动态库,展示Unity视图。
 
原生与Unity通信交互
首先定义一套接口,用于规定原生到Unity发送消息时,参数对应的意义。
 
然后在场景中添加DispatchGO游戏对象,在此对象上面添加DispatchGO组件,DispatchGO组件用于接收原生发送过来的消息,并进行逻辑处理。
 
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];
}
 

iOS使用Unity容器动态加载3D模型的更多相关文章

  1. WPF动态加载3D 放大-旋转-平移

    原文:WPF动态加载3D 放大-旋转-平移 WavefrontObjLoader.cs 第二步:ModelVisual3DWithName.cs public class ModelVisual3DW ...

  2. 使用 Assimp 库加载 3D 模型

    前言 要想让自己的 3D 之旅多一点乐趣,肯定得想办法找一些有意思一点的 3D 模型.3D 模型有各种各样的格式,obj的,stl的,fbx的等等不一而足.特别是 obj 格式的 3D 模型,完全是纯 ...

  3. Unity Lightmap动态加载研究

    什么情况下需要Lightmap? 移动平台上目前暂时还不能开实时光影效果,会卡成幻灯片.所以就需要将光影烘焙到贴图上. 什么情况下需要动态加载Lightmap? 1.当项目抛弃了Unity的多场景模式 ...

  4. WPF动态加载3D 放大-旋转-平移

    第一步:新建WavefrontObjLoader.cs using System; using System.Collections.Generic; using System.Windows; us ...

  5. 关于Unity里动态加载图片

    Resources.Load 使用该方法可以动态加载资源 过程: 1.首先需要在Project面板里创建一个名为Resources的文件夹(名字必须是这个 不能写错啊) 2.把要加载的游戏对象放到该目 ...

  6. Unity www动态加载网上图片

    一. 1.新建一个UGUI的Button,删掉它的Image组件,添加一个Raw Image组件.如图: 由于删除了Image组件,所以画圈的位置是空的,运行后会自动把Raw Image添加到那里. ...

  7. Unity中 动态加载 Resources.Load()和Asset Bundle 的区别

    版权声明:本文为博主原创文章,未经博主允许不得转载. 初学Unity的过程中,会发现打包发布程序后,unity会自动将场景需要引用到的资源打包到安装包里,没有到的不会跟进去.我们在编辑器里看到的Ass ...

  8. unity动态加载FBX模型(Http下载到Rescources文件,场景Load直接调用):

    using UnityEngine; using System.Collections; using System.IO; using System.Net; using System; using ...

  9. cesium加载gltf模型点击以及列表点击定位弹窗

    前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 之 ...

  10. Unity动态加载和内存管理(三合一)

    原址:http://game.ceeger.com/forum/read.php?tid=4394#info 最近一直在和这些内容纠缠,把心得和大家共享一下: Unity里有两种动态加载机制:一是Re ...

随机推荐

  1. vim 从嫌弃到依赖(10)——缓冲区列表

    之前的一系列文章主要介绍了vim文本相关的操作,并且也介绍了vim的几种模式.通过前面的内容,相信各位小伙伴们已经对vim有了一个基本的了解,同时也能够使用vim快速编辑文本,从这篇开始,我们将要介绍 ...

  2. python入门之后须掌握的知识点(excel文件处理+邮件发送+实战:批量化发工资条)【二】

    相关文章: python处理Excel实现自动化办公教学(含实战)[一] python处理Excel实现自动化办公教学(含实战)[二] python处理Excel实现自动化办公教学(数据筛选.公式操作 ...

  3. 使用s3fs-fuse挂载minio文件时无法删除问题排查过程

    使用s3fs-fuse挂载minio文件时无法删除问题排查过程 结论:部分场景无法满足,具体问题详见正文 1. 部署minio docker run    -p 9000:9100    -p 909 ...

  4. Vite4+Typescript+Vue3+Pinia 从零搭建(1) - 项目初始化

    项目代码同步至码云 weiz-vue3-template 前提准备 1. node版本 Node.js版本 >= 12,如果有老项目需要旧版本的,推荐用 nvm 管理node版本. PS C:\ ...

  5. Git企业开发控制理论和实操-从入门到深入(七)|企业级开发模型

    前言 那么这里博主先安利一些干货满满的专栏了! 首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助. 高质量博客汇总 然后就是博主最近最花时间的一 ...

  6. 【算法】基于hoare快速排序的三种思想和非递归,基准值选取优化【快速排序的深度剖析-超级详细的注释和解释】你真的完全学会快速排序了吗?

    文章目录 前言 什么是快速排序 快速排序的递归实现 快速排序的非递归实现 单趟排序详解 hoare思想 挖坑法 前后指针法 快速排序的优化 三数取中 小区间优化 快速排序整体代码 尾声 前言 先赞后看 ...

  7. 生活小技巧:Excel中PMT函数的使用

    关于PMT函数,从百科中就可以搜到基本解释: PMT函数即年金函数,基于固定利率及等额分期付款方式,返回贷款的每期付款额. PMT(Rate, Nper, Pv, Fv, Type). 语法参数 ●R ...

  8. 探索C语言中的联合体与枚举:数据多面手的完美组合!

    ​ 欢迎大家来到贝蒂大讲堂 养成好习惯,先赞后看哦~ 所属专栏:C语言学习 贝蒂的主页:Betty's blog 1. 联合体的定义 联合体又叫共用体,它是一种特殊的数据类型,允许您在相同的内存位置存 ...

  9. Java方法重载浅谈

    Java方法重载浅谈 目录: 方法重载的定义 方法重载的满足条件 方法重载的传递 基本类型 引入类型 方法重载的好处 方法重载的定义以及满足条件: 定义: 方法重载指同一类中定义多个方法之间的联系: ...

  10. NC53074 Forsaken喜欢独一无二的树

    题目链接 题目 题目描述 ​ 众所周知,最小生成树是指使图中所有节点连通且边权和最小时的边权子集. ​ 不过最小生成树太简单了,我们现在来思考一个稍微复杂一点的问题. ​ 现在给定一个 \(n\) 个 ...