Hello World of Cocos2d-JS
2014.07.31 by cocos
教程
在这个教程中,我将会从头开始向你展示如何去建立一个新的 Cosos2d-JS 工程。在开始之前,我先简短地介绍一下 Cocos2d-JS 总体的目录结构。

Cocos2d-JS 目录结构概览

下面是 Cocos2d-JS 的目录结构: 图片1 directory

理解目录结构

目录结构可以被分成4个部分来理解:

第一部分: 引擎相关文件

  • frameworks 目录包含了 Cocos2d-html5 引擎和 Cocos2d-x JavaScript 代码的汇集.
    • Cocos2d-html5 目录包含了所有 Cocos2d-html5 的引擎模块,例如引擎核心模块、音频模块、外部物理库,CocosBuilder Reader, CocoStudio Reader 和其他的模块。所有的模块都用JS实现并且可以在WEB环境中运行。
    • js-bindings 目录包含了 Cocos2d-x 引擎, 绑定的和外部预置 SpiderMonkey 库的项目文件。外部接口采用 JS 编写,但是基础模块使用 Cpp 实现,可以在许多不同的本地平台上运行,例如IOS,android,MAC,win32等平台。

第二部分: 测试文件,游戏样本和模板

  • template 目录是被用来创建一个新的Cocos2d-JS 工程。它包括了 HTML5 工程和默认的本地工程。cocos 控制台使用它来创建一个新的工程。
  • samples directory 包含了 Cocos2d-JS 全部的测试工程。它还包括了一个可以运行的游戏样本,MoonWarriors。全部的测试工程和游戏可以从 cocos 控制台执行,也可以通过 javascript 的接口绑定机制在WEB或者本地平台运行。

第三部分: 其他

  • README 包含一些对 Cocos2d-JS 的介绍。
  • LICENSE 正如我们之前提到的,Cocos2d-JS 的许可证是 MIT,你可以查阅在引擎根目录下的 the license 文件夹来获得更多关于 Cocos2d-html5 and Cocos2d-x 的许可证细节。
  • tools 目录包括 cocos 控制台工具和 绑定生成工具(bingings-generator)。template 文件夹下包含一个 build.xml 文件,里面存放着闭包编译器的控制信息,通过 ANT 这个命令,你可以将你的游戏打包成为一个单个文件。
  • build 目录包含着内置的工程样本文件。
  • docs 目录包含 JavaScript 编码风格指导和 release 的信息。
  • CHANGELOG 包含所有版本的修改信息。
  • setup.py 是一个环境搭建的 python 脚本。

查看内置样本

当你已经成功地下载和配置好你的 Cocos2d-JS 开发环境之后,去看看 Cocos2d-JS 内置的样本工程是非常好的一个选择。它也是你正确地学习 Cocos2d-JS 中一个非常好的学习资源。

查看测试文件

进入目录 Cocos2d-JS/samples/js-tests, 然后通过 cocos 控制台执行测试文件。
    cocos run -p web
它将会向你展示全部 Cocos2d-JS 的内置测试工程。 以下是屏幕截图: 图片 2 tests 这些测试工程是你最好的学习资源。它们很好地展示了 Cocos2d-html5 的每个特性。你可以调整这些测试文件,然后在你刷新浏览器后你会立即得到对应的反馈信息。在开始的学习中,通过这种方式学习 Cocos2d-html5 比在开始时就阅读大量的文档更有效。 你也可以在 IOS,android,Mac 上运行这些测试工程文件。
    cocos run -p ios|android|mac

浏览游戏样本

这里有一个使用 Cocos2d-JS 完成的完整游戏样本。全部的源代码都是免费和开源的。以下是对这个游戏样本的简单介绍。

MoonWarrior

这个游戏的名字叫 MoonWarrior。你可以通过 cocos 控制台进入 js-moonwariors的根目录来运行它。
    cocos run -p web|ios|android|mac
这是一个纵向的射击游戏。很多有用的游戏技术被应用到这个游戏里面,包括瓦片地图(tiled-map), 动画(animation), 视差背景(parallax background)等等。 这里是屏幕截图,你可以在源代码中获得更多的信息。 图片 3 moonwarrior

创建你的第一个 "Hello World" 工程

最后,我们开始教程最终也是最重要的一部分。 我不会真的创建一个 "Hello World" 工程。我将会把 跑酷(Parkour)游戏作为一个例子。在以后,所有的这些精品教程都将会围绕怎么去完成一个在Cocos2d-JS 下的的跑酷(Parkour)游戏这个主题。 你还可以忍受等待吗?来,让我们马上开始!

完成Parkour工程框架

正如之前所说,我们可以使用一个指定的名字来创建一个新的工程。进入你的工作台(workspace),使用 cocos 控制台去创建Parkour 游戏。
    cocos new Parkour -l js
现在运行你的 WebStorm ,打开 Parkour 的目录。现在这个工程的结构是这个样子: 图片 4 newnavigator 在 WebStorm 中右击 index.html ,选择Debug 'index.html'。它将会自动打开你的 Chrome 浏览器,然后你就成功地创建了一个新的工程。恭喜你!当前浏览器的地址是:
http://localhost:63342/Parkour/index.html
这是一张经典的 Hello World 截图: 图片 5 helloworld

游戏样本模板的代码分析

虽然 template 带给我们这么多礼物, 但是我们甚至对它一无所知。 这个模板工程的主入口在哪里?这些文件是如何组织的?每个文件又完成什么工作?在这节中,我将会帮助你解答这些问题。

查看整个工程的文件

首先, 我们一起查看在图片4中全部的文件和目录结构: 在图片4, 我们可以看到:
  • res 目录.它包含了工程中所有被需要的资源文件。现在它仅仅包含一些图片样本。 但是如果你想要增加一些游戏的元素或者一些极好的游戏音乐,你应该将它们放在这个文件夹下并给为每个文件取一个合适的名字。
  • src 文件夹. 它包含你真实游戏的所有逻辑代码。如果这里有成千上百个 javascript 源文件,你最好将它们用子文件夹分成许多小部分。现在,我们的模板工程拥有2个 javascript 源文件夹。 app.js 包含我们模板的第一个场景代码。在resource.js 定义了许多资源的全局变量。
  • index.html 文件是 HTML5 基于web应用程序的入口点。它是一种 HTML5 兼容的格式。它定义了许多元数据,例如设置视角和全屏参数等。
  • project.json 文件是我们工程的配置文件。请参考网站project.json获得更多详情 。
  • main.js 是一个创建你的第一个游戏场景和在浏览器显示这个场景的一个文件。 在这个文件里,你也可以定义你的分辨率策略和预加载你的资源。
好了, 你已经知道了这些文件和文件夹是什么。现在,该是我们理解这些源代码和执行路径的时候了。

分析工程执行路径

知道一个工程的执行路径是非常重要的。 这个工程被装载进index.html。然后它执行 frameworks/Cocos2d-html5/CCBoot.js。它将会尝试从 project.json 文件中装载工程的设置信息。
{
    "project_type": "javascript",

    "debugMode" : 1,
    "showFPS" : true,
    "frameRate" : 60,
    "id" : "gameCanvas",
    "renderMode" : 0,
    "engineDir":"frameworks/cocos2d-html5",

    "modules" : ["cocos2d"],

    "jsList" : [
        "src/resource.js",
        "src/app.js"
    ]
}

查看这个代码片段,这里有一个叫做engineDir的对象属性,它能够决定下一个程序的执行路径。在默认的情况下,我们可以修改这个 engineDir 的内容。 main.js 会在装载完frameworks/Cocos2d-html5/CCBoot.js 这个文件之后开始装载。它将会初始化配置和装载所有的在 modulesjsList文件上被指定的 JavaScript 文件。阅读源代码比阅读我的纯文字描述将会更清晰地向你展示这个过程。

让你的项目做一些小改动

正如我们前几节提到过的,在我们真正开始写代码之前,我们一起来对项目做一点小改动来热热身吧!

隐藏你游戏屏幕左边角落的最大帧数

