项目背景
我们的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组件用于接收原生发送过来的消息,并进行逻辑处理。
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];
}