英雄之舞—凌波微步(利用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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,296评论 5 22
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,139评论 9 118
  • 你不知道JS:异步 第四章:生成器(Generators) 在第二章,我们明确了采用回调表示异步流的两个关键缺点:...
    purple_force阅读 946评论 0 2
  • 选择了一个能接近的阳光的位置,窗外的阳光正好透进来,忍不住想拍下这阳光,以及窗外阳光下的人。模糊记得高中某个时间貌...
    止一阅读 120评论 0 0
  • 我希望我足够强大,可以保护好所有我爱的人和事物。在别人侵犯自己专属的国度时,可以毫不犹豫,狠狠地回击。 这个世界太...
    零落成泥h阅读 395评论 0 0