这一节可能会有一点琐碎。我们可以很容易地实现这个效果,通过在project.json文件修改把showFPS属性为false。 以下是相关的代码:
{
    "project_type": "javascript",

    "debugMode" : 1,
    "showFPS" : false,
    "frameRate" : 60,
    "id" : "gameCanvas",
    "renderMode" : 0,
    "engineDir":"frameworks/Cocos2d-html5",

    "modules" : ["cocos2d"],

    "jsList" : [
        "src/resource.js",
        "src/app.js"
    ]
}
通过修改这些对象的属性,我们可以对这个项目做许多小改动。以下,我会向你介绍每个属性的作用。
property name options explanation
debugMode 0,1,2,3,4,5,6 0: close all 1: info level 2: warn level 3: error level 4: info level with web page 5: warn level with web page 6: error level with web page
showFPS true or false toggle FPS visibility
id "gameCanvas" the dom element to run cocos2d on
frameRate a positive number above 24, usually 60-30 adjust the frame rate of your game
renderMode 0,1,2 Choose of RenderMode: 0(default), 1(Canvas only), 2(WebGL only)
engineDir the engine directory related to your project specify the directory the engine code
modules engine modules you could customize your engine by modules. Module names are in the module of moduleConfig.json in root of frameworks/Cocos2d-html5directory
jsList a list of your game source code add your own file lists after myApp.js

修改设计的分辨率大小

Cocos2d-JS 把 web browser 作为一个全屏游戏的画布。我们不需要手动地去匹配这个大小,我们仅仅需要去关注设计的分辨率大小。为了使我们的游戏流畅地在 IOS 和 Android 平台使用 javascript binding 技术运行,我们将会修改分辨率为 480320。打开 main.js 文件,在函数cc.game.onStart*里修改分辨率大小为480, 320。 你也可以调整分辨率策略为SHOW_ALL:
    cc.view.setDesignResolutionSize(480, 320, cc.ResolutionPolicy.SHOW_ALL);
如果你对我们为什么需要做这些工作而感到好奇,请参阅 Resolution Policy Design for Cocos2d-html5 获得更多详情.

总结

在这个教程中,我们谈到了整个工程的目录结构以及内置的测试工程和 Cocos2d-JS 的游戏样本。我们也根据 Cocos2d-JS 提供的模板创建了自己的第一个工程。在下一个部分里,我们将会尝试分析这些文件以及模板中的代码结构。

下章简介

在接下来的教程中,我将会告诉你如何在你的工程里去创建一个游戏菜单场景。我们将会使用 Cocos2d-JS 做更多的编码工作。  
在为你的游戏创建第一个场景前,你应该熟悉一些Cocos2d的基础概念。如果你已经熟悉这些概念,你可以跳到下一个章节。

基础概念

在一个Cocos2d game游戏中,每一个元素都是一个节点。游戏通常是由三种节点构成的:
  • 场景(Scene)
  • 层(Layer)
  • 精灵(Sprite)
现在我们关注这个游戏中的层,你可以在这里找到更多关于场景和精灵的细节。

层(Layer)

一个cc.Layer是一个cc.Node,知道怎样去渲染自己,而且可能是半透明的, 能让玩家可以看见在当前层之下的其他层。cc.Layer在定义你的游戏外观和行为方面非常有用,所以你应该要花很多时间去对付cc.Layer子类以来达到你的预期。 layer 因为复杂的应用程序会要求你去定义自定义cc.Layer子类,所以Cocos2d提供了几个预定义的层。这些例子包括cc.Menu(简单的菜单层),cc.ColorLayer(填充色层),还有cc.LayerMultiplex(可以复用它的子节点,每次激活其中的一个子节点,同时禁用其它子节点)。 层可以包含任意cc.Node作为子节点,包括cc.Sprite,cc.Label,甚至是其它的cc.Layer对象。因为层是cc.Node的子类,所以层可以手动或者使用cc.Action进行切换。

坐标系

Cocos2d-JS使用与OpenGl相同的坐标系,其被叫做笛卡尔右手系。它在游戏产业非常流行,但是它与网页设计所使用的传统的左上角坐标系是不同的。 Coordination 你可以在这里找到更多关于坐标系的细节。

锚点(Anchor Point)

锚点被用在一个对象的定位和旋转。锚点坐标是相对坐标,举例来说,在(0,1)位置的锚点(我们经常在Cocos2d中简化定义为cc.p(0,0))对应于那个对象的左下角, 同时cc.p(0.5,0.5)对应于对象的中心。当设置一个对象的位置时,这个对象被定位以致于锚点会位于setPosition()函数调用所指定的坐标。同样地,当旋转一个对象,它是绕着锚点旋转的。 这些性质(properties)可以在Cocos2d-JS v3.0作为属性(attribute)被设置。 例如,这个精灵有一个cc.p(0,0)的锚点和一个cc.p(0,0)的位置。
// create sprite 
var sprite = new cc.Sprite ( "bottomleft.png" ) ; 
sprite.attr({
        x: 0,
        y: 0,
        anchorX: 0,
        anchorY: 0
    });
this.addChild ( sprite ) ;

动作(Action)

更多关于动作的细节请看这里。 运行cc.MoveBy动作的例子:
// Move a sprite 50 pixels to the right, and 10 pixels to the top over 2 seconds.
sprite.runAction(new cc.MoveBy(2, cc.p(50, 10)));

动画(Animation)

更多关于动画的细节请看这里。 播放动画的例子:
var animation = new cc.Animation ( ) ;
for ( var i = 1 ; i < 15 ; i ++ ) {         
    var frameName = "res/Images/grossini_dance_" + ( ( i < 10 ) ? ( "0" + i ) : i ) + ".png" ; 
    animation.addSpriteFrameWithFile ( frameName ) ;
}

animation.setDelayPerUnit ( 2.8 / 14 ) ; 
animation.setRestoreOriginalFrame ( true ) ; 
var action = new cc.Animate ( animation ) ; 
sprite.runAction ( new cc.Sequence( action, action.reverse ( ) ) ) ;

调度器(Scheduler)和定时器回调(Timer Callback)

更多关于调度器和定时器回调的细节请看这里

事件管理器(EventManager)

Cocos2d-JS v3.0移植了一种对用户事件响应的新机制。 基本要素:
  • 事件监听器(Event listeners) 封装事件处理代码。
  • 事件管理器(Event Manager) 管理用户事件的监听器。
  • 事件对象(Event objects) 含有有关事件的信息。
为了响应事件,你必须首先创建一个cc.EventListener。这里有五种不同的事件监听器:
  • cc.EventListenerTouch - 响应触摸事件
  • cc.EventListenerKeyboard - 响应键盘事件
  • cc.EventListenerAcceleration - 响应加速计事件
  • cc.EventListenMouse - 响应鼠标事件
  • cc.EventListenerCustom - 响应自定义事件
然后,添加你的事件处理代码到事件监听器上恰当的回调(例如,onTouchBegan对应EventListenerTouch监听器,或者onKeyPressed对应键盘事件监听器)。 接下来,利用cc.eventManager注册你的事件监听器。 当事件出现(例如,用户触摸屏幕,或者在键盘打字),cc.eventManager通过调用你的回调来分发事件对象(Event objects)(比如 EventTouchEventKeyboard)到恰当的事件监听器。每个事件对象包含有关事件的信息(比如触摸的坐标)。 请参考事件处理器(EventManager)来获得更多细节。

制作你的第一个游戏场景

在上一篇教程,我们已经分析一个Cocos2d-JS游戏的执行通路。我们知道在main.js里我们利用cc.game.onStart加载我们的第一个游戏场景,这是起作用的代码片段:
cc.game.onStart = function(){
cc.view.setDesignResolutionSize(480, 320, cc.ResolutionPolicy.SHOW_ALL);
cc.view.resizeWithBrowserSize(true);
//load resources
cc.LoaderScene.preload(g_resources, function () {
    cc.director.runScene(new HelloWorldScene());
}, this);
};
这里,我们使用cc.LoaderScene来预加载我们游戏的资源,而在加载完成后,导演则会运行我们的第一个游戏场景。 注意: cc.game是真正的,会初始化游戏配置和启动游戏的游戏对象。

清理工作(Cleanup Work)

好了,我认为背景信息已经足够。让我们做些清理工作。
清理myApp.js
这个过程非常简单。首先,我们应该删除myApp.js的全部内容。因为我们会从头开始写这些代码。 其次,我们应该改变main.js中的这一行:
cc.director.runScene(new HelloWorldScene());
cc.director.runScene(new MenuScene());
没错,我猜你已经抓住要点了。我们会定义我们的第一个类,名为MenuScene。 最后,我们应该添加需要的资源和定义一些资源变量以便于简单的访问。 res 打开resource.js然后改变它的内容成这样:
var res = {
helloBG_png : "res/helloBG.png",
start_n_png : "res/start_n.png",
start_s_png : "res/start_s.png"
};

