第 3 章 Cocos2d-JS...
Transcript of 第 3 章 Cocos2d-JS...
第3 章 Cocos2d-JS 的平面世界
在前两章中,我们初步认识了 Cocos2d-JS,接着搭建了环境,还把
HelloWorld 跑起来了,部分新手读者是否有一点成就感了呢?不过,也许大家已
经发现之前都是填鸭式的节奏,几乎没有需要自己理解和操作的内容。这样很不满
足,是不是?所以,从这一章开始,我们正式开始学习 Cocos2d-JS。在第 1 章
中说过,有图形学基础或者网页开发基础的读者会更容易理解本书的内容,在这里
为了照顾新手读者,笔者还是需要稍微讲解一下最基础的平面世界知识。
3.1 Cocos2d 世界的经纬度——坐标系
首先,我们看一个典型的游戏画面。这个图有几个元素,首先最明显的应该是一个
超人,然后右侧有一个“HUNGRY HERO”字样的图标,另外还有两个按钮(PLAY 和
ABOUT),最后就是底下的背景图,如图 3-1所示。
在这个游戏画面中,超人和按钮的位置是怎么表示的呢?我们知道,地球上某一点的
位置可以用经纬度来表示。例如北京是东经 116度、北纬 40度左右,这是一个二维的数据。
同理,游戏的二维世界也可以通过这样的方式表示,具体来说,就是用 x 和 y 分别表示横
向位置和纵向位置。有没有似曾相识的感觉,对了,这个就是我们中学时代的坐标系,如
图 3-2所示。
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);
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,得到
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,
...省略后续代码
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();
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();
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扩
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
所示。
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节再
详细探讨。
另外,精灵和层一样,还可以继续嵌套其他节点,精灵还可以包含精灵。这个嵌套
机制可能在开始时难以理解,但使用起来将非常方便。举个例子,例如游戏中飞机得到
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;
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)
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({
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.
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则把当前场景销毁,然后
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();
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));
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所示。
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
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);
}
});
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实现的。
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参数。
第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遍。为什么这里用“尝试”字眼呢?因为你设定
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(); //用于记录球的运动轨迹
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 自定义动画
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);
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 的效果
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
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可以让精灵的色调发生变化,例如如下的代码:
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
所示。
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所示。
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)。
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, …)
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所示。
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);
运行上述代码,可以发现,小球从左侧移动到屏幕中间,同时完成两次放大缩小的变
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));
}
});
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所示。
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({
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
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({
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);
这样就可以完整监听到动作序列的执行情况了。
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 和
71
第4 章 让世界来点动静
setMusicVolume。
具体用法:
cc.audioEngine.setEffectsVolume(0);
cc.audioEngine.setMusicVolume(0);
两个方法都接受 1 个参数,该参数范围是 0 到 1。上述代码同时把背景音乐和音效都
设置为静音。
相反,恢复音量的方式是:
cc.audioEngine.setEffectsVolume(1);
cc.audioEngine.setMusicVolume(1);