第 3 章 Cocos2d-JS...

42
3 Cocos2d-JS 的平面世界 在前两章中,我们初步认识了 Cocos2d-JS,接着搭建了环境,还把 HelloWorld 跑起来了,部分新手读者是否有一点成就感了呢?不过,也许大家已 经发现之前都是填鸭式的节奏,几乎没有需要自己理解和操作的内容。这样很不满 足,是不是?所以,从这一章开始,我们正式开始学习 Cocos2d-JS。在第 1 章 中说过,有图形学基础或者网页开发基础的读者会更容易理解本书的内容,在这里 为了照顾新手读者,笔者还是需要稍微讲解一下最基础的平面世界知识。 3.1 Cocos2d 世界的经纬度——坐标系 首先,我们看一个典型的游戏画面。这个图有几个元素,首先最明显的应该是一个 超人,然后右侧有一个“HUNGRY HERO”字样的图标,另外还有两个按钮(PLAY ABOUT),最后就是底下的背景图,如图 3-1 所示。 在这个游戏画面中,超人和按钮的位置是怎么表示的呢?我们知道,地球上某一点的 位置可以用经纬度来表示。例如北京是东经 116 度、北纬 40 度左右,这是一个二维的数据。 同理,游戏的二维世界也可以通过这样的方式表示,具体来说,就是用 x y 分别表示横 向位置和纵向位置。有没有似曾相识的感觉,对了,这个就是我们中学时代的坐标系,如 3-2 所示。

Transcript of 第 3 章 Cocos2d-JS...

Page 1: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

第3 章 Cocos2d-JS 的平面世界

在前两章中,我们初步认识了 Cocos2d-JS,接着搭建了环境,还把

HelloWorld 跑起来了,部分新手读者是否有一点成就感了呢?不过,也许大家已

经发现之前都是填鸭式的节奏,几乎没有需要自己理解和操作的内容。这样很不满

足,是不是?所以,从这一章开始,我们正式开始学习 Cocos2d-JS。在第 1 章

中说过,有图形学基础或者网页开发基础的读者会更容易理解本书的内容,在这里

为了照顾新手读者,笔者还是需要稍微讲解一下最基础的平面世界知识。

3.1 Cocos2d 世界的经纬度——坐标系

首先,我们看一个典型的游戏画面。这个图有几个元素,首先最明显的应该是一个

超人,然后右侧有一个“HUNGRY HERO”字样的图标,另外还有两个按钮(PLAY 和

ABOUT),最后就是底下的背景图,如图 3-1所示。

在这个游戏画面中,超人和按钮的位置是怎么表示的呢?我们知道,地球上某一点的

位置可以用经纬度来表示。例如北京是东经 116度、北纬 40度左右,这是一个二维的数据。

同理,游戏的二维世界也可以通过这样的方式表示,具体来说,就是用 x 和 y 分别表示横

向位置和纵向位置。有没有似曾相识的感觉,对了,这个就是我们中学时代的坐标系,如

图 3-2所示。

Page 2: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

31

第3 章 Cocos2d-JS 的平面世界

图 3-1 某游戏的菜单界面 图 3-2 Cocos2d-JS坐标系

在 Cocos2d-JS 游戏中,画面的左下角是(0,0),x 从左往右递增,而 y 是从下往上递增

的,跟数学的二维坐标系一致。

有网页开发经验的读者是否发现 Cocos2d-JS 的坐标方向跟 Canvas 的方向相反。在

Canvas中,左上角才是(0,0),而 y值是从上往下递增的。这是因为 Canvas遵循网页的布局

规则,从上往下排列图片文字等。

3.2 场景(Scene)组成了 Cocos2d 世界

来到 Cocos2d-JS 的世界,我们需要了解一个概念——场景(Scene)。这跟我们日常生

活中的场景概念类似。例如,刚进入游戏的菜单界面是一个场景,游戏过程是一个场景,

游戏的商店也是一个场景。Cocos2d-JS 框架把游戏拆分为很多个场景,当玩家在不同界面

切换的时候,框架实际上就是让游戏画面在不同场景中切换。

实际上,我们已经跟场景见过一面,在上一章的 Hello World程序的 main.js中,有这

样一段,如代码清单 3-1所示。

代码清单 3-1

cc.LoaderScene.preload(g_resources, function () {

cc.director.runScene(new HelloWorldScene());

}, this);

Page 3: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

32

上述代码中使用 cc.LoaderScene.preload预加载 g_resources数组中指定的资源,并设置

一个匿名回调函数。当加载完成之后,LoaderScene会调用刚才设置的回调函数,也就是执

行 cc.director.runScene。关于导演(Director)的说明,我们放到 3.7节,这里姑且先理解为

框架切换场景的意思。这段代码完整的功能就是先加载资源,当加载完完成后就新建一个

类型为 HelloWorldScene的场景,并显示这个场景,如图 3-3所示。

图 3-3 HelloWorld场景

使用场景拆分游戏界面,能让游戏结构更清晰,代码更便于维护,而且有利于运行时

的性能优化。从一个场景切换到另外一个场景的时候,Cocos2d-JS 框架会把前者销毁,该

场景中包含的所有图片、文本等资源都会被清除。

接下来我们看如何拆分场景、自定义一个场景。在新建的 cocos2s-jd 工程中,可以找

到 HelloWorldScene.js,如代码清单 3-2所示。

代码清单 3-2

var HelloWorldScene = cc.Scene.extend({

onEnter:function () {

this._super();

var layer = new HelloWorldLayer();

this.addChild(layer);

}

});

这段代码就定义了一个场景,其中重点是 extend 函数,它扩展了 cc.Scene,得到

Page 4: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

33

第3 章 Cocos2d-JS 的平面世界

HelloWorldScene。cc 是一个 JS 对象,目的是实现跟 C++的命名空间类似的作用,把各种

Cocos2d-JS 原生类型都封装在这个命名空间中。extend 函数接受了一个对象作为参数,在

这里我们姑且理解 extend 函数就是继承或扩展的意思。在这个继承中,我们重定义了

onEnter函数,当游戏加载进入 HelloWorldScene这个场景时,就会执行这个函数。在 onEnter

函数中,首先执行 this._super()调用父类(cc.Scene)原有的 onEnter,然后再添加一个名为

HelloWorldLayer的子节点。如果你觉得不理解这段继承的意思,不要紧,我们在 3.8节会

再详细说明。在这里,我们理解一下代码的表面含义即可。

3.3 Cocos2d 世界物体的祖宗——节点(Node)

既然 Cocos2d由一个一个的场景组成,那么场景中的物体又用什么来表示和实现呢?这

一节我们要重点了解节点(Node),因为节点是 Cocos2d 世界中最基础的东西,所有显示在

屏幕中的东西实际上都有节点的影子。这就好比现实生活中,一切东西都可以称为物体一样。

节点封装了一些基本的操作或功能,例如缩放、坐标变化、缩放变化、透明度、可见

性等。而其他的显示类都继承节点,并按照各自需要扩展节点的功能,例如后续两节将要

介绍的层(Layer)、精灵(Sprite)。这里涉及面向对象的思想,不理解的读者可以自行查

阅相关的资料。节点的源代码如代码清单 3-3所示。

代码清单 3-3