var g_resources = [
//image
res.helloBG_png,
res.start_n_png,
res.start_s_png
];

定义你的第一个场景-MenuScene

打开app.js然后开始定义菜单层(MenuLayer):
var MenuLayer = cc.Layer.extend({
ctor : function(){
    //1. call super class's ctor function
    this._super();
},
init:function(){
    //call super class's super function
    this._super();

    //2. get the screen size of your game canvas
    var winsize = cc.director.getWinSize();

    //3. calculate the center point
    var centerpos = cc.p(winsize.width / 2, winsize.height / 2);

    //4. create a background image and set it's position at the center of the screen
    var spritebg = new cc.Sprite(res.helloBG_png);
    spritebg.setPosition(centerpos);
    this.addChild(spritebg);

    //5.
    cc.MenuItemFont.setFontSize(60);

    //6.create a menu and assign onPlay event callback to it
    var menuItemPlay = new cc.MenuItemSprite(
        new cc.Sprite(res.start_n_png), // normal state image
        new cc.Sprite(res.start_s_png), //select state image
        this.onPlay, this);
    var menu = new cc.Menu(menuItemPlay);  //7. create the menu
    menu.setPosition(centerpos);
    this.addChild(menu);
},

onPlay : function(){
    cc.log("==onplay clicked");
}
});
让我们从1-6回顾全部细节:
  1. 它调用它超类的初始函数。
  2. 得到你游戏的屏幕大小。
  3. 计算你的屏幕的中心点,这将会被用于将背景图片放于中心。
  4. 利用文件名创建一个背景图片然后设置它到屏幕中央的位置。最后,作为子节点添加这个精灵到菜单层。
  5. 调用MenuItemFont的setFontSize函数来调整字体大小。在这个例子,它是无效的。但如果你想要使用MenuItemFont来创建一些菜单项,它会影响菜单项的标签大小。
  6. 创建一个有两张图片的菜单,一张对应正常状态而另一张对应选中状态。然后我们设置菜单的位置到屏幕的中心。最后,添加它到当前的层中。
然后我们也应该定义一个Menu scene:
var MenuScene = cc.Scene.extend({
onEnter:function () {
    this._super();
    var layer = new MenuLayer();
    layer.init();
    this.addChild(layer);
}
});
创建一个菜单场景(MenuScene)的过程是非常直观的。你定义了一个从cc.Scene派生的变量。你应该记住extend这个符号,这被用于继承类(extenal classes)。 一旦这个场景被创建,一个onEnter函数应该被定义。它定义菜单层作为它的孩子。我们也可以定义一个ctor函数代替onEnter函数。onEnter函数在ctor函数后被调用。

总结

在这篇教程,我已经向你展示了当你第一次开始编写Cocos2d-JS游戏所需要知道的基础概念。然后也给你详细解释了怎样建立你的第一个游戏场景。希望你享受它同时能快乐地编程!相关的样板工程可以在这里下载。它只包括用户部分而不包括框架。你可以使用它们去替代Cocos2d-JS模版的对应部分。

从这里要去哪(Where to go from here)

在下一章,我会向你展示怎样去定义你的游戏场景以及各种游戏层,怎样去设计这些层还有这些层的责任是什么。
   

介绍

在上篇指南中,我们实现了让玩家从一个点移动到另一个点的功能。但是这个移动有点怪异。在本篇指南中,我们将会向你展示如何让玩家运行动画。这样玩家的移动就会更加的逼真。 在开始具体的展示之前,请允许我给你展示一款特别酷的工具--TexturePacker

TexturePacker的介绍

TexturePacker是一款可以使用图形用户界面和命令行来创建精灵表的工具。如果你想了解更多关于TexturePacker的知识,可以访问此网站。 这里我会向你简要的介绍一下怎么样使用TexturePacker生成我们游戏中需要的动画文件。

用TexturePacker生成你自己的动画

下面是生成的过程: 1.打开TexturePacker,把res/TexturePacker里面的 TexturePacker文件夹拖到TexturePacker中的精灵区中。 dragTheResource 当你向TexturePacker文件夹中添加新的图片资源的时候,TexturePacker会检测到发生的改变并自动的加载图片资源。 2.在纹理设置面板(TextureSettings panel)中,把数据格式设成导出为“cocos2d”和“png”。 3.制定数据文件和纹理文件的路径。这里我们把路径设置成res目录,把数据文件名设成“running.plist”,把纹理文件名设成“running.png”。 specifypath 4.点击“生成”,然后弹出一个对话框。如果没有错误的话,就会在制定的文件下生成"running.png" 和 "running.plist"文件。generating 到此,我们已经生成了动画文件,下面就让我们播放生成的动画。

在cocos2d-JS中加载动画资源文件

准备阶段

首先,我们应该把“running.plist” 和 “running.png”添加到 resource.js文件中。
var res = {
helloBG_png : "res/helloBG.png",
start_n_png : "res/start_n.png",
start_s_png : "res/start_s.png",
PlayBG_png  : "res/PlayBG.png",
runner_png  : "res/running.png",
runner_plist : "res/running.plist"
};

var g_resources = [
//image
res.helloBG_png,
res.start_n_png,
res.start_s_png,
res.PlayBG_png,
res.runner_png,
res.runner_plist
];
这里我们把变量 runner_ png 的值置成精灵表的文件名"running.png"。稍后,我们会用变量 runner_png 来创建我们的玩家精灵。

创建玩家动画

首先,我们应该在文件AnimationLayer.js中添加下面的成员变量:
spriteSheet:null,
runningAction:null,
sprite:null,
然后,我们用下面的内容替换玩家的创建方法:
this.sprite = new cc.Sprite("#runner0.png");
我们可以用下面的代码很轻松的创建一个动画:
//1.加载精灵表
 cc.spriteFrameCache.addSpriteFrames(res.runner_plist);

//2.创建精灵帧数组
var animFrames = [];
for (var i = 0; i < 8; i++) {
var str = "runner" + i + ".png";
var frame = cc.spriteFrameCache.getSpriteFrame(str);
animFrames.push(frame);
}
//3.用精灵帧数组和一定的时间间隔创建一个动画
var animation = new cc.Animation(animFrames, 0.1);

//4.用一个重复持续动作封装这个精灵动作
this.runningAction = new cc.RepeatForever(new cc.Animate(animation));
这个动画是用精灵表中一系列小的图片(从runner0.png到runner7.png)构造出来的。 下面是在cocos2d-JS中创建一个动画的完整的处理过程: 1.往SpriteFrameCache类中加载精灵表的plist文件。 2.往数组animFrames中添加动画帧。 3.用动画帧数组和表示每两个精灵帧之间的时间间隔来创建一个cc.Animation的对象。 4.创建最终的cc.Animate对象,并用一个重复的持续性动作封装起来。 这样这个动画就会一直运动下去。 一般来说,如果我们在cocos2d-JS中使用动画,我们经常会用SpriteBatchNode来提高在WebGL模式或cocos2d-X JSB模式下的游戏性能。 AnimationLayer.js文件中的最终代码如下:
var AnimationLayer = cc.Layer.extend({
spriteSheet:null,
runningAction:null,
sprite:null,
ctor:function () {
this._super();
this.init();
},

init:function () {
this._super();

// create sprite sheet
cc.spriteFrameCache.addSpriteFrames(res.runner_plist);
this.spriteSheet = new cc.SpriteBatchNode(res.runner_png);
this.addChild(this.spriteSheet);


// init runningAction
var animFrames = [];
for (var i = 0; i < 8; i++) {
var str = "runner" + i + ".png";
var frame = cc.spriteFrameCache.getSpriteFrame(str);
animFrames.push(frame);
}

var animation = new cc.Animation(animFrames, 0.1);
this.runningAction = new cc.RepeatForever(new cc.Animate(animation));
this.sprite = new cc.Sprite("#runner0.png");
this.sprite.attr({x:80, y:85});
this.sprite.runAction(this.runningAction);
this.spriteSheet.addChild(this.sprite);
}
});
现在,你可以运行这个项目,将会有一个一直跑动的玩家显示的你的屏幕上。 finalresult

