项目背景
我们的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; ////// /// 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页面展示。
//////读取原生沙盒documents/assetsbundle目录下的文件,documents/assetsbundle下的文件通过native原生下载的资源 /// /// documents/assetsbundle下的ab文件 ///读取到的字符串 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(gameobjectname); profab.tag = tagname; instantiate(profab); gesturecontroller gc = gameobject.findobjectoftype (); 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@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 ) aapi; @end
nativecallproxy.mm文件实现如下:
#import "nativecallproxy.h" @implementation nativecallproxy idapi = null; (void)registerapifornativecalls:(id ) 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的详细流程可以参考文章:。
然后其他需要拥有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(); 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 (); launity.loadstreamingassetbundlewithabname(res.packagename, res.name, res.tag); } else { loadassetutility launity = gameobject.findobjectoftype (); 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 (); 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
extern int argcapp; extern char ** argvapp; @interface unityscenemanager()unity容器的原生实现,其实也是在一个普通的viewcontroller里面包含了unity视图的view。@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
#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]; }