cc.Node = cc.Class.extend({

_localZOrder: 0,

_globalZOrder: 0,

_vertexZ: 0.0,

_rotationX: 0,

_rotationY: 0.0,

_scaleX: 1.0,

_scaleY: 1.0,

_position: null,

_skewX: 0.0,

_skewY: 0.0,

_children: null,

_visible: true,

...省略后续代码

Page 5: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

34

节点有一个很重要的属性——_children,正如这个名字的意思一样,表示节点的孩子。

节点包含节点孩子,节点孩子又可以包含节点孩子,子子孙孙无穷尽也。这个关系就类似

一棵树,如图 3-4所示。

图 3-4 节点关系

这样的包含关系有非常重要的特性:父节点位置变化,它所包含的子节点也会跟着变

化,以整体的形式移动;父节点拉伸变大,子节点会按照一样的比例变大。除了位置和拉

伸,还有一些属性会有这种整体效应,包括可见性(visible)、透明度(opaque)、旋转角度

(rotation)、倾斜值(skewX和 skewY)等。

3.4 让 2D 世界层次化——层(Layer)

上一节讲到节点可以包含子节点,实现一个整体的效果,而在实际开发中,我们用得

更多的是层(Layer)。层继承了节点,加入了更多功能,例如背景颜色。

3.4.1 按层管理所有物体

“层”更符合我们对世界的认识,就好像百货大厦,每一层销售的商品种类都不一样。

相对应的,在一个典型游戏中,往往会包含一些层:背景层、人物层、道具层、背景层、

系统信息层。背景层是固定的图片或地图,人物层中主角和各种小怪可以不断移动,两层

之间互不干扰,各自管理层内的子节点。层继承节点的特点,可以在层中继续嵌套子层,

例如人物层中又可以继续拆分为主角层、NPC层。

使用层的方式很简单,首先新建一层:

var layer = new cc.Layer();

Page 6: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

35

第3 章 Cocos2d-JS 的平面世界

把层添加到舞台上:

scene.addChild(layer);

再把子节点添加到这个层上:

layer.addChild(child);

这三句代码都是面向对象的风格,代码的语义容易理解。跟 addChild 相对应,还有

removeChild,可以移除某个子节点。

3.4.2 把层扩展成各种功能的面板

Cocos2d-JS 提供了两个常用的 Layer 给我们使用:LayerColor 和 LayerGradient。顾名

思义,LayerColor 是一个纯色背景的层,利用这个类,我们可以很方便地实现纯色背景。

在自定义 Scene的 onEnter函数中增加代码:

var layerColor = new cc.LayerColor(cc.color(255, 255, 255),100, 100);

this.addChild(layerColor);

这样就新建了一个纯白色宽和高都是 100像素的背景,并添加到舞台上。其中 cc.color

是一个全局函数,接受 0到 255的 R、G、B三通道的值,产生一个颜色值。 如果不设置

宽和高,层将默认为全屏大小。

LayerGradient是颜色渐变的层。例如以下代码可以实现由一个红色渐变为蓝色的背景:

var layerGradient = new cc.LayerGradient(cc.color(255,0,0), cc.color(0,0,255));

this.addChild(layerGradient);

除了 Cocos2d-JS提供的固定 Layer 外,我们可以自行扩展层 ,扩展方式跟自定义场景

类似。例如我们自定义一个背景层,就可以在不同的场景中复用。在 HelloWorld代码中就有

HelloWorldLayer,里边的代码较为复杂,这里我们看一个简化版本,如代码清单 3-4所示。

代码清单 3-4

var HelloWorldLayer = cc.Layer.extend({

ctor:function () {

this._super();

var bg = new cc.Sprite(res.HelloWorld_png);

var size = cc.director.getWinSize();

Page 7: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

36

bg.x = size.width / 2;

bg.y = size.height / 2;

this.addChild(bg, 1);

return true;

}

});

在上述代码中,HelloWorldLayer扩展了 cc.Layer,默认加载了一张图作为背景。addChild

(bg,1)表示把背景添加在最底层(层号从 1开始),Layer会根据第二个参数把孩子节点做排

序,数字越小叠放在越低层次。而 cc.Sprite的含义将在下一节讲解。函数最后一句 return true

表示函数成功执行完成,不过,即使缺失了这一句代码,程序也不会出现问题。大家可以

理解 return true为习惯性写法。

在任意一个场景中,如果要加载同样的背景,就只需要简单两句代码即可,这就是复

用的好处。

var layer = new HelloWorldLayer();

this.addChild(layer);

那么,Layer的继承关系就如图 3-5所示。Layer继承 Node,而 LayerColor、LayerGradient

和自定义的扩展层又继承 Layer,拥有 Layer的全部功能。

图 3-5 Layer的继承关系

3.5 二维世界的人物——精灵(Sprite)

上一节出现了 cc.Sprite,它就是本节要介绍的精灵(Sprite)。精灵也是由节点 Node扩

Page 8: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

37

第3 章 Cocos2d-JS 的平面世界

展而成的,原来的目的是用于表示游戏中的人物或 NPC等,但在实际中,因为精灵封装了

图片加载等常用功能,我们还可以用精灵来加载背景图、障碍物等非人物内容。

使用 Sprite非常简单,只需要两句代码就可以完成加载工作:

var ball1 = new cc.Sprite("res/item_2.png");

this.addChild(ball1, 0);

新建 Sprite时,只需要在构造函数中传入图片的路径即可。

另外,再增加设置 ball1坐标的代码,让 ball1在屏幕中间显示:

var size = cc.director.getWinSize();

ball1.x = size.width/2;

ball1.y = size.height/2;

这里 cc.director.getWinSize可以获得窗口的设计尺寸,也就是我们在 main.js中设置的

宽和高。那么,上述代码的运行结果就如图 3-6所示,一个球显示在画面的中间。

图 3-6 加载图片

在游戏中,无论是主角还是 NPC,还是各种飞机大炮,都可以使用精灵来加载。精灵

替我们把图片处理的烦琐工作都处理好了,我们只需要设置精灵的位置和后续动作即可。

不过,在 HTML5游戏中,我们还需要更新 resource.js,把所有图片路径添加到数组中,这

样 Cocos2d-JS在初始化时会预加载这些资源,最后我们才能正常新建精灵,如代码清单 3-5

所示。

Page 9: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

38

代码清单 3-5

var res = {

bg : "res/bg.jpg",

item2 : "res/item_2.png",

item3 : "res/item_3.png"

};

var g_resources = [];

for (var i in res) {

g_resources.push(res[i]);

}

如果单纯加载一个图片还无法满足需要,例如飞机有血量、导弹数量等信息,我们可

以扩展精灵,定义自己新的类,例如代码清单 3-6中的写法:

代码清单 3-6

var Plane = cc.Sprite.extend({

life: 100,

ctor: function (imageURL) {

this._super(imageURL);

this.life = 100;

},

onHit: function () {

this.life -= 20;

}

});

当飞机碰到障碍物时,我们调用 Plane的 onHit函数,控制飞机的生命减 20。这些都是

面向对象的编程方式,而关于 Cocos2d-JS的面向对象或继承的内容,我们还是留到 3.8节再

详细探讨。

另外,精灵和层一样,还可以继续嵌套其他节点,精灵还可以包含精灵。这个嵌套

机制可能在开始时难以理解,但使用起来将非常方便。举个例子,例如游戏中飞机得到

Page 10: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

39

第3 章 Cocos2d-JS 的平面世界

了某个加强能力,画面上需要表现为加上一个尾翼之类的效果,我们可以选择用一张全

新的图片来表现,也可以在原图片上叠加一个小的尾翼图片。第二个方案可以节省图片

的大小,而且,在多个飞机都需要做一样处理时这个方案将显得更加实用。这个方案的

代码如代码清单 3-7所示。

代码清单 3-7

var plane = new cc.Sprite("res/plane.png");

this.addChild(plane);

var tail = new cc.Sprite("res/tail.png");

plane.addChild(tail);

tail.x = -50;

tail.y = 10;

把 tail添加到 plane上之后,需要设置 tail的位置,而这个坐标并不是相对整个画面的

左下角定位的,而是相对 plane 自身内部的坐标系。这真是天外有天,我们在下一节慢慢

探讨这个细节。

3.6 天外有天——当层和精灵嵌套时怎么设置坐标

我们在 3.1节学习了 Cocos2d-JS世界的坐标系,这个坐标系是整个游戏画面的坐标,

称为全局坐标,而当层和精灵不断嵌套时将产生新的坐标系。层添加在舞台上,这时候层

的定位使用全局坐标,同时,层的内部也有一个局部坐标,而再把精灵添加到层上时,精

灵将以层的局部坐标定位。就好像军训,排头兵在操场上找到自己的位置,旁边的兵以排

头兵作为参考,而第三个兵又以第二个兵为参考。

我们来看个例子,如代码清单 3-8所示。

代码清单 3-8

var bg = new cc.LayerColor(cc.color(100,100,100), 200, 200);

bg.x = 100;

bg.y = 100;

this.addChild(bg, 1);

var ball1 = new cc.Sprite("res/item_2.png");

ball1.x = 100;

ball1.y = 300;

Page 11: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

40

this.addChild(ball1, 2);

var ball2 = new cc.Sprite("res/item_3.png");

ball2.x = 100;

ball2.y = 100;

bg.addChild(ball2, 1);

在舞台上添加一个宽、高为 200 像素的灰色层,并把层移动到画面的(100,100)位置,

再添加一个球,放到画面(100,300)位置,最后再添加一个球,但这个球放到灰色层上,坐

标设置为(100,100)。这段代码将得到如图 3-7所示的效果。

我们可以发现:灰色层以整个画面为参照,左下角的位置正好就是(100,100),而 ball1

在层的左上角,球的中心跟层的左上角正好重合,球心的位置相对整个画面来说,正好是

(100,300),而层中间的球 ball2 又正好在层的中间位置,球心相对层的左下角来说位置是

(100,100),如图 3-8所示。

图 3-7 精灵嵌套关系 图 3-8 精灵局部坐标系

由此我们可以总结出,每个节点都有自己内部的局部坐标系,这个局部坐标系的原点

位置就是该节点的左下角,例如上边的灰色层,它的局部坐标系原点在全局的位置就是

(100,100)。嵌套的子节点都以父节点的左下角作为坐标原点,再设置自身的位置。

另外,在上述例子中大家可以明显发现精灵和层不一样,同样设置坐标为(100,100),

层的左下角移动到(100,100),而精灵却是把中心点移动到(100,100)。我们这样理解:精灵

加载图片后做了特殊处理,把图片中心移动到精灵的坐标点位置。不过,无论层还是精灵,

如果再添加子节点,都以左下角作为子节点的局部坐标系原点,对于 LayerColor来说,就

是色块的左下角,而对于 Sprite,则是图片的左下角。

(100,300)

(200,200)

(100,100)

Page 12: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

41

第3 章 Cocos2d-JS 的平面世界

这里顺便再介绍另外一个重要的属性——锚点(anchor)。层和精灵的 anchorX和 anchorY

默认都是 0.5,表示以中心点作为锚点。锚点的作用是,当这个节点缩放旋转时,将以这个

点作为参考点,锚点为 0.5 能让节点缩放和旋转时保持位置不变,如图 3-9 所示。左侧小人

以左下角作为锚点,而右侧小人的锚点为右上角,中间小人的锚点则保持为默认中心点。

图 3-9 锚点

这部分有点难理解,如果大家一时半刻没理解,也不必纠结,反正都是可视化的东西,

哪天自己练习发现位置不对,再回头想起这里有这样一节内容即可。

3.7 导演(Director)指挥一切

我们在运行 HelloWorld时就知道有 cc.director.runScene,但只见过一面,不知这个导演

(Director)为何方神圣?Cocos2d-JS 世界拆分为若干个场景,那么场景和场景之间的切换

工作就交给导演了。顾名思义,这个跟实际拍电影是一样的,导演说,现在拍第一场,那

么第一场的演员就纷纷各就各位;导游说 cut,大家就停下来;导游说换下一场,那么当前

的演员就退出而下一场的演员就上台。

3.7.1 场景的切换

首先,我们需要记住的是 cc.director.runScene,这个函数用于加载或切换场景。

接着,由于我们已经有 HelloWorldScene,我们再新建一个 SecondScene,如代码清

单 3-9所示。

代码清单 3-9

var SecondScene = cc.Scene.extend({

Page 13: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

42

onEnter: function () {

this._super();

var layer = new cc.LayerGradient(cc.color(255,0,0),cc.color(0,0,255));

this.addChild(layer);

}

});

这个场景只有一个渐变的层。

另外,再在 HelloWorldScene中增加切换场景的定时器。为了方便和容易理解,这里的

定时器使用 JS的 setTimeout,控制 3秒后自动切换到场景 2。但为了原生版本兼容性,实

际上我们不推荐使用这个函数,而使用 Cocos2d-JS提供的定时器,如代码清单 3-10所示。

关于定时器的内容,我们在第 6章再学习。

代码清单 3-10

var HelloWorldScene = cc.Scene.extend({

onEnter:function () {

this._super();

var layer = new HelloWorldLayer();

this.addChild(layer);

setTimeout(function(){

cc.director.runScene(new SecondScene());

}, 3000);

}

});

大家运行后发现,从场景 1到场景 2的过程是瞬间的,没有任何的过渡效果。是不是

觉得这样不够高大上?那我们看看 Cocos2d-JS给我们预备了什么高大上的套餐。

首先介绍滑动切换(Slide):

cc.director.runScene(new cc.TransitionSlideInT(2, new SecondScene()));

跟原来稍有不同的是,传递给 runScene 的参数并不是直接的 Scene,而是经过了 cc.

Page 14: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

43

第3 章 Cocos2d-JS 的平面世界

TransitionSlideInT的包装。TransitionSlideInT(2, new SecondScene())就表示让 SecondScene

在 2秒内从上往下滑进舞台,旧场景也同时滑出。如图 3-10所示,展示了两个场景的切换

过程。

图 3-10 场景切换效果

顾名思义,T表示 Top,从上往下,那么还有从左往右 TransitionSlideInL,从右往左

TransitionSlideInR,从下往上 TransitionSlideInB。

大家可以从项目 frameworks中找到 Cocos2d-JS的库文件 CCTransition.js,在源代码中

我们可以找到全部效果。另外也可以在官方 demo的 SceneTest中看到一些效果。这里笔者

再列举几个:

• TransitionMoveInB:从下往上移进(跟 TransitionSlideInB的区别在于旧场景不会

移动);

• TransitionRotoZoom:旧场景旋转变小消失,新场景旋转变大出现;

• TransitionJumpZoom:旧场景缩小再跳跃离开,新场景跳跃进入;

• TransitionShrinkGrow:旧场景变小,新场景在旁边逐渐变大;

• TransitionFlipX:旧场景横向翻转,翻转后便是新场景;

• TransitionFlipAngular:跟 FlipX类似,旧场景斜向旋转;

• TransitionFade:旧场景逐渐变淡消失,新场景渐显出现;

• TransitionTurnOffTiles:新场景直接出现在背后,而旧场景以马赛克方式逐渐消失;

• TransitionSplitCols:画面分开 3列,分别滑出滑进;

runScene会销毁旧场景上的所有内容,下次回到该场景时,所有内容都需要重新建立。

如果频繁切换场景,我们可以使用 pushScene 和 popScene。pushScene 跟 runScene 用法一

致,但并不销毁场景内容,而是把上一个场景存起来;popScene则把当前场景销毁,然后

Page 15: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

44

快速回到上一个场景中,这时候只需要重新唤起原来的内容,并不需要重新建立。这对方

法组合特别适合一些游戏设置界面,例如从游戏主界面切换到音效设置界面,由于玩家设

置完音效后会马上切换回到游戏界面,所以这里如果把游戏界面销毁再重建就会浪费效率。

3.7.2 导演可以提供的信息

导演这个老大不是只懂得指挥就可以当的,它必须还有求必应,懂得如何解决拍摄中

的各种问题。在 Cocos2d-JS中,导演 cc.director可以为我们提供很多信息或功能,这里笔

者列出一些常用的功能:

• 窗口的设计尺寸。getWinSize,这个信息在布局 UI的时候尤其重要;

• 窗口的实际尺寸:getVisibleSize,这个信息在适配手机屏幕的时候非常必要,具体

用法我们在第 20章讲解;

• 获取全局的定时器:getScheduler;

• 暂停/恢复场景:pause/resume。

3.8 额外说说 Cocos2d-JS 的语法

3.8.1 JS面向对象和继承

Cocos2d-JS的 JS语法对于 JS初学者来说可能难以理解,因为 JS基本语法中并没有关

于类、继承等知识。JS语言本来并没有这些概念,在网页开发中,一般只需要用到全局变

量和局部变量,并不会接触 new这个关键字。但在 HTML5游戏编程中,由于有太多的精

灵,太多的图片,我们不得不利用面向对象的经验,为 JS加入对象和继承的概念。

已经有不少实现 JS 继承的方案,都是基于函数 function 和原型 Prototype 进行的。

Cocos2d-JS 使用 John Resig 的继承方案,详细地址:http://ejohn.org/blog/simple-javascript-

inheritance/。这里举个最简单的面向对象例子,如代码清单 3-11所示。

代码清单 3-11

var object = function(){

};

var object1 = new object();

object1.name = "object1";

var object2 = new object();

Page 16: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

45

第3 章 Cocos2d-JS 的平面世界

object2.name = "object2";

console.log(object1.name, object2.name);

把这段代码贴到 Chrome 控制台,按回车键运行可以发现输出了 object1、object2。这

个例子利用了 function的 Prototype实现了不同的对象。

Cocos2d-JS 定义了 Class 作为基类,可以在 CCClass.js 看到源代码,如代码清单 3-12

所示。

代码清单 3-12

cc.Class = function () {

};

cc.Class.extend = function (props) {

var _super = this.prototype;

//And make this Class extendable

Class.extend = cc.Class.extend;

//add implementation method

Class.implement = function (prop) {

for (var name in prop) {

prototype[name] = prop[name];

}

};

Class类封装了 extend、implement方法,实现了类似 C++或 Java的面向对象功能。

在扩展某个类时,我们使用以下的写法,代码清单 3-13所示。

代码清单 3-13

var MyScene = cc.Scene.extend({

ctor: function (color) {

this._super();

var layer = new cc.Layer (color);

this.addChild(layer);

}

});

var scene = new MyScene(cc.color(255,255,255));

Page 17: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

46

利用 extend函数可以扩展某个类,在参数对象中编写需要扩展的内容,例如新添加的

属性和需要覆盖的函数定义。上述代码就覆盖了 ctor函数,这个函数是 Cocos2d-JS类固有

的构造函数,可以理解为跟 C++的构造函数一致。新建MyScene对象时需要传入构造函数

所需的参数,例如上述例子中的 new MyScene(cc.color(255,255,255))。

重定义的函数中如果需要调用父类的同名函数,可以通过 this._super()来完成,而 ctor、

onEnter 等 Cocos2d-JS 原生函数,在重定义时必须先调用父类的同名函数。另外,在子类

中调用本对象内任意方法都必须带 this前缀,例如例子中的 this.addChild不能只写 addChild,

否则运行时浏览器会在全局查找该函数,然后报出函数不存在的错误(Uncaught Reference-

Error:addChild is not defined)。

3.8.2 有点麻烦的 this

this 在 JS 中是一个非常晦涩难懂的关键字,虽然在网站开发中不经常用到,但由于

Cocos2d-JS中都是面向对象的编写风格,所以我们必须先学习或复习一下这个知识点。

简单来说,this 会随着函数使用的场合不同而代表不同的值,但有一个原则,也就是

指向调用这个函数的对象。

情况一:全局函数,如代码清单 3-14所示。

代码清单 3-14

var x = 1;

function test() {

console.log(this.x);

}

test();//1

var x = 1;

function test() {

this.x = 0;

}

test();

console.log(x);//0

如上述代码所示,全局函数中 this指向的是 global对象。

情况二:某个对象的方法,如代码清单 3-15所示。

Page 18: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

47

第3 章 Cocos2d-JS 的平面世界

代码清单 3-15

function test() {

console.log(this.x);

}

var o = {};

o.x = 1;

o.m = test;

o.m(); //1

情况三:作为构造函数。

使用 new关键字,再调用 function会生成一个新的对象,而 this则指向这个对象,如

代码清单 3-16所示。

代码清单 3-16

function test() {

this.x = 1;

}

var o = new test();

console.log(o.x);//1

情况四:apply/call调用。

这种情况下,this将指向 apply/call的第一个参数 target,如代码清单 3-17所示。

代码清单 3-17

function test() {

console.log(this.x);

}

var o = {};

o.x = 1;

o.m = test;

var o2 = {};

o2.x = 2;

o.m.apply(o2);//2

o.m.call(o2);//2

Page 19: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

48

再回到 Cocos2d-JS中,后续我们在菜单的事件处理中经常会碰到 this情况,所以这里

直接拿 HelloWorld作为例子,给大家讲解 this的注意事项,如代码清单 3-18所示。

代码清单 3-18

var HelloWorldLayer = cc.Layer.extend({

helloLabel:null,

ctor:function () {

this._super();

var size = cc.winSize;

var closeItem = new cc.MenuItemImage(

res.CloseNormal_png,

res.CloseSelected_png,

this.closeClicked);

closeItem.attr({

x: size.width - 20,

y: 20,

anchorX: 0.5,

anchorY: 0.5

});

var menu = new cc.Menu(closeItem);

menu.x = 0;

menu.y = 0;

this.addChild(menu, 1);

this.helloLabel = new cc.LabelTTF("Hello World", "Arial", 38);

this.helloLabel.x = size.width / 2;

this.helloLabel.y = size.height / 2;

this.addChild(this.helloLabel, 5);

return true;

},

closeClicked: function () {

this.removeChild(this.helloLabel);

}

});

Page 20: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

49

第3 章 Cocos2d-JS 的平面世界

这段代码只保留了关闭按钮和标题文字,另外把单击按钮的事件处理函数设置为

closeClicked,尝试在单击的时候把文字移除。

初步看一下,感觉不会有问题。尤其是习惯了 C++/Java的朋友,很难发现这里有什么

毛病。

在 Chrome中运行,单击关闭按钮后会出现错误:

uncaught TypeError: Object [object global] has no method 'removeChild'

实际就是说 closeClicked函数中,this对象的指向错了,并没有像我们想象的那样指向

HelloWorldLayer。

这个问题的根源是,虽然开始把 closeClicked传递给按钮 closeItem,但 closeItem被单

击的时候,执行 closeClicked的时候是在全局对象下调用的,this对象指向的 Global并不是

HelloWorldLayer。这也就是刚才列举的四个情况的第一种情况。

要解决这个问题,我们可以使用 bind函数或者在新建 closeItem时传递 this作为 target。

先看看 bind函数:把代码

var closeItem = new cc.MenuItemImage(

res.CloseNormal_png,

res.CloseSelected_png,

this.closeClicked);

改为

var closeItem = new cc.MenuItemImage(

res.CloseNormal_png,

res.CloseSelected_png,

this.closeClicked.bind(this));

Cocos2d-JS 给 function 增加了 bind 方法,bind 返回一个新的 function,这个 function

执行的时候 this将固定指向 bind的第一个参数。

如上述代码中 this.closeClicked.bind(this),此时的 this正好是 HelloWorldLayer,经过这

样处理后,closeClicked执行的时候 this就能正确指向 HelloWorldLayer了。

bind的实现方式,大家可以利用WebStorm的功能,查看 Cocos2d-JS源代码,实际上

是通过 apply实现的。

Page 21: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

50

再看看新建 closeItem时传递 this作为 target:把代码

var closeItem = new cc.MenuItemImage(

res.CloseNormal_png,

res.CloseSelected_png,

this.closeClicked);

改为

var closeItem = new cc.MenuItemImage(

res.CloseNormal_png,

res.CloseSelected_png,

this.closeClicked, this);

这里的 this是第 4个参数,也是最后 1个参数。MenuItemImage初始化时识别到最后

这个参数为对象,而并非函数,就会把这个参数作为回调函数的 target。也就是说,在单击

closeItem 后,框架底层会使用 call 来触发回调函数 callbackFunction.call(target)。这样原来

的 this参数(HelloWorldLayer)就原封不动地传递到回调函数 closeClicked中了。

总结一下,在 Cocos2d-JS编程过程中,大家需要注意合理使用 this:

(1)在对象中需要访问本对象的属性或方法都必须加入 this 前缀,例如上述例子中的

this.closeClicked。

(2)在绑定事件处理函数或定时器回调函数时,如果回调函数中使用了 this,需要注

意使用 bind或传递 this作为 target参数。

Page 22: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

第4 章 让世界来点动静

上一章我们学习了 Cocos2d-JS 的基本元素节点,也学习了如何加载图片,

但一切还是静止的画面。在这一章,我们给这个世界来点动静,让它更像一个游戏。

4.1 帧的概念

帧是动画或影像的最基本单位。每一帧是一个画面,连续的多帧画面组合在一起播放

就形成了影像,就好像电影胶卷的一格。而帧频就是一秒内帧的数量,通常用 fps(Frames

Per Second)表示,帧频越高,画面就越流畅。

一般电影为一秒 24帧,而游戏则一般以 60 fps作为最高帧频,因为 60 fps已经是人眼

正常识别的最高频率了,设置再高的频率只会造成性能浪费。相反,如果游戏画面的帧频

低于 30 fps,我们就会感觉到不流畅,也就是所谓的“卡”。游戏流畅度跟游戏的设计好坏,

还有手机的硬件水平都有关系。当然,我们开发一个游戏不可能指望大家都在 iPhone 6上

玩,为了保证游戏的流畅度,我们只能尽力优化游戏的代码,提高渲染的效率。第 10章会

讲解 Cocos2d-JS性能优化的内容。

在 Cocos2d-JS中,也有帧频这个概念,在项目文件 project.json中就有帧频的设置,一

般默认是 60。Cocos2d-JS会以项目配置设定的帧频运行游戏,如果帧频设置为 60 fps,就

会“尝试”每秒对画面中所有节点重绘 60遍。为什么这里用“尝试”字眼呢?因为你设定

Page 23: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

52

了 60 fps,Cocos2d-JS不一定能真的一秒重绘 60遍,这个只是设定了一个最高值或者说是

目标值。如果游戏在 PC上运行,可能很轻松达到 60 fps,但如果在手机上运行就可能因为

手机性能差,Cocos2d-JS尽力重绘也只有 40 fps。相反,如果你设置较低帧频,例如 30 fps,

虽然 Cocos2d-JS 很轻松达到了这个目标,但它后边就会偷懒,无论机器性能多好,

Cocos2d-JS只会把帧频维持在 30 fps,而不会再变高。

4.2 模仿胶卷电影——逐帧变化

按照帧的概念,我们需要在每一帧中做点变化,整个画面就会出现动画效果。那么我

们接下来尝试一下。

在节点 Node 中,有一个接口可以让我们轻松实现每一帧做点小动作的想法。这个接

口就是 scheduleUpdate和 update。

我们做个简单的小动画,画面中只加载一个小球,然后让这个球做正弦波动,代码如

代码清单 4-1所示。

代码清单 4-1

var BallLayer = cc.Layer.extend({

deltaX:1,

ball:null,

frame:0,

bg:null,

ctor:function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = 0;

ball.y = size.height/2;

this.addChild(ball, 1);

this.ball = ball;

this.bg = new cc.DrawNode(); //用于记录球的运动轨迹

Page 24: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

53

第4 章 让世界来点动静

this.addChild(this.bg);

this.scheduleUpdate();

return true;

},

update: function () {

var size = cc.director.getWinSize();

this.ball.x += this.deltaX;

if(this.ball.x >= size.width || this.ball.x <= 0){

this.deltaX *= -1;

}

this.ball.y = Math.sin(this.frame/20)*50 + size.height/2;

this.bg.drawDot(new cc.Point(this.ball.x, this.ball.y), 2, cc.color(255,

0,0)); //把球的运动轨迹画到背景板上

this.frame++;

}

});

代码中的关键点是 scheduleUpdate 和 update 这两个函数。scheduleUpdate 通知当前节

点在每帧重绘之前调用 update函数,update这个函数是 Cocos2d-JS原生指定的函数名,一

般来说我们没必要修改。然后,我们重写 update函数。在本例中,我们把小球的 x值不断

地自增或者自减,实现左右来回移动的效果;把小球的 y 值设置为跟当前帧相关的一个正

弦值,作用就是实现不断的上下波动。最后,我们再玩玩小花样,在小球运动过程中画下

它的轨迹,代码中的 DrawNode就是做这个用途的,我们可以理解为一个画板,可以画点,

也可以画线或者长方形等。最后,我们在画面中看到这样的效果,如图 4-1所示。

图 4-1 自定义动画

Page 25: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

54

每一帧对画面内容做一些调整,从而形成动画,这种方式是最原始的,也是最灵活的,

大家可以尽情发挥自己的想象来做一些特殊的动画效果。

4.3 现成的既定动作

在实际游戏开发中,有时候讲究的不是效果,而是开发效率,如果有现成的既定动作,

我们肯定首先考虑选用这些方式,没必要重复造轮子。而 Cocos2d-JS已经把很多常见的动

作封装成简易的接口,极大提高了开发的效率。这一节,我们来看看如何使用这些动作。

4.3.1 基本动作

Cocos2d-JS的节点 Node有一个接口叫 runAction,专门用于执行既定动作。这个接口

使用非常简单,只需要新建一个 Action,然后传给这个接口即可。在这一节,我们将会学

习几个常用的基本动作,包括 moveTo/moveBy、scaleTo/scaleBy、fadeTo/fadeIn/fadeOut、

blink、tintTo。

1.移动——moveTo和 moveBy

这两个动作都可以实现在指定时间内让节点平滑地移动到某个位置。说起来太抽象,

我们马上来写一段代码试试。基于前一节的例子,我们新建一个 ActionLayer,专门来测试

各种简单动作,如代码清单 4-2所示。

代码清单 4-2

var SimpleActionLayer = cc.Layer.extend({

ctor:function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = 0;

ball.y = size.height/2;

this.addChild(ball, 1);

var action = cc.moveTo(2, cc.p(size.width, size.height/2));

ball.runAction(action);

Page 26: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

55

第4 章 让世界来点动静

return true;

}

});

在上述代码中,还是加载了一个小球,核心的两句代码是 cc.moveTo 和 runAction。

cc.moveTo 是 Cocos2d-JS 的全局函数,用于快速建立一个 MoveTo 对象。这句代码的意思

是,2秒内让小球移动到屏幕右侧中间。

大家果断运行代码看看吧,简单的两句代码,就可以完成小球的动画了,比上一节自

己写逐帧控制便利多了。

同理,我们再试试 moveBy,把 cc.moveTo替换为以下代码:

cc.moveBy(1, cc.p(size.width, 100));

moveBy 跟 moveTo 的不同点就在于,moveBy 控制节点走多远,而 moveTo 控制节点

走到哪里。

2.放缩——scaleTo和 scaleBy

ball.x = size.width/2;

var action = cc.scaleTo(1, 2, 2);

上述代码把小球放到屏幕中间,并设置小球在 1 秒内变大到原始图的 2 倍,如图 4-2

所示。由于小球是精灵,默认的锚点在中心,放大时小球保持在屏幕中间。另外,scaleTo

的参数也可以省略为只有 2个,例如 cc.scaleTo(1,2),这样则表示横向的比例和纵向的比例

都是 2。

图 4-2 scaleTo 的效果

Page 27: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

56

同理,scaleBy 的意思就是让节点在当前缩放比例基础上再乘以一个比例,而 scaleTo

以精灵的原始图为参考,不考虑当前的缩放比例。

这里顺便给大家说个小技巧,一些横屏打斗游戏,人物需要往左往右两个方向的图片,

实际上这里可以通过水平翻转来实现,从而节省一张图片。如果有 Flash AS2.0经验的读者

一定很了解这个方法。具体方式:设置节点的 scaleX为−1则可以让图片水平翻转。

这里我们再玩个动画,如代码清单 4-3所示。

代码清单 4-3

ball.scale = 2;

var action = cc.scaleTo(2, -2, 2);

ball.runAction(action);

首先把小球放大,方便观看效果。scaleTo的 3个参数分别表示 2秒、水平翻转和垂直

保持不变。运行这个代码,我们可以看到小球匀速做水平翻转,如图 4-3所示。

图 4-3 翻转效果

同理,如果设置 cc.scaleTo(2, 2, −2)就可以让小球垂直翻转。

3.淡入淡出——fadeTo、fadeIn和 fadeOut

顾名思义,fade 表示淡出的意思。利用这几个动作,我们可以轻易实现精灵逐渐淡入

或淡出画面的动画。

var action = cc.fadeTo(2, 0);

ball.runAction(action);

fadeTo接受 2个参数,第一个是时间,第二个是 0到 255的整数,表示目标透明值,0

Page 28: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

57

第4 章 让世界来点动静

表示不可见,255 表示完全不透明。运行代码可以发现小球在 2 秒内逐渐变暗到消失,如

图 4-4所示。

图 4-4 谈出效果

其实可以使用 fadeOut代替上述的代码,直接让小球逐渐淡化到消失。

var action = cc.fadeOut(2);

ball.runAction(action);

相反,我们也可以反其道而为之,让小球淡入到画面:

ball.opacity = 0;

var action = cc.fadeIn(2);

ball.runAction(action);

首先需要设置小球的透明度为 0,然后运行 fadeIn。

4.一闪一闪亮晶晶——blink

利用 blink动作,我们可以实现精灵闪烁。说到这里,大家是否想起游戏中主角掉坑后

重生时那一闪一闪的无敌状态?

下面的代码可以让精灵 2秒内闪烁 10次。

var action = cc.blink(2, 10);

ball.runAction(action);

5.给点颜色看看——tintTo

利用 tint可以让精灵的色调发生变化,例如如下的代码:

Page 29: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

58

var action = cc.tintTo(2, 100, 0, 0);

ball.runAction(action);

可以让小球在 2秒内,红色通道变暗,绿蓝通道将为 0,整个画面变红色,如图 4-5所示。

图 4-5 TintTo 效果

TintTo最终效果等于设置精灵的 color值,上述效果等同于:

ball.color = cc.color(100,0,0);

大家看到这里,肯定会疑惑,这个“给点颜色看看”到底是怎么变换的呢?

如果大家细心追查 Node的源代码,可以看到 updateDisplayedColor函数,在这里将能

发现答案。

这里实际上就是把精灵图片每一个像素点的 R、G、B三通道值分别做了 multiply的图

像处理。假设原来的颜色是 oldRGB,而设置的 tintTo颜色为 rgb,那么新的颜色 newRGB

则可以由如下算法得到,如代码清单 4-4所示。

代码清单 4-4

newR = oldR * r / 255;

newG = oldG * g / 255;

newB = oldB * b / 255;

不小心又说到数学了,可能部分读者看得一头雾水了,我们暂且跳过这个公式。

大家能想到这个 tintTo最大的作用是什么吗?给个提示,游戏中主角受伤的时候,tintTo

可以实现精灵颜色闪变一下。下列代码可以实现小球变红后马上恢复原状,如代码清单 4-5

所示。

Page 30: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

59

第4 章 让世界来点动静

代码清单 4-5

var action1 = cc.tintTo(0.3, 100, 0, 0);

var action2 = cc.tintTo(0.3, 255, 255, 255);

ball.runAction(cc.sequence(action1, action2));

这里有一个新的知识点——sequence,我们在 4.3.2节将会详细讲解,这个函数的作用

就是让多个动作一个接着一个地执行。

6.其他的动作

关于更多动作,大家可以查看 Cocos2d-JS源代码 CCActionInterval.js,这里笔者简单列

举出来:

• delayTime——延迟一段时间,在做组合动作的时候特别好用;

• RotateTo/rotateBy——对节点做旋转;

• SkewTo/skewBy——对节点做倾斜变化;

• JumpTo/jumpBy——让节点跳跃移动;

• BezierTo/bezierBy——让节点沿贝塞尔曲线移动。

4.3.2 放一个连招——组合动作

在上一节,我们认识了 Cocos2d-JS提供的一些基本动作,但这些基本动作都是孤立的,

多少显得有点单薄,如果要制作类似街机打斗游戏的连招效果,我们该怎么做呢?不用着

急,Cocos2d-JS 为我们提供了很多方案,我们可以很轻松地把基本动作组合起来。只要我

们把降龙十八掌每一掌都做到灵活贯通,那么就可以通过组合实现千变万化的效果了。

Cocos2d-JS提供了顺序(sequence)、重复(repeat)、无限重复(repeatForever)、同时

执行(spawn)和反向(reverseTime/reverse)一共 5种组合基本动作的方式。

1.顺序(sequence)

Sequence可以让任意多个动作串联起来,一个接着一个按顺序播放。

使用方法:

cc.sequence(action1,action2,…);

这个函数返回一个新的动作,其中每个参数都必须是动作,可以是上边提到的基本动

作,也可以是组合动作,就好像节点可以嵌套节点一样,如代码清单 4-6所示。

Page 31: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

60

代码清单 4-6

var ComposeActionLayer = cc.Layer.extend({

ctor:function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = 0;

ball.y = size.height/2;

this.addChild(ball, 1);

var action1 = cc.moveTo(2, cc.p(size.width/2, size.height/2));

var action2 = cc.scaleTo(1, 2, 2);

var sequence1 = cc.sequence(action1,action2);

var action3 = cc.scaleTo(1, 1, 1);

var sequence2 = cc.sequence(sequence1, action3);

ball.runAction(sequence2);

return true;

}

});