总结

在这篇指南中,我们学习了怎么样使用TexturePacker生成动画文件以及怎么样在cocos2d-JS中让玩家运行动画。 你可以从这里现在这个完整的项目。

学习方向

在下篇指南中,我们将会往我们的物理世界中添加chipmunk物理引擎。到那时,我们的游戏将会更加的逼真。  

介绍

Cocos2d-JS 能赋予我们创建令人印象深刻的游戏世界的能力。但它缺少了真实性。尽管我们能够处理复杂的运算来使得游戏更加真实,但有一种更轻松的方式来完成这个任务。回答是,物理引擎。 物理引擎提供重力、碰撞检测以及模拟物理场景,它们会让我们的游戏世界看上去更加真实。 在这篇教程中,我们将会在我们的parkour游戏中介绍Chipmunk物理引擎。

为什么选择Chipmunk?

为什么我们应该选择Chipmunk物理引擎?因为它相比其他的2D物理引擎,能赋予我们更多的力量。 除了Chipmunk物理引擎外,还有别的选项——Box2D。 Box2D是一个优秀的物理引擎,它存在了很长一段时间。很多2D游戏都采用了Box2D作为自己的物理引擎。 但Chipmunk有它自己的优势。你可以浏览Chipmunk的网站查阅更多的信息。

在Cocos2d-JS中激活Chipmunk

准备工作

首先,让我们在Cocos2d-JS中激活Chipmunk。 打开 project.json 文件,然后作如下修改: 将
 "modules" : ["cocos2d"],
改为:
 "modules" : ["cocos2d","chipmunk"],
这样,当Cocos2d-JS启动的时候,它会自动将在Chipmunk库。 然后,让我们创建一个名叫globals.js的新文件,并在里面添加两个全局变量。
var g_groundHeight = 57;
var g_runnerStartX = 80;
最后,我们应该告诉框架在启动后,去加载globals.js文件。 在jsList数组的最后,添加globals.js的路径:
    "jsList" : [
        "src/resource.js",
        "src/app.js",
        "src/AnimationLayer.js",
        "src/BackgroundLayer.js",
        "src/PlayScene.js",
        "src/StatusLayer.js",
        "src/globals.js"
    ]
注意: 任何时候你在Cocos2d-JS里添加了一个新的文件,你要记得把它添加到jsList数组中。

初始化Chipmunk 物理世界

在Chipmunk中,用一个space 对象来表示物理世界。 首先,让我们添加一个名叫space的新成员变量到PlayScene.js文件中:
space:null,
总的来说,一个游戏只需要一个space对象。space对象可以被不同的层所共享。我们经常把space对象的初始化代码 放在PlayScene。 以下是设置物理世界的代码:
    // init space of chipmunk
    initPhysics:function() {
        //1. new space object 
        this.space = new cp.Space();
        //2. setup the  Gravity
        this.space.gravity = cp.v(0, -350);

        // 3. set up Walls
        var wallBottom = new cp.SegmentShape(this.space.staticBody,
            cp.v(0, g_groundHeight),// start point
            cp.v(4294967295, g_groundHeight),// MAX INT:4294967295
            0);// thickness of wall
        this.space.addStaticShape(wallBottom);
    },
上述的代码能够很好的解释它们的用途,所以我们可以把它们保持原样。如果你想要了解这些API的细节,你应该查阅Chipmunk的官方文档来获取更多信息。 在update函数中,我们告诉Chipmunk让它开始模拟物理场景。 在教程往下走之前,让我们在AnimationLayer增加一个小小的改动。既然我们将要在AnimationLayer里创建演员,所以我们应该修改AnimationLayer的构造器,让我们能传递space对象参数。
ctor:function (space) {
        this._super();
        this.space = space;
        this.init();
    },
当然我们应该在AnimationLayer里定义一个弱引用成员变量,并把它初始化为null。 我们的准备工作到这里就结束了。让我们最后在onEnter函数里调用:
    onEnter:function () {
        this._super();
        this.initPhysics();

        this.addChild(new BackgroundLayer());
        this.addChild(new AnimationLayer(this.space));
        this.addChild(new StatusLayer());

        this.scheduleUpdate();
    },
注意:你必须初始化物理引擎的space变量并将它传递到AnimationLayer中去。

在Runner 精灵中添加物理组件

在上一篇教程中,我们通过spritsheet创建了runner精灵。在这一节中,我们将要使用PhysicsSprite对runner进行重写。 PhysicsSprite是一个可重用的组件,它能够将cocos2d精灵和物理body组合关联在一起。 以下是使用PhysicsSprite创建runner的代码:
        //1. create PhysicsSprite with a sprite frame name
        this.sprite = new cc.PhysicsSprite("#runner0.png");
        var contentSize = this.sprite.getContentSize();
        // 2. init the runner physic body
        this.body = new cp.Body(1, cp.momentForBox(1, contentSize.width, contentSize.height));
        //3. set the position of the runner
        this.body.p = cc.p(g_runnerStartX, g_groundHeight + contentSize.height / 2);
        //4. apply impulse to the body
        this.body.applyImpulse(cp.v(150, 0), cp.v(0, 0));//run speed
        //5. add the created body to space
        this.space.addBody(this.body);
        //6. create the shape for the body
        this.shape = new cp.BoxShape(this.body, contentSize.width - 14, contentSize.height);
        //7. add shape to space
        this.space.addShape(this.shape);
        //8. set body to the physic sprite
        this.sprite.setBody(this.body);
代码和注释都很容易看明白。把这些代码添加到AnimationLayerinit方法中去。

调试和测试

恭喜!你完成了的细节了。你可以在Cocos Code IDE里面按下debug按钮 run 现在你可以看到runner在屏幕上跑动了。

总结

在这篇教程中,我们向你展示了如何设置Chipmunk物理世界,如何设置物理世界的边界,如何创建一个刚性的body和与其关联的shape。我们把物理属性添加到精灵上让它显得更真实。你可以在这里获得完整的代码。

下一步

在下一篇教程中,我们将会介绍游戏里面摄像机(camera)的移动。然后我们也将会使用tiledMap代替背景图片。更重要的是,我们会使得游戏中得背景循环。在下一篇教程再见。  

介绍

在这篇教程中,我会向你展示如何在parkour游戏中添加TiledMap作为新的背景。 我们也将会学习如何使背景不断滚动以及玩家不断奔跑。 这背后的奥秘就是移动cocos2d的层。

完成一些准备工作

在我们下手前,让我们添加资源文件以及相对应我们游戏的名字。

设置资源和全局变量

既然我们将要在每一层引用其他的层。所以最好的方式来检索层是通过标签。 在globals.js添加下述的代码:
if(typeof TagOfLayer == "undefined") {
    var TagOfLayer = {};
    TagOfLayer.background = 0;
    TagOfLayer.Animation = 1;
    TagOfLayer.Status = 2;
};
以下我们给背景层、动画层、状态层一个标签名字,这样我们就能通过标签检索其他层。 我们也需要在resource.js添加资源变量。
//Our two tiled map are named s_map00 and s_map01.
var res = {
    helloBG_png : "res/helloBG.png",
    start_n_png : "res/start_n.png",
    start_s_png : "res/start_s.png",
    PlayBG_png  : "res/PlayBG.png",
    runner_png  : "res/running.png",
    runner_plist : "res/running.plist",
    map_png: "res/map.png",
    map00_tmx: "res/map00.tmx",
    map01_tmx: "res/map01.tmx"
};

var g_resources = [
    //image
    res.helloBG_png,
    res.start_n_png,
    res.start_s_png,
    res.PlayBG_png,
    res.runner_png,
    res.runner_plist,
    res.map_png,
    res.map00_tmx,
    res.map01_tmx
];
上述的代码能自己解释自己,所以让我们跳到下一节。

激活Chipmunk调试绘画

如果我们使用Chipmunk物理引擎,你最好激活调试绘画功能。所以调试的过程会更加方便。 在AnimationLayer.js的ctor方法中添加下面的代码:
this._debugNode = new cc.PhysicsDebugNode(this.space);
// Parallax ratio and offset
this.addChild(this._debugNode, 10);
当你再次运行游戏,你将会看见在奔跑的玩家上的一个红色的盒子: debugdraw

