英雄之舞—凌波微步(利用async.js编写异步动画)

1字数 3004阅读 1108
凌波微步

凌波微步有云:

此步法精妙异常,习者可以用来躲避众多敌人的进攻,此外「凌波微步」每踏出一步,都与内力息息相关,决非单是迈步行走而已,若无内功根基之人,将「凌波微步」强行走将起来,会造成自绝经脉的危境。

一、英雄的窘境

上篇《英雄之舞—迷踪“安可心”》一章中,我们研习了迷踪步,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,水管比较小,加载可能会有点慢请海涵。如果觉得教程对你有帮助,分享给更多的朋友,谢谢!


欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!

奎特尔星球

推荐阅读更多精彩内容