凌波微步有云:
此步法精妙異常,習者可以用來躲避眾多敵人的進攻,此外「凌波微步」每踏出一步,都與內力息息相關,決非單是邁步行走而已,若無內功根基之人,將「凌波微步」強行走將起來,會造成自絕經脈的危境。
一、英雄的窘境
上篇《英雄之舞—迷蹤“安可心”》一章中,我們研習了迷蹤步,runAction是建立英雄與安可心之間的鏈接,最后還學習了逍遙訣,而逍遙訣則是建立英雄與萬物之間鏈接。
1. 多人動作協同
多人指的是多個節點,當兩個節點在舞步中有先后次序時,我們有那些可控制的方法呢?來看下面這段演示:
上圖是一個男孩與女孩的故事,我們的重點不是講故事,而是講他們發生的動作,研究相對高效可控的舞步控制手段。
言歸正傳,演示中男孩與Label,一前一后,使用逍遙訣cc.callFunc很容易控制,同時在一個完整動作完畢時,使用一個完成回調,顯示行動完成,請看代碼:
//_moveAndCall函數分享具體的節點,具體的迷蹤步就不贅述了
//參數1:移動的節點
//參數2:移動的位置
//參數3:要說的話
//參數4:動作完成回調
this._moveAndCall(this._boy, cc.p(this._boy.x, 200),'妹妹快過來!', () => {
this.log('呼叫妹妹完畢');
});
函數比較簡單,_moveAndCall主要是迷蹤步的封裝,細節這里不表,我們繼續看女孩的回答:
女孩做了相同的動作,這里我們可以復用this._moveAndCall方法
this._moveAndCall(this._boy, '妹妹快過來!', () => {
this._moveAndCall(this._gril, '喊我過來做啥子嘛!', () => {
this.log('妹妹回答完畢');
});
});
我們高效地的利用_moveAndCall的最后一個回調,讓女孩即時做出了回應,繼續看他們的完整互動:
男孩向女孩提出了一個無理的要求,女孩大怒,大喝一聲,一招“大海無量”,被女孩給揍飛了!再次聲明,故事不重點,節奏控制才是我們的重點:
//男孩對女孩說:妹妹快過來!
this._moveAndCall(this._boy, cc.p(this._boy.x, 200), '妹妹快過來!', () => {
//女孩回答
this._moveAndCall(this._gril, cc.p(this._gril.x, 200), '喊我做啥子!', () => {
//男孩對女孩提出無理要求
this._boy.$Hero.sing('我要親親!', () => {
//女孩大怒
this._gril.$Hero.sing('流氓,看招', () => {
//女孩準備發招
this._gril.$Hero.sing('大海無量', () => {
//女孩向男孩發起攻擊
this._gril.$Hero.attack(this._boy, () => {
//男孩被打暈
this._boy.runAction(cc.rotateBy(2, 1000));
//同時被打跑了
this._moveAndCall(this._boy, this._boyPt, '不要啊...!', () => {
cc.log('流氓被妹妹揍了!');
})
});
});
})
});
})
});
需要說明一下,這里的_boy和_gril是兩個預制node,綁定了一個Hero的魔靈(組件),同時這個代碼中使用了uikiller,所以可以直接用$Xxx訪問節點上的組件(具體細節請參考《雷神之錘》),node.$xxx與node.getComponent(‘xxx’) 是同樣的功能。
Hero魔靈提供了sing\attcak方法,除了必要的參數外,還提供了一個完成回調,通過這種層層回調,可以嚴格地控制多人舞步的順序,代碼排版呈現出">"形!
2. 面臨大敵
男孩被打飛了,他非常地不甘心,經過深刻總結與勤奮修練,準備再來一次:
男孩利用『乾坤大挪移』輕松化解了女孩的『大海無量』,并轉換成了愛心!再次提醒,邏輯控制才是重點,請看下面代碼:
this._moveAndCall(this._boy, cc.p(this._boy.x, 200), '妹妹快過來!', () => {
this._moveAndCall(this._gril, cc.p(this._gril.x, 200), '喊我做啥子!', () => {
this._boy.$Hero.sing('我想與你聊聊人生!', () => {
this._gril.$Hero.sing('流氓,看招', () => {
this._gril.$Hero.sing('大海無量', () => {
cc.director.getScheduler().setTimeScale(0.3);
/**
*注意段代碼:
*在Hero上有一個onWeaponEvent事件,這里轉換攻擊為愛心
*/
this._gril.$Hero.onWeaponEvent = (weapon) => {
let delayTime = cc.delayTime(1);
let delayTime = cc.delayTime(1);
let pt = cc.p(_.random(-200, 200), _.random(-200, 200));
weapon.string = ";";
weapon.node.color = cc.Color.RED;
pt.x += _.random(-200, 200);
pt.y += _.random(-200, 200);
let moveTo = cc.moveTo(1, pt).easing(cc.easeCircleActionInOut(0.5));
weapon.node.runAction(cc.sequence(moveTo, delayTime, cc.removeSelf()));
};
this._boy.$Hero.sing('乾坤大挪移');
this._gril.$Hero.attack(this._boy, () => {
cc.director.getScheduler().setTimeScale(2);
this._moveAndCall(this._gril, this._grilPt, "暈,遇到個瘋子!", () => {
this.log('行動完畢');
});
});
});
})
});
})
});
上面的代碼越來越多了,不好意思,看起來可能會比較累!這里簡單講解一下Hero.attack,它會發射出許多的武器節點,其實是用Lable + BFMFont的方案:
這里使用了GlyphDesigner這個字體成生工具
還需要注意Hero.onWeaponEvent事件函數,用于監聽女孩發出的招數,此處給Label.string設置了新值,同時改變節點的顏色:
//分號對應了愛心圖案
weapon.string = ";";
//設置成紅色,在演示中其實BFM字體用的是白色,這樣可以通過node.color進行疊色
weapon.node.color = cc.Color.RED;
為了把女孩的發招過程演示的更加清晰,我還特地放慢鏡頭:
//使用setTimeScale函數進行整個游戲的時間縮放
cc.director.getScheduler().setTimeScale(0.3);
其中參數0.3表示放慢到0.3倍的速度,如果是2則是2倍速。
?男孩這次是有備而來,悲催的是女孩被包圍的愛心給嚇跑了!
二、 窘境中的思考
男孩百思不得其解,再回頭看看我們的控制代碼!我想聰明的你多半已經明白了,我們正踏入了:Call Hell !
1. 地獄之路
call hell,又稱之為:回調地獄
由于舞步完成回調是異步響應,每一層的回調都需要依賴上一層的回調執行完成,形成了層層嵌套的關系,最終造成類似上面的回調地獄!
任何舞步都是英雄在一定時間上的形態變化,多個節點之間的協同最核心的是在時間上的同步與空間上協調!
男孩之前也算把迷蹤步給研習精通了,也能靈活運用逍遙訣,但面對流程較長,節點較多的多人舞步,總是感覺力不從心,此刻想起『凌波微步』有言:
每踏出一步,都與內力息息相關,決非單是邁步行走而已,若無內功根基之人,將「凌波微步」強行走將起來,會造成自絕經脈的危境。
男孩之前一直沒有領悟文中之意,此刻一股寒襲來:
每踏出的一步,難道就是回調函數嗎?
簡單的邁步行走,就是走進了一層層回調?
強行走將起來,不就進入了回調地獄,造成自絕經脈的危境?
2. 心靈感應
男孩輾轉反側難以入眠,仔細回憶著與女孩過招的每一幀,發現女孩的『大海無量』有些蹊蹺。大量的Label節點不斷涌出一個接著一個,幸好女孩只是個新手,一招『大海無量』施展出來只能算是娟娟細流!
回家后女孩心想,自己的『大海無量』從來沒失過手,怎么會被輕易化解了呢?
“下次讓我再遇到這種人,一定將他打個半死!”,女孩一邊想著,一邊開始分析其中的破綻:
/**
* 攻擊函數
* @param {Node} target 要攻擊的目標節點
* @param {Function} cb 攻擊完畢的回調函數
*/
attack(target, cb) {
//攻擊關生的最大節點數,怪不得威力不大才20個
let num = 20;
let array = [];
//循環生成20個預制節點
for(let i = 0; i < num; i++) {
let weapon = cc.instantiate(this.weapon);
//預制是一個Label組件,隨機設置string屬性
weapon.getComponent(cc.Label).string = _.sample(WEAPON);
//添加到父節點讓它可見
this._weapons.addChild(weapon);
//將所有weapon放入一個數組
array.push(weapon);
}
//關鍵來了:async.eachOfLimit用于異步控制,一次做次發射3動作
async.eachOfLimit(array, 3, (weapon, i, cb) => {
//向目標target扔出武器
this._throwWeapon(target, weapon, cb);
}, cb);
},
請打起十二分的精神注意async.eachOfLimit函數,它正是一記大招:
async.eachOfLimit(array, 3, (weapon, i, callback) => {
...
}, (error) => {
...
});
男孩似夢非夢之中將女孩的一招一式看的清清楚楚,async是異步,each是遍歷,limit是并發控制,遍歷的是array。完整詮釋就是,遍歷array數組中的元素,一次拿3個調用迭代函數,當3次迭代函數異步返回,又開始新一輪。
重點是async.eachOfLimit的第三個參數,稱之為迭代函數,迭代函數的第一個參數weapon是array中的一元素,i是weapon在array中的下標,最后一個callback回調,因為要做的是節點的連綿飛行,當一個節點飛出一定距離,調用callback告訴eachOfLimit一次異步任務完成。我們這里是一次打出三個節點,當三個節點都調用了callback后,eachOfLimit繼續調用迭代器函數,進行下一輪的任務。
當array中的所有元素被迭代函數執行完畢后,eachOfLimit第四個參數會被響應,此時所有任務完成。
女孩把『大海無量』在腦子里溫習過了一遍,她發現了招數威力不大的原因:一是節點數量較少只有20個,二是并發一次只有3個節點。
與此同時,男孩的腦子里就像播放錄象一樣,將女孩的『大海無量』也觀看了一遍,一字一句,清晰無比!男孩驚嘆地發現原來:“async.js就是的『凌波微步』!”
三、凌波微步
男孩讀取到女孩的思考,不知不覺中學會了eachOfLimit,更重要的是他發現async.js就是『凌波微步』這個秘密,他現在唯一想做的就是擼起袖子開干!
請先看解劇情發展,gif太大效果不好切換成視頻: http://v.youku.com/v_show/id_XMzE3OTg0OTgyNA==.html#paction
(慘了,Markdown不知道怎么插入視頻)
1. 飛鳧若神—async.series
男孩不知從那里藝成歸來(我猜多半是奎特爾星球上),這次的逼格完全上升了N個檔次!
async.series([
cb => this._moveAndCall(this._boy, cc.p(this._boy.x, 200), '妹妹快過來!', cb),
cb => this._moveAndCall(this._gril, cc.p(this._gril.x, 200), '喊我做啥子!', cb),
cb => {
this.log('男孩這次開始吟詩了...');
this._boy.$Hero.sing('仿佛兮若輕云之蔽月', cb);
},
cb => {
this.log('女孩,還是同樣的暴脾氣...');
this._gril.$Hero.sing('流氓,看招', cb);
},
cb => this._gril.$Hero.sing('大海無量', cb),
...
]);
男孩對行云流水的代碼發出了贊嘆“仿佛兮若輕云之蔽月”,async.series可以將多個異步函數串行執行,每一個函數都有一個cb(callback)回調參數,當異步動作完成需要執行下callback回調,數組中的下一個異步函數接著執行!代碼排版不在像之前像個頂著大肚子油膩的老男人了。
可能有人看不明白這里的”=>”,它被我稱之為一陽指(箭頭函數),這里為了方便大家,再給一個老式的寫法:
var self = this;
async.series([
function(cb) {
self._moveAndCall(self._boy, cc.p(self._boy.x, 200), '妹妹快過來!', cb);
},
function(cb) {
self._moveAndCall(self._gril, cc.p(self._gril.x, 200), '喊我做啥子!', cb);
},
function(cb) {
self.log('男孩這次開始吟詩了...');
self._boy.$Hero.sing('仿佛兮若輕云之蔽月', cb);
},
function(cb) {
self.log('女孩,還是同樣的暴脾氣...');
self._gril.$Hero.sing('流氓,看招', cb);
},
function(cb) {
self._gril.$Hero.sing('大海無量', cb);
}
...
]);
async.series除了可以串行執行一個數組中的函數外,還支持對象作為參數:
async.series({
//男孩說
boySaid: cb => this._moveAndCall(this._boy, cc.p(this._boy.x, 200), '妹妹快過來!', cb),
//女孩說
grilSaid: cb => this._moveAndCall(this._gril, cc.p(this._gril.x, 200), '喊我做啥子!', cb),
//男孩吟詩
boyPoetry: cb => {
this.log('男孩這次開始吟詩了...');
this._boy.$Hero.sing('仿佛兮若輕云之蔽月', cb);
},
//女孩發怒
grilAngy: cb => {
this.log('女孩,還是同樣的暴脾氣...');
this._gril.$Hero.sing('流氓,看招', cb);
},
//女孩吟唱準備發招
grilSing: cb => this._gril.$Hero.sing('大海無量', cb),
...
});
async.series使用對象做為參數,key為舞步名,value必須是異步函數,在這個函數中執行舞步動作。在一段舞步完成之后記得調用cb回調,告訴async.series當前任務完畢,請執行下一個任務。
2. 微步生塵—async.eachSeries
繼續解讀下面的舞步:
async.series([
...接上面series中的代碼...
//女孩發起攻擊,具體操作封裝在this._grilAttackBoy函數中
cb => this._grilAttackBoy(cb),
//攻擊完畢,男孩繼續吟詩
cb => {
this.log('男孩繼續吟詩...');
this._boy.$Hero.sing('體迅飛鳧,飄忽若神', () => {
this._boy.$Hero.sing('凌波微步,羅襪生塵', cb);
});
},
//女孩見狀驚訝,開始搭話...
cb => {
this.log('對白....');
//注意eachSeries
async.eachSeries([
{node: this._gril, text:'啊!「凌波微步」'},
{node: this._boy, text:'妹妹也曉得「凌波微步」?'},
{node: this._gril, text:'有所耳聞,但未見過...'},
{node: this._boy, text:'你想學嗎?'},
{node: this._gril, text:'好呀!好呀!'},
{node: this._boy, text:'請關注『奎特爾星球』微信公眾號吧!'},
], (item, cb) => {
item.node.$Hero.sing(item.text, cb);
}, cb);
},
//顯示奎特爾星球二維碼
(cb) => {
this._qr.active = true;
this._qr.runAction(cc.sequence(cc.rotateBy(2, 360*6), cc.callFunc(() => cb())));
}
], () => {
cc.log('舞步結束');
});
終于與女孩搭上話了!我們將重點聚交在async.eachSeries函數上:
//女孩見狀驚訝,開始搭話...
cb => {
async.eachSeries([
{node: this._gril, text:'啊!「凌波微步」'},
{node: this._boy, text:'妹妹也曉得「凌波微步」?'},
{node: this._gril, text:'有所耳聞,但未見過...'},
{node: this._boy, text:'你想學嗎?'},
{node: this._gril, text:'好呀!好呀!'},
{node: this._boy, text:'請關注『奎特爾星球』微信公眾號吧!'},
], (item, cb) => {
item.node.$Hero.sing(item.text, cb);
}, cb);
}
async.eachSeries的第一個參數是一個數組,數組元素中的內容可以是任意類型。
第二個參數是一個迭代器函數,迭代器函數的第一個參數是之前數組中的元素,第二個參數是一個回調函數,這與之前講到的async.eachOfLimit差不多,async.eachOfLimit提供了并發控制參數,其實async.eachSeries就是并發控制為1的async.eachOfLimit,一次只拿數組中的一個元素交給迭代器函數,形成串行執行。
第三個參數是一個完成回調,數組中的所有元素被迭代器消耗完畢執行這個回調,在我們這里形成了一個async的嵌套調用。
async.series([
...
], () => {
cc.log('舞步結束');
})
async.series的最后一個參數,同樣是一個完成回調,整個多人舞步華麗結束!
結語
男孩與女孩的演出終于結束,兩個菜鳥演員,終于可以退場休息了!分享async.js在Cocos中應用的想法很早就有了,但一直沒付諸行動,有網友在公眾號上留言問什么時候出一篇使用async優雅處理動畫的教程,我當時一口就答應了。但從《英雄之舞—預告篇》開始到今天有20多天了,對此不好意思,我一拖再拖,來晚一步請見諒!
async.js教程在網上有很多,這篇文章算是給不熟悉的人引進門,我這只介紹了async.js的一點皮毛,async除了處理動畫以外,可以處理各種異步的任務,比如連續的網絡請求,客戶端的對話框交互等等。
本文的demo演示也準備好了點擊這里可以預覽,服務器是阿里云1核1G1M,水管比較小,加載可能會有點慢請海涵。如果覺得教程對你有幫助,分享給更多的朋友,謝謝!
歡迎關注「奎特爾星球」微信公眾號,有代碼、有教程、有視頻、有故事,一起玩來玩吧!