TiledMap介绍

TileMap是一个2d游戏里面常见的概念。它对于构建非常大的地图和一些视差滚动背景非常有用。 TiledMap比一般的PNG文件消耗更少的内存。如果你想要构建一些很大型的地图,这一定是你最好的选择。 闲话少说,让我们深入看看TiledMap。

用TiledMap代替以前的背景

现在,是时候用酷炫的tiled map代替旧的静态的背景图。 我们将要在BackgroundLayer.js完成下面的工作。首先,我们应该在BackgroundLayer中添加四个成员变量:
map00:null,
map01:null,
mapWidth:0,
mapIndex:0,
我们应该删除旧的创建静态背景的代码。 (注意:下面我取注释代码片段,你可以完整地删除。)
//        var winSize = cc.Director.getInstance().getWinSize();
//
//        var centerPos = cc.p(winSize.width / 2, winSize.height / 2);
//        var spriteBG = new cc.Sprite(s_PlayBG);
//        spriteBG.setPosition(centerPos);
//        this.addChild(spriteBG);
最后,我们将要添加新的代码片段来创建tiledmap 背景。
this.map00 = new cc.TMXTiledMap(res.map00_tmx);
this.addChild(this.map00);
this.mapWidth = this.map00.getContentSize().width;
this.map01 = new cc.TMXTiledMap(res.map01_tmx);
this.map01.setPosition(cc.p(this.mapWidth, 0));
this.addChild(this.map01);
保存所有的改动并运行: replacebg 在这里,我们添加两片地图。map01就在map00的旁边。在下面的章节中,我们将要解释为什么我们要添加两片地图。

Scene Display介绍

既然物理body将要不断地向右移动,精灵会和物理body同步它的位置。 过了一段时间后,玩家会跑到屏幕外面,就像上一篇教程说的那样。 所以我们需要在每帧移动游戏层的x坐标,让它保持在可见的范围内。以下是AnimationLayer.js中代码片段:
getEyeX:function () {
    return this.sprite.getPositionX() - g_runnerStartX;
},
这里getEyeX函数计算动画层的偏移(delta)。 我们应该在包含背景层和动画层的this.gameLayer反方向移动相同的偏移(delta)。所以我们能够通过在PlayScene.js里的,每帧调用的update函数中添加下列代码:
     update:function (dt) {
        // chipmunk step
        this.space.step(dt);

        var animationLayer = this.gameLayer.getChildByTag(TagOfLayer.Animation);
        var eyeX = animationLayer.getEyeX();

        this.gameLayer.setPosition(cc.p(-eyeX,0));
    }

移动背景层

设置背景层的移动的方法跟我们上一节做的基本是一样的。不过我们需要针对两个tiled map完成一些计算。 让我们来完成它把。在BackgroundLayer里添加一个新的成员函数checkAndReload
    checkAndReload:function (eyeX) {
        var newMapIndex = parseInt(eyeX / this.mapWidth);
        if (this.mapIndex == newMapIndex) {
            return false;
        }
        if (0 == newMapIndex % 2) {
            // change mapSecond
            this.map01.setPositionX(this.mapWidth * (newMapIndex + 1));
        } else {
            // change mapFirst
            this.map00.setPositionX(this.mapWidth * (newMapIndex + 1));
        }
        this.mapIndex = newMapIndex;
        return true;
    },
