玩转javascript中this!

在javascript异步编程、函数式编程中,有两个至关重要的技术callbackthis变量,又称之为回调当前对象上下文

一、星际迷航

javascript中的回调函数,我借用科幻小说的比喻,有点类似不同的宇宙空间。而且宇宙空间有两类:
1. 一类就像从地球到火星,在代码上的表现是,在同一个时刻(帧)代码执行有严格的先后顺序。
2. 另一类回调函数,像从当下去了天堂或冥界,跟现在下不属于同一个宇宙空间,代码在未来某一时刻才会进入。

而且这些宇宙空间还相可以互嵌套,简单理解可以用同步、异步函数来区别。

image.png

举个列子,先看看从地球到火星的旅行:

onLoad() {
    let array = ['1','2','3','4','5'];
    //过虑出数组中的奇数元素
    this._num = 2;
    array = array.map(function(i) {
        return parseInt(i);
    }).filter(function(i) {
        return i % this._num;
    }, this); //注意这里的this参数
}

上面代码中array对象上的map与filter中的匿名函数,就像两个小行星。onLoad外层就是地球,他们是在同一个时空之中,array中的元素像是做了一次星际旅行,断点会从上到下一句一句地执行。

Shawn对es6太过依赖,忍不住写了一行es6的等价代码:

//再看看es6的写法
array = array.map(i => parseInt(i)).filter(i => i % this._num);

这里解释一下,注意两点:

  1. 箭头函数中参数只有一个时,可以省略参数上的圆括号(arg)直接写成arg。
  2. 箭头函数中函数体只有一行代码,可以省略大扩号{}直接写表达示,同时将表达式的值默认为函数返回值,所以不需要写return。

再来看看Creator中常见的回调用法,在不同的宇宙空间的穿梭:

onLoad() {
    this._button.active = false;
    this.scheduleOnce(() => {
        this._button.active = true;
    }, 5);
}

使用scheduleOnce延时5秒显示_button节点,他与上面的map、filter函数不同的是异步执行。在调试中会发现断点在代码前后跳跃,断点前后跳跃不是关键,关键的是scheduleOnce函数他不会阻塞,不论scheduleOnce函数中的回调函数如何复杂都不会影响当前这一帧的运行效率。

在Creator中cc.loader.loadRes、cc.loader.load就是异步回调的,如果资源已经被加载过了,可以使用cc.loader.getRes通过函数返回值同步获取。理解同步与异步是编写javascript函数的重要心法,善于驾驭异步流程你就能在javascript中自由遨游,使用async.js来控制异步流程是一个高效的作法。

二、搞清楚this是谁

在纷繁复杂的星际旅行中,不论是同步还是异步,最为重要的是不要忘记“我是谁”。No不好意思,搞清楚我不重要,在你人生旅途中,要时间清醒,此刻的你到底是谁更重要。
我是谁

对于javascript中的回调函数来说,函数中的this变量到底是谁,搞不清这个你很可能就会在旅行中回不来了,回到之前代码中的filter中的回调函数:

onLoad() {
    let array = ['1','2','3','4','5'];
    let array = [];
    let this._num = 2;
    array.filter(function(i) {
        return i % this._num;
    }, this); //<-----注意这里的this参数
}

filter的第二个参数this是用来改变回调函数中的this变量,如果不传这个this参数,里面的this._num访问就会有问题。

例如在Creator中有不少需要注册回调的API,后面都会紧跟一个target参数,target将来回调后的this变量。

this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction, this);

如果你不传入第三个参数this你的代码很可能会挂掉,函数的this上下文默认受调用者所控制。

//模拟一个组件中的点击事件
_onButtonTouchEnd() {
    //定义一个回调函数
    let callback = function() { 
        cc.log(this);  //<----这个this是全局window
    }
    //执行回调函数,函数中的this是全局window
    callback(); 
}

上面代码callback中的this是全局window,这里我使用惯用方式总结了几个大招可以用来改变callback中的this变量。

三、星际巡航

javascript与c/c++、java等语言有个最大区别就是,函数中的this变量是可变的。几乎每个人都会在这一点栽跟头,这个特性既成就了javascript的高度灵活性,但也让不少初学者产生迷惑。改变js函数中this变量的技法我将其称之为:星际巡航术,为的是在迷航中认清自己。

第一式:凝神诀

Function.bind

javascript中所有的函数对象上都有bind方法,执行它将返回一个新的函数变量,这个返回的函数执行时的this上下文由bind的第一个参数所决定。看看在节点事件中的运用:

//去掉了第三个target参数
this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction.bind(this));

使用bind搞定,是不是很简单,我看好多人是这样做的。但请你思考一下那为什么Array.map、Array.filter、CreatorAPI要设计target参数呢?使用bind注册回调,容易踩到一个坑,稍后说明一下我的理解。我们再稍微深入一点,看看bind更多的用法:

//模拟一个组件中的点击事件
_onButtonTouchEnd() {
    //定义一个回调函数
    let callback = function(name, event) { 
        cc.log(this);         //打印当前this
        cc.log(name, event);  //打印参数 
    }
    //施展绑定诀,将callback中的this绑定为当前函数上下文中的this
    let callback1 = callback.bind(this); 
    //执行回调函数,函数中的this是曾经bind传入参数,这里就是当前组件对象
    callback1('button', 'touchEnd');

    //将callback中的this绑定为当前this上的_button节点对象
    let callback2 = callback.bind(this._button); 
    //执行回调函数,函数中的this是bind传入的_button节点
    callback2('button', 'touchEnd');

凝神诀要义在于bind时的参数设定,就像是搓出一股波动拳,蓄而未发,“啊啰啰啰啰……”就是不“哽”出去。

啊啰啰啰啰......

而且bind函数还可以给函数传递参数,请仔细阅读下面代码:

 //定义一个回调函数
 let callback = function(arg1, arg2) { 
     cc.log(this);         //打印当前this
     cc.log(arguments)     //打印隐藏参数对象 
     cc.log(arg1, arg2);  //打印参数 
 }

 //绑定决还可以传入参数,传入的参与会排在原函数定义的参数之前
 let callback1 = callback.bind(this._button, 'button', 'touchEnd');
 //参数已经在bind时传入了,此时可以不用传入参数了
 callback1();
 //如果传入参数,调用时的参数会排在绑定时的参数后面
 callback1(1, 2); //参数顺序:['button', 'touchEnd', 1, 2]

将这股凝聚的能量任意流动(一系列的参数传递、变量赋值),在适合的地方释放出来,其中this变量与参数是由你之前精心设计的,这时会产生情人的效果,是一般静态语言难以做到的。

发动

还需要特别的注意,每一股搓出的一股波动拳都是不同的函数对象。

let func1 = callback.bind(xxx);
let func2 = callback.bind(xxx);
//f1与f2是两个不同的函数对象
f1 === f2;  //返回false

这就是为什么在节点事件注册时使用bind容易掉入进的坑,当你想使用node.off你不能将之前事件回调给删除掉,这就是为什么要给你一个target参数的原因了。

不过Shawn还有更简单的办法注册事件,而且也不需要传入target,因为bind是es5时代的产物,es6有更好用的招数。

第二式:召唤诀

Function.call

你可能在想,Creator的API是如何利用target参数修改的回调中的this的呢?其实与Function.bind一样,javascript中所有的函数对象上都有一个call方法

//模拟一个组件中的点击事件
_onButtonTouchEnd() {
    //定义一个回调函数
    let callback = function(name, event) { 
        cc.log(name, event); 
    }
    //call的第一个参数是想变换的this上下文,后面为该函数的实际参数
    callback.call(this, 'button', 'touchEnd'); 
}

召唤诀的特点是:随喊随到,立即执行,其中最为重要的是call传入的第一个参数,就是你想变换的this变量,后面紧跟此函数的参数。

召唤

一个更有趣的实践hack一下Creator的cc.Button组件,做个神奇的勾子:

//先保存button状态切换函数
let updateState = cc.Button.prototype._updateState;
//自己写个函数来将他覆盖了
cc.Button.prototype._updateState = function () {
    //执行时的第一句,执行原来保存的_updateState,相当于执行基类函数
    //这里不能直接调用updateState,需要用call将内部this修正为当前button
    updateState.call(this); 
    if (this.node.interactable === this.interactable) {
        return;
    }
    //下面是根据是否禁用,设置button节点下的子节点变灰
    //做了条件判断只在不设置disabledSprite时生效
    this.node.interactable = this.interactable;
    if (this.enableAutoGrayEffect && this.transition !== cc.Button.Transition.COLOR) {
        if (!(this.transition === cc.Button.Transition.SPRITE && this.disabledSprite)) {
            this.node.children.forEach((node) => {
                let sprite = node.getComponent(cc.Sprite);
                if (sprite && sprite._sgNode) {
                    sprite._sgNode.setState(this.interactable ? 0 : 1);
                }

                //原生平台退出 
                if(cc.sys.isNative) {
                    return;
                }

                //Label的置灰实现目前只能在web端使用
                let label = node.getComponent(cc.Label);
                if (label && label._sgNode) {
                    let shaderProgram = this.interactable ?
                        cc.shaderCache.programForKey(cc.macro.SHADER_SPRITE_POSITION_TEXTURECOLOR) :
                        cc.Scale9Sprite.WebGLRenderCmd._getGrayShaderProgram();

                    label._sgNode._renderCmd.setShaderProgram(shaderProgram);
                }
            });
        }
    }
};

来看看演示效果:

Shawn还尝试了,将bind过的函数对象,再调用call,this任然是之前bind时的this不受call的第一个参数控制。

let func = callback.bind(xxx);
//执行时func函数的this任然是xxx,函数参数有效
func.call(yyy, arg1, arg2); 

es5的时候call出现的频率是非常高的,但现在使用了es6除了做一些hack行为与面向对象的模拟外,大多数回调都可以用更加简单的一阳指可以搞定。

第三式:降龙诀

Function.apply

javascript中函数的参数变化无穷,参数个数可长可短(参数个数0~n),神鬼莫测,犹如一条游龙!降龙诀就是用来驯服这条善变的怪兽的!

_onButtonTouchEnd() {
    //定义一个回调函数,根据不同的参数个数有不同的处理
    let callback = function() { 
          switch(arguments.lenght) {
              case 1:
                   ...
                   break;
              case 2:
                   ...
                   break;
        }
    }
    //call的第一个参数是想变换的this上下文,后面接一个数组参数
    callback.apply(this, ['button', 'touchEnd']); 

同样的,所有函数上都有一个apply方法降龙诀的精髓有两点:
1. 控制this上下文的变化,
2. 可以将参数用一个数组打包进行传递,

函数执行任然是像普通调用一样,在平时用的地方不多,但在类的继承、执行基类函数、模拟面向对象等技术上是离不开它的。

第四式:一阳指

箭头函数 () => { … }

一阳指又称箭头函数,所指之处的函数this上下文,皆为当时调用时的this,看似平淡无其,实则威力巨大。

//模拟一个组件中的点击事件
_onButtonTouchEnd() {
    //定义一个箭头函数,当前this为组件对象
    let callback = (arg1, arg2) => { 
        //此刻的this为定义函数时的this上下文对象
        cc.log(this);
    }
   callback(xxx, yyy); 
}

一阳指.jpg

凝神诀和召唤诀的运用大多数是为了修正匿名函数中的this为当前调用时的this,可显的有点啰哩叭嗦,一记一阳指轻松搞定!

在一阳指还没有被创造之前,使用的是闭包变量来做的:

var self = this;
function callback() {
    //使用self变量,指向调用时的this上下文
    cc.log(self);
    ...
}
callback(xxx, yyy);

此方法也正是Bable编译器将es6转es5时生成的套路。

对于this的控制是凌波微步的内功基本详见《
英雄之舞—凌波微步》,如果运用的不好,就会如文中所讲的,强行走将起来,会造成经脉堵塞的危境!

#四、结束
最后总结一下我们介绍的招数

凝神诀—Function.bind
召唤诀—Function.call
降龙诀—Function.apply
一阳指—箭头函数

这些招数都是为了在回调函数中不要迷失this,或都说在回调中可以任意控制this。在javascript中函数是第一位的,函数可以动态生成,可以当参数传递,可以说javascript是披着c/c++的狼,骨子里其实是函数式编程语言。
宇宙在你手中

英雄之舞-迷踪“安可心”

奎特尔的进化之路可追溯到远古可可思大陆,从可可思大陆上一直流传这样一句真言:

「英雄」是舞者,「安可心」是舞步

「英雄」是谁?英雄是Node,英雄就是你!
「安可心」为何物?不好意思,安可心其实是cc.Action,是我为他起的中文名字。之前我还给他起过:俺可行、安可行,感觉太俗了,「安可心」更贴切。

# 一、初识安可心

安可心

「安可心」可不是一个人,他是一个家族,迷踪舞步中的安可心大多是为英雄的属性而孕育的,其中绝大多数还是双胞胎。
– 英雄的位置(position):MoveTo、MoveBy,她们控制英雄的位移,是迷踪步的核心,需要的能量是位置(cc.p/cc.Vect2)。
– 英雄的旋转(rotation): RotateTo、RotateBy,这两姐妹控制英雄的转身,华丽无比,需要的能量是角度(Angle)。
– 英雄的缩放(scale): ScaleTo、ScaleBy,他们让英雄伸缩自如,有时如泰山压顶,有时突如其来,需要的能量是缩放比例(scale)。
– 英雄的隐形(opacity)能力:FadeTo现身、FadeOut隐身,相互配合让英雄若隐若现,Blink则是控制英雄时隐时现,奇幻无比。
– 英雄成为变色龙(color):TintTo、TintBy控制英雄的颜色过度,需要的能量是一个目标颜色(rgb值)。

以上「安可心」是实现迷踪步的核心,需要谨记于心。同时还有一些「安可心」是英雄位移的变种或增强:
– 跳跃:JumpTo、JumpBy
– 曲线:bezierBy、bezierTo
其中曲线位移是英雄舞步杀手锏,可惜Shawn也学的不好,在这里诚心呼唤高手,可以来奎特尔星球上讲讲bezier曲线!

二、链接安可心

链接

我们在上面介绍了安可心家族迷踪步的核心成员,但具体怎么施展迷踪步呢?首先要记住,奎特尔发源地是可可思大陆,需要牢记:色色点是根基,例如:

cc.MoveTo、cc.ScaleTo

在色色点后面,大写字母是具体的安可心家族成员,而在真实战场上,通常是用的召唤术(工厂函数)呼唤的安可心分身(实例化):

//注意cc.MoveTo与cc.moveTo的区别
let moveTo = cc.moveTo(5, cc.p(100, 100));

cc.MoveTo是可娜丝的安可心化身,而cc.moveTo只召唤术,它召唤了cc.MoveTo的一个分身moveTo,具像化他是做的: new cc.MoveTo(…)

迷踪安可心还有一个重要的,特性是:

持续时间

任何一个舞步或动作其实都是英雄在一定时间内的空间与形态的变化,我们来看看具体的操作:

let moveTo = cc.moveTo(5, cc.p(100, 100));
node.runAction(moveTo);

此时不管node英雄在何处,在5秒内,他会移动到坐标x=100,y=100的位置上,runAction就是英雄舞动迷踪步的关键,通过它与安可心建立链接。

三、混元步法

我们知道通过英雄的runAction可以与安可心建立链接,但要同时与多个安可心链接怎么办呢?比如一边移动,一边旋转?

1. 并行诀

要实现这类舞步,就需要运用到迷踪混元步法中的:并行诀
并行诀cc.Spawn也是一个安可心,他可以将其她多个安可心打包起来,成为一个安可心,交给英雄,请看下面:

//3秒匀速移动到到坐标100,100的位置
let moveTo = cc.moveTo(3, cc.p(100, 100));
//3秒匀速旋转1080度(三圈)
let rotateTo = cc.rotateTo(3, 1080);
//并行诀:移动与旋转同时舞动
let spawn = cc.spawn(moveTo, rotateTo);
//执行并行诀安可心
node.runAction(spawn);

2. 串行诀

你肯定又要问,如果要移动后再旋转,旋转后再移动,这类舞步怎么做呢?这是串行诀cc.sequence解诀的问题。

//3秒匀速移动到到坐标100,100的位置
let moveTo = cc.moveTo(3, cc.p(100, 100));
//3秒匀速旋转1080度(三圈)
let rotateTo = cc.rotateTo(3, 1080);
//1秒后移动到150,150的位置
let moveToo = cc.moveTo(1, cc.p(150, 150));
//串行诀:先移动,再旋转,然后又移动
let sequence = cc.sequence(moveTo, rotateTo, moveToo);
//执行并行诀安可心
node.runAction(sequence);

3. 混元诀

将并行诀与串元诀,合理组合就形成了混元诀,例如:英雄一边移动一边旋转,此时动作完后,做一个缩放消失:

//3秒匀速移动到到坐标100,100的位置
let moveTo = cc.moveTo(3, cc.p(100, 100));
//3秒匀速旋转1080度(三圈)
let rotateTo = cc.rotateTo(3, 1080);
//并行诀:移动同时旋转
let spawn = cc.spawn(moveTo, rotateTo);
//3秒匀速缩小
let scaleTo = cc.scaleTo(3, 0);
//串行诀打包了一个并行诀与scaleTo
let sequence = cc.sequence(spawn, scaleTo);
//执行并行诀安可心
node.runAction(sequence);

混元诀的要义:

并行诀Spawn和串行诀Sequece相互嵌套包装,可以无限组合,完成无比复杂的舞步

四、行云流水

按理来说,将混元诀运用自如之后,就算进入就算是迷踪步入门了。

“我嚓!看了半天才算是入门”?

英雄们,请不要着急,要达到行云流水的境界还需要进一步的修炼,我们还有不少问题还没解诀。
## 1. 分身诀与逆行诀

编排一段混元舞步需要念诵不少的咒语,如果想将这段舞步给另一个英雄,可通过分身诀clone一份出来。

//编排舞步
...
//使用串行诀打
let action1 = cc.sequence(...)
//使用action1的分身诀,生成action2
let action2 = action1.clone();
//node1与node2共同起舞
node1.runAction(action1);
node2.runAction(action2);

还有一种情况,需要将舞步按之前的顺序颠倒重来一遍,这时就需要用到逆行诀reverse

...
//串行诀:先移动,再缩放,最后旋转
let sequence1 = cc.sequence(moveTo, scaleTo, rotateTo);
//逆行诀:先旋转,再缩放,最后移动
let sequence2 = sequence.reverse();

上面的逆行sequence外,sequence中串行的安可心也会逆行,以前是放大的,现在是缩小,以前是向左移动,逆行会移动会原位。

分身诀与逆行诀本质上,都是安可心的分身,逆行诀将输入次序颠倒或取反执行,以得到来去自如的能力。

2. 根本停不下来?

思考一下:

如何与春晚的小彩旗比旋转?
如何与炫迈口香糖比谁更持久?

你可能想到下面的做法:

//看我的999大法
node.runAction(cc.rotateTo(99999999999, 99999999999));

不好意思,告诉你这是非常不科学的,数字再大总会有比你更大的。
这里就需要使用到repeat: 复重诀,为什么叫复重诀,不叫重复诀呢?因为我也不在道怎么取名字好,复重诀这样念起来更坳口一点,意思似乎更明确:“反复地重来”,让你不会忘记。

//旋转
let rotateBy = cc.rotateBy(1, 360);
//复重诀:将旋转重复99999999次
let repeat = cc.repate(rotateBy, 99999999);
node.runAction(repeat);

还是不解诀问题嘛,重复再多也干不过炫迈口香糖的“久到离谱”。
没关系,我们还有杀手锏,比看谁才离谱:真•复重诀 repeatForever

//旋转
let rotateBy = cc.rotateBy(1, 360);
//真•复重诀:这才叫根本停不下来
let repeat = cc.repeatForever(rotateBy);

不论是小彩旗还是炫迈,跟他们比持久都太没意思了,不论英雄怎么舞动,我们要求的是随心所欲,想停就停才是更高境界:

node.stopAction(action);

使用stopAction可以立即停止所指定的舞步,使用stopAllActions可中止英雄当前所有舞步,无需要指定舞步。

node.stopAllActions();

##3. 行云流水
何为行云流水?云可散、水可断,云行水流英雄想怎样就怎样!
远处传来,可不可以不动,什么都不做呢?这个嘛,当然!DelayTime 懒人诀

//移动
let moveTo = ...
//懒人诀: 懒惰2秒
let delayTime = cc.delayTime(2);
//缩放
let scaleTo = ...
//串行诀:先移动、暂停、缩放
let sequence = cc.sequence(moveTo, delayTime, scaleTo);
node.runAction(sequence);

大风起兮云飞扬,微风拂面暧洋洋,之前的基础步法都是做的匀速运动,这个怎么破呢?哈哈最后必杀技:缓动诀 easeAction。
缓动诀是对基础步法安可心的修饰,将其从匀速运动调整为各种速率运动,大至分为

入•缓动诀、出•缓动诀、入出•缓动诀

//缓入、缓出式的位移,令人欲罢不能
let moveTo = cc.moveTo(3, cc.p(100, 100)).easeInOut(3.0));
node.runAction(moveTo);

缓动诀有很多,这里不一一举例了,在实际战斗中需要多多尝试。

##4. 逍遥诀
英雄的迷踪步就介绍完了,迷踪步的核心是安可心舞步,与几个重要的步诀:

并行诀、串行诀、混元诀、分身诀、逆行诀、复重诀、真•复重诀、懒人诀、缓动诀

灵活运用基础步法,配合这些步诀就可以让英雄独步舞林了。对叫独步舞林了,就是说能是一个人跳舞,如果要在自己舞完后,接上下一个英雄,自己去逍遥快活,这里还需要一个逍遥诀: cc.callFunc

...
//逍遥诀:将后续舞步传导下去
let callFunc = cc.callFunc(() =>{
    ...
    let action = ...
    node2.runAction(action);
});
//将逍遥诀放在最后
let sequence = cc.sequence(..., callFunc);
//当sequence执行完后,node2出场
node1.runAction(sequnece);

逍遥诀除了可以与其他英雄建立链接外,还可以用于执行其它事件通知等,奥妙无穷,存乎一心!

这次就到这里,希望我的分享望能让你舞动起来!


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

英雄之舞—凌波微步

凌波微步

凌波微步有云:

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

一、英雄的窘境

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


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

奎特尔星球

使用Cocos Creator制作PPT

Shawn前段时间尝试用Cocos Creator做了一个PPT,发现还挺好玩的,有很多东西值得去研究,这里给大家分享一点使用Creator制作PPT的一些思路和方法。

1. 页面切换

相信大多数人都知道,PPT是由一页页顺序组成的幻灯片所构成,在Creator中直接使用场景来充当灯片页面是最简单的方式,看下图:

使用场景做PPT页面

页面之间的衔接有两种方式,一种是使用我们之前讲在过的场景切换组件LoadScene,将组件挂载到一个节点上,点击即可切换,看下图:

LoadScene组件

这种方式就像做超链接,用起来是简单,但需要一页页地去配置。Shawn这里还尝试了一种方式,编写一个导航组件,解析所有场景文件名并做一个排序,提供向前翻页、向后翻页的接口函数,可以提供Button组件调用,看下面代码:

//Navigation.js
cc.Class({
    extends: cc.Component,
    onLoad () {
    this.pageIndex = 0;
        //获取所有场景并排序
        this._sceneArray = cc.game._sceneInfos.sort((info1, info2) => {
            let num1 = parseFloat(cc.path.basename(info1.url, '.fire').split('-')[1]);
            let num2 = parseFloat(cc.path.basename(info2.url, '.fire').split('-')[1]);
            return num1 - num2;
        });
        //设置为常驻节点
        cc.game.addPersistRootNode(this.node);
        this.node.on(cc.Node.EventType.TOUCH_END, () => {
            this.loadScene();
        });
    },

    //根据pageIndex变量加载场景
    loadScene() {
        let info = this._sceneArray[this.pageIndex];
        if (info) {
            cc.director._loadSceneByUuid(info.uuid);
        }
    },

    //修改pageIndex
    next() {
        this.pageIndex++;
        this.loadScene();
    },

    //修改pageIndex
    back() {
        this.pageIndex--;
        this.loadScene();
    },
});

将上面这个组件挂载到首场景的一个节点上,在内部放入两个按钮,一个调用组件的next方法,一个调用back方法,看下图:

Navigation组件.jpg

有了这个导航组件,只需要注意场景命名就行了,场景中的内容尽量使用Widget组件做好相对布局与自适应。

2. 内容逐一显示

在PPT中加入一些互动操作,比如点击屏幕时让文字或内容逐一显示,比如像下面演示一样:
点击逐一显示.gif

这个操作实现的方法有很多,我在这里写了一个组件脚本,将要控制的节点收集起来,在组件start运行时将节点全部隐藏。

收集要控制的节点.png

老方法使用Button去调用ActiveNode.setActive函数:
使用Button调用组件方法.png

利用这个组件不管是显示图片和文字都可以,现在来看它还有些缺点,如果我们有几十个节点需要逐一显示,要拖动几十次,很是麻烦,如何解决这个问题,大家可以想一下。

在这个组件上还可以做一些扩展,一种是点击显示,一种是由时间间隔显示,可以让我们这个PPT的表现更丰富一点。

3. 点击切换图片

点击图片,切换下一张,可以使用之前讲过的SpriteIndex搞定,不过这里为了让组件少一点,使用起来简单一些,我是从cc.Sprite上继承的,看下代码:

let SpriteEx = cc.Class({
    extends: cc.Sprite,

    properties: {
        spriteFrames: [cc.SpriteFrame],

        _index: 0,
        index: {
            type: cc.Integer,
            range: [0, 100],
            set(value) {
                this._index = value % this.spriteFrames.length;
                this.spriteFrame = this.spriteFrames[this._index];
            },
            get() {
                return this._index;
            }
        }
    },

    next() {
        this.index++;
    },

});

//屏蔽一些cc.Sprite不需要的属性
cc.Class.Attr.setClassAttr(SpriteEx, 'spriteFrame', 'visible', false);
cc.Class.Attr.setClassAttr(SpriteEx, '_atlas', 'visible', false);
//条件显示cc.Sprite上的一些属性
cc.Class.Attr.setClassAttr(SpriteEx, 'fillType', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});

