查看原文
其他

英雄之舞—凌波微步

ShawnZhang Creator星球游戏开发社区 2021-08-09

前言

本文主要介绍使用async.js优雅地控制Cocos异步动画,文中有较多的代码与gif演示,为了获得更佳体验建议在电脑上阅读,如果当下没有环境,建议看代码时用横屏,看gif动画用竖屏!

凌波微步有云:

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

一、英雄的窘境

上篇《迷踪“安可心”》一章中,我们研习了迷踪步,runAction是建立英雄与安可心之间的链接,最后还学习了逍遥诀,而逍遥诀则是建立英雄与英雄之间链接

1. 多人之间的动作协同

多人其实指的是多个节点,当两个节点在舞步中有先后次序时,我们有那些可控制的方法呢?来看下面这段演示:


上图是一个男孩与女孩的故事,重点不是讲故事,而是讲他们发生的动作,研究相对高效可控的舞步控制手段。

言归正传,演示中男孩Label,一前一后,使用逍遥诀cc.callFunc很容易控制,同时在一个完整动作完毕时,使用一个完成回调,显示行动完成,请看代码:

//移动后呼叫 //参数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) => {  ... }, function(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太大效果不好,切换成视频:

https://v.qq.com/txp/iframe/player.html?vid=o1328yneu6m&width=500&height=375&auto=0

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演示也准备好了,请点击下方阅读原文体验,也可以在电脑上打开(http://www.ixuexie.com/cocos),服务器是阿里云1核1G1M,水管比较小,加载可能会有点慢。

如果对源码感兴趣,可以在文章下面留言,如果觉得教程对你有帮助,也请你发到朋友圈,分享给更多的朋友,谢谢!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存