当eyeX超出了屏幕的宽度,表达式parseInt(eyeX / this.mapWidth 将会获得一个大于0的值。 我们将要使用newMapIndex来决定哪片地图需要移动以及多少像素需要移动。 那么,我们要在每帧调用这个函数。
    update:function (dt) {
        var animationLayer = this.getParent().getChildByTag(TagOfLayer.Animation);
        var eyeX = animationLayer.getEyeX();
        this.checkAndReload(eyeX);
    }
最后,我们要在背景层的init的最后要调用scheduleUpdate函数。
 this.scheduleUpdate();

收尾

好。我们要完成最后的收尾工作。 修改PlayScene的onEnter方法来添加层的标签(tag),在游戏层中添加背景层和动画层。
    onEnter:function () {
        this._super();
        this.initPhysics();
        this.gameLayer = new cc.Layer();

        //add Background layer and Animation layer to gameLayer
        this.gameLayer.addChild(new BackgroundLayer(), 0, TagOfLayer.background);
        this.gameLayer.addChild(new AnimationLayer(this.space), 0, TagOfLayer.Animation);
        this.addChild(this.gameLayer);
        this.addChild(new StatusLayer(), 0, TagOfLayer.Status);

        this.scheduleUpdate();
    },
恭喜!你成功完成了这篇教程。运行并看看它。 注意:如果你不想展示一个chipmunk刚体body的调试绘画信息。你可以放心地在创建PhysicsDebugNode下添加下列的代码:
this._debugNode.setVisible(false);

总结

在这篇教程,我们已经学到了TiledMap 和 display。这两个概念在你开发一个物理游戏是非常重要的。 你可以在这里下载整个项目。

下一步

在下一篇教程中,我们将要在我们的游戏中添加金币和障碍物。在这篇教程里,我们将要学习如何重构我们的代码让它更具有扩展性。 我们会在PlayScene类作清理并封装Coin和Rock两个类。 继续在下篇教程里调整代码并开心地编程!
 

引言

在这个教程中,我们将试图在我们的跑酷游戏中添加硬币和障碍物。 学习完这个教程之后,我们的玩家将可以在跑步时候收集硬币并且当他碰撞到障碍物时候他将死亡。 我们也将涉及到如何用一个平铺的地图编辑器设计一个游戏等级。因为游戏逻辑与之前相比有些复杂,所以我们将在添加新的游戏组件之前重构代码。

准备

在我们开始之前,让我们先完成一些准备工作。

设置Resource和Globals

因为我们将在跑酷游戏中再添加两个游戏元素。所以我们需要再添加一些全局整型标记来标识每一个游戏项。 让我们在globals.js底部添加以下代码片段
// 小花栗鼠的碰撞类型
if(typeof SpriteTag == "undefined") {
    var SpriteTag = {};
    SpriteTag.runner = 0;
    SpriteTag.coin = 1;
    SpriteTag.rock = 2;
};
这里我们使用 0,1,2来代表跑步者,硬币和岩石。 我们也介绍另一个名叫background.png 的spritesheet和background.plist 。我们已经将硬币和岩石精灵打包到叫做background.png 的spritesheet中。 如何打包这些精灵的细节将留在下一节讲述。 接下来,让我们复制资源文件到我们的res目录并且再添加两个变量来进一步参考。
var res = {
    helloBG_png : "res/helloBG.png",
    start_n_png : "res/start_n.png",
    start_s_png : "res/start_s.png",
    PlayBG_png  : "res/PlayBG.png",
    runner_png  : "res/running.png",
    runner_plist : "res/running.plist",
    map_png : "res/map.png",
    map00_tmx : "res/map00.tmx",
    map01_tmx : "res/map01.tmx",
    background_png :"res/background.png",
    background_plist : "res/background.plist"
};

var g_resources = [
    //图片
    res.helloBG_png,
    res.start_n_png,
    res.start_s_png,
    res.PlayBG_png,
    res.runner_png,
    res.runner_plist,
    res.map_png,
    res.map00_tmx,
    res.map01_tmx,
    res.background_png,
    res.background_plist
];

打包硬币和岩石到包含纹理图册的Spritesheet

在前一章中,我们已经学习了如何打包一串小精灵到一个大的压缩的spritesheet中。让我们打包另一个spritesheet。 首先,你应该启动TexturePacker 并且拖动所有的assets到res/TexturePacker/coins and rocks目录下。(注意:你可以获取到之前下载的所有游戏资源。) 在拖动资源之后,你应该详细说明Data fileTexture format ,他们的路径类似于xxx/chapter8/res/background.png 或者 xxx/chapter8/res/background.plist这样的格式。 如果你不想要任何spritesheet的最优化,只需要忽略他们并且按下Publish来产生最终的spritesheet。 packcoins

TiledMap Object Layer的介绍

我们已经为我们的平面地图使用TiledMap,但是它缺少游戏项。所以在这个章节中,我们将涉及如何使用TiledMap Object Layer来设计平面项。

添加硬币Object Layer

首先,我们将添加硬币Object Layer。 1.启动Tiled 并且打开map00.tmxmap01.tmx. 2.在map00.tmx 和map01.tmx中创建一个名为coin的Object layer。 objectlayer 3.通过拖放矩形对象到地图来设计object layer。 你可以改变矩形的尺寸和它的位置。你也可以复制或者删除对象。 designobjectlayer 4.设计object layer的一些小提示: 你可以改变平铺地图中图层的透明度以便你可以轻易地放置对象。

添加岩石Object Layer

创建岩石object layer的过程和创建硬币 object layer的过程差不多一样。 所以我们将在你自己的实现中忽略这部分内容。

重构BackgroundLayer类并且添加一些有帮助的方法

有时候,当你在编码的时候,你可能发现在现有的结构中添加新功能是很困难的事。 这是一种很糟糕的代码风格,我们应该立刻停止它并且开始重构。

重构BackgroundLayer 类

因为我们将添加Chipmunk 物体到我们的背景中,所以我们需要一个方法来获取在PlayScene中创建的space对象。 让我们改变Background Layer中的ctor 方法的名字,并且传递一个叫做space的参数到这个方法中。我们还应该在BackgroundLayer类中添加一个新的成员变量。这里是相关的代码片段:
    ctor:function (space) {
        this._super();

        // 这里清空旧数组
        this.objects = [];
        this.space = space;

        this.init();
    },
这里我们已经添加附加的初始化代码。我们添加一个名叫objects数组并且将它初始化为一个空数组。 (注意:你应该在this.space = space的分配后调用this.init() 方法。因为我们将在初始化方法中创建物理对象

添加有用的方法

1.再在BackgroundLayer中添加一些成员变量:
    space:null,
    spriteSheet:null,
    objects:[],
2.在init方法中初始化spritesheet
  //创建sprite sheet
        cc.spriteFrameCache.addSpriteFrames(res.background_plist);
        this.spriteSheet = new cc.SpriteBatchNode(res.background_png);
        this.addChild(this.spriteSheet);
3.添加一个叫做loadObject的方法来初始化岩石和硬币。
 loadObjects:function (map, mapIndex) {
        //添加硬币
        var coinGroup = map.getObjectGroup("coin");
        var coinArray = coinGroup.getObjects();
        for (var i = 0; i < coinArray.length; i++) {
            var coin = new Coin(this.spriteSheet,
                this.space,
                cc.p(coinArray[i]["x"] + this.mapWidth * mapIndex,coinArray[i]["y"]));
            coin.mapIndex = mapIndex;
            this.objects.push(coin);
        }

        // 添加岩石
        var rockGroup = map.getObjectGroup("rock");
        var rockArray = rockGroup.getObjects();
        for (var i = 0; i < rockArray.length; i++) {
            var rock = new Rock(this.spriteSheet,
                this.space,
                rockArray[i]["x"] + this.mapWidth * mapIndex);
            rock.mapIndex = mapIndex;
            this.objects.push(rock);
        }
    },
这里我们在平铺地图中反复申明所有的对象信息并且创建响应的Chipmunk 刚体。最后我们在objects数组中存储这些对象。 所有这些代码是自我解释的。你应该只注意mapIndex 参数。我们使用这个参数来计算我们应该将刚体放在哪个位置。 我们需要在init方法底部调用loadObject方法来创建前两个屏幕地图中的物理对象。
this.loadObjects(this.map00, 0);
this.loadObjects(this.map01, 1);
4.再添加另两个有用的方法来移除没用的小花栗鼠刚体。 第一个方法叫做removeObjects。它通过mapIndex移除一个对象。这里是它的实现:
removeObjects:function (mapIndex) {
        while((function (obj, index) {
            for (var i = 0; i < obj.length; i++) {
                if (obj[i].mapIndex == index) {
                    obj[i].removeFromParent();
                    obj.splice(i, 1);
                    return true;
                }
            }
            return false;
        })(this.objects, mapIndex));
    },
另一个方法叫做removeObjectByShape:
   removeObjectByShape:function (shape) {
        for (var i = 0; i < this.objects.length; i++) {
            if (this.objects[i].getShape() == shape) {
                this.objects[i].removeFromParent();
                this.objects.splice(i, 1);
                break;
            }
        }
    },
这个方法将通过它的形状来移除小花栗鼠对象。

包裹以上内容:在checkAndReload 方法中添加创建和废除的逻辑

当地图被移除,我们也应该调用loadObject方法来重建”Coins & Rocks”. 并且,我们应该通过调用removeObjects方法移除所有没用的对象 这里是代码片段:
  checkAndReload:function (eyeX) {
        var newMapIndex = parseInt(eyeX / this.mapWidth);
        if (this.mapIndex == newMapIndex) {
            return false;
        }

        if (0 == newMapIndex % 2) {
            // 改变mapSecond
            this.map01.setPositionX(this.mapWidth * (newMapIndex + 1));
            this.loadObjects(this.map01, newMapIndex + 1);
        } else {
            // 改变mapFirst
            this.map00.setPositionX(this.mapWidth * (newMapIndex + 1));
            this.loadObjects(this.map00, newMapIndex + 1);
        }
        this.removeObjects(newMapIndex - 1);
        this.mapIndex = newMapIndex;

        return true;
    },

添加硬币和岩石

现在是时候添加硬币和岩石的实现了。除了实现细节外,你还应该注意这两个类背后的设计思想。这里我们更倾向于从cc.Class继承而不是cc.Sprite。我们让每个对象拥有一个cc.Sprite实例。

设计并且实现Coin类

1.创建一个叫做coin.js的新文件。我们将在这个文件中定义我们的Coin类。确保将这个文件放在你的src目录下。 2. 从cc.Class中派生一个叫做Coin的类,让我们看看所有的实现:
var Coin = cc.Class.extend({
    space:null,
    sprite:null,
    shape:null,
    _mapIndex:0,// map属于这个参数
    get mapIndex() {
        return this._mapIndex;
    },
    set mapIndex(index) {
        this._mapIndex = index;
    },

    /** 构造器
     * @param {cc.SpriteBatchNode *}
     * @param {cp.Space *}
     * @param {cc.p}
     */
    ctor:function (spriteSheet, space, pos) {
        this.space = space;

        // 初始化硬币动画
        var animFrames = [];
        for (var i = 0; i < 8; i++) {
            var str = "coin" + i + ".png";
            var frame = cc.spriteFrameCache.getSpriteFrame(str);
            animFrames.push(frame);
        }

        var animation = new cc.Animation(animFrames, 0.2);
        var action = new cc.RepeatForever(new cc.Animate(animation));

        this.sprite = new cc.PhysicsSprite("#coin0.png");

        // 初始化physics
        var radius = 0.95 * this.sprite.getContentSize().width / 2;
        var body = new cp.StaticBody();
        body.setPos(pos);
        this.sprite.setBody(body);

        this.shape = new cp.CircleShape(body, radius, cp.vzero);
        this.shape.setCollisionType(SpriteTag.coin);
        //Sensors 只是调用碰撞机回调函数,并且永远不生成真实的碰撞机
        this.shape.setSensor(true);

        this.space.addStaticShape(this.shape);

        // 添加sprite 到sprite sheet
        this.sprite.runAction(action);
        spriteSheet.addChild(this.sprite, 1);
    },

    removeFromParent:function () {
        this.space.removeStaticShape(this.shape);
        this.shape = null;
        this.sprite.removeFromParent();
        this.sprite = null;
    },

    getShape:function () {
        return this.shape;
    }
});
让我们一段一段地解释一下代码。 首先,我们添加三个成员变量分别叫做:space, spriteshape.我们将使用这些变量来创建硬币对象的物理实体和它的显示属性。 然后,我们添加另一个成员变量_mapIndex。我们使用get/set语法糖来定义变量的存取。 ctor方法是Coin类的构造器。我们将使用spritesheet创建一个Coin类,spritesheet是接下来的空间和位置对象。 因为硬币是圆形的,所以我们创建了CircleShape 附加于刚体上。ctor 函数的剩余部分是自我解释的。 最后,我们需要定义一个方法来清理工作。它就是removeFromParent方法。它首先从空间移除刚体,然后从它的父类移除精灵。getShape方法只是一个用于访问存储于硬币对象中的形状对象的一个只读方法。

设计并且实现Rock类

设计Rock类的原理和硬币类差不多,除了刚体形状类型部分。 因为我们的Rock类是一个矩形盒子。所以相比于Coin类,在Rock类中我们使用cp.BoxShape来替代cc.CircleShape。 这里是rock.js是所有源代码:
var Rock = cc.Class.extend({
    space:null,
    sprite:null,
    shape:null,
    _map:0,// map 属于这个参数
    get map() {
        return this._map;
    },
    set map(newMap) {
        this._map = newMap;
    },

    /** 构造器
     * @param {cc.SpriteBatchNode *}
     * @param {cp.Space *}
     * @param {cc.p}
     */
    ctor:function (spriteSheet, space, posX) {
        this.space = space;

        this.sprite = new cc.PhysicsSprite("#rock.png");
        var body = new cp.StaticBody();
        body.setPos(cc.p(posX, this.sprite.getContentSize().height / 2 + g_groundHeight));
        this.sprite.setBody(body);

        this.shape = new cp.BoxShape(body,
            this.sprite.getContentSize().width,
            this.sprite.getContentSize().height);
        this.shape.setCollisionType(SpriteTag.rock);

        this.space.addStaticShape(this.shape);
        spriteSheet.addChild(this.sprite);
    },

    removeFromParent:function () {
        this.space.removeStaticShape(this.shape);
        this.shape = null;
        this.sprite.removeFromParent();
        this.sprite = null;
    },

    getShape:function () {
        return this.shape;
    }
});

改善PlayScene

重构PlayScene的onEnter 函数

  1. 首先, 让我们添加一个叫做shapesToRemove的额外数组并且在 PlayScene.js的onEnter 函数开头初始化它。
//以下这行在定义地区的初始成员变量中运行
shapesToRemove :[],

//以下这行在*onEnter* 函数开头运行.
this.shapesToRemove = [];
2.然后,修改BackgroundLayer的创建。这里我们简单地传递space对象给BackgroundLayer的构造器。
    this.gameLayer.addChild(new BackgroundLayer(this.space), 0, TagOfLayer.background);

添加碰撞检测函数

  • 首先, 我们应该在initPhyiscs 方法末尾调用这两个函数:
 // 设置小花栗鼠CollisionHandler
        this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.coin,
            this.collisionCoinBegin.bind(this), null, null, null);
        this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.rock,
            this.collisionRockBegin.bind(this), null, null, null);
