凌波微步有云:
此步法精妙异常,习者可以用来躲避众多敌人的进攻,此外「凌波微步」每踏出一步,都与内力息息相关,决非单是迈步行走而已,若无内功根基之人,将「凌波微步」强行走将起来,会造成自绝经脉的危境。
一、英雄的窘境
上篇《英雄之舞—迷踪“安可心”》一章中,我们研习了迷踪步,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,水管比较小,加载可能会有点慢请海涵。如果觉得教程对你有帮助,分享给更多的朋友,谢谢!
欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!