上述代码设定了 3个基本的动作:把小球从左侧移动到中间,把小球放大到 2倍,把

小球缩小到原大小。首先使用 1个 sequence把 action1和 action2串联起来,得到 sequence1,

然后再把 sequence1和 action3串联起来,得到最终要执行的 sequence2。运行后可以发现,

上述代码的效果跟 cc.sequence(action1,action2,action3)是一致的。

2.重复(repeat)

repeat可以让某个动作重复执行多次,结合 sequence或 reverseTime会更实用。

使用方法:

cc.repeat(action, times)

函数返回一个新的动作,第一个参数传入需要重复的动作,第二个参数就是重复的次

数。另外,动作本身也有 repeat方法,跟 cc.repeat效果一致,例如 action.repeat(5)。

Page 32: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

61

第4 章 让世界来点动静

我们把上边 sequence例子的代码修改一下,如代码清单 4-7所示。

代码清单 4-7

ball.x = size.width/2;

var action1 = cc.scaleTo(1, 2, 2);

var action2 = cc.scaleTo(1, 1, 1);

var sequence = cc.sequence(action1, action2);

var repeat = cc.repeat(sequence, 5);

ball.runAction(repeat);

把小球移动到屏幕中间,然后设置 2 个动作,分别是放大和缩小,然后再让这个放大