当碰撞发生时,addCollisionHandler 方法需要一个回调函数。
  • 然后, 让我们定义这两个回调函数来操纵玩家与硬币和岩石的碰撞.
 collisionCoinBegin:function (arbiter, space) {
        var shapes = arbiter.getShapes();
        // shapes[0] is runner
        this.shapesToRemove.push(shapes[1]);
    },

    collisionRockBegin:function (arbiter, space) {
        cc.log("==game over");
    },
  • 删除背景层中没用的刚体. 你应该在 update 方法末尾添加以下代码:
        // 模拟cpSpaceAddPostStepCallback
        for(var i = 0; i < this.shapesToRemove.length; i++) {
            var shape = this.shapesToRemove[i];
            this.gameLayer.getChildByTag(TagOfLayer.background).removeObjectByShape(shape);
        }
        this.shapesToRemove = [];
我们不能在物理模拟过程中删除物体。所以我们使用一个叫做shapesToRemove的额外的数组来存储需要被删除的临时数据。

包裹以上所有这些东西

恭喜你!你快要成功了。在我们点击debug按钮来查看结果之前,让我们添加一些额外的胶水代码来将所有东西连接在一起。 打开project.json并且再在jsList数组末尾添加两个数组项。
    "jsList" : [
        "src/resource.js",
        "src/app.js",
        "src/AnimationLayer.js",
        "src/BackgroundLayer.js",
        "src/PlayScene.js",
        "src/StatusLayer.js",
        "src/globals.js",
        "src/coin.js",
        "src/rock.js"
    ]
编译运行!干杯,我们完成它了!:) 让我们看看我们最终的果实吧: fruit

总结

在这个教程中,我们已经度过了一个愉快的漫长的旅程,但是很值得,不是吗? 我们已经学习了如何使用TiledMap对象图层来设计复杂的游戏等级还有如何定制你自己的类来扩展你的代码结构。 你可以在这里下载全部的结构代码.

接下来我们要学习什么呢?

在下一个教程中,我们将涉及如何连续更新游戏HUD,我们还将在游戏中添加游戏结束逻辑还有简单的手势识别器来使玩家可以跳过障碍物。保持调优状态!  

介绍

在这篇教程中,我们将完成我们的游戏结束逻辑,更新HUD,并实现一个简单的手势识别器。 废话不多说,让我们开始吧。

更新游戏HUD

更新玩家奔跑的距离

首先,让我们在StatusLayer类中加入一个updateMeter方法:
    updateMeter:function (px) {
        this.labelMeter.setString(parseInt(px / 10) + "M");
    }
这个方法不断地改变labelMeter的值。我们使用parseInt函数来将结果转化为一个整型数据。 参数px代表像素,所以10个像素对应1米。 显然的,我们应该在每一帧中都调用这个方法。 打开AnimationLayer.js,在update方法的最前面加上如下几行代码:
        // update meter
        var statusLayer = this.getParent().getParent().getChildByTag(TagOfLayer.Status);
        statusLayer.updateMeter(this.sprite.getPositionX() - g_runnerStartX);

更新金币数

当玩家获得了一个金币时,我们需要更新金币数显示器。 首先,让我们在StatusLayer中增加一个addCoin方法:
    addCoin:function (num) {
        this.coins += num;
        this.labelCoin.setString("Coins:" + this.coins);
    },
当玩家碰撞到金币时,我们将调用这个方法。 接下来我们就来实现它。 打开PlayScene.js,在colisionCoinBegin方法的最后加上如下几行代码:
        var statusLayer = this.getChildByTag(TagOfLayer.Status);
        statusLayer.addCoin(1);
这样每次玩家碰撞到金币时,colisionCoinBegin方法就会被调用,金币计数器就会自动加1。 保存文件,运行一下试试:) 截图如下: updatehud

在游戏中加入游戏结束逻辑

设计并实现游戏结束层

为了使事情尽可能简单,我们只在游戏结束层的中间放置一个菜单。 当你点击了restart菜单时,游戏就会重新开始。 所以这个设计还是很简单的,让我们来完成它。 这里是GameOverLayer.js的全部内容:
var GameOverLayer = cc.LayerColor.extend({
    // constructor
    ctor:function () {
        this._super();
        this.init();
    },
    init:function () {
        this._super(cc.color(0, 0, 0, 180));
        var winSize = cc.director.getWinSize();

        var centerPos = cc.p(winSize.width / 2, winSize.height / 2);
        cc.MenuItemFont.setFontSize(30);
        var menuItemRestart = new cc.MenuItemSprite(
            new cc.Sprite(res.restart_n_png),
            new cc.Sprite(res.restart_s_png),
            this.onRestart, this);
        var menu = new cc.Menu(menuItemRestart);
        menu.setPosition(centerPos);
        this.addChild(menu);
    },
    onRestart:function (sender) {
        cc.director.resume();
        cc.director.runScene(new PlayScene());
    }
});
这里我们用了两个sprite,s_restart_ns_restart_s,来创建我们的restart菜单项。 因此我们需要将这些资源文件加入res目录中,并定义资源路径。 打开resource.js,添加如下几行代码:
    restart_n_png : "res/restart_n.png",
    restart_s_png : "res/restart_s.png"

//add the following two lines at the end of g_resources array.
    res.restart_n_png,
    res.restart_s_png
init方法的代码很容易理解,但你需要注意onRestart这个回调函数。 我们调用了cc.Director中的resume方法。为什么要这样做呢?因为当玩家死亡时我们会调用pause方法。

当玩家撞到障碍物时,游戏结束

现在,让我们实现一下当玩家撞到障碍物时,显示游戏结束层。 打开PlayScene,在collisionRockBegin方法的最后加上如下几行代码:
  collisionRockBegin:function (arbiter, space) {
        cc.log("==game over");
        cc.director.pause();
        this.addChild(new GameOverLayer());
    },
好的,完成了。让我们再跑一下程序试试看。 这是最终的效果图: gameover

