项目背景
我们的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. Flask四剑客

    目录 Flask四剑客 Flask四剑客 ''' 响应字符串 响应html页面 跳转页面 返回json字符串 ''' from flask import Flask, render_template, ...

  2. .net web发布至阿里云服务器

    1.发 布网站第一步:右键网站主项目,选择 发布网站.如下图 第二步 填写发布网站的相关配置,选择配置文件,新建配置文件 第三步 选择发布的文件存放的位置 第四步,选择Release 再点击下一步,点 ...

  3. 即时通讯(IM)开源项目OpenIM每周迭代版本发布-音视频实时通话-v2.0.4

    介绍 OpenIM每周五发布新版,包括新特性发布,bug修复,同时合并PR 由于2.0版本重构完毕,架构更清晰,代码更规范,先邀请各位参与OpenIM社区建设,包括技术开发,技术分享等,特性开发,性能 ...

  4. Leetcode 92题反转链表 II(Reverse Linked List II) Java语言求解

    前言 反转链表可以先看我这篇文章: Leetcode 206题 反转链表(Reverse Linked List)Java语言求解 题目链接 https://leetcode-cn.com/probl ...

  5. 强化学习从基础到进阶-案例与实践[5.1]:Policy Gradient策略梯度-Cart pole游戏展示

    强化学习从基础到进阶-案例与实践[5.1]:Policy Gradient策略梯度-Cart pole游戏展示 强化学习(Reinforcement learning,简称RL)是机器学习中的一个领域 ...

  6. MySQL【一】基本使用----超详细教学

    相关文章: win10下MySQL安装教程(MySql-8.0.26超级详细)_丨汀.的博客-CSDN博客 1.RDBMS(Relational Databases Management System ...

  7. 9.4 Windows驱动开发:内核PE结构VA与FOA转换

    本章将继续探索内核中解析PE文件的相关内容,PE文件中FOA与VA,RVA之间的转换也是很重要的,所谓的FOA是文件中的地址,VA则是内存装入后的虚拟地址,RVA是内存基址与当前地址的相对偏移,本章还 ...

  8. PyTorch中实现Transformer模型

    前言 关于Transformer原理与论文的介绍:详细了解Transformer:Attention Is All You Need 对于论文给出的模型架构,使用 PyTorch 分别实现各个部分. ...

  9. ElasticSearch7.3学习(三十四)----生产环境集群部署总结

    1.集群部署 2.结点的三个角色 主结点:master节点主要用于集群的管理及索引 比如新增结点.分片分配.索引的新增和删除等. 数据结点:data 节点上保存了数据分片,它负责索引和搜索操作. 客户 ...

  10. Excel中文本型数字转换为数值型数字的方法

    背景 工作中经常遇到需要将Excel中的内容进行求和或者其他计算,但是由于格式为文本,无法进行计算和求和. 单元格的左上角都有绿色小三角,且用自动求和公式计算无法计算结果,显示为0,说明单元格格式为文 ...