缩小动作重复播放 5遍。运行后,大家可以发现效果跟预期一致。

3.无限重复(repeatForever)

repeatForever可以让某个动作不断重复,跟 repeat类似。

使用方法:

cc.repeatForever(action)

函数返回一个新的动作,只接受 1个参数。另外,动作本身也有 repeatForever方法,

跟 cc.repeatForever效果一致,例如 action.repeatForever ()。

把 repeat的例子修改一下,就可以让小球不断地放大缩小,如代码清单 4-8所示。

代码清单 4-8

var action1 = cc.scaleTo(1, 2, 2);

var action2 = cc.scaleTo(1, 1, 1);

var sequence = cc.sequence(action1, action2);

var repeat = cc.repeatForever(sequence);

ball.runAction(repeat);

4.同时执行(spawn)

spawn这个单词意思比较有意思,表示产卵或大量生产。在实际代码中,spawn可以让

多个动作同时开始执行,所以这个单词的意思大家就意会一下好了。

使用方法:

cc.spawn(action1, action2, …)

Page 33: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

62

函数返回新的动作,接受任意多个参数。

我们继续修改代码,这次让小球从左侧移到中间,同时放大到原来的 2 倍,如代码清

单 4-9所示。

代码清单 4-9

var action1 = cc.moveTo(2, cc.p(size.width/2, size.height/2));

