概述
在去年CocosCreator 2.4发布的时候,做了一次版本更新的盘点,对其中Asset Manage
r和Bundle
部分专门做了重点盘点,用了一个小例子,大概展示了其能力,以及吹了一波应用场景,后来很多小伙伴问我了很多实际使用的问题,我发现很多问题来自于使用方法不对造成的疑惑,今天正好有一些时间,专门撰写一篇文稿,详细讲解Bundle的使用方法,用三个实际应用展示它。
B站传送门:CocosCreator 2.4.0 更新盘点
目录
本文将会按照下面的结构:
- 例子展示
- 手撸工程
2.1 同项目Bundle,动态加载
2.2 跨项目Bundle,大厅+子游戏
2.3 跨项目Bundle,代码互调 - 踩坑总结
本项目有视频版本,目前还未剪辑完成,等发布完成,再做更新。
GIT
地址在文章后面 ╮( ̄▽  ̄)╭
先看例子
我将完成结果先放在前面,帮助各位可以快速找到自己所需要的部分。
第一个例子,同项目Bundle,动态加载
直接加载内部的bundle包,它是项目本身的某个目录,只不过被设置为bundle,里面包含了动画、脚本、场景、资源等,我们可以看到,项目运行起来的时候,被设置为bundle的部分,不会被加载,只有主动加载的时候才会载入,通过API也可以读取内部的素材,就像我们现在做得这样,读取了一个Prefab创建成Node,还有就是读取一张图片显示出来。
第二个例子,跨项目Bundle,大厅+子游戏
加载另外一个项目的bundle包,它没有和主项目在一起,而是另外一个CocosCreator项目的一部分,我把生成的素材放在一个资源服务器上,让主项目去载入它,然后运行,通过远程载入创建出来,它是实现动态加载的核心,一会儿后面咱们详细讲解。
第三个例子,跨项目Bundle,代码互相调方案
不同bundle内资源或代码的互相调用测试,这个例子主项目为子游戏bundle提供调用接口,我们可以看到除了能调用主项目,也可以通过一些方法被调用,那么怎么做到呢?且听后面分解。
手撸详解
看完了例子,那么我们把上面的全部手撸一遍,注意本视频中的所有代码为Typescript,Cocos Creator 版本使用的是2.4.5版本。
一、同项目Bundle,动态加载
首先我们先要建立一个CocosCreator的项目,这个项目必须使用的2.4.0以上的版本,这样才能有Bundle的特性,起一个名字就叫BundleLobby。
打开项目,建立一些目录和场景,随便来一个目录就叫:
aaa
吧,这个名字越随便越好,以后就会设置成为bundle,然后在它下面建立一些基本的项目目录,比如res、src,用来存放你的资源和代码,创建一个主场景叫做Main,搭建一下基本的UI功能,这里作了两个按钮:一个是读取,一个是跳转,还有一个进度条,用来表示Bundle的读取成功与否。现在为aaa的目录里增加一些素材,创建一个aaa的场景,简单摆放一下,为了方便展示它的复杂性,所以我加入了一个spine动画,并且把spineboy制作成了一个Prefab。
先实现一下基本的场景跳转,也就是没有把它配置成为bundle的情况下,它只是整个项目的一部分,写一些代码让Main和aaa两个场景互相跳转,需要完成两个组件代码,我这里为aaa场景和Main场景各自加入了一个组件Script,并且为各自的场景添加了场景跳转代码:
onClickSceneTo(){
cc.director.loadScene('aaa');
}
这种跳转是最普通的情况,当把aaa这个目录给配置成为bundle之后,就不一样了。
此时跳转到aaa的按钮已经不管用了,我们看调试信息里已经给出了报错信息,它找不到名为aaa的场景资源。
这是因为bundle资源不会在启动的时候加载,而是需要用asset manager的loadBundle之后,所以,我们为读取按钮添加一个click事件,并且实现如下代码
onClickLoad(){
cc.assetManager.loadBundle('aaa',(err,bundle)=>{
if(!err){
this.progressBar.progress = 1;
}
});
}
运行项目后,先点击读取,读取成功之后再点击跳转aaa
,就会跳转到对应的场景当中,不会报错。
既然基本的场景已经实现,那么能否更进一步,从Bundle包里面读取资源呢?我们需要对Main场景进行改造和调整一下,并且组件脚本中的对应处理也需要调整,在这里,我使用了两个空Node来显示读取出来的Prefab和图片,它们分别叫target1和target2,场景结构大概是这个样子:
MainScript1.ts
的代码如下:
const {ccclass, property} = cc._decorator;
@ccclass
export default class MainScript1 extends cc.Component {
@property(cc.ProgressBar)
progressBar:cc.ProgressBar = null;
@property(cc.Node)
target1:cc.Node = null;
@property(cc.Node)
target2:cc.Node = null;
private _bundle:cc.AssetManager.Bundle;
start () {
this.progressBar.progress = 0;
this.target1.active = this.target2.active = false;
}
onClickLoad(){
cc.assetManager.loadBundle('aaa',(err,bundle)=>{
if(!err){
this._bundle = bundle;
this.progressBar.progress = 1;
this.target1.active = this.target2.active = true;
}
});
}
onClickSceneTo(){
cc.director.loadScene('aaa');
}
onClickLoadPrefab(s:cc.Event.EventTouch){
this._bundle.load('res/spineboy',cc.Prefab,(err,asset:cc.Prefab)=>{
if(!err){
this.target1.addChild(cc.instantiate(asset));
s.currentTarget.active = false;
}
});
}
onClickLoadSpriteFrame(s:cc.Event.EventTouch){
this._bundle.load('res/button',cc.Texture2D,(err,tex:cc.Texture2D)=>{
if(!err){
s.currentTarget.active = false;
const node = new cc.Node();
node.addComponent(cc.Sprite).spriteFrame = new cc.SpriteFrame(tex);
this.target2.addChild(node);
}
});
}
}
最终结果:
在第一个例子中,通过把一个目录设置成为bundle,实现动态加载资源内容的能力,这也是2.4版本最让开发者们兴奋的更新,它能大大的加强游戏的载入体验,而它的强大不止于此,官方文档中明确说明,它可以把其他项目的bundle给载入进来,下面第二个例子我们就来试试这个功能。
二、跨项目Bundle,大厅+子游戏
现在计划把这个项目当成大厅场景,通过loadBundle完成对子游戏的bundle加载,对BundleLobby的项目改造一下,为了方便区分和展示,先把Main给重命名为Main1,以及MainScript改名为MainScript1,新建一个场景叫Main2,然后重新建立一个组件脚本,名字叫MainScript2,把一些代码从1复制到2当中,这么是为了避免重写,先不用着急改代码,等子项目完成后回来再说,同样的,Main2场景当中的一些界面元素,就直接复用即可,只保留一个target的Node当容器,为两个按钮指定Click事件,作为大厅的项目已经准备好了。
构建子游戏项目
关掉主项目,下一步要完成子项目,新建一个Games的CocosCreator 2.4以上版本的项目,然后建立一个目录,名字就叫Game1
吧,未来还得有Game2
,我将会选择一个相对比较有趣的内容用于展示,内容尽量和大厅工程不一样,在后面的展示中,将不止是场景跳转,还有从内部创建,从而让它更像是一个动态载入进来的小游戏,这个部分我作了一些简单的开发,具体细节略过,它是一个一直在推进的连续场景,看起来不错,也很酷。
我们把游戏内容的舞台部分制作成为Prefab,当然了你要注意把逻辑脚本挂载在Prefab这个节点上,不然的话,嘿嘿嘿。。。。
现在一个最简单的子游戏创建好了,复杂的内容,咱们放在第三个例子中详细说,现在直接build一下,通过菜单项“项目”=》“构建发布”构建,目标平台为了方便测试先选择Mobile,稍加等待之后,完成。
构建完成后,进入到项目目录,找到\build\web-mobile\assets下面
这里有个Game1,它就是bundle包了,其他的都不需要,咱们只需要这个部分。
资源服务器
但是,跨项目读取必须通过远程方式,所以,你需要用一个小服务器来当资源服务器,在我的项目中提供了一个小网站Node项目,来实现对它的远程读取,方便主项目远程加载,这个工程和大厅、子游戏放在了同一个地方,名字叫RemoteHttpServer
的目录,这个你也可以用别的方式,取决于您的喜好,把build下面的asset下的Game1移动或复制到这个资源server下,确保能够通过网络能够访问到它。
切换CocosCreator的项目到BundleLobby下,要对远程的Bundle进行加载了,我们打开代码
MainScript2.ts
,使用第一个例子中的同样方法loadBundle,但是需要改成远程资源URL,在我的例子中,远程bundle在 127.0.0.1:8080/Game1
当中,所以代码读取需要修改,同时,由于子游戏在Build的时候为文件加了md5标记,所以,直接打开是不行的,需要借助可选参数的version字段来解决这个问题,因此最终的代码如下:
const {ccclass, property} = cc._decorator;
@ccclass
export default class MainScript2 extends cc.Component {
@property(cc.ProgressBar)
progressBar:cc.ProgressBar = null;
@property(cc.Node)
target1:cc.Node = null;
private _bundle:cc.AssetManager.Bundle;
start () {
this.progressBar.progress = 0;
this.target1.active = false;
}
onClickLoad(){
const options = {
version:"08f26",
onFileProgress:(n,t)=>{
this.progressBar.progress = n / t;
}
}
cc.assetManager.loadBundle('http://127.0.0.1:8080/Game1',
options,
(err,bundle)=>{
if(!err){
this._bundle = bundle;
this.target1.active = true;
}
});
}
onClickSceneTo(e:cc.Event.EventTouch){
e.currentTarget.active = false;
this._bundle.load("prefab/Game1Stage",cc.Prefab,(err,asset:cc.Prefab)=>{
if(!err){
this.target1.addChild(cc.instantiate(asset));
}
});
}
}
最终结果如下:
到目前为之,第二个例子已经结束了,虽然已经完成了远程包体的载入流程,但是真正的实现一个大厅加子游戏,或者动态功能模块的话,还似乎差了一些什么,这种的项目需求是要求子包和大厅之间的代码调用,或者互相通讯,下面我们开始尝试用第三个例子来解决这个问题
三、跨项目Bundle,代码互调
在这之前,我们可能需要了解和梳理Bundle的机制,在官方文档中描述Asset Bundle 的构造提到,内容分为代码和资源两个部分,资源的入口是config.json,代码入口为index.js,按照我的测试结果来看,Bundle在下载成功后,会立即将index.js中的代码加入到主包中,打开这个文件看看就能猜到个大概,因此,我们只需要设计大厅接口,在子游戏中实现同样的接口,最后不把它们build到bundle即可,设计思路大致为主包大厅和bundle子游戏内创建的控制组件,并开发通用接口,互相之间通过这种方法调用,为了开发的便捷性,可以为子游戏中创建虚拟的接口类,实现独立开发的能力。
在大厅项目中,我们新建一个场景Main3和MainScript3组件脚本,并且按照之前Main2样子搭建,有一些部分还得需要结合子游戏修改,先放在这里,现在用VS Code在src目录中,实现一个接口文件,就叫IMainController吧,我这里就简单实现一个输出文本接口
export interface IMainController {
outString(str: string): void;
}
在实际项目中,接口可能要比这个复杂的多,主要看你的项目需求,现在我们再建立一个MainController的组件脚本,为了区分,我加上了Script为后缀,实现基础的组件类代码,并且实现IMainController的接口,回到Main3的场景中,为Canvas挂上刚刚的组件,在场景中创建并且指定一个cc.Label
作为输出组件,现在主场景已经准备好了。
import { IMainController } from "./IMainController";
const {ccclass, property} = cc._decorator;
@ccclass
export default class MainControllerScript extends cc.Component implements IMainController {
@property(cc.Label)
outLabel:cc.Label = null;
outString(str: string): void {
this.outLabel.string = str;
}
}
下一步开发子游戏,关闭大厅项目,打开子游戏项目,为了避免和之前的重复,新建一个Game2文件夹,放进去了一个龙骨制作的小熊,现在我将实现点击一下舞台区域,就变化一次动作,并且将动作名字输出给大厅。
布局好了基本的场景元素,创建Game2Logic的组件脚本,先实现点击舞台变化动作的功能,这些代码并不复杂,所以就暂时略过,参看后面的完整代码。
下一步就是实现前面的MainController接口,由于子游戏会运行在大厅环境中,并且可能会有很多游戏使用,所以它可以作为公共代码存在,完全没有必要将它也输出,现在我们建立一下相关的代码文件。
新建common目录:
新建
IMainController.d.ts
文件,对照大厅实现接口代码,借助一下Window的公共接口声明来达到调试类应用的目的,在这里我又弄了一个调试用的DebugMainController。
IMainController.d.ts
declare interface IMainController {
outString(str: string): void;
}
declare interface Window{
debugMainCtrl:IMainController;
}
DebugMainController.ts
class DebugMainController implements IMainController{
outString(str: string): void {
console.warn('Method is debug,str is ' + str);
}
}
if(!window.debugMainCtrl){
window.debugMainCtrl = new DebugMainController();
}
这个做法是为了当不在大厅的时候,本地调试的功能可以来测试真实的反馈,现在我们到Game2Logic的组件脚本中,先把名字给提取出来作为变量,然后我们通过获取当前场景的根节点进行组件遍历查找,getComponentInChildren
获得大厅的组件脚本,还记得大厅组件已经接口实现了吧,如果找到就用它,如果没有找到就使用调试类,然后作输出。
MainScript3.ts
const {ccclass, property} = cc._decorator;
@ccclass
export default class Game2Logic extends cc.Component {
@property(dragonBones.ArmatureDisplay)
actor:dragonBones.ArmatureDisplay = null;
start () {
this.node.on(cc.Node.EventType.TOUCH_END,this.onTouchEnd,this);
this.node.on("ActorAnimationPlay",this.onActorAnimationPlay,this);
}
onDestroy(){
this.node.off(cc.Node.EventType.TOUCH_END,this.onTouchEnd,this);
this.node.off("ActorAnimationPlay",this.onActorAnimationPlay,this);
}
private onActorAnimationPlay(aniname:string){
this.actor.playAnimation(aniname,-1);
}
private index = 0;
private onTouchEnd(){
const arr = this.actor.getAnimationNames("ubbie");
const aniName = arr[this.index % arr.length];
this.node.emit("ActorAnimationPlay",aniName);
this.index += 1;
let mainCtrl:IMainController = cc.director.getScene().getComponentInChildren('MainControllerScript');
if(!mainCtrl){
mainCtrl = window.debugMainCtrl;
}
mainCtrl.outString(aniName);
}
}
现在调试一下看看效果,可以看到它确实调用的是本地调试类的方法
在上面的代码中,加入了一个动画播放的事件监听,事件名为
ActorAnimationPlay
,这个监听主要是用来从大厅项目向子游戏通讯用的,具体细节后面详述。
下一步设置子游戏包,详细步骤参考例子二,并且将舞台做成一个Prefab,然后在build一下,在build目录下asset中复制或者移动Game2到HttpServer项目中,刷新一下页面看到有了Game2即可,记录一下名字中间的版本编号,更新到对应的代码中。
运行一下基本上可以能够得到如例子二一样的结果,只不过目前只是单向的,即子游戏向大厅调用,反过来也是一样,主场景也能调用子游戏代码,用事件是一个很好的办法,因此我上面加入了 ActorAnimationPlay
这个事件名的监听,用这个事件来实现控制子游戏的小熊动画,具体代码请参看后续代码,不明白的,可以看代码以及官方文档当中有关事件的部分,子游戏也可以用事件的方式来处理向大厅通讯,但是按照我的经验来看,写接口调用的方式会更加严谨,也比较容易排查错误,有时候甚至还得用上Promise异步,如果真的是需要用上事件,也最好封装一下。
因此,最终 MainScript3.ts
的代码如下:
const {ccclass, property} = cc._decorator;
@ccclass
export default class MainScript3 extends cc.Component {
@property(cc.ProgressBar)
progressBar:cc.ProgressBar = null;
@property(cc.Node)
target1:cc.Node = null;
private _bundle:cc.AssetManager.Bundle;
start () {
this.progressBar.progress = 0;
this.target1.active = false;
}
onClickLoad(){
const options = {
version:"78969",
onFileProgress:(n,t)=>{
this.progressBar.progress = n / t;
}
}
cc.assetManager.loadBundle('http://127.0.0.1:8080/Game2',
options,
(err,bundle)=>{
if(!err){
this._bundle = bundle;
this.target1.active = true;
}
});
}
onClickSceneTo(e:cc.Event.EventTouch){
// cc.director.loadScene('Game1');
e.currentTarget.active = false;
this._bundle.load("prefab/Game2Stage",cc.Prefab,(err,asset:cc.Prefab)=>{
if(!err){
this.target1.addChild(this._gameStage = cc.instantiate(asset));
}
});
}
private _gameStage:cc.Node;
onClickActonWalk(){
this._gameStage.emit("ActorAnimationPlay","walk");
}
onClickActonStand(){
this._gameStage.emit("ActorAnimationPlay","stand");
}
}
Main3的场景结果大致为这样的
可能有一些细节需要再作修正,不过我感觉已经很详细了,项目源码和视频已经准备好了,可以详细了解。
注意事项
- 第一是各个bundle中的代码中不要有一样的类名,或者全局变量名,这样的代码会在读取bundle后直接报重名错误;
- 第二是bundle包代码尽量不要互相引用,如果你的业务需求必须这样做,应该用设置载入优先级解决,但只能解决在同一个项目中的bundle 读取,跨项目使用还是得自己控制先后顺序,建议可以把通用代码整合成一个包,在开始的时候读下来;
- 第三是跨bundle的资源尽量互相保持独立,对象管理只是一方面,关键是有一些不可预期的奇怪错误,往往会从缓存和释放的地方出问题。
总结
bundle的方式是一个好东西,游戏行业总是再想办法尽可能缩短用户体验到游戏内容的时长,从而降低因等待造成的流失成本,CocosCreator的bundle包,不止可以应用到大厅和子游戏模式,它还比较适用于推进式关卡,人物角色形象包,教育用的图书绘本等等,相信有了上面的例子,您对bundle的使用一定能够更进一步理解。
文稿内容就到这里了,这个手撸Bundle当然还有视频版本,配有语音讲解,工程已经放在Git当中
GIT 地址
https://github.com/Nowpaper/CreatorBundleTest
视频地址
爆肝剪辑中~~~
如果您喜欢我的文章或视频,请给个点赞+关注,帖子回复个“支持”,那将是对我莫大的鼓励,我是Nowpaper,一爸作游戏,越来越有戏~~ 我们下次再见!