在去年 Cocos Creator 2.4 發(fā)布的時候,Cocos 老朋友 B 站 up 主 Nowpaper 為我們做了一次版本更新的盤點,對其中 Asset Manager 和 Bundle 部分專門做了重點盤點,并用一個小例子展示能力,講解了一波應(yīng)用場景。視頻非常受歡迎,也收到了許多小伙伴更多關(guān)于實際使用問題的咨詢。Nowpaper 發(fā)現(xiàn)很多問題是由于使用方法不對造成的,所以他抽時間專門撰寫本篇文章,詳細講解 Bundle 的使用方法,并通過三個實際應(yīng)用展示。歡迎閱讀!此外,Nowpaper 曾做客我們 Cocos 人物志接受專訪,感興趣的同學(xué)可以點擊閱讀噢。例子展示
手?jǐn)]工程
2.1 同項目 Bundle,動態(tài)加載
2.2 跨項目 Bundle,大廳+子游戲
2.3 跨項目 Bundle,代碼互調(diào)
踩坑總結(jié)
本文有視頻版本,目前還未剪輯完成,敬請期待發(fā)布完成。工程項目已發(fā)布至 GitHub 地址在文章后面 ╮( ̄▽  ̄)╭我將完成結(jié)果先放在前面,幫助各位可以快速找到自己所需要的部分。第一個例子,同項目 Bundle,動態(tài)加載 直接加載內(nèi)部的 Bundle 包,它是項目本身的某個目錄,只不過被設(shè)置為 Bundle,里面包含了動畫、腳本、場景、資源等。我們可以看到,項目運行起來的時候,被設(shè)置為 Bundle 的部分,不會被加載,只有主動加載的時候才會載入,通過 API 也可以讀取內(nèi)部的素材,就像我們現(xiàn)在做的這樣,讀取了一個 Prefab 創(chuàng)建成 Node,還有就是讀取一張圖片顯示出來。加載另外一個項目的 Bundle 包,它沒有和主項目在一起,而是另外一個 Cocos Creator 項目的一部分,我把生成的素材放在一個資源服務(wù)器上,讓主項目去載入它,然后運行,通過遠程載入創(chuàng)建出來,它是實現(xiàn)動態(tài)加載的核心,一會兒后面咱們詳細講解。第三個例子,跨項目 Bundle,代碼互相調(diào)方案不同 Bundle 內(nèi)資源或代碼的互相調(diào)用測試,這個例子主項目為子游戲 Bundle 提供調(diào)用接口,我們可以看到除了能調(diào)用主項目,也可以通過一些方法被調(diào)用,那么怎么做到呢?且聽后面分解。看完例子,那么我們把上面的全部手?jǐn)]一遍,注意本視頻中的所有代碼為 TypeScript,使用的是 Cocos Creator 2.4.5。首先我們先創(chuàng)建一個 Cocos Creator 項目,這個項目必須使用的 v2.4.0 以上的版本創(chuàng)建,這樣才能有 Bundle 的特性,項目名稱叫做 BundleLobby。打開項目,建立一些目錄和場景,隨便來一個目錄就叫:aaa 吧。這個名字越隨便越好,以后就會設(shè)置成為 Bundle,在它下面建立一些基本的項目目錄,比如 res、src,用來存放你的資源和代碼。創(chuàng)建一個主場景叫做 Main,搭建一下基本的 UI 功能,這里做了兩個按鈕:一個用來讀取,一個用來跳轉(zhuǎn),還有一個進度條,用來表示 Bundle 的讀取成功與否。現(xiàn)在為 aaa 的目錄里增加一些素材,創(chuàng)建一個 aaa 的場景,簡單擺放一下,為了方便展示它的復(fù)雜性,所以我加入了一個 Spine 動畫,并且把 spineboy 制作成了一個 Prefab。先實現(xiàn)一下基本的場景跳轉(zhuǎn),也就是沒有把它配置成為 Bundle 的情況下,它只是整個項目的一部分,寫一些代碼讓 Main 和 aaa 兩個場景互相跳轉(zhuǎn),需要完成兩個組件代碼,我這里為 aaa 場景和 Main 場景各自加入了一個組件 Script,并且為各自的場景添加了場景跳轉(zhuǎn)代碼:onClickSceneTo(){ cc.director.loadScene('aaa');}
這種跳轉(zhuǎn)是最普通的情況,當(dāng)把 aaa 這個目錄給配置成為 Bundle 之后,就不一樣了。此時跳轉(zhuǎn)到 aaa 的按鈕已經(jīng)不管用了,我們看調(diào)試信息里已經(jīng)給出了報錯信息,它找不到名為 aaa 的場景資源。這是因為 Bundle 資源不會在啟動的時候加載,而是需要用 assetManager 的 loadBundle,所以我們?yōu)樽x取按鈕添加一個 Click 事件,并且實現(xiàn)如下代碼:onClickLoad(){ cc.assetManager.loadBundle('aaa',(err,bundle)=>{ if(!err){ this.progressBar.progress = 1; } });}
運行項目后,先點擊讀取,讀取成功之后再點擊跳轉(zhuǎn) aaa,就會跳轉(zhuǎn)到對應(yīng)的場景當(dāng)中,不會報錯。既然基本的場景已經(jīng)實現(xiàn),那么能否更進一步,從 Bundle 包里面讀取資源呢?我們需要對 Main 場景進行改造和調(diào)整一下,并且組件腳本中的對應(yīng)處理也需要調(diào)整,在這里,我使用了兩個空 Node 來顯示讀取出的 Prefab 和圖片,它們分別叫 target1 和 target2,場景結(jié)構(gòu)大概是這個樣子:
const {ccclass, property} = cc._decorator;@ccclassexport 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); } }); }}
在第一個例子中,通過把一個目錄設(shè)置成為 Bundle,實現(xiàn)動態(tài)加載資源內(nèi)容的能力,這也是 2.4 版本最讓開發(fā)者們興奮的更新,它能大大地加強游戲的載入體驗。而它的強大不止于此,官方文檔中明確說明,它可以把其他項目的 Bundle 給載入進來,下面第二個例子我們就來試試這個功能。現(xiàn)在計劃把這個項目當(dāng)成大廳場景,通過 loadBundle 完成對子游戲的 Bundle 加載,對 BundleLobby 的項目改造一下。為了方便區(qū)分和展示,先把 Main 給重命名為 Main1,以及 MainScript 改名為 MainScript1,新建一個場景叫 Main2,然后重新建立一個組件腳本,名字叫 MainScript2,把一些代碼從 1 復(fù)制到 2 當(dāng)中。這是為了避免重寫,先不用著急改代碼,等子項目完成后回來再說,同樣的,Main2 場景當(dāng)中的一些界面元素,直接復(fù)用即可,只保留一個 target 的 Node 當(dāng)容器,為兩個按鈕指定 Click 事件,作為大廳的項目已經(jīng)準(zhǔn)備好了。關(guān)掉主項目,下一步要完成子項目,新建一個 Games 的 Cocos Creator 2.4 以上版本的項目,然后建立一個目錄,名字就叫 Game1 吧,未來還得有 Game2。我將會選擇一個相對比較有趣的內(nèi)容用于展示,內(nèi)容盡量和大廳工程不一樣,在后面的展示中,將不止是場景跳轉(zhuǎn),還有從內(nèi)部創(chuàng)建,從而讓它更像是一個動態(tài)載入進來的小游戲,這個部分我作了一些簡單的開發(fā),具體細節(jié)略過,它是一個一直在推進的連續(xù)場景,看起來不錯,也很酷。我們把游戲內(nèi)容的舞臺部分制作成為 Prefab,當(dāng)然了你要注意把邏輯腳本掛載在 Prefab 這個節(jié)點上,不然的話,嘿嘿嘿......現(xiàn)在一個最簡單的子游戲創(chuàng)建好了,復(fù)雜的內(nèi)容,咱們放在第三個例子中詳細說,現(xiàn)在直接 Build 一下,點擊主菜單中的“項目 -> 構(gòu)建發(fā)布”進行構(gòu)建,目標(biāo)平臺為了方便測試先選擇 Web Mobile,稍加等待之后,完成。構(gòu)建完成后,進入到項目目錄,找到 \build\web-mobile\assets 下面:這里有個 Game1,它就是 Bundle 包了,其他的都不需要,咱們只需要這個部分。
但是,跨項目讀取必須通過遠程方式,所以你需要用一個小服務(wù)器來當(dāng)資源服務(wù)器,在我的項目中提供了一個小網(wǎng)站 Node 項目,來實現(xiàn)對它的遠程讀取,方便主項目遠程加載,這個工程和大廳、子游戲放在了同一個地方,名字叫 RemoteHttpServer 的目錄。你也可以用別的方式,取決于您的喜好。把 Build 下面的 asset 下的 Game1 移動或復(fù)制到這個資源 server 下,確保能夠通過網(wǎng)絡(luò)能夠訪問到它。切換 Cocos Creator 的項目到 BundleLobby 下,要對遠程的 Bundle 進行加載了,我們打開代碼 MainScript2.ts,使用第一個例子中的同樣方法 loadBundle,但是需要改成遠程資源 URL。在我的例子中,遠程 Bundle 在 127.0.0.1:8080/Game1 當(dāng)中,所以代碼讀取需要修改,同時,由于子游戲在 Build 的時候為文件加了 MD5 標(biāo)記,所以直接打開是不行的,需要借助可選參數(shù)的 version 字段來解決這個問題,因此最終的代碼如下:const {ccclass, property} = cc._decorator;
@ccclassexport 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)); } }); }}
到目前為之,第二個例子已經(jīng)結(jié)束了,雖然已經(jīng)完成了遠程包體的載入流程,但是真正實現(xiàn)一個大廳加子游戲,或者動態(tài)功能模塊的話,似乎差了一些什么。這種項目需求是要求子包和大廳之間的代碼調(diào)用,或者互相通訊,下面我們開始嘗試用第三個例子來解決這個問題。在這之前,我們可能需要了解和梳理 Bundle 的機制,在官方文檔中描述 Asset Bundle 的構(gòu)造提到,內(nèi)容分為代碼和資源兩個部分,資源的入口是 config.json,代碼入口為 index.js。按照我的測試結(jié)果來看,Bundle 在下載成功后,會立即將 index.js 中的代碼加入到主包中,打開這個文件看看就能猜到個大概。因此,我們只需要設(shè)計大廳接口,在子游戲中實現(xiàn)同樣的接口,最后不把它們 Build 到 Bundle 即可。設(shè)計思路大致為主包大廳和 Bundle 子游戲內(nèi)創(chuàng)建的控制組件,并開發(fā)通用接口,互相之間通過這種方法調(diào)用,為了開發(fā)的便捷性,可以為子游戲中創(chuàng)建虛擬的接口類,實現(xiàn)獨立開發(fā)的能力。在大廳項目中,我們新建一個場景 Main3 和 MainScript3 組件腳本,并且按照之前 Main2 樣子搭建,有一些部分還得需要結(jié)合子游戲修改,先放在這里,現(xiàn)在用 VS Code 在 src 目錄中,實現(xiàn)一個接口文件,就叫 IMainController 吧,我這里就簡單實現(xiàn)一個輸出文本接口:export interface IMainController { outString(str: string): void;}
在實際項目中,接口可能要比這個復(fù)雜的多,主要看你的項目需求,現(xiàn)在我們再建立一個 MainController 的組件腳本。為了區(qū)分,我加上了 Script 為后綴,實現(xiàn)基礎(chǔ)的組件類代碼,并且實現(xiàn) IMainController 的接口,回到 Main3 的場景中,為 Canvas 掛上剛剛的組件,在場景中創(chuàng)建并且指定一個 cc.Label 作為輸出組件,現(xiàn)在主場景已經(jīng)準(zhǔn)備好了。import { IMainController } from "./IMainController";
const {ccclass, property} = cc._decorator;
@ccclassexport default class MainControllerScript extends cc.Component implements IMainController { @property(cc.Label) outLabel:cc.Label = null; outString(str: string): void { this.outLabel.string = str; } }
下一步開發(fā)子游戲,關(guān)閉大廳項目,打開子游戲項目,為了避免和之前的重復(fù),新建一個 Game2 文件夾,放進去了一個龍骨制作的小熊,現(xiàn)在我將實現(xiàn)點擊一下舞臺區(qū)域,就變化一次動作,并且將動作名字輸出給大廳。布局好基本的場景元素,創(chuàng)建 Game2Logic 的組件腳本,先實現(xiàn)點擊舞臺變化動作的功能,這些代碼并不復(fù)雜,所以就暫時略過,參考后面的完整代碼。下一步就是實現(xiàn)前面的 MainController 接口,由于子游戲會運行在大廳環(huán)境中,并且可能會有很多游戲使用,所以它可以作為公共代碼存在,完全沒有必要將它也輸出,現(xiàn)在我們建立一下相關(guān)的代碼文件。新建 IMainController.d.ts 文件,對照大廳實現(xiàn)接口代碼,借助一下 Window 的公共接口聲明來達到調(diào)試類應(yīng)用的目的,在這里我又弄了一個調(diào)試用的 DebugMainController。declare interface IMainController { outString(str: string): void;}declare interface Window{ debugMainCtrl:IMainController;}
class DebugMainController implements IMainController{ outString(str: string): void { console.warn('Method is debug,str is ' + str); }}if(!window.debugMainCtrl){ window.debugMainCtrl = new DebugMainController();}
這個做法是為了當(dāng)不在大廳的時候,本地調(diào)試的功能可以來測試真實的反饋。現(xiàn)在我們到 Game2Logic 的組件腳本中,先把名字給提取出來作為變量,然后我們通過獲取當(dāng)前場景的根節(jié)點進行組件遍歷查找,getComponentInChildren 獲得大廳的組件腳本。還記得大廳組件已經(jīng)接口實現(xiàn)了吧,如果找到就用它,如果沒有找到就使用調(diào)試類,然后作輸出。
const {ccclass, property} = cc._decorator;@ccclassexport 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); }}
現(xiàn)在調(diào)試一下看看效果,可以看到它確實調(diào)用的是本地調(diào)試類的方法。 在上面的代碼中,加入了一個動畫播放的事件監(jiān)聽,事件名為 ActorAnimationPlay,這個監(jiān)聽主要是用來從大廳項目向子游戲通訊用的,具體細節(jié)后面詳述。下一步設(shè)置子游戲包,詳細步驟參考例子二,并且將舞臺做成一個 Prefab,然后再 Build 一下,在 Build 目錄下 asset 中復(fù)制或者移動 Game2 到 HttpServer 項目中,刷新一下頁面看到有了 Game2 即可,記錄一下名字中間的版本編號,更新到對應(yīng)的代碼中。運行一下基本上可以能夠得到如例子二一樣的結(jié)果,只不過目前只是單向的,即子游戲向大廳調(diào)用,反過來也是一樣,主場景也能調(diào)用子游戲代碼,用事件是一個很好的辦法,因此我上面加入了 ActorAnimationPlay 這個事件名的監(jiān)聽,用這個事件來實現(xiàn)控制子游戲的小熊動畫,具體代碼請參看后續(xù)代碼。不明白的,可以看代碼以及官方文檔當(dāng)中有關(guān)事件的部分,子游戲也可以用事件的方式來處理向大廳通訊。但是按照我的經(jīng)驗來看,寫接口調(diào)用的方式會更加嚴(yán)謹(jǐn),也比較容易排查錯誤,有時候甚至還得用上 Promise 異步,如果真的是需要用上事件,也最好封裝一下。因此,最終 MainScript3.ts 的代碼如下:const {ccclass, property} = cc._decorator;
@ccclassexport 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){ 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 的場景結(jié)構(gòu)大致為這樣的:可能有一些細節(jié)需要再作修正,不過我感覺已經(jīng)很詳細了,項目源碼和視頻已經(jīng)準(zhǔn)備好了,可以進一步了解。第二是 Bundle 包代碼盡量不要互相引用。如果你的業(yè)務(wù)需求必須這樣做,應(yīng)該用設(shè)置載入優(yōu)先級解決。但只能解決在同一個項目中的 Bundle 讀取,跨項目使用還是得自己控制先后順序。建議可以把通用代碼整合成一個包,在開始的時候讀下來。
第三是跨 Bundle 的資源盡量互相保持獨立,對象管理只是一方面,關(guān)鍵是有一些不可預(yù)期的奇怪錯誤,往往會從緩存和釋放的地方出問題。
Bundle 的方式是一個好東西,游戲行業(yè)總是想辦法盡可能縮短用戶進入游戲以及游戲加載內(nèi)容的時長,從而降低因等待造成的流失成本。Cocos Creator 的 Bundle 包,不僅可以應(yīng)用到大廳和子游戲模式,還比較適用于推進式關(guān)卡、人物角色形象包、教育用的圖書繪本等等,相信有了上面的例子,你對 Bundle 的使用一定有更進一步的理解。文章內(nèi)容就到這里了,這個手?jǐn)] Bundle 還有視頻版本,配有語音講解,項目工程已經(jīng)放在 GitHub 當(dāng)中。https://github.com/Nowpaper/CreatorBundleTest
以上就是本期教程啦,感謝 Nowpaper 的傾情分享,他的講解視頻在爆肝剪輯中,如果大家對他的文章或視頻感興趣,戳【閱讀原文】前往他的 B 站首頁了解更多噢~