var action2 = cc.scaleTo(2, 2, 2);

var spawn = cc.spawn(action1, action2);

ball.runAction(spawn);

5.反向(reverseTime/reverse)

reverseTime可以让动作反向播放,只能处理有限长度的动作,也就是说经过了 repeat-

Forever的动作不能使用 reverseTime。

使用方法:

cc.reverseTime(action)

该方法返回一个新的动作,只接受 1个参数。

不过,笔者不建议大家使用 reverseTime,因为最初始需要瞬间完成原始动作,让节点

到达终点状态,然后再反向播放。这样会出现闪烁的问题,如代码清单 4-10所示,运行时

我们会发现小球闪一下跑到屏幕中间。

代码清单 4-10

var action1 = cc.moveTo(2, cc.p(size.width/2, size.height/2));

var reverseTime = cc.reverseTime(action1);

ball.runAction(reverseTime);

这里笔者需要重点介绍另外一个实现反向的办法:reverse函数。

使用方法:

action.reverse()

函数执行后返回一个新的动作。语义上很好理解,动作自己执行反向,然后返回一个

反面的自己。不过,reverse也不是万能的,因为 Cocos2d-JS v3.1暂时只支持基本动作中的

xxxBy,例如 scaleBy、moveBy等。

举个例子,如代码清单 4-11所示。