构造你自己的手势识别器

这一节中,我们将构造一个简单的手势识别器。 当我们用手指从屏幕下端划到上端时,识别器能够检测到它。 检测手势的算法很直观。当检测到触控开始时,我们将第一个触控点的坐标存入数组,当检测到触控移动时,我们将触控点的坐标存入数组的末尾。 这样我们只需要比较一下相邻两点的x和y坐标就能计算出划动的方向了。 代码如下:
function Point(x, y)
{
    this.X = x;
    this.Y = y;
}

// class define
function SimpleRecognizer()
{
    this.points = [];
    this.result = "";
}

// be called in onTouchBegan
SimpleRecognizer.prototype.beginPoint = function(x, y) {
    this.points = [];
    this.result = "";
    this.points.push(new Point(x, y));
}

// be called in onTouchMoved
SimpleRecognizer.prototype.movePoint = function(x, y) {
    this.points.push(new Point(x, y));

    if (this.result == "not support") {
        return;
    }

    var newRtn = "";
    var len = this.points.length;
    var dx = this.points[len - 1].X - this.points[len - 2].X;
    var dy = this.points[len - 1].Y - this.points[len - 2].Y;

    if (Math.abs(dx) > Math.abs(dy)) {
        if (dx > 0) {
            newRtn = "right";
        } else {
            newRtn = "left";
        }
    } else {
        if (dy > 0) {
            newRtn = "up";
        } else {
            newRtn = "down";
        }
    }

    // first set result
    if (this.result == "") {
        this.result = newRtn;
        return;
    }

    // if diretcory change, not support Recongnizer
    if (this.result != newRtn) {
        this.result = "not support";
    }
}

// be called in onTouchEnded
SimpleRecognizer.prototype.endPoint = function() {
    if (this.points.length < 3) {
        return "error";
    }
    return this.result;
}

SimpleRecognizer.prototype.getPoints = function() {
    return this.points;
}
当手势被成功识别后,我们可以调用SimpleRecognizer中的endPoint方法来得到最终的结果。 目前为止只支持四种简单的类型:上,下,左,右。你可以自行扩展。

处理触控事件,玩家跳跃逻辑及动画

添加玩家的跳跃动画

为了实现跳跃动画,我们先要做一些准备工作,这里我们已经为你做好了。 你可以从总结版块下载整个工程文件,将running.plist和running.png复制&粘贴进res目录。 当游戏开始后,玩家将不停地奔跑,直到撞上障碍物。我们希望能通过向上划动来使玩家跳跃。 这样或许每一局的游戏时间可以长一点。 当玩家向上跳或是向下落时,我们需要播放相应的动画。 所以我们第一步要做的,就是在AnimationLayer中添加两个animation action:
jumpUpAction:null,
jumpDownAction:null,
然后新建一个名为initAction的函数:
  initAction:function () {
        // init runningAction
        var animFrames = [];
        // num equal to spriteSheet
        for (var i = 0; i < 8; i++) {
            var str = "runner" + i + ".png";
            var frame = cc.spriteFrameCache.getSpriteFrame(str);
            animFrames.push(frame);
        }

        var animation = new cc.Animation(animFrames, 0.1);
        this.runningAction = new cc.RepeatForever(new cc.Animate(animation));
        this.runningAction.retain();

        // init jumpUpAction
        animFrames = [];
        for (var i = 0; i < 4; i++) {
            var str = "runnerJumpUp" + i + ".png";
            var frame = cc.spriteFrameCache.getSpriteFrame(str);
            animFrames.push(frame);
        }

        animation = new cc.Animation(animFrames, 0.2);
        this.jumpUpAction = new cc.Animate(animation);
        this.jumpUpAction.retain();

        // init jumpDownAction
        animFrames = [];
        for (var i = 0; i < 2; i++) {
            var str = "runnerJumpDown" + i + ".png";
            var frame = cc.spriteFrameCache.getSpriteFrame(str);
            animFrames.push(frame);
        }

        animation = new cc.Animation(animFrames, 0.3);
        this.jumpDownAction = new cc.Animate(animation);
        this.jumpDownAction.retain();
    },
在这个函数中,我们初始化了玩家所有的动画。 最后,让我们把之前在init函数中写的runningAction的初始化代码删掉,然后调用initAction方法。
//init  actions
this.initAction();
//        // init runningAction
//        var animFrames = [];
//        for (var i = 0; i < 8; i++) {
//            var str = "runner" + i + ".png";
//            var frame = cc.spriteFrameCache.getSpriteFrame(str);
//            animFrames.push(frame);
//        }
//        var animation = new cc.Animation(animFrames, 0.1);
//        this.runningAction = new cc.RepeatForever(new cc.Animate(animation));

处理触控事件

是时候处理触控事件了。首先,我们先要开启AnimationLayer的触控。 将下面这几行代码插入init方法的末尾:
 cc.eventManager.addListener({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            swallowTouches: true,
            onTouchBegan: this.onTouchBegan,
            onTouchMoved: this.onTouchMoved,
            onTouchEnded: this.onTouchEnded
        }, this)
这几行代码能够开启触控调度功能。 为了处理触控事件,需要添加三个回调函数:
 onTouchBegan:function(touch, event) {
        var pos = touch.getLocation();
        event.getCurrentTarget().recognizer.beginPoint(pos.x, pos.y);
        return true;
    },

    onTouchMoved:function(touch, event) {
        var pos = touch.getLocation();
        event.getCurrentTarget().recognizer.movePoint(pos.x, pos.y);
    },

    onTouchEnded:function(touch, event) {
        var rtn = event.getCurrentTarget().recognizer.endPoint();
        cc.log("rnt = " + rtn);
        switch (rtn) {
            case "up":
                event.getCurrentTarget().jump();
                break;
            default:
                break;
        }
    },
当你触控屏幕时,onTouchBegan方法会被调用。当你手指按住屏幕并拖动时,onTouchMoved方法会被调用。当你抬起手指时,onTouchEnded方法会被调用。 这里我们用自己构建的识别器来识别这些动作。

把所有的东西拼起来

是时候把所有的东西拼起来了。 首先,在AnimationLayer的开头加入如下枚举:
// define enum for runner status
if(typeof RunnerStat == "undefined") {
    var RunnerStat = {};
    RunnerStat.running = 0;
    RunnerStat.jumpUp = 1;
    RunnerStat.jumpDown = 2;
};
我们用这些枚举来表示玩家的不同状态。 接下来我们需要在AnimationLayer加入另外两个变量:
 recognizer:null,
 stat:RunnerStat.running,// init with running status
init方法的最后初始化识别器:
 this.recognizer = new SimpleRecognizer();
最后,完成我们的jump方法:
 jump:function () {
        cc.log("jump");
        if (this.stat == RunnerStat.running) {
            this.body.applyImpulse(cp.v(0, 250), cp.v(0, 0));
            this.stat = RunnerStat.jumpUp;
            this.sprite.stopAllActions();
            this.sprite.runAction(this.jumpUpAction);
        }
    },
update方法中将他们拼凑起来:
//in the update method of AnimationLayer
    // check and update runner stat
        var vel = this.body.getVel();
        if (this.stat == RunnerStat.jumpUp) {
            if (vel.y < 0.1) {
                this.stat = RunnerStat.jumpDown;
                this.sprite.stopAllActions();
                this.sprite.runAction(this.jumpDownAction);
            }
        } else if (this.stat == RunnerStat.jumpDown) {
            if (vel.y == 0) {
                this.stat = RunnerStat.running;
                this.sprite.stopAllActions();
                this.sprite.runAction(this.runningAction);
            }
        }
别忘了清理工作,在AnimationLayer销毁时我们需要释放已创建的action。
    onExit:function() {
        this.runningAction.release();
        this.jumpUpAction.release();
        this.jumpDownAction.release();
        this._super();
    },
你可能还会想确认下是否创建的js文件都已在cocos2d.js文件中被装载。

总结

恭喜,你离成功又近了一步! 让我们回顾一下这篇教程的内容。 首先,我们学会了怎么更新游戏hud元素。 然后我们加入了游戏结束逻辑。 最后,我们创建了一个简单的手势识别,用来处理玩家的跳跃动作。 你可以从这里下载最终的代码。

下一步

在下一篇教程中, 我们将完成跑酷游戏最后的部分,敬请期待!