英雄之舞—凌波微步(利用async.js編寫異步動畫)

凌波微步

凌波微步有云:

此步法精妙異常,習者可以用來躲避眾多敵人的進攻,此外「凌波微步」每踏出一步,都與內力息息相關,決非單是邁步行走而已,若無內功根基之人,將「凌波微步」強行走將起來,會造成自絕經脈的危境。

一、英雄的窘境

上篇《英雄之舞—迷蹤“安可心”》一章中,我們研習了迷蹤步,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. 面臨大敵

男孩被打飛了,他非常地不甘心,經過深刻總結與勤奮修練,準備再來一次:


4.gif

男孩利用『乾坤大挪移』輕松化解了女孩的『大海無量』,并轉換成了愛心!再次提醒,邏輯控制才是重點,請看下面代碼:

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,水管比較小,加載可能會有點慢請海涵。如果覺得教程對你有幫助,分享給更多的朋友,謝謝!


歡迎關注「奎特爾星球」微信公眾號,有代碼、有教程、有視頻、有故事,一起玩來玩吧!

奎特爾星球
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容

  • 異步編程對JavaScript語言太重要。Javascript語言的執行環境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,325評論 5 22
  • 前言 人生苦多,快來 Kotlin ,快速學習Kotlin! 什么是Kotlin? Kotlin 是種靜態類型編程...
    任半生囂狂閱讀 26,249評論 9 118
  • 你不知道JS:異步 第四章:生成器(Generators) 在第二章,我們明確了采用回調表示異步流的兩個關鍵缺點:...
    purple_force閱讀 975評論 0 2
  • 選擇了一個能接近的陽光的位置,窗外的陽光正好透進來,忍不住想拍下這陽光,以及窗外陽光下的人。模糊記得高中某個時間貌...
    止一閱讀 127評論 0 0
  • 我希望我足夠強大,可以保護好所有我愛的人和事物。在別人侵犯自己專屬的國度時,可以毫不猶豫,狠狠地回擊。 這個世界太...
    零落成泥h閱讀 428評論 0 0