Page 34: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

63

第4 章 让世界来点动静

代码清单 4-11

var action = cc.moveBy(2, cc.p(size.width/2, 0));

var reverse = action.reverse();

var sequence = cc.sequence(action, reverse);

ball.runAction(sequence);

上述代码可以让小球从左侧移动到屏幕中间,然后紧接着又回到左侧。

在这里还要举出 reverseTime的 bug:如果在上述代码中,把 reverse改为 reverseTime,

情况会大不同,如代码清单 4-12所示。小球移动到中间后,突然蹦到最后侧,然后再移动

到中间。

代码清单 4-12

var action = cc.moveBy(2, cc.p(size.width/2, 0));

var reverse = cc.reverseTime(action);

//var reverse = action.reverse();

var sequence = cc.sequence(action, reverse);

ball.runAction(sequence);

其实原因也是很好理解的,因为 reverseTime不管节点原来是什么状态,它只管把给它

的动作反过来执行,也就是说,先让小球瞬间往右移动半屏幕(size.width/2),然后再匀速

回到中间。

6.耍一个降龙十八掌

学习了这么多组合动作,我们接着试试玩点小花样,看能否把上边说的组合方式揉合

到一块去,如代码清单 4-13所示。

代码清单 4-13

var action1 = cc.moveBy(5, cc.p(size.width/2, 0));

var action2 = cc.scaleBy(1, 2);

var reverse = action2.reverse();

var sequence = cc.sequence(action2, cc.delayTime(0.5), reverse);

var repeat = cc.repeat(sequence, 2);

var spawn = cc.spawn(action1, repeat);

ball.runAction(spawn);

运行上述代码,可以发现,小球从左侧移动到屏幕中间,同时完成两次放大缩小的变

Page 35: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

64

化。cc.sequence(action2, cc.delayTime(0.5), reverse)这句代码比较关键,可以让 3个动作串联

执行,分别是放大、等待半秒和缩小。

通过这个例子,希望大家能更好地体会到 Cocos2d-JS动作的灵活性。

4.3.3 让运动轨迹来多点花样

前边的动作,无论是基本动作还是组合动作,小球移动的速度都是匀速的,而且移动

的轨迹都是直线。这一节,我们给这个运动过程加点小花样。

首先介绍一下主角——动作的 easing 方法,用于设置该动作执行过程中采用哪种缓动

方式,具体使用方法如下:

action.easing(easeObject)

easeObject是一个Object,存储着某种缓动方式的信息。至于如何得到 easeObject,Cocos2d-

JS为我们准备了很完整的全套缓动方式,我们可以在源代码 CCActionEase.js中找到。

直接举一个例子,大家会更容易理解,如代码清单 4-14所示。

代码清单 4-14

var TrickyActionLayer = cc.Layer.extend({

ctor: function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = size.width/2;

ball.y = size.height;

this.addChild(ball, 1);

var action = cc.moveBy(2, 0, -(size.height-ball.height/2));

action.easing(cc.easeIn(2));

var back = action.clone().reverse();

back.easing(cc.easeBounceIn());

ball.runAction(cc.sequence(action, back));

}

});

Page 36: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

65

第4 章 让世界来点动静

在上述代码中,小球先执行一个简单动作,从屏幕正上方掉到屏幕最下方,这里使用

加速的缓动方式。然后,小球再弹跳着回到原位。这里我们重点关注 cc.easeIn 和 cc.ease-

BounceIn,前者表示加速,参数 2 表示以 2 次方加速,后者表示弹性加速。另外,代码中

使用了 clone方法,用于把动作复制一份。

easeIn 表示加速,那么也有相应的 easeOut 和 easeInOut,分别表示减速和先加速后减

速。除了基本的 ease外,还有许多其他类型的缓动方式,方法的名称也是类似的。由于这