cc.Class.Attr.setClassAttr(SpriteEx, 'fillCenter', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});
cc.Class.Attr.setClassAttr(SpriteEx, 'fillStart', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});
cc.Class.Attr.setClassAttr(SpriteEx, 'fillEnd', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});
cc.Class.Attr.setClassAttr(SpriteEx, 'fillRange', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});

cc.Class.Attr.setClassAttr(SpriteEx, 'srcBlendFactor', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});
cc.Class.Attr.setClassAttr(SpriteEx, 'dstBlendFactor', 'visible', function() {
    return this._type === cc.Sprite.Type.FILLED;
});

module.exports = SpriteEx;

上面代码有点多,主要是继承cc.Sprite后会有一大堆属性会显示到属性面板上,会对组件的使用者产生不好的体验,使用可以cc.Class.Attr.setClassAttr来控制组件属性的显示。

SpriteEx.png

编写兼容Creator 1.x和2.x的代码

前言

Creator 2.x出来有一段时间了,在原生应用上仍然不能让人满意,许多小伙伴花了大量精力把项目升级到2.x,结果发现性能内存大打折扣。

官方也意识这个问题,他们正组织核心人马,在对原生框架进行各种优化,包括Spine,Dragonbones,压缩纹理,文字优化等等,相信在不久的将来可以见到一些成效。

不过如果项目急着要上线,现在可能来不及了,并且优化的成果如何,也要实际放出来时测试过才能知道。在这种情况下,比较可行的方法是先用1.x发布你的应用,但在代码上作一些兼容性处理,确保到时升级时尽量平滑。甚至在最坏情况要回退,也少一些麻烦。

这篇文章试图将1.9和2.x的差异列举出来,并且告诉你如何写出在两个版本都可以运行的代码。1.10我们没有用过,使用1.10的小伙伴只能自行研究。

先定义版本常量

//如果是1.x的项目定义为true,如果是2.x的项目定义为false
window.CC_1X  = true

ZOrder的差异

1.9可以用node.setLocalZOrdernode.zIndex,2.x去掉了setLocalZOrder函数,要兼容的话统一使用node.zIndex

setCascadeOpacityEnabled废除

2.x去掉了node.setCascadeOpacityEnabled,1.9要兼容的话就不能使用。另外cc.Node上还有多个API被废除,详见Creator API文档,链接:https://docs.cocos.com/creator/api/zh/。

播放声音的差异

  • 1.9的例子:
let sId = cc.audioEngine.play(cc.url.raw("resources/sound/test.mp3")), loop, volume);
  • 2.x的例子:
cc.loader.loadRes("sound/test", cc.AudioClip, function (err, clip) {
    if (err) {console.error(err); return;}
    let sId = cc.audioEngine.play(clip, !!loop, volume);
});
  • 兼容的做法是写一个包装函数:
playSound = function (name, loop, volume=1, cb) {
    if (CC_1X) {
        let sId = cc.audioEngine.play(cc.url.raw("resources/sound/"+name+".mp3", !!loop, volume);
        if (cb) {
            cb(sId);
        }
    } else {
        cc.loader.loadRes("sound/"+name, cc.AudioClip, function (err, clip) {
            if (err) { console.error(err); return;}
            let sId = cc.audioEngine.play(clip, !!loop, volume);
            if (cb) {
                cb(sId);
            }
        });
    }
};

自定义事件的差异

  • 1.9的事件
// message 会被保存在回调函数的 event 参数的 detail 属性上
eventTarget.emit(type, message); 
eventTarget.on(type, function (event) {
    // 通过 event.detail 获取message
});
  • 2.x的事件
// emit 时可以传递至多五个额外参数,都会被扁平的直接传递给回调函数
eventTarget.emit(type, message, target); 
eventTarget.on(type, function (message, target) {
    // 直接通过回调参数来获取 emit 时传递的事件参数
});
  • 兼容的做法是确保参数只传一个,然后在事件处理是这样判断:
eventTarget.on(type, function (event) {
    let msg = event.detail ? event.detail : event;
    // 这样就能兼容1.9和2.x的事件机制
});
  • 由于自定义事件的变化,导致按钮,动画组件等事件也有相应的变化,兼容的做法如下:
// 按钮的
button.node.on("click", this.onClick, this);
onClick(event) {
    let button = event.detail ? event.detail : event;
}
// 动画的
anim.on("finished", this.onFinished, this);
onFinished(event, target) {
    var aniState = target ? target : event.target;
}

一些API的变化:

  • radiansToDegrees
if (CC_1X) {
    return cc.radiansToDegrees(dirRad);
} else {
    return cc.misc.radiansToDegrees(dirRad);
}
  • cc.KEY
if (CC_1X) {
    cc.macro.KEY = cc.KEY;
}
  • setKeepScreenOn
if (CC_1X) {
    cc.Device.setKeepScreenOn(true);
} else {
    jsb.Device.setKeepScreenOn(true);
}
  • 其他API不同,通过查询文档,然后用上面的方式写一个包装函数。

prefab的差异

我们当时将项目从2.0.5回退到1.9的时候,发现修改代码还不能成功,有些组件序列化格式的变化,导致用1.9打开会失败。

所以这里也将一些不兼容的地方列出来,方便有像我们一样想回退的小伙伴参考:

  • RichText:如果在2.0中设置了字符串,1.9打不开,解决办法是先在2.0编辑器中,将RichText的文本清空,1.9编辑器就可以正常打开了。

  • ScaleX和ScaleY属性如果不是1,回退到1.9会恢复成1。这也是因为格式不一致导致1.9没法解析出来。似乎没有好的办法,只能手动一个个修正过来。

  • EditBox 2.0多出几个子结点,要回到1.9只能手动删掉了,请看下图:
    EditBox.png

  • TTF字体:当字体文件体积大于10M时,2.0会加载失败,这应该属于引擎的BUG,期待后面修复。

meta文件的修改

  • .meta文件的版本号有变化,在2.x中有些是2.0.0的,如果想回退,可以用批处理替换回1.0.1。

其它差异

  • 2.x资源不存在直接报错,在运行时,1.x时资源不存在时只是做警告提示,2.x资源不存在直接报错。
  • 2.x构建资源全部以UUID命名:如果要在2.x上做热更新,需要建立Assets资源与构建资源的对应关系,相比1.x要复杂一些了。

  • rect.contains废弃使用cc.rectContainsPoint代替

image.png

  • cc.pXXX系列函数被废弃,使用cc.Vec2成员函数
    image.png
    cc.pAdd需改为p.add,cc.pMult改为p.mul

image.png

两个点的距离计算cc.pDistance改为p1.sub(p2).mag()。

Creator模块介绍—领略模块化的力量

从Cocos2d-js到Creator,一直以来都有模块裁剪的能力,可以将游戏中没有用到的功能代码,在编译输时移除,从而减少包体大小,这对H5游戏来说影响比较明显。Creator比Cocos2d-js做的更好,通过主菜单->项目设置->模块设置有一个图形化的界面,可以方便模块配置。

image.png

image.png

下面简单说一下各模块的功能和作用,以及对应的组件。

一、模块配置

在Creator的模块设置界面中,可以看到Core、Canvas、Sprite这三个模块是禁止编辑的,他们是构建Creator应用的基石,没有他们就没法构建游戏。

其实Core能不算是模块,而是一类模块的集合,里面包含了Canvas和Sprite,Label、Button等等,引擎提供的组件。

打开CocosCreator的安装路径有一个modules.json

/Applications/CocosCreator.app/Contents/Resources/engine/modules.json

此文件中描述了模块设计中界面信息,内容如下:

[
  {
    "name": "Core",
    "locked": true,
    "entries": []
  },
  {
    "name": "Canvas",
    "locked": true,
    "entries": [
      "./cocos2d/core/components/CCCanvas.js"
    ]
  },
  {
    "name": "Sprite",
    "locked": true,
    "entries": [
      "./cocos2d/core/components/CCSprite.js"
    ]
  },
  {
    "name": "Label",
    "entries": [
      "./cocos2d/core/components/CCLabel.js"
    ]
  },
  ...

浏览这个文件,可以看到一个模块通常对应一个或多个js文件。

这里我编写了一个脚本代码,通过解析这些代码路径,简单统计了一下模块文件的大小、代码行数。而且你可由些找到Creator的组件源代码,了解组件实现的具体细节,而且通过学习源码是提高开发水平的重要途径。

在Web调试中也能读到源码:

Ctrl + P搜索代码

二、Creator模块说明

根据module.json文件中的信息,我们大概了解一下CocosCreator有那些模块,这些模块提供了那些功能。

1. Core模块

功能:Core其实指的是CocosCreator下的engine/core目录,里面内容很丰富。
字节数: 0 代码行数: 0 文件名: []

2. Canvas模块

功能: 屏幕适配,作为UI根节点,为所有子节点提供视窗四边的位置信息以供对齐,另外提供屏幕适配策略接口,方便从编辑器设置。
字节数: 9059 代码行数: 285 文件名: CCCanvas.js

3. Sprite模块

功能:场景中渲染精灵,支持九宫、拉升、平铺、裁剪等功能。
字节数: 19825 代码行数: 650 文件名: CCSprite.js

4 Label模块

功能:文字标签组件,实现系统字体、fnt、艺术字的渲染。
字节数: 18686 代码行数: 599 文件名: CCLabel.js

5 Mask模块

功能:遮罩组件,为子节点提供遮罩裁剪能力支持圆形、椭圆、图像模版三种模式。
字节数: 11155 代码行数: 348 文件名: CCMask.js

6.  CCSpriteDistortion模块

功能:扭曲效果组件,用于改变SIMPLE类型sprite的渲染,只有当sprite组件已经添加后,才能起作用.
字节数: 4268 代码行数: 118 文件名: CCSpriteDistortion.js

7. LabelOutline模块

功能:描边效果组件,用于字体描边,只能用于系统字体。
字节数: 3943 代码行数: 118 文件名: CCLabelOutline.js

8. ParticleSystem模块

功能:粒子系统组件。
字节数: 29961 代码行数: 1017 文件名: CCParticleSystem.js

9. TiledMap模块

功能:TileMap地图组件,渲染tmx格式的Tile Map。
字节数: 26241 代码行数: 885 文件名: CCTiledMap.js

10. Spine Skeleton模块

功能:Spine骨骼动画渲染模块,与Spine相关的所有的类,函数,属性,常量都在sp这个命名空间中定义。
字节数: 4082 代码行数: 139 文件名: index.js

11. Widget模块

功能:Widget 组件,用于设置和适配其相对于父节点的边距,自动调整坐标和宽高,Widget通常被用于UI界面适配。
字节数: 19549 代码行数: 613 文件名: CCWidget.js

12. Button模块

功能:Button按钮组件。
字节数: 20454 代码行数: 670 文件名: CCButton.js

13. ProgressBar模块

功能:进度条组件
字节数: 9581 代码行数: 295 文件名: CCProgressBar.js

14. ScrollBar模块

功能:滚动条组件。
字节数: 11525 代码行数: 352 文件名: CCScrollBar.js

15. ScrollView模块

功能:滚动视图组件。
字节数: 56872 代码行数: 1630 文件名: CCScrollView.js

16. Toggle模块

功能:Toggle 是一个 CheckBox,当它和 ToggleGroup 一起使用的时候,可以变成 RadioButton。
字节数: 6843 代码行数: 230 文件名: CCToggle.js

17. ToggleGroup模块

功能:不是一个可见的 UI 组件,它可以用来修改一组 Toggle  组件的行为。当一组 Toggle 属于同一个 ToggleGroup 的时候,任何时候只能有一个 Toggle 处于选中状态。
字节数: 4513 代码行数: 130 文件名: CCToggleGroup.js

18. PageView模块

功能:页面视图组件,实现分页功能。
字节数: 19105 代码行数: 611 文件名: CCPageView.js

19. PageViewIndicator模块

功能:页面视图每页标记组件,常用与PageView配合使用。
字节数: 6106 代码行数: 200 文件名: CCPageViewIndicator.js

20. Slider模块

功能:滑动器组件
字节数: 7770 代码行数: 232 文件名:  CCSlider.js

21. Layout模块

功能:Layout 组件相当于一个容器,能自动对它的所有子节点进行统一排版。
字节数: 32508 代码行数: 933 文件名: CCLayout.js

22. EditorBox模块

功能:EditBox组件,用于用户进入文本录入
字节数: 19490 代码行数: 643 文件名: CCEditBox.js

23. VideoPlayer模块

功能:Video组件,用于在游戏中播放视频
字节数: 13910 代码行数: 460 文件名: CCVideoPlayer.js

24 WebView模块

功能:WebView组件,用于在游戏中显示网页
字节数: 6333 代码行数: 201 文件名: CCWebView.js

25. RichText模块

功能:富文本组件
字节数: 25732 代码行数: 772 文件名: CCRichText.js

26. AudioSource模块

功能:音频源组件,可进行音频剪辑
字节数: 9432 代码行数: 359 文件名: CCAudioSource.js

27. Animation模块

功能:Animation 组件用于播放动画。
字节数: 22206 代码行数: 652 文件名: CCAnimation.js,index.js

28. MotionStreak模块

功能:运动轨迹,用于游戏对象的运动轨迹上实现拖尾渐隐效果。
字节数: 9727 代码行数: 296 文件名: CCMotionStreak.js

29. Collider模块

功能:碰撞检测模块,包含多个文件。

require('./CCCollisionManager');
require('./CCCollider');
require('./CCBoxCollider');
require('./CCCircleCollider');
require('./CCPolygonCollider');

字节数: 150 代码行数: 7 文件名: index.js 

30. Action模块

功能:Node动作模块,例如:cc.MoveTo

require('./CCActionManager');
require('./CCAction');
require('./CCActionInterval');
require('./CCActionInstant');
require('./CCActionEase');
require('./CCActionCatmullRom');

字节数: 191 代码行数: 6 文件名: index.js

31. Audio模块

功能:cc.audioengine是单例对象,主要用来播放音频。
字节数: 14543 代码行数: 528 文件名: CCAudioEngine.js

32. Graphics模块

功能:绘图组件,提供绘制线条、矩形、圆形等绘图方法,它对应cocos2dx上的DrawNode。
字节数: 1768 代码行数: 42 文件名: index.js

33. DragonBones模块

功能:DragonBones动画模块。
字节数: 2832 代码行数: 105 文件名: index.js

34. Physics模块

功能:物理引擎,使用Box2d
字节数: 955 代码行数: 32 文件名: index.js

35. StudioComponent模块

功能:CocosStudio资源转换模块,构建时默认会去除。
字节数: 6430 代码行数: 267 文件名: CCStudioComponent.js

36. RenderTexture模块

功能:纹理渲染模块。
字节数: 29687 代码行数: 858 文件名: CCRenderTexture.js、CCRenderTextureCanvasRenderCmd.js、CCRenderTextureWebGLRenderCmd.js

37. Chipmunk模块

功能:Chipmunk物理引擎。
字节数: 173736 代码行数: 6183 文件名: chipmunk.js

38. Camera模块

功能:摄像机在制作卷轴或是其他需要移动屏幕的游戏时比较有用,使用摄像机将会比移动节点来移动屏幕更加高效。
字节数: 10358 代码行数: 383 文件名: CCCamera.js

39. Intersection模块

功能:碰撞检测辅助类,用于测试形状与形状是否相交
字节数: 8122 代码行数: 334 文件名: CCIntersection.js

三、模块化的探究

CocosCreator让我对模块化又有了更多的认识和理解。乐高积木大多数人都玩过,没玩过至少看到过吧!一个个标准的积木方块,可以拼出变化无穷的造型,令人爱不释手。

还有我们现在的电脑,有主板、CPU、内存条、硬盘、显示器等部件,通过规范的接口将他们连接拼装起来,你只需要会使用这些部件的功能,具体怎么实现的可以不用管太多。

对于软件开发来说,对于模块化的声音一直不绝于耳,模块化本质到底是什么呢?

1. 分工

image.png

对模块化的论述可以追溯到亚当·斯密所说的:模块化最原始的形式就是分工。
这里分享一个亚当·斯密《国富论》中阐述分工的经典故事:

亚当·斯密的从事教学的大学所在地——格拉斯哥又是当时苏格兰的工业中心,制铁工业和纺织工业都很发达,这使斯密有可能实地观察工业区的经济,为他进行写作提供依据。

他考察了一个最平凡的生产大头针的工场。当时制针的过程,从绞铁条到拔细抛光,再磨细针头,装针后边的那个小球,然后包装到一个纸盒里,共18个环节。

在分工不完善的工场里,至少有一个工人师傅要干两个以上的环节。通常每天最多生产不会超过20根针,有可能每人每天仅仅生产出1根针。

但是,有一个典型的手工工场,它有18位师傅,都没受过多少教育,但他们组成一个有效的团队,分工做18样工作,每人只做一件事:
第一个工匠拉出铁丝
第二个工匠把它弄直
第三个工匠把它剪断
第四个把它磨尖
第五个把另一头磨平

这样下来,每个工人平均每天能生产4800根针。这意味着劳动生产率提高了至少240倍。

分工使每个人专门从事某项作业,可以节省与其生产没有直接关系的时间;分工有利于发明创造和改进工具。

这让我想到,很多时候,在网上看到不少招聘广告,要求一个工作了一两年的程序员,十八般武艺,样样都要会,要一个人独立负责一个项目(客户端或服务器)的方方面面。表面上看是为了减少成本,但实际中不管是在工作效率还是产品质量上可能与原来的初衷却是背道而驰。

2. 狭义与广义模块化

模块

在《设计规则模块化的力量》中提到,模块化有狭义和广义之分。

狭义模块化:是指产品生产和工艺设计的模块化。

广义模块化:是指把一系统(包括产品、生产组织和过程等)进行模块分解与模块集中的动态整合过程。

对于软件开发来说,一个项目可以分成多个模块,一个模块由多个子模块组成,一个子模块可以是一堆模块化的类、函数等组成,一个类又是由一系列的函数、变量构成。

模块化不仅可以用于生产,也能用于生产组织的过程。一个公司有多个部门,每个部分都是一个模块;一个公司也相当于一个模块,在社会活动中又有自己的分工。

3. 自律性与协作

协作

日本产业经济学者青木昌彦引用经典的制针的例子来说明模块化的含义,他给“模块”下的定义是:

“模块”是指半自律性的子系统,通过和其他同样的子系统按照一定规则相互联系而构成的更加复杂的系统或过程。

半自律性:一个模块可能刚开始只能处理简单的问题,但允许实现内部进化与创新,最终可以让整体获得较高收益。

协作:为什么模块是半自律性?因为它需要按照一定规则与其它模块协作构成更加复杂的系统。

在当我发现模块的自律性协作的两个特点时,我非常的惊讶!这不仅道出了模块的特质,也启发了我在参与社会活动中人需要具备的能力。

4. 效率

我的世界

分工、自律、协作,说了这么多模块化到底是为了什么呢?模块化是在为构建复杂系统时减少依赖,降低耦合,从而提高效率。

记得在吴军老师的《硅谷方法论中》讲到,如果要制造一张桌子或椅子,一般人会直接去做,而具有模块化思维思的人,首先会制作几个非常简单的的积木块如:木条或板子,然后将他们组装成一张桌子,就像组装乐高玩具一样。这样,你可以用这些积木块组装成桌子或椅子、床等其它家具,这样就大大提高了做事情的效率。这样处理问题就不是一个一个地,而是找出问题的共同因子,处理这一类事情 。

之前的做法是在做加法运算,而后面一种是在做乘法,比如:

1.5+1.5+1.5=4.5
1.5×1.5×1.5=3.375

前几次做加法运行是要快一些1,但做到后面,就完全不是在一个数量级的了。

1.5+1.5+1.5+ … 1.5,就算加100次也就150
1.5×1.5×1.5×1.5x … 1.5,1.5乘10次57.665, 乘100次为4.0656E17

摩尔定律告诉我们计算机硬件每18个月性能会翻一倍,而且价格还会更低,因为每一代的CPU都是在前一代的基础上研发出来的。十年前的iPhone1代与现在的iPhoneX正好10年,在性能上提升了100倍。

结语

如果将成果模块化,以及如何利用现有的模块提高我们的工作效率,是我最近思考的最多的问题。回想我所编写的代码、组件那些是可以被复用的,可以像乐高积木一样可以自由组合,而且还可以自我进化,这样的代码才更具价值!

庆幸的是在2017年年初,辗转到CocosCreator阵营,我将曾经在coco2d-js中的一些开发经验进行总结和提炼涉及:UI编程、网络、异步动画编程、MVC框架、工程自动化方面。

在UI编程方面,我的uikiller库在最近又做了一翻更新,增加了Thor类(我称之为雷神组件),使用会更加的简单,测试范例都已经更新,欢迎大家体验!
github: https://github.com/ShawnZhang2015/uikiller