些缓动方式是跨语言通用的(详情可以访问 http://code.google.com/p/tweener/了解),这里借

用 Flash的 Tweener库的配图,讲解一下 Cocos2d-JS的缓动方式。

二次曲线:cc.easeQuadraticActionIn/Out/InOut(图 4-6的横坐标是时间,纵坐标是移动

的距离,后续三个图同理)。

图 4-6 二次曲线

三次曲线:cc.easeCubicActionIn/Out/InOut,如图 4-7所示。

图 4-7 三次曲线

出去再返回:cc.easeBackIn/Out/InOut,如图 4-8所示。

图 4-8 easeBack 曲线

弹性和反弹:cc.easeElasticIn/Out/InOut、cc.easeBounceIn/Out/InOut,如图 4-9所示。

Page 37: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

66

图 4-9 弹性和反弹的曲线

以上只列举了部分常用的缓动方式,如果大家想知道全部缓动方式,可以利用WebStorm

的提示功能或直接浏览 CCActionEase.js。

4.4 控制动作

前边学习了基本动作,也学习了各种各样的玩法,但是大家是否意识到还缺了什么?

开始了的动作就好像泼出去的水,收不住了,对不对?其实在 Cocos2d-JS中,我们还有一

些妙招,可以把动作一直掌控在我们手中。

4.4.1 停止动作

节点的 runAction方法可以让一个动作开始,而相对应的,我们可以用 stopAction、stop-

ActionByTag 和 stopAllAction 来停止动作的执行,前两个方法可以停止某一个方法,第三

个方法可以停止全部动作。

使用方法:

node.stopAction(action)

node.stopActionByTag(tag)

node.stopAllAction()

其中 tag是预先设置给 action的标记,例如 action.tag = 123。

举个例子,如代码清单 4-15所示。

代码清单 4-15

var ControlActionLayer = cc.Layer.extend({

Page 38: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

67

第4 章 让世界来点动静

ctor:function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = 0;

ball.y = size.height/2;

this.addChild(ball, 1);

var action = cc.moveBy(3, cc.p(size.width/2, 0));

ball.runAction(action);

setTimeout(function(){

ball.stopAction(action);

}, 2000);

return true;

}

});

在上述代码中,先让小球在 3秒内从左侧移动到屏幕中间,然后我们在 2秒的时候停

止这个动作。结果,我们发现小球停在半路上了,如图 4-10所示。

图 4-10 小球停止在半路

4.4.2 暂停/恢复动作

使用节点的 runAction方法可以运行一个动作,相应的,节点还提供了 pause和 resume

Page 39: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

68

方法,分别用于暂停和恢复动作。如代码清单 4-16所示。

代码清单 4-16

var action = cc.moveBy(3, cc.p(size.width/2, 0));

ball.runAction(action);

setTimeout(function(){

ball.pause();

}, 2000);

setTimeout(function(){

ball.resume();

}, 3000);

我们运行这个例子可以发现小球在半路停下来了,然后过了 1秒又继续前进。

节点的 pause和 resume只影响节点自身范围内的动作,不会影响其他节点。如果要统

一做全局的暂停和恢复可以使用:

cc.director.pause();

cc.director.resume();

4.4.3 监听动作的开始与结束

Cocos2d-JS没有在 action 中提供直接的接口来用于监听动作的开始和结束,但有一个

更灵活的机制:CallFunc。

CallFunc 可以把一个普通函数封装成一个动作,再结合 sequence,我们就可以让一个

动作结束后执行某个指定函数了。

CallFunc的使用方法:

cc.callFunc(function, target, extra parameters…)

CallFunc 接受 1 个或多个参数,第一个参数是某个函数,第二个参数是该函数最后被

调用时的目标对象,类似原生 JS 的 apply(target, function),第三个参数以后是额外附加的

数据,在调用 function时传递过去。

接下来,我们直接看一个例子,如代码清单 4-17所示。

代码清单 4-17

var ControlActionLayer = cc.Layer.extend({

Page 40: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

69

第4 章 让世界来点动静

ctor:function () {

this._super();

var size = cc.director.getWinSize();

var ball = new cc.Sprite("res/item_2.png");

ball.x = 0;

ball.y = size.height/2;

this.addChild(ball, 1);

var action = cc.moveBy(1, cc.p(size.width/2, 0));

var callback = cc.callFunc(this.callback, this, "message");

var sequence = cc.sequence(action, callback);

ball.runAction(sequence);

return true;

},

callback: function (nodeExecutingAction, data) {

trace(nodeExecutingAction instanceof cc.Sprite, data);

}

});

上述代码中利用 sequence和 callFunc实现了监听小球移动结束的功能。当小球移动结

束时,方法 callback就会被执行。在 Chrome中运行,我们可以在控制台中看到输入“true,

message”,这表示 callback 收到的两个参数,第一个参数 nodeExecutingAction 是当前运行

action的节点,第二个参数 data是我们在 callFunc中设置的额外数据——字符串“message”。

事实上,这个额外数据,我们可以设置任意多个,只需要跟 callFunc 对应即可。另外,再

提醒一下,trace 函数是笔者自定义的 log 函数,类似 console.log,在第 2 章最后一节可以

找到相关说明,后续章节将不再说明这个函数的功能。

另外,如果我们串联了多个动作,就可以在每个动作之间都安置一下监听函数,例如:

cc.sequence(action1, cc.callFunc(function1), action2, cc.callFunc(function2),

action3);

这样就可以完整监听到动作序列的执行情况了。

Page 41: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

70

4.5 播放声音

Cocos2d-JS 提供了很便捷的接口让我们播放背景音乐和音效,在这一章的最后,我们

快速学习一下。

4.5.1 背景音乐

我们可以使用 cc.audioEngine.playMusic播放背景音乐,相应使用 stopMusic停止音乐。

具体用法:

cc.audioEngine.playMusic("res/sounds/bgWelcome.mp3", true)

cc.audioEngine.stopMusic()

playMusic 的第一个参数是音乐 url,第二个参数设置是否重复播放。由于背景音乐同

时只能有一个在播放,所以 stopMusic不需要传递参数。

在 HTML5版本中,我们还需要注意预加载音乐,在 resource.js中加入该音乐的 url。

4.5.2 音效

我们可以使用 cc.audioEngine.playEffect播放音效,相应使用 stopEffect停止音效。

音效跟背景音乐的不同在于音效可以多个同时播放,使用 stopEffect停止音效时需要指

定停止哪个音效,另外也可以使用 stopAllEffects把全部音效关闭。

具体用法:

var effect = cc.audioEngine.playEffect("res/sounds/eat.mp3", false);

cc.audioEngine.stopEffect(effect)

cc.audioEngine.stopAllEffects()

其中 playEffect的第一个参数是音效的 url,第二个参数设置是否重复播放。

4.5.3 音量

Cocos2d-JS 游戏可以单独设置背景音乐和音效的音量,分别是 setEffectsVolume 和

Page 42: 第 3 章 Cocos2d-JS 的平面世界cdn.cocimg.com/bbs/attachment/Fid_59/59_304691_7f7a698307eb2… · 超人,然后右侧有一个“hungry hero”字样的图标,另外还有两个按钮(play

71

第4 章 让世界来点动静

setMusicVolume。

具体用法:

cc.audioEngine.setEffectsVolume(0);

cc.audioEngine.setMusicVolume(0);

两个方法都接受 1 个参数,该参数范围是 0 到 1。上述代码同时把背景音乐和音效都

设置为静音。

相反,恢复音量的方式是:

cc.audioEngine.setEffectsVolume(1);

cc.audioEngine.setMusicVolume(1);