Cocos2d-JavaScript-游戏开发学习指南-全-
Cocos2d JavaScript 游戏开发学习指南(全)
原文:
zh.annas-archive.org/md5/e8982117e49c93e428f62c4da53691e7
译者:飞龙
前言
移动 HTML5 休闲游戏正经历着一个黄金时代。不仅包括著名的 Flash 热门游戏和成功的本地移动游戏,还包括为移动浏览器量身定制的原创游戏,每天都有数百万玩家在玩。
随着越来越多的移动设备可供玩 HTML5 游戏,并且每个设备都有其自己的分辨率、显示尺寸和比例,为每个设备创建一个游戏版本将让你浪费很多时间。这就是为什么有一种新的制作 HTML5 游戏的方法,称为 跨平台。这意味着你只需创建一个游戏,一些魔法就会将其适配到所有能够运行 HTML5 内容的设备。
这种魔法被称为 Cocos2d-JS,它允许你专注于你真正热爱的事情,制作游戏,并处理屏幕分辨率和比例。此外,你将使用最简单、最广为人知的语言之一:JavaScript。最后但同样重要的是,它是免费的。你可以免费创建下一个大游戏。
在整本书中,你将学习如何以最有趣和最有效的方式使用 Cocos2d-JS 制作游戏。我们知道你讨厌无聊的理论,所以我们尽最大努力将整本书聚焦于一个单词:行动。
通过从最成功的游戏类型中提取的示例学习 Cocos2d-JS;看看创建一个游戏有多容易,然后让它运行在每个设备上。
本书涵盖内容
第一章, Hello World – 一个跨平台游戏,将向你展示如何创建一个蓝图,你将使用它来制作每一个用 Cocos2d-JS 制作的游戏。这一章还将指导你创建一个环境,用于创建和测试你的跨平台游戏。
第二章, 添加交互性 – 浓缩游戏的制作,指导你创建最受欢迎的游戏之一,让你学习如何创建精灵并与它们交互,无论是用鼠标还是用手指。
第三章, 在屏幕上移动精灵 – 无尽跑酷,介绍了使用运动缓动创建以太空为主题的无尽跑酷游戏中的滚动和精灵移动。粒子效果和碰撞检测完善了游戏体验。
第四章, 通过制作 Sokoban 学习滑动,展示了如何创建一个经典的益智游戏,你可以通过最直观的方式控制游戏:在游戏上滑动。
第五章, 成为音乐大师,探讨了在游戏中拥有音效和背景音乐的重要性。这一章还展示了如何通过调整音量来开始和停止声音和循环。
第六章, 使用虚拟摇杆控制游戏,提供了三种不同的方法,在构建水果游戏时使用虚拟摇杆来控制您的游戏。虚拟按钮和模拟摇杆将成为您创建下一个游戏时的最佳伙伴。
第七章, 使用 Box2D 引擎为您的游戏添加物理效果,展示了在制作热门游戏《图腾破坏者》时物理引擎的神奇之处,您将轻松地使用 Box2D 物理引擎构建它。您还将学习如何在真实的物理环境中创建、销毁、皮肤和与物理体交互。
第八章, 使用 Chipmunk2D 引擎为您的游戏添加物理效果,创建了相同的《图腾破坏者》游戏,这次使用的是 Chipmunk2D 而不是 Box2D。尽管网络被Box2D 与 Chipmunk2D 之战所分割,您将学习两者,将您喜欢的物理引擎的选择权留给自己。
第九章, 创建您的自己的热门游戏 - 一个完整的匹配 3 游戏,通过创建当今最成功的游戏类型之一:匹配 3 游戏来完善本书。在章节末尾,您还将找到一些关于保护、推广、营销和货币化您游戏的提示和建议。
您需要本书的内容
Cocos2d-JS 非常易于使用,除了免费之外;它基本上不需要除了文本编辑器和服务器来运行您的项目。如果您愿意,可以使用 Cocos2d-JS 官方 IDE,但如果您更喜欢,欢迎使用您喜欢的文本编辑器。
本书面向对象
无论您是来自其他语言的资深游戏开发者还是完全的新手,这本书都可以指导您通过创建跨平台游戏。建议您具备一些 JavaScript 的基本知识,然后只需跟随书中的示例即可。
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"我发现自己在用一系列if
.. then
.. else
尝试使游戏在任何设备上看起来都很好。"
以下是一个代码块的设置:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"如果您现在打开开发者控制台,您应该看到:我的精彩游戏从这里开始"。
注意
警告或重要注意事项以这种方式出现在一个框中。
小贴士
小技巧和技巧以这种方式出现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者的反馈对我们开发您真正能从中受益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>
,并在您的邮件主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者了,我们有许多事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com
的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从:www.packtpub.com/sites/default/files/downloads/0075OS_ColoredImages.pdf
下载此文件。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata
来报告它们,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support
选择您的标题来查看任何现有勘误。
盗版
在互联网上盗版版权材料是一个跨所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以追究补救措施。
如果您发现了疑似盗版材料,请通过 <copyright@packtpub.com>
联系我们,并提供链接。
我们感谢您在保护我们作者以及为我们提供有价值内容方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决。
第一章. Hello World – 一个跨平台游戏
传说中,当你学习一门新语言时,第一个应该做的有效脚本就是将经典的 Hello World 打印在屏幕上的某个位置。
本章将指导你创建一个跨平台的 Hello World 示例,涵盖以下概念:
-
跨平台游戏创建背后的理论
-
Cocos2d-JS 安装和设置
-
Cocos2d-JS 项目蓝图
-
场景、层和精灵
-
预加载图片
-
添加图片
-
移除图片
到本章结束时,你将能够创建一个模板项目,用于创建任何类型的 Cocos2d-JS 跨平台游戏,该游戏能够在不同分辨率的多种设备上运行。
为什么我应该制作跨平台游戏?
这是一个非常重要的问题。当 HTML5 移动游戏开始变得流行时,我多次问自己这个问题。我只是认为仅仅关注不同的屏幕分辨率和宽高比是浪费时间,所以我的第一个 HTML5 游戏是为了完美适配我的 iPad 2 平板。
当我终于向赞助商展示它时,他们中的大多数人都说了类似这样的话:“嘿,我喜欢这个游戏,但不幸的是,它在我的 iPhone 上看起来并不好。”“别担心,”我说,“你会得到为 iPad 和 iPhone 优化的游戏。”不幸的是,它在 Galaxy Note 上看起来也不太好。在 Samsung S4 上也是一样。
你可以想象这个故事的其他部分。我发现自己几乎是用一系列的 if.. then.. else
循环重写游戏,试图让它在任何设备上都看起来不错。
这就是为什么你应该制作一个跨平台游戏的原因:一次编码,统治所有。专注于游戏开发,让框架为你做脏活。
Cocos2d-JS 是什么以及它是如何工作的
Cocos2d-JS 是一个免费的开放源代码 2D 游戏框架。它可以帮助你开发跨平台浏览器游戏和原生应用程序。这个框架允许你用 JavaScript 编写游戏。所以,如果你已经开发了 JavaScript 应用程序,你不需要从头开始学习一门新语言。在这本书中,你将学习如何使用熟悉且直观的语言创建几乎任何类型的跨平台游戏。
运行 Cocos2d-JS 的要求
在你开始之前,让我们看看你需要在你电脑上安装哪些软件,以便开始使用 Cocos2d-JS 进行开发:
-
首先,你需要一个文本编辑器。Cocos2d-JS 编码的官方 IDE 是 Cocos Code IDE,你可以免费在
www.cocos2d-x.org/products/codeide
下载。它具有自动完成、代码提示和一些其他有趣的功能,可以帮助你加快编码速度。如果你习惯使用你喜欢的代码编辑器,那也行。有很多这样的编辑器,但我个人在我的 Windows 机器上使用 PSPad(你可以在www.pspad.com/
找到它),在 Mac 上使用 TextWrangler(你可以在www.barebones.com/products/textwrangler/
) 找到它)。它们都是免费的,而且易于使用,所以你可以下载并在几分钟内安装它们。 -
要测试你的 Cocos2d-JS 项目,你需要在电脑上安装一个网络服务器来覆盖运行本地项目时的安全限制。我在我的 Windows 机器上使用 WAMP (
www.wampserver.com/
),在 Mac 上使用 MAMP (www.mamp.info/
)。小贴士
再次强调,两者都是免费的,因为你不需要 PRO 版本,这个版本也适用于 Mac 电脑。解释所有背后的理论超出了本书的范围,但你可以在官方网站上找到所有必需的信息以及安装文档。
-
如果你愿意,你可以直接在线测试你的项目,通过上传到你拥有的 FTP 空间,并直接从网页上调用它们。在这种情况下,你不需要在电脑上安装网络服务器,但我强烈建议使用 WAMP 或 MAMP。
-
我个人使用 Google Chrome 作为默认浏览器来测试我的项目。由于这些项目旨在成为跨平台游戏,它们应该在每个浏览器上以相同的方式运行,所以请随意使用你喜欢的浏览器。
关于 Cocos2d-JS 的最新信息可以在官方网站 www.cocos2d-x.org/wiki/Cocos2d-JS
上找到,而最新版本可以在 www.cocos2d-x.org/download
下载。
注意
Cocos2d-JS 更新相当频繁,但截至本书编写时,最新的稳定版本是 v3.1。虽然新版本总是带来一些变化,但本书中包含的所有示例都应该与任何标记为 3.x 的版本兼容,因为路线图上没有巨大的变化。
你会注意到下载的文件是一个大于 250 MB 的 ZIP
文件。不用担心。包中的大部分内容是由文档、图形资产和示例组成的,而目前唯一必需的文件夹是名为 cocos2d-html5
的文件夹。
你的 Cocos2d-JS 项目的结构
每个 HTML5 游戏基本上都是一个带有一些魔法的网页;这就是你将使用 Cocos2d-JS 创建的:一个带有一些魔法的网页。
要执行这个魔法,需要创建一定的文件结构,所以让我们看看一个包含 Cocos2d-JS 项目的文件夹的截图:
这就是你将要构建的内容;说实话,这是我为本章要解释的示例项目所构建的实际项目文件夹的图片,它放置在我的电脑上的 WAMP localhost
文件夹中。它再真实不过了。
那么,让我们看看将要创建的文件:
-
cocos2d-html5
: 这是你在 zip 压缩文件中找到的文件夹。 -
index.html
: 这是将包含游戏的网页。 -
main.js
: 这是一个由 Cocos2d-JS 调用的文件,用于启动游戏。你将在接下来的几分钟内创建它。 -
project.json
: 这是一个包含一些基本配置的 JavaScript 对象表示法 (JSON) 文件。这是你使游戏运行所必需的。好吧,几乎是这样,因为实际的游戏将放置在src
文件夹中。我们先看看其他一些事情。
欢迎来到跨世界
时候到了,无聊的理论结束了,我们现在可以开始编写我们的第一个项目了。让我们开始吧!
-
首先,在游戏文件夹的根目录下创建一个名为
index.html
的页面,并编写以下 HTML 代码:<!DOCTYPE html> <head> <title> My Awesome game </title> <script src="img/CCBoot.js" type="text/javascript"> </script> <script src="img/main.js" type="text/javascript"> </script> </head> <body style="padding:0;margin:0;background-color:#000000;"> </body> </html>
目前里面没有什么有趣的东西,因为它只是普通的 HTML。让我们仔细看看这些行,看看发生了什么:
<script src="img/CCBoot.js "></script>
在这里,我包括了 Cocos2d-JS 启动文件以启动框架:
<script src="img/main.js"></script>
从上一行,这是我们调用实际将要构建的游戏脚本的地方。接下来,我们有以下代码:
<canvas id="gameCanvas"></canvas>
这是我们将用来显示游戏的画布。注意这里,画布没有宽度和高度,因为它们将由游戏本身定义。
-
接下来是创建
main.js
: 我们将从主index.html
页面调用的唯一文件。这更像是一个配置文件,而不是游戏本身,所以你现在不会编写任何与游戏相关的代码。然而,你将要构建的文件将成为你在所有 Cocos2d-JS 游戏中使用的蓝图。main.js
的内容如下:cc.game.onStart = function(){ cc.view.setDesignResolutionSize(320, 480, cc.ResolutionPolicy.SHOW_ALL); cc.director.runScene(new gameScene()); }; cc.game.run();
目前不必担心代码;它看起来比实际复杂得多。目前,我们唯一需要担心的是定义分辨率策略的那一行。
小贴士
在跨平台开发中最具挑战性的任务之一是提供良好的游戏体验,无论游戏运行在什么浏览器或设备上。然而,问题在于每个设备都有自己的分辨率、屏幕尺寸和比例。
Cocos2d-JS 允许我们以类似于网页设计师在构建响应式设计时的方式处理不同的分辨率。目前,我们只想使游戏画布适应浏览器窗口,并针对最流行的分辨率,即 320x480(纵向模式)。这就是这一行的作用:
cc.view.setDesignResolutionSize(320, 480, cc.ResolutionPolicy.SHOW_ALL);
使用这些设置,你应该相当确信你的游戏可以在每个设备上运行,尽管你将在低分辨率下工作。
还请看看这一行:
cc.director.runScene(new gameScene());
基本上,一个 Cocos2d-JS 游戏是由一个场景构成的,其中游戏本身运行。同一个游戏中可以有多个场景。想象一下,有一个标题屏幕的场景,一个游戏结束屏幕的场景,以及一个游戏本身的场景。目前,你只有一个名为
gameScene
的场景。记住这个名称,因为你稍后将会用到它。 -
接下来,你将要构建的下一个必需的蓝图文件是
project.json
,它有一些有趣的设置。让我们首先看看这个文件:{ "debugMode" : 0, "showFPS" : false, "frameRate" : 60, "id" : "gameCanvas", "renderMode" : 0, "engineDir":"cocos2d-html5/", "modules" : ["cocos2d"], "jsList" : [ "src/gamescript.js" ] }
这些行代表什么意思?让我们逐个看看:
-
debugMode
:这是确定调试警告级别的对象键。它的范围从 0 到 6。目前,由于项目非常简单,我们不会出错,所以请将其保留为 0。 -
showFPS
:这个对象可以是true或false;它会在屏幕上显示或隐藏 FPS 计。 -
frameRate
:这个对象设置游戏的帧率。将其设置为60
以获得流畅的游戏体验。 -
id
:这是运行游戏所需的 DOM 元素。你还记得你给你的 canvas 分配了gameCanvas
的 id 吗?这里就是。 -
engineDir
:这是 Cocos2d-JS 安装的文件夹。 -
modules
:这个对象负责加载模块。目前,我们只需要基本的 Cocos2d 库。 -
jsList
:这是一个包含游戏中使用的文件的数组。这意味着我们将创建我们的游戏在src/gamescript.js
中。
-
-
最后,我们到达了游戏脚本本身。这是包含实际游戏内容的脚本,
gamescript.js
,目前它只是一个对游戏场景的简单声明:var gameScene = cc.Scene.extend({ onEnter:function () { this._super(); console.log("my awesome game starts here"); } });
在这里,你想要保存一切,并在浏览器中从你的
localhost
(参考你的 WAMP 或 MAMP 文档)调用index.html
页面。如果你现在打开开发者控制台,你应该会看到:我令人惊叹的游戏从这里开始
恭喜!这意味着你已经成功创建了一个 Cocos2d-JS 模板文件来构建你未来的游戏。
让我们立即构建我们的第一个迷你游戏!
预加载和添加图片
在这个例子中,我使用了一个 64x64 的PNG
图像来表示一个目标,如图所示:
你显然可以使用你喜欢的任何图像。
当你加载一个网页时,在大多数情况下,页面会在所有图片加载之前加载并显示。这在网页上听起来可能没问题,因为读者不会介意在图片显示之前等待几秒钟,但在游戏中绝对不能这样。这意味着我们的图片需要预加载,而 Cocos2d-JS 可以轻松处理这一点。在游戏中预加载图片的步骤如下:
-
这是第一次你在
project.json
文件中添加这一行:{ "debugMode" : 0, "showFPS" : false, "frameRate" : 60, "id" : "gameCanvas", "renderMode" : 0, "engineDir":"cocos2d-html5/", "modules" : ["cocos2d"], "jsList" : [ "src/loadassets.js", "src/gamescript.js" ] }
这意味着你需要在刚刚创建
gamescript.js
的同一src
文件夹中创建另一个名为loadassets.js
的文件。这是
loadassets.js
的内容:var gameResources = [ "assets/target.png" ];
一个名为
gameResources
的数组存储要预加载的资产。因此,你应该创建一个名为assets
的文件夹,并将target.png
图像放入此文件夹中。注意
为了保持项目组织清晰,我打算将所有游戏资产放置在一个名为
assets
的文件夹中。 -
现在,Cocos2d-JS 已知哪些图像需要预加载,我们只需要告诉游戏在场景开始之前必须预加载它们,因此我们需要在
main.js
中添加几行代码:cc.game.onStart = function(){ cc.view.setDesignResolutionSize(320, 480, cc.ResolutionPolicy.SHOW_ALL); cc.LoaderScene.preload(gameResources, function () { cc.director.runScene(new gameScene()); }, this); }; cc.game.run();
cc.LoaderScene.preload
构造函数将预加载loadassets.js
中定义的gameResources
数组中指定的场景资源。所有拼图块都完美匹配。 -
最后,让我们通过重写
gamescript.js
文件来将目标添加到游戏中:var gameScene = cc.Scene.extend({ onEnter:function () { this._super(); var gameLayer = new game(); gameLayer.init(); this.addChild(gameLayer); } }); var game = cc.Layer.extend({ init:function () { this._super(); var target = cc.Sprite.create("assets/target.png"); this.addChild(target,0); } });
如果你使用 AS3(ActionScript 3)开发了 Flash 游戏,你会发现 Cocos2d-JS 的资产层次结构对显示对象很熟悉。如果你是新手,让我来解释一下发生了什么:
-
就像所有处理图形资源的框架一样,Cocos2d-JS 有层次规则。在这个层次结构的顶部,我们找到了
Scene
对象。每个场景包含一些游戏逻辑;想想主菜单场景、游戏场景和游戏结束场景。 -
每个场景包含一个或多个
Layer
对象;层定义了哪些内容应该位于其他内容之上。在现实世界的例子中,关卡背景位于最底层的层,玩家和敌人将在背景之上的层中创建,而游戏信息,如得分和剩余生命,将放置在最顶层的层上。 -
最后,所有层都可以有一个或多个
Sprite
对象,它们是图形资产本身,如玩家、敌人,或者在这种情况下,目标是。 -
总结来说,代码意味着一旦
gameScene
执行,就创建并添加game
层,并在该层中添加target
精灵。
是时候通过调用 index.html
文件来测试项目了,以下截图是你应该得到的:
尽管这只是一个基本项目,但有几个需要注意的事项:
-
图像已预加载,并显示默认的加载屏幕。这意味着预加载器正在工作。
-
尽管我们的项目设置为在 320x480 的分辨率下工作,但由于之前设置的分辨率策略,游戏会拉伸以填满整个浏览器。
-
图像在其中心的注册点,而大多数框架的图像注册点位于左上角。
-
场景的原始(0,0)位置位于左下角,而大多数框架的原始位置在左上角。
总之,你已经能够创建你的第一个项目。要更改目标位置并将其放置在屏幕中间,只需使用 setPosition
方法,这样修改 gamescript.js
:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
var gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
var target = cc.Sprite.create("assets/target.png");
this.addChild(target,0);
target.setPosition(160,240);
}
});
测试项目,你将在屏幕中间看到目标图像。
移除图片和更改背景颜色
现在你已经知道了如何添加图片,你可能还想知道如何移除它们。这非常直观:你使用 addChild
方法添加图片,所以你将使用 removeChild
方法移除它们。
此外,我们将通过添加一个实际的背景层来更改背景颜色,这个背景层将整个场景覆盖为一个纯色。
只需在 gamescript.js
中添加几行代码:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
var gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var backgroundLayer;
var game = cc.Layer.extend({
init:function () {
this._super();
backgroundLayer = cc.LayerColor.create(new cc.Color(40,40,40,255), 320, 480);
this.addChild(backgroundLayer);
var target = cc.Sprite.create("assets/target.png");
backgroundLayer.addChild(target,0);
target.setPosition(160,240);
setTimeout(function(){
backgroundLayer.removeChild(target);
}, 3000);
}
});
在前面的代码中,backgroundLayer
是一个新层,它将以 RGBA 格式(在这种情况下,全不透明的深灰色)填充新的颜色,并将包含目标图像。
自创建以来经过三秒钟,目标图像将通过 removeChild
方法从 backgroundLayer
中移除。
概述
在本章中,你学习了如何安装、配置和运行你的第一个 Cocos2d-JS 项目。你还学习了如何在屏幕上放置图片。
在下一章中,我们将讨论放置相同对象的更多实例,你还将创建你的第一个游戏,所以现在不要急于查看!
通过尝试在屏幕上随机位置放置 10 个目标来测试自己。
第二章. 添加交互性 – 制作注意力游戏
根据定义,游戏在某种程度上是交互式的。玩家必须通过做事情来成为游戏的一部分。最简单的交互形式是在游戏中点击或触摸瓷砖。
注意力游戏很容易解释,但它将涵盖一些新的重要概念,例如:
-
创建多个游戏资源实例
-
扩展类以增强其功能。实际上,JavaScript 中没有类,但它们通过变量和原型进行模拟
-
添加渐变
-
使资产对点击和触摸做出反应
-
动态更改精灵图像
-
添加文本标签
-
从游戏中移除精灵
到本章结束时,你将能够使用空间定制创建一个完整的注意力游戏。
由于上一章创建的项目不仅仅是 Hello World 游戏,而更像是你未来所有项目的蓝图,你将开始从之前完成的项目构建我们的注意力游戏。
创建多个游戏资源实例
制作注意力游戏的第一件事是绘制你将在游戏中使用的瓷砖。以下是用于覆盖瓷砖和可能匹配的八种不同瓷砖的图片,所有这些图片都保存在assets
文件夹中,如前一章所述:
每个瓷砖是一个 64 x 64 的PNG
文件,其中覆盖的瓷砖称为cover.png
,而要匹配的瓷砖则用从 0 到 7 的递增数字命名:tile_0、tile_1,直到tile_7。这是因为实际的棋盘瓷砖值将存储在一个数组中,其值范围从 0 到 7,将值 0 分配给tile_0、值 1 分配给tile_1,以此类推。
在资产文件夹中有这九个文件后,你就可以通过位于我们项目src
文件夹中的loadassets.js
文件来加载它们,感谢这些文件:
var gameResources = ["assets/cover.png",
"assets/tile_0.png",
"assets/tile_1.png",
"assets/tile_2.png",
"assets/tile_3.png",
"assets/tile_4.png",
"assets/tile_5.png",
"assets/tile_6.png",
"assets/tile_7.png"
];
图片的加载方式与上一章相同;然后放置所有 16 个覆盖瓷砖。
这是gamescript.js
的内容,基本上与上一章中使用的相同,只是精灵创建在一个将执行 16 次的for
循环中:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
for(i=0;i<16;i++){
var tile = cc.Sprite.create("assets/cover.png");
this.addChild(tile,0);
tile.setPosition(49+i%4*74,400-Math.floor(i/4)*74);
}
}
});
setPosition
方法中的奇怪数字将 64 x 64 瓷砖组放置在舞台上的 4 x 4 网格中。你可以使用一些数学来以你喜欢的任何方式更改瓷砖的位置。
测试游戏,你将在屏幕上看到以下内容:
这是一个覆盖的瓷砖网格,但背景很糟糕。是时候再好好工作一下了。
添加渐变背景
改善背景的一个快速简单的方法是添加渐变。你最喜欢的游戏背景中看到的大部分天空和风景都是渐变。
你将通过在 gamescript.js
中添加这两行代码,方便地将名为 gradient
的渐变层添加到游戏中:
var gameScene = cc.Scene.extend({
// same as before
});
var game = cc.Layer.extend({
init:function () {
this._super();
var gradient = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(gradient);
for(i=0;i<16;i++){
var tile = cc.Sprite.create("assets/cover.png");
this.addChild(tile,0);
tile.setPosition(49+i%4*74,400-Math.floor(i/4)*74);
}
}
});
渐变层的创建是通过 cc.LayerGradient.create
方法实现的,它需要以 RGBA(红,绿,蓝,透明度)格式提供起始和结束渐变色。
关于添加的行,你需要注意两点:
-
渐变层应该在瓦片之前添加,这样瓦片就会放在背景前面,因为你可以通过调整层的深度来动态调整 Z 调序中的层,但这并不是这种情况。
-
渐变色可以用十进制(从 0 到 255)和十六进制(从 0x00 到 0xFF)的值指定。
现在,再次测试游戏,你应该会看到一个漂亮的黑到蓝的背景。
此时,你已经在美丽的渐变背景上放置了 16 个瓦片。现在,是时候让玩家有能力拾取其中的一些了。
不幸的是,精灵只是图像,不能被拾取。
超越 Sprite 类的能力
当我说 有时,我的意思是 大多数时候,默认的 Cocos2d-JS 类不允许你用它们做你需要做的所有事情。
虽然这看起来像是 Cocos2d-JS 的限制,但这却是它的最佳特性之一。你被提供了一组基本的类,你可以按需扩展,这意味着你可以添加新的功能。
那么,扩展一个类到底意味着什么呢?想象一个现实世界的例子:你走进一家自行车店,买了一辆山地自行车。你的山地自行车是一个类;使用这个类,你可以做所有你实际上可以用山地自行车做的事情,即踩踏和转向。
不幸的是,你有点懒惰,不想一直踩踏,所以你买了一个小马达并将其添加到你的山地自行车上。现在,你仍然可以像通常那样使用你的自行车,但你也可以休息你的腿,打开马达,让它代表你踩踏。
你只是扩展了山地自行车,创建了一辆电动山地自行车,这基本上仍然是一辆自行车,并继承了所有其功能以及一些新功能。
为了扩展 Sprite
类并使其能够完成制作你的注意力游戏所需的所有操作,你必须在 gamescript.js
中添加一些行:
var gameScene = cc.Scene.extend({
onEnter:function () {
// same as before
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
var gradient = cc.LayerGradient.create(cc.c4b(0,0,0,255), cc.c4b(0x46,0x82,0xB4,255));
this.addChild(gradient);
for(i=0;i<16;i++){
var tile = new MemoryTile();
this.addChild(tile,0);
tile.setPosition(49+i%4*74,400-Math.floor(i/4)*74);
}
}
});
var MemoryTile = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/cover.png");
}
});
如果你此时测试游戏,你会看到与 4x4 网格覆盖瓦片相同的背景渐变。让我们看看有什么变化。
首先,瓦片的创建不再使用以下代码:
var tile = cc.Sprite.create("assets/cover.png");
相反,它已经被以下内容替换:
var tile = new MemoryTile();
现在,你并不是在创建精灵本身,而是在创建一个名为 MemoryTile
的新类型,它将扩展 Sprite
类。
这就是声明你正在扩展一个类的样子:
var MemoryTile = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/cover.png");
}
})
在这里,MemoryTile
变量被声明为 Sprite
类的扩展。
ctor
方法是一个构造函数,基本上是变量创建时立即执行的内容。在这种情况下,initWithFile
使用你之前使用旧方法放置瓦片时分配的相同封面图像。
你可能会争论说,使用四行代码来做本可以用一行代码完成的事情,这是真的,但这是你为了向 Cocos2d-JS 内置类添加新功能所付出的微小代价。
现在,你有一个新的类,它扩展了 Sprite。让我们给它添加交互性。
使资产对点击和触摸做出反应
有两种选择瓦片的方法,无论你是使用触摸或鼠标驱动的设备。你可以轻触瓦片,或者点击它。
选择一个瓦片作为初始尝试
无论你如何使用 Cocos2d-JS,总的来说,你都在创建跨平台游戏。你必须告诉 Cocos2d-JS 你将允许用户触摸或点击一些瓦片,因此MemoryTile
类将这样改变:
var MemoryTile = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/cover.png");
cc.eventManager.addListener(listener.clone(), this);
}
})
刚才发生了什么?你刚刚向事件管理器添加了一个事件监听器。事件管理器是触发游戏或玩家引发的事件的实体。addListener
方法向事件管理器添加了一个监听器,但目前你还没有监听器。让我们创建一个:
var listener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
var target = event.getCurrentTarget();
var location = target.convertToNodeSpace(touch.getLocation());
var targetSize = target.getContentSize();
var targetRectangle = cc.rect(0, 0, targetSize.width, targetSize.height);
if (cc.rectContainsPoint(targetRectangle, location)) {
console.log("I picked a tile!!");
}
}
})
这是一个基本的监听器。你会在大多数项目中使用它,所以让我们更仔细地看看它:
var listener = cc.EventListener.create({
这就是如何使用cc.EventListener.create
方法创建一个监听器。你将其命名为listener
以匹配之前的调用:
cc.eventManager.addListener(listener.clone(), this);
然后,你修改了MemoryTile
类:
event: cc.EventListener.TOUCH_ONE_BY_ONE,
在这里,你指定事件类型:cc.EventListener.TOUCH_ONE_BY_ONE
告诉游戏你正在等待触摸,但一次只等待一个。请注意,游戏谈论的是触摸,但游戏也可以与鼠标一起工作。这就是 Cocos2d-JS 在处理跨平台开发时的真正力量。
swallowTouches: true,
这将基本上忽略所有触摸,当有一个活动触摸时:
onTouchBegan: function (touch, event) {
现在,随着你准备在触摸或鼠标点击开始时触发,事情开始变得严肃:
var target = event.getCurrentTarget();
getCurrentTarget
方法返回当前的点击目标:
var location = target.convertToNodeSpace(touch.getLocation());
通过调用touch.getLocation
方法,你将获得游戏内触摸或点击的坐标,而convertToNodeSpace
方法将此类坐标转换为相对于瓦片的坐标。这样,location
变量将包含相对于瓦片的触摸或点击坐标:
var targetSize = target.getContentSize();
getContentSize
函数仅返回目标的宽度和高度,在这种情况下是瓦片:
var targetRectangle = cc.rect(0, 0, targetSize.width, targetSize.height);
现在,你使用cc.rect
方法定义一个与瓦片大小相同的矩形。这将使我们知道点击或触摸动作是否在这个矩形内部。某个瓦片已被点击:
if (cc.rectContainsPoint(targetRectangle, location)) {
此外,这也是你确定一个点是否在矩形内部的方法,因此你可以说你点击了瓦片。
所以,基本上:
-
每个瓦片都会检测到
touch
或click
动作,这个动作可以在瓦片内部或外部。 -
你得到相对于瓷砖的触摸/点击坐标。
-
你可以看到这些坐标是否在瓷砖内。
-
你可以说哪个瓷砖被点击了,如果有的话。
你准备好点击瓷砖了吗?运行游戏并点击一个瓷砖,你就会看到。
我选中了一个瓷砖!!
是的,它工作了!让我再展示这一行:
cc.eventManager.addListener(listener.clone(), this);
你在第一次编写它时注意到 clone()
方法了吗?你使用 clone
方法是因为事件监听器只能添加一次。addListener
方法在事件监听器上设置一个注册标志,如果标志已经设置,则不会再次添加事件监听器。换句话说,你只能在分配给第一个瓷砖的事件监听器上检查点击或触摸。
使用 clone
,你基本上是复制了监听器,所以每个瓷砖都将有自己的监听器运行起来。
在线更改精灵图片
现在我们来了解一下如何更改精灵图片。
显示瓷砖图片
一旦选中一个瓷砖,它必须显示其图片。图片只是瓷砖值的图形表示,你最初将其存储在一个名为 gameArray
的数组中,该数组在 gamescript.js
文件的开头声明:
var gameArray = [0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7];
vargameScene = cc.Scene.extend({
onEnter:function () {
// same as before
}
});
然后,一旦你创建了一个新的瓷砖,你可以通过将 gameArray
的 i-th 元素的值分配给一个自定义属性 pictureValue
来为它分配一个:
var game = cc.Layer.extend({
init:function () {
this._super();
var gradient = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(gradient);
for(i=0;i<16;i++){
var tile = new MemoryTile();
tile.pictureValue = gameArray[i];
this.addChild(tile,0);
tile.setPosition(49+i%4*74,400-Math.floor(i/4)*74);
}
}
});
此外,一旦选中瓷砖,你还可以再次使用 initWithFile()
方法根据其值分配另一个图片:
var listener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
var target = event.getCurrentTarget();
var location = target.convertToNodeSpace(touch.getLocation());
var targetSize = target.getContentSize();
var targetRectangle = cc.rect(0, 0, targetSize.width, targetSize.height);
if (cc.rectContainsPoint(targetRectangle, location)) {
target.initWithFile("assets/tile_"+target.pictureValue+".png");
}
}
}
现在,应该很清楚为什么瓷砖图片文件是从 0 到 7 编号的。这是因为它们将与 gameArray
元素分配的瓷砖值相匹配。
运行游戏并开始选择瓷砖;看看它们是如何揭示它们的实际图片的:
现在,添加一些游戏逻辑,这将允许你只选择两个瓷砖,如果它们匹配,则从游戏中移除它们,或者再次覆盖它们。
你需要另一个名为 pickedTiles
的数组:
Var gameArray = [0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7];
var pickedTiles = [];
然后,你需要在我们的 listener
变量中添加几行:
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
if(pickedTiles.length<2){
var target = event.getCurrentTarget();
var location = target.convertToNodeSpace(touch.getLocation());
var targetSize = target.getContentSize();
var targetRectangle = cc.rect(0, 0, targetSize.width, targetSize.height);
if (cc.rectContainsPoint(targetRectangle, location)) {
if(pickedTiles.indexOf(target)==-1){
target.initWithFile("assets/tile_"+target.pictureValue+".png";
pickedTiles.push(target);
if(pickedTiles.length==2){
checkTiles();
}
}
}
}
}
})
一旦 pickedTiles
数组包含两个瓷砖,这阻止了玩家再次选择相同的瓷砖,然后调用 checkTiles
函数。
小贴士
在这个步骤中,我不会过多解释代码,因为没有与 Cocos2d-JS 相关的内容;这只是老式的 JavaScript 逻辑。
function checkTiles(){
var listener = cc.EventListener.create({
function checkTiles(){
var pause = setTimeout(function(){
if(pickedTiles[0].pictureValue!=pickedTiles[1].pictureValue){
pickedTiles[0].initWithFile("assets/cover.png");
pickedTiles[1].initWithFile("assets/cover.png");
}
else{
gameLayer.removeChild(pickedTiles[0]);
gameLayer.removeChild(pickedTiles[1]);
}
pickedTiles = [];
},2000);
}
基本上,checkTiles
等待两秒钟,给玩家一些时间来记住选中的瓷砖,然后如果它们不匹配,就简单地通过将它们的图片再次更改为覆盖瓷砖的图片,或者使用 removeChild
方法从游戏中移除它们。
在这两种情况下,玩家将被允许通过清空 pickedTiles
数组来选择新的瓷砖。
测试游戏并创建一些匹配,看看瓷砖是如何从游戏中移除的。
恭喜!你创建了你的第一个 Cocos2d-JS 工作游戏。现在,让我们添加一些收尾工作。
洗牌瓷砖并添加分数
你应该已经注意到游戏并不那么难,因为你只是匹配相邻的瓷砖。第一块瓷砖匹配第二块瓷砖,第三块瓷砖匹配第四块,以此类推。
首先,你需要洗牌,然后你将给游戏添加分数。玩家喜欢竞争高分。
你首先添加两个新变量 scoreText
和 moves
,它们将处理显示分数的文本并计算玩家所做的移动(选择)次数:
Var gameArray = [0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7];
var pickedTiles = [];
var scoreText;
var moves=0;
然后,你需要对 gameArray
进行洗牌。使用真正的随机化来洗牌超出了这本书的范围,所以对于这个游戏,你将使用可以在jsfromhell.com/array/shuffle
找到的基本洗牌函数:
var shuffle = function(v){
for(var j, x, i = v.length; i; j = parseInt(Math.random() * i),x = v[--i], v[i] = v[j], v[j] = x);
return v;
};
然后,在游戏开始时对 gameArray
进行洗牌:
vargameScene = cc.Scene.extend({
onEnter:function () {
gameArray = shuffle(gameArray);
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
要将分数文本添加到游戏中,你需要一个标签。以下是如何创建一个名为 scoreText
的文本标签,它包含文本 移动:0
,并使用 32 像素的 Arial 字体:
var game = cc.Layer.extend({
init:function () {
this._super();
var gradient = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(gradient);
scoreText = cc.LabelTTF.create("Moves: 0","Arial","32",cc.TEXT_ALIGNMENT_CENTER);
this.addChild(scoreText);
scoreText.setPosition(90,50);
for(i=0;i<16;i++){
var tile = new MemoryTile();
tile.pictureValue = gameArray[i];
this.addChild(tile,0);
tile.setPosition(49+i%4*74,400-Math.floor(i/4)*74);
}
}
});
最后,一旦你检查到瓷砖匹配,很容易增加步数,并使用 setString
方法更新 scoreText
文本标签:
function checkTiles(){
moves++;
scoreText.setString("Moves: "+moves);
var pause = setTimeout(function(){
if(pickedTiles[0].pictureValue!=pickedTiles[1].pictureValue){
pickedTiles[0].initWithFile("assets/cover.png");
pickedTiles[1].initWithFile("assets/cover.png");
}
else{
gameLayer.removeChild(pickedTiles[0]);
gameLayer.removeChild(pickedTiles[1]);
}
pickedTiles = [];
},2000);
}
测试脚本,你将能够玩一个带有随机生成的棋盘和分数文本的完整游戏。
现在,你真的拥有了一个完整且精致的游戏!
摘要
通过扩展 Sprite 类并添加一些交互性,你创建了自己的专注力游戏。现在,你也知道了如何动态更改 Sprite 图像和处理文本标签。
专注力是一种很好的脑力游戏。然而,有时你想要更多的动作。为了使其更难,你可以尝试制作自己的 6 x 6 游戏,而不是这个简单的 4 x 4 游戏。翻到下一章,让我们让事情动起来吧!
第三章. 在屏幕上移动精灵 - 无尽跑酷
很久以前,无尽跑酷从一个名为直升机的游戏开始,你必须驾驶直升机穿过无尽的洞穴,只需按一个按钮给它推力。
然后,游戏变得更加复杂,直到移动游戏开始传播。由于简单的单次点击控制,无尽跑酷开始获得新的流行。玩家只需在需要时触摸任何地方来控制直升机。
在本章中,你将构建一个无尽跑酷游戏,其中一艘太空船在城市中飞行,同时避开危险的陨石。
在制作这个游戏的过程中,你将学习如何做其他事情,例如:
-
滚动大图像以在无尽背景上给出一个概念
-
安排事件
-
控制帧率
-
检查精灵之间的碰撞
-
创建一个简单的粒子系统
-
手动移动精灵或使用动作
此外,尽管这是一个跨平台游戏,你还将了解仅使用鼠标控制。当你已经遇到它们在制作注意力游戏时,将它们更改为触摸或点击控制将很容易。总的来说,如果你计划一个仅适用于桌面的浏览器游戏,这将是有用的东西。
加载和放置图形资源
你需要在第一章中创建的蓝图上构建游戏,所以这里是assets
文件夹的内容:
你可以看到太空船、你必须避免的陨石、一个小圆圈用于创建粒子效果,以及滚动背景。
你将花一些时间尝试理解背景。由于游戏是 480 x 320 像素,你的背景至少应该是4802=960*像素宽,这是由两个 480 x 320 的无缝图像组成的。
通过以下图像,你将能够给玩家提供飞越无尽城市景观的可能性:
所有这些图像都需要通过位于src
的loadassets.js
文件预加载,它将变成:
var gameResources = ["assets/background.png","assets/ship.png","assets/particle.png","assets/asteroid.png"];
你还需要对main.js
做一些修改,因为这次你想要一个横幅模式的游戏:
cc.game.onStart = function(){
cc.view.setDesignResolutionSize(480, 320, cc.ResolutionPolicy.SHOW_ALL);
cc.LoaderScene.preload(gameResources, function () {
cc.director.runScene(new gameScene());
}, this);
};
cc.game.run();
突出的行显示了新的分辨率设置。
添加无限滚动背景
现在,是时候添加城市景观背景了,它将无限且无缝地滚动。最后,你可以开始编辑gamescript.js
:
var background;
var gameLayer;
var scrollSpeed = 1;
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
},
update:function(dt){
background.scroll();
}
});
var ScrollingBG = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/background.png");
},
onEnter:function() {
this.setPosition(480,160);
},
scroll:function(){
this.setPosition(this.getPosition().x-scrollSpeed,this.getPosition().y);
if(this.getPosition().x<0){
this.setPosition(this.getPosition().x+480,this.getPosition().y);
}
}
});
你可能会认为这有很多代码,但其中大部分只是复制和粘贴你在制作注意力游戏时已经看到的内容。
让我们看看有趣的新内容:
var background;
var gameLayer;
var scrollSpeed = 1;
这三个变量将代表背景精灵、主游戏层和每帧像素的滚动速度。这意味着你希望背景在每一帧滚动一个像素,即每秒 60 像素。
现在,应该非常清楚为什么你正在使用固定帧率。在像 Chrome 这样的快速浏览器上,其刷新率为 120 fps,你将能够看到背景以与 Firefox 60 fps 浏览器相同的速度滚动。
gameScene
代码中没有包含任何新内容,所以让我们跳转到游戏定义,这将引入一个新特性:
var game = cc.Layer.extend({
init:function () {
this._super();
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
},
update:function(dt){
background.scroll();
}
});
在声明了一个ScrollingBG
类之后,你将像在制作集中注意力游戏时创建瓷砖那样扩展内置的Sprite
类;你可以看到一个对scheduleUpdate
方法的调用。
通常,Cocos2d-JS 游戏是静态的。它永远不会更新。就像之前的集中注意力游戏一样,如果你长时间不玩游戏,什么都不会发生。
为了帮助你添加一些动作,Cocos2d-JS 允许你安排在特定时间发生的事件。
安排事件的简单方法就是scheduleUpdate
方法。这就像说你想在游戏刷新时做某事,在我们的例子中是每 1/60 秒。
当你调用scheduleUpdate
时,一个自定义的update
函数将在每一帧被调用:
update:function(dt){
background.scroll();
}
目前,你只是在调用你的ScrollingBG
新类的自定义方法。
如果你掌握了上一章中解释的类继承基础知识,ScrollingBG
的定义也非常简单:
var ScrollingBG = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/background.png");
},
onEnter:function() {
this.setPosition(480,160);
},
scroll:function(){
this.setPosition(this.getPosition().x-scrollSpeed,this.getPosition().y);
if(this.getPosition().x<0){
this.setPosition(this.getPosition().x+480,this.getPosition().y);
}
}
});
在这里,一旦你加载并将背景图像添加到屏幕上,你通过scrollSpeed
像素向左移动它,给人一种它在向右移动的感觉。一旦背景图像移动超过 480 像素,即其长度的一半或你的游戏分辨率的完整长度,你将精确地将其移动回 480 像素,给玩家一种无限滚动的错觉。
我可以放一张你现在得到的图片,但最好是你亲自测试并看到美丽的滚动背景。
注意,这只是移动屏幕上资产的一种方法,因为 Cocos2d-JS 提供了一套管理精灵位置的方法。我将在添加屏幕上的小行星时介绍它们,但此时,让我们专注于游戏的主要角色:飞船!
添加飞船
你将要添加的飞船只是一个另一个精灵,但你将给它像受重力规则支配的行为。
首先,让我们添加几个变量:
var background;
var gameLayer;
var scrollSpeed = 1;
var ship;
var gameGravity = -0.05;
ship
变量将是飞船本身,而gameGravity
是吸引飞船向屏幕底部运动的力。
然后,在game
声明中的init
函数内,你以添加背景的方式添加飞船:
var game = cc.Layer.extend({
init:function () {
this._super();
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
ship = new Ship();
this.addChild(ship);
},
update:function(dt){
background.scroll();
ship.updateY();
}
});
然后,在update
函数中(记住这个函数是自动在每一帧被调用的)。多亏了scheduleUpdate
方法,一个名为updateY
的自定义方法被调用。
飞船本身的创建与仅仅扩展Sprite
类没有太大区别:
var Ship = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/ship.png");
this.ySpeed = 0;
},
onEnter:function() {
this.setPosition(60,160);
},
updateY:function() {
this.setPosition(this.getPosition().x,this.getPosition().y+this.ySpeed);
this.ySpeed += gameGravity;
}
});
飞船被分配了一个图像和一个名为ySpeed
的自定义属性,初始值由ctor
构造函数设置为零。
一旦它被放置在舞台上,onEnter
函数将其放置在 60
,160
(记住它的 x 位置永远不会改变)。然后,setPosition
函数,它由游戏的 update
函数在每一帧调用,将 gameGravity
值添加到飞船的垂直速度 (ySpeed
),并通过向当前位置添加速度来更新其 y 位置。
这是处理重力、力和推力(将在下一步介绍)最便宜但最快的方法,并且在简单的游戏如无尽跑酷中效果良好。
现在,运行游戏并看看会发生什么:
你应该看到我们之前创建的漂亮的滚动背景和可怜的飞船正在向下坠落并消失在屏幕底部。
你刚刚学会了宇宙飞船创建的第一条规则:记住引擎。
控制宇宙飞船
玩家可以通过在屏幕上按住鼠标或手指来给宇宙飞船提供推力。
正如你应该能够检测到玩家触摸屏幕时的情况,我将向你展示一种仅使用鼠标控制宇宙飞船的方法,让你学习一些新知识。你可以自由地用你喜欢的控制方式来替换这种控制飞船的方式。
你将只用几行代码来管理飞船控制,首先通过添加一个新的全局变量:
var background;
var gameLayer;
var scrollSpeed = 1;
var ship;
var gameGravity = -0.05;
var gameThrust = 0.1;
gameThrust
变量代表引擎功率,这是使飞船在空中飞行的力量。
你正在用鼠标控制游戏,所以这就是你更改 game
声明的方式:
var game = cc.Layer.extend({
init:function () {
this._super();
cc.eventManager.addListener({
event: cc.EventListener.MOUSE,
onMouseDown: function(event){
ship.engineOn = true;
},
onMouseUp: function(event){
ship.engineOn = false;
}
},this)
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
ship = new Ship();
this.addChild(ship);
},
update:function(dt){
background.scroll();
ship.updateY();
}
});
与上一章不同,这里你是在运行时添加了监听器,而没有将其声明为变量然后调用。基本上和以前一样,只是现在你正在使用鼠标,所以你必须将事件类型定义为 cc.EventListener.MOUSE
。事件是 onMouseDown
当玩家按下鼠标时,以及 onMouseUp
当鼠标释放时。现在,使用 onMouseDown
和 onMouseUp
,你只需将飞船的引擎打开或关闭,这实际上是一个名为 engineOn
的布尔 ship
属性。
你打算如何使用这样的属性?你只需像处理重力一样更新飞船的垂直速度:
var Ship = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/ship.png");
this.ySpeed = 0;
this.engineOn = false;
},
onEnter:function() {
this.setPosition(60,160);
},
updateY:function() {
if(this.engineOn){
this.ySpeed += gameThrust;
}
this.setPosition(this.getPosition().x,this.getPosition().y+this.ySpeed);
this.ySpeed += gameGravity;
}
})
这真的很简单。首先,将 engineOn
设置为 false
,然后根据其值,你决定是否需要将 gameThrust
添加到 ySpeed
。
测试游戏并尝试通过按住和释放鼠标按钮来控制飞船。
最后,宇宙飞船可以和平和谐地飞过城市。不幸的是,游戏设计师有点疯狂,他们可能会决定通过添加一个足够大的致命小行星带来改变宇宙飞船的计划,足以摧毁一艘宇宙飞船。
添加小行星
当宇宙飞船从左到右飞行时(实际上它并没有,但看起来是这样),你必须添加小行星,它们从游戏的右侧进入屏幕。
现在,你只需在屏幕的右侧放置一些小行星精灵,并让它们向左侧移动,就像你之前处理背景城市景观那样,但如果你那样做,你不会学到任何新东西,所以让我们看看另一种管理精灵移动的方法。
首先,在你能够移动小行星之前,你必须先创建它。
在这个游戏中,每半秒就会出现一个新的小行星,所以是时候在game
类声明中安排另一个事件了:
var game = cc.Layer.extend({
init:function () {
this._super();
cc.eventManager.addListener({
event: cc.EventListener.MOUSE,
onMouseDown: function(event){
ship.engineOn = true;
},
onMouseUp: function(event){
ship.engineOn = false;
}
},this)
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
this.schedule(this.addAsteroid,0.5);
ship = new Ship();
this.addChild(ship);
},
update:function(dt){
background.scroll();
ship.updateY();
},
addAsteroid:function(event){
var asteroid = new Asteroid();
this.addChild(asteroid,1);
},
removeAsteroid:function(asteroid){
this.removeChild(asteroid);
}
});
要在给定的时间间隔安排一个事件,你使用schedule
方法,它的工作方式类似于scheduleUpdate
,但这次你还可以定义要调用的函数,在这个例子中是addAsteroid
,以及时间间隔(以秒为单位)。
很容易看出addAsteroid
的作用:它通过扩展Sprite
类来添加小行星,就像你之前看到的那样。你还添加了一个removeAsteroid
函数,因为你不希望小行星永远留在游戏中;一旦它们不再必要,你将看到如何移除它们。
这是Asteroid
类:
var Asteroid = cc.Sprite.extend({
ctor:function() {
this._super();
this.initWithFile("assets/asteroid.png");
},
onEnter:function() {
this._super();
this.setPosition(600,Math.random()*320);
var moveAction= cc.MoveTo.create(2.5, new cc.Point(-100,Math.random()*320));
this.runAction(moveAction);
this.scheduleUpdate();
},
update:function(dt){
if(this.getPosition().x<-50){
gameLayer.removeAsteroid(this)
}
}
});
运行游戏,你会看到一个小行星带沿着随机路径飞向飞船。
简单的类声明中有很多内容,所以让我们仔细看看前面的代码。
ctor
构造函数简单地创建实例并为其分配一个图像,就像往常一样。所以,魔法发生在其他地方。
onEnter
事件将小行星放置在屏幕的右侧,高度随机;下一行负责整个动画:
var moveAction = cc.MoveTo.create(2.5,new cc.Point(-100,Math.random()*320));
Cocos2d-JS 允许你创建动作,在这个例子中,基本上是缓动,可以看作是在特定时间点要做的事情。
这个动作应该在 2.5 秒内移动到屏幕左侧的随机点。就是这样。Cocos2d-JS 将如何执行这项任务并不重要;你只需说“把这个小行星带到那里,让它飞到那个点”。
这些动作的力量是惊人的,你将在本书的更多示例中处理它们。
一旦创建了动作,你就可以使用以下方式让 Cocos2d-JS 执行它:
this.runAction(moveAction);
小行星的旅行已经完成。你也应该看到是否有scheduleUpdate
调用,因为你想要在小行星从屏幕左侧退出时移除它们;所以,在update
函数中(记住,每个scheduleUpdate
方法在每个帧上都会调用一个update
函数)。你只需检查小行星是否在屏幕外,然后使用你之前创建的removeAsteroid
方法将其移除。
这真的很简单,不是吗?
很遗憾!小行星和飞船还没有发生碰撞,但别担心,这不会超过几行代码。
小行星与飞船碰撞
检查两个精灵是否碰撞的最简单方法,也是你在当前构建的简单快节奏街机游戏中最常用的方法,就是检查精灵的边界框是否以某种方式相交。
图像的边界框是包含图像本身的矩形,这个方法的原则可以通过以下图像解释:
在这个 4 倍缩放的图像中,您可以看到边界框碰撞的三个不同反应方式:
-
边界框不相交。没有发生碰撞。
-
边界框相交。发生了碰撞。
-
边界框相交,尽管没有发生碰撞。
在复杂的碰撞引擎中,为了防止情况 3,一旦边界框相交,就会执行像素完美的碰撞,但这很消耗 CPU 资源,目前您不想达到如此高的精度。
因此,如果您想尽可能少地让情况 3 发生,您可以将精灵绘制得尽可能接近矩形,或者您可以考虑两个比原始边界框略小的框之间的交集。
记住,玩家希望得到宽恕,所以如果只是几像素的问题,最好不要看到实际的碰撞,而是一个假阳性碰撞。
话虽如此,以下是如何通过 Cocos2d-JS 帮助您管理边界框碰撞,在 asteroid 的 update
函数中工作:
update:function(dt){
var shipBoundingBox = ship.getBoundingBox();
var asteroidBoundingBox = this.getBoundingBox();
if(cc.rectIntersectsRect(shipBoundingBox,asteroidBoundingBox)){
gameLayer.removeAsteroid(this);
restartGame();
}
if(this.getPosition().x<-50){
gameLayer.removeAsteroid(this)
}
}
getBoundingBox
方法返回一个矩形,这是实际的精灵边界框,而rectIntersectsRect
方法检查两个矩形是否相交。
这很简单。碰撞检测发生在两行代码中。然后,移除小行星并调用restartGame
函数。这个函数只是重置飞船的变量,如下面的代码所示:
function restartGame(){
ship.ySpeed = 0;
ship.setPosition(ship.getPosition().x,160);
}
现在,测试游戏,您将看到飞船与小行星碰撞后位置被重置。这样,当您死亡时,游戏会变得有些惩罚性,因为您通常会复活在小行星前面,导致立即死亡。记住,玩家希望得到宽恕。
不可摧毁性
这个特性与新的 Cocos2d-JS 无关,这只是游戏设计中的打磨,但请记住,打磨总是比添加特性更好。
人们玩愤怒的小鸟系列游戏玩到死,因为它很精致,并不是因为木块确实像现实世界中的木块一样破碎。
因此,当玩家将飞船撞向小行星后,让我们让飞船在有限的时间内不可摧毁;通过使其闪烁来让玩家看到飞船无法被摧毁。
您将在飞船的ctor
构造函数中添加一个invulnerability
属性:
ctor:function() {
this._super();
this.initWithFile("assets/ship.png");
this.ySpeed = 0;
this.engineOn = false;
this.invulnerability = 0;
}
当invulnerability
设置为零时,这意味着飞船不具有不可摧毁性,可以被小行星摧毁。您必须在小行星的update
方法中添加这个情况:
update:function(dt){
var shipBoundingBox = ship.getBoundingBox();
var asteroidBoundingBox = this.getBoundingBox();
if(cc.rectIntersectsRect(shipBoundingBox,asteroidBoundingBox) &&ship.invulnerability==0){
gameLayer.removeAsteroid(this);
restartGame();
}
if(this.getPosition().x<-50){
gameLayer.removeAsteroid(this)
}
}
如您所见,只有当不可摧毁性设置为零时,才会处理碰撞,当您在restartGame
函数中重新启动游戏时,您将其赋予一个高值,比如说 100:
function restartGame(){
ship.ySpeed = 0;
ship.setPosition(ship.getPosition().x,160);
ship.invulnerability=100;
}
这意味着现在飞船不能被摧毁。为了给玩家提供视觉反馈并减少无敌值,让我们在飞船的 updateY
函数中添加两行代码:
updateY:function() {
if(this.engineOn){
this.ySpeed += gameThrust;
}
if(this.invulnerability>0){
this.invulnerability --;
this.setOpacity(255-this.getOpacity());
}
this.setPosition(this.getPosition().x,this.getPosition().y+this.ySpeed);
this.ySpeed += gameGravity;
}
如果无敌值大于零,就减少它,并通过将飞船的不透明度从完全不透明(255)切换到完全透明(0)来使飞船闪烁。
setOpacity
和 getOpacity
方法处理精灵的不透明度。
测试游戏,在你撞上小行星后,你应该大约有半秒钟的 上帝模式。
防止飞船飞出屏幕
最后你需要做的是防止飞船飞出屏幕。如果你按住鼠标时间过长,或者根本不按鼠标,你的飞船将分别飞出屏幕的顶部或底部。
你需要通过惩罚来防止飞船飞出屏幕,即让飞船无法被摧毁。
只需将这些两行代码添加到飞船的 updateY
函数中:
updateY:function() {
if(this.engineOn){
this.ySpeed += gameThrust;
}
if(this.invulnerability>0){
this.invulnerability --;
this.setOpacity(255-this.getOpacity());
}
this.setPosition(this.getPosition().x,this.getPosition().y+this.ySpeed);
this.ySpeed += gameGravity;
if(this.getPosition().y<0 || this.getPosition().y>320){
restartGame();
}
}
无需注释它们,这只是一个检查飞船垂直位置的 if
语句。
添加粒子
你还记得在你的 assets
文件夹中有一个叫做 particle.png
的黄色圆圈吗?你将使用它来创建一个漂亮的粒子效果来模拟飞船引擎。
讨论粒子系统超出了本书的范围,因此有关更详细的信息以及与 Cocos2d-JS 兼容的完整粒子生成软件,请查看 71squared.com/particledesigner
。
在这里,你只是将要添加最简单的粒子效果,但你将看到它具有以下图示的视觉吸引力:
首先,创建一个新的全局变量:
var background;
var gameLayer;var scrollSpeed = 1;
var ship;
var gameGravity = -0.05;
var gameThrust = 0.1;
var emitter;
发射器将在游戏的 init
函数中创建和配置:
init:function () {
this._super();
this.setMouseEnabled(true);
background = new ScrollingBG();
this.addChild(background);
this.scheduleUpdate();
this.schedule(this.addAsteroid,0.5)
ship = new Ship();
this.addChild(ship);
emitter = cc.ParticleSun.create();
this.addChild(emitter,1);
var myTexture = cc.textureCache. addImage("assets/particle.png");
emitter.setTexture(myTexture);
emitter.setStartSize(2);
emitter.setEndSize(4);
}
在这里,你可以看到发射器是通过一个 太阳 效果创建的,一个图像被分配给它,并且给出了起始和结束图像的大小。
这足以生成一个始终工作的粒子发射器,但你还需要在引擎工作时通过更新飞船的 updateY
函数来跟踪飞船:
updateY:function() {
if(this.engineOn){
this.ySpeed += gameThrust;
emitter.setPosition(this.getPosition().x-25,this.getPosition().y);
}
else{
emitter.setPosition(this.getPosition().x-250,this.getPosition().y);
}
if(this.invulnerability>0){
this.invulnerability --;
this.setOpacity(255-this.getOpacity());
}
this.setPosition(this.getPosition().x,this.getPosition().y+this.ySpeed);
this.ySpeed += gameGravity;
if(this.getPosition().y<0 || this.getPosition().y>320){
restartGame();
}
}
在这里,当引擎开启时,你只需将发射器移动到飞船的尾部,当引擎关闭时,将其移出屏幕。虽然简单粗暴,但确实有效。
测试游戏;当引擎开启时,你应该会看到之前看到的图像中的炫酷效果。
概述
在这一章中,你学习了如何创建补间动画、碰撞等。你还学习了如何通过粒子生成创建一个完整的游戏。
为了更熟悉 Cocos2d-JS,你应该做的是,运用你到目前为止所学的内容来改进游戏。你可以尝试将控制模式从鼠标驱动切换到触摸驱动,显示没有撞击小行星时行驶的最大距离,并且每 n 秒通过使小行星更快或更频繁地出现来提高难度级别。
现在,你已经准备好进入下一章了,在这一章中,由于触控检测,触控操作将变得更加互动。
第四章. 通过 Sokoban 的制作了解滑动操作
你知道 Sokoban 游戏吗?这是一个有趣的益智游戏,玩家需要推动箱子到指定的位置。通常,在电脑上,这类游戏——称为基于图块的游戏——是通过箭头键控制的,但鉴于我们的游戏需要跨平台,我们将让玩家通过滑动来控制游戏中的移动。
我们将要构建的游戏与我制作的 iOS 游戏 BWBan 非常相似。它是免费的;你可以在bit.ly/1fUXP8c
上玩。
在制作这个游戏的过程中,我们将称之为 Cocosban,你将学习以下主题:
-
如何检测滑动操作
-
如何通过精灵表加载图形资源
-
通过抗锯齿玩转 8 位像素游戏的创建方法
有很多事情要做,所以让我们从第一章中制作的好老蓝图开始,并在此基础上进行工作。
加载图形资源
不必说,你应该做的第一件事是将你的图形资源放在assets
文件夹中,但这一步有一些新的内容。
在之前的示例中,我们总是为每个游戏角色在assets
文件夹中填充一个PNG
图像——太空船和小行星有自己的图像。这也适用于所有注意力集中瓷砖等等。
Cocos2d-JS 在处理多个图像时没有问题,但作为一条黄金法则,记住你处理的图像越少,你的游戏性能越好。
那么,技巧在哪里?为了有一个太空船和小行星,你可能认为你需要加载一个太空船图像和一个小行星图像,但还有另一种更好的方法来做这件事,那就是使用精灵表。
精灵表是由将各种小图像组合成单个图像而制成的图像。如果你从事网页设计,它们被称为CSS 精灵,如果你已经制作了一些原生 iOS 应用程序,它们被称为纹理图集。
这是否意味着你必须手动创建一个大型图像,并将所有图形资源都放在里面?嗯,虽然你可以手动完成,但有几个软件解决方案可以加快这个过程。我使用并推荐给你的是 TexturePacker,你可以在www.codeandweb.com/texturepacker找到它。它使用直观的拖放界面,并支持 Cocos2d 导出。
这些是为游戏创建的四个图像,直接来自我的 Photoshop:
我想让你注意图像的缩放因子——实际上,它们真的很小。由于我们正在制作像素游戏,我制作的图像都非常小,从标题图像的 96 x 64 像素到箱子玩家的 5 x 5 像素。
一旦由 TexturePacker 处理并导出到 Cocos2d,你的资源文件夹应该包含以下两个文件:
你应该很容易识别所有之前绘制并打包到 spritesheet.png
中的图形资源,你可能会想知道为什么我们需要 spritesheet.plist
文件。
打开它,你基本上会找到一个包含所有图像信息的 XML 文件,从它们的原始文件名到它们在 spritesheet.plist
中的当前大小和坐标:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPEplist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>frames</key>
<dict>
<key>background.png</key>
<dict>
<key>frame</key>
<string>{{2,2},{96,64}}</string>
<key>offset</key>
<string>{0,0}</string>
<key>rotated</key>
<false/>
<key>sourceColorRect</key>
<string>{{0,0},{96,64}}</string>
<key>sourceSize</key>
<string>{96,64}</string>
</dict>
<key>crate.png</key>
<dict>
<key>frame</key>
<string>{{39,68},{5,5}}</string>
<key>offset</key>
<string>{0,0}</string>
<key>rotated</key>
<false/>
<key>sourceColorRect</key>
<string>{{0,0},{5,5}}</string>
<key>sourceSize</key>
<string>{5,5}</string>
</dict>
</dict>
</plist>
在各种 Cocos2d-JS 指南和参考资料中,这被称为精灵表。实际上,它更像是一个纹理图集;多亏了 XML 文件,它解释并描述了图集中包含的每个图像。
因此,现在是时候使用 loadassets.js
加载这两个文件了:
var gameResources = [
"assets/spritesheet.plist",
"assets/spritesheet.png"
];
同时,main.js
将在横屏模式下将我们的分辨率策略设置为 480 x 320 像素:
cc.game.onStart = function(){
cc.view.setDesignResolutionSize(480, 320, cc.ResolutionPolicy.SHOW_ALL);
cc.LoaderScene.preload(gameResources, function () {
cc.director.runScene(new gameScene());
}, this);
};
cc.game.run();
varmyGame = new cocos2dGame(gameScene);
现在,是时候创建游戏本身了。
构建关卡
通常,基于瓦片的关卡存储在二维数组中,Cocosban 也遵循这一趋势。因此,我们在 gamescript.js
中声明的第一个全局变量,它是一个包含关卡数据的数组,如下所示:
var level = [
[1,1,1,1,1,1,1],
[1,1,0,0,0,0,1],
[1,1,3,0,2,0,1],
[1,0,0,4,0,0,1],
[1,0,3,1,2,0,1],
[1,0,0,1,1,1,1],
[1,1,1,1,1,1,1]
];
每个项目代表一个瓦片,每个值代表一个项目,我这样编码:
-
0
:这个项目是一个空瓦片 -
1
:这个项目是墙壁 -
2
:这个项目是放置箱子的地方 -
3
:这个项目是箱子 -
4
:这个项目是玩家 -
5
:这个项目是在放置箱子的地方(3+2) -
6
:这个项目是在放置箱子的地方的玩家(4+2)
我们的 gameScene
声明始终相同:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
最后,我们准备扩展 game
类。
在我们开始之前,我想简要讨论一下移动端基于瓦片的游戏。
如果你查看 level
数组,你会看到它是一个 7x7=49 项目的数组。这意味着我们将放置 49 个瓦片 = 49 个精灵 在屏幕上。
没关系,但在屏幕上放置东西会消耗性能。由于我们不知道我们的游戏将在哪些设备上运行,屏幕上潜在的移动元素越少,性能就越好。
由于我们游戏中唯一移动的元素是箱子和小英雄,而所有墙壁和地板瓦片始终保持在它们的位置,我简单地手绘了关卡,只添加了可移动的角色作为瓦片。
在设计跨平台应用时,你应该做同样的事情,除非你使用的是随机或程序生成的内容。
话虽如此,这是 game
类的声明方式:
var game = cc.Layer.extend({
init:function () {
this._super();
cache = cc.spriteFrameCache;
cache.addSpriteFrames("assets/spritesheet.plist", "assets/spritesheet.png");
var backgroundSprite = cc.Sprite.create(cache.getSpriteFrame("background.png"));
backgroundSprite.setPosition(240,160);
backgroundSprite.setScale(5);
this.addChild(backgroundSprite);
var levelSprite = cc.Sprite.create(cache.getSpriteFrame("level.png"));
levelSprite.setPosition(240,110);
levelSprite.setScale(5);
this.addChild(levelSprite);
}
如你所见,大部分代码已经在之前的章节中解释过了。我们使游戏能够触摸驱动,并在舞台上添加了一些精灵。只需看看我是如何加载精灵表的:
cache = cc.spriteFrameCache;
cache.addSpriteFrames("assets/spritesheet.plist", "assets/spritesheet.png");
你可以这样从精灵表中选择单个图像:
var backgroundSprite = cc.Sprite.create(cache.getSpriteFrame("background.png"));
最后,由于我们的精灵非常非常小,它们需要放大。setScale
方法允许我们放大精灵:
backgroundSprite.setScale(5);
现在,我们准备启动游戏并看到我们的精灵被放大了 5 倍:
前面的图像不是一个模糊的低分辨率图像。它就是你将在屏幕上看到的实际游戏,因为 Cocos2d-JS 应用了抗锯齿效果,在这种情况下,浪费了我们想要给游戏的 8 位外观。抗锯齿在你想获得平滑图像时非常有用,但如果你的计划是创建像素游戏,它会使你的游戏看起来真的很糟糕。
你可以用 setAliasTexParameters
方法通过添加这一行来防止将抗锯齿应用于纹理:
var game = cc.Layer.extend({
init:function () {
this._super();
cache = cc.spriteFrameCache;
cache.addSpriteFrames("assets/spritesheet.plist", "assets/spritesheet.png");
var backgroundSprite = cc.Sprite.create(cache.getSpriteFrame("background.png"));
backgroundSprite.getTexture().setAliasTexParameters();
backgroundSprite.setPosition(240,160);
backgroundSprite.setScale(5);
this.addChild(backgroundSprite);
var levelSprite = cc.Sprite.create(cache.getSpriteFrame("level.png"));
levelSprite.setPosition(240,110);
levelSprite.setScale(5);
this.addChild(levelSprite);
}
});
再次运行游戏,你将看到你的像素完美游戏:
此外,我还想让你注意,setAliasTexParameters
方法只调用一次,并且作用于所有精灵——并且将作用于在这个游戏中创建的每个其他精灵——因为它应用于整个精灵图集。
在这个时候,我们可以创建玩家和箱子。它们只是根据 level
数组中的位置和关卡图像在舞台中的位置手动定位的精灵。
构建关卡的其他脚本与 Cocos2d-JS 无关,因为它完全是 JavaScript,所以我将稍微加快一点。首先,我需要三个更多的全局变量:
var cratesArray = [];
var playerPosition;
var playerSprite;
这就是它们所代表的:
-
cratesArray
:这是一个将包含所有箱体精灵的数组 -
playerPosition
:这是一个将用于在迷宫中存储玩家位置的变量 -
playerSprite
:这个变量代表玩家本身
然后,在添加了 level
精灵的行之后,我们可以放置玩家和箱子:
var game = cc.Layer.extend({
init:function () {
this._super();
// same as before
this.addChild(levelSprite);
for(i=0;i<7;i++){
cratesArray[i]=[];
for(j=0;j<7;j++){
switch(level[i][j]){
case 4:
case 6:
playerSprite = cc.Sprite.create(cache.getSpriteFrame("player.png"));
playerSprite.setPosition(165+25*j,185-25*i);
playerSprite.setScale(5);
this.addChild(playerSprite);
playerPosition = {x:j,y:i};
cratesArray[i][j]=null;
break;
case 3:
case 5:
var crateSprite = cc.Sprite.create(cache.getSpriteFrame("crate.png"));
crateSprite.setPosition(165+25*j,185-25*i);
crateSprite.setScale(5);
this.addChild(crateSprite);
cratesArray[i][j]=crateSprite;
break;
default:
cratesArray[i][j]=null;
}
}
}
}
});
你看到了吗?通过纯 JavaScript,我们只是在 level
数组项为 3 或 5 时添加了箱子精灵,在 level
数组项为 4 或 6 时添加了玩家精灵。
那些奇怪的数学运算只是用来根据它们的位置将瓦片放置在正确的位置。
以下截图是当你运行脚本时应看到的结果:
就这样!你的像素关卡已经准备好可以玩了。让我们检测玩家的移动。
检测滑动
如果我们分析一个滑动,我们可以将其分解为三个部分:
-
玩家在舞台的某个点上触摸。
-
玩家正在某个方向上拖动他们的手指。
-
玩家正在释放手指。
通过比较拖动开始和结束的点坐标,我们可以确定滑动的方向,并相应地移动玩家。
我们需要添加三个新的全局变量:
var startTouch;
var endTouch;
var swipeTolerance = 10;
它们的名字相当自解释:startTouch
和 endTouch
将存储滑动操作的起始点和结束点,而 swipeTolerance
是 startTouch
和 endTouch
之间允许的最小像素距离,以便将整个操作视为滑动。
现在,我们将让 game
检测触摸开始或结束:
var game = cc.Layer.extend({
init:function () {
// same as before
cc.eventManager.addListener(listener, this);
}
});
如同往常,我们添加了一个附加到名为 listener
的变量的监听器,我们将这样定义它:
var listener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan:function (touch,event) {
startTouch = touch.getLocation();
return true;
},
onTouchEnded:function(touch, event){
endTouch = touch.getLocation();
swipeDirection();
}
});
onTouchBegan
函数将注册初始触摸并更新startTouch
内容;多亏了getLocation
方法。注意,该函数返回true
。你确保这个函数返回true
非常重要,否则onTouchEnded
不会被触发。
对于onTouchEnded
也是如此,它将更新endTouch
。然后,调用swipeDirection
函数。它将允许我们移动玩家:
function swipeDirection(){
var distX = startTouch.x - endTouch.x;
var distY = startTouch.y - endTouch.y;
if(Math.abs(distX)+Math.abs(distY)>swipeTolerance){
if(Math.abs(distX)>Math.abs(distY)){
if(distX>0){
playerSprite.setPosition(playerSprite.getPosition().x-25,playerSprite.getPosition().y);
//move(-1,0);
}
else{
playerSprite.setPosition(playerSprite.getPosition().x+25,playerSprite.getPosition().y);
//move(1,0);
}
}
else{
if(distY>0){
playerSprite.setPosition(playerSprite.getPosition().x,playerSprite.getPosition().y-25);
//move(0,1);
}
else{
playerSprite.setPosition(playerSprite.getPosition().x,playerSprite.getPosition().y+25);
//move(0,-1);
}
}
}
}
运行游戏,以下是你将看到的截图:
一旦你向一个方向滑动,玩家就会相应地移动。
让我们看看swipeDirection
函数中会发生什么:
var distX = startTouch.x - endTouch.x;
var distY = startTouch.y - endTouch.y;
从开始触摸到结束触摸的水平距离和垂直距离被计算:
if(Math.abs(distX)+Math.abs(distY)>swipeTolerance){
只有当水平和垂直距离之和大于允许的最小像素容差,才能说移动实际上是一个滑动时,整个函数才会执行:
if(Math.abs(distX)>Math.abs(distY)){
下一步是确定玩家是水平还是垂直滑动。没有检查滑动是否严格水平或垂直;因此,对角线滑动将被视为水平或垂直,根据它们最大的分量:
if(distX>0){
一旦我们知道移动是水平还是垂直,就到了检查方向的时候:左或右?上或下?代码的其余部分只是检查这些问题,并相应地移动玩家 25 像素。不幸的是,你将能够穿过箱子和墙壁。是时候编写游戏规则了。
完成游戏
我即将编写的代码与 Cocos2d-JS 无关,因为它只是纯 JavaScript,解释它将超出本书的范围。我只是检查合法移动,并相应地移动玩家和箱子。
所有一切都由move
函数管理,该函数将检查合法移动并更新箱子和玩家的位置。move
函数有两个参数,deltaX
和deltaY
,它们代表玩家试图水平或垂直移动的瓷砖数量。
这意味着move(0,1)
将尝试将玩家向上移动(水平方向 0 个瓷砖,垂直方向 1 个瓷砖),move(-1,0)
将尝试将玩家向左移动,以此类推。
swipeDirection
函数改变了这一点:
function swipeDirection(){
var distX = startTouch.x - endTouch.x;
var distY = startTouch.y - endTouch.y;
if(Math.abs(distX)+Math.abs(distY)>swipeTolerance){
if(Math.abs(distX)>Math.abs(distY)){
if(distX>0){
move(-1,0);
}
else{
move(1,0);
}
}
else{
if(distY>0){
move(0,1);
}
else{
move(0,-1);
}
}
}
}
每次调用move
函数时,都会检测到一个有效的滑动。
这是move
函数:
function move(deltaX,deltaY){
switch(level[playerPosition.y+deltaY][playerPosition.x+deltaX]){
case 0:
case 2:
level[playerPosition.y][playerPosition.x]-=4;
playerPosition.x+=deltaX;
playerPosition.y+=deltaY;
level[playerPosition.y][playerPosition.x]+=4;
playerSprite.setPosition(165+25*playerPosition.x,185-25*playerPosition.y);
break;
case 3:
case 5:
if(level[playerPosition.y+deltaY*2][playerPosition.x+deltaX*2]==0 || level[playerPosition.y+deltaY*2][playerPosition.x+deltaX*2]==2){
level[playerPosition.y][playerPosition.x]-=4;
playerPosition.x+=deltaX;
playerPosition.y+=deltaY;
level[playerPosition.y][playerPosition.x]+=1;
playerSprite.setPosition(165+25*playerPosition.x,185-25*playerPosition.y);
level[playerPosition.y+deltaY][playerPosition.x+deltaX]+=3;
var movingCrate = cratesArray[playerPosition.y][playerPosition.x];
movingCrate.setPosition(movingCrate.getPosition().x+25*deltaX,movingCrate.getPosition().y-25*deltaY);
cratesArray[playerPosition.y+deltaY][playerPosition.x+deltaX]=movingCrate;
cratesArray[playerPosition.y][playerPosition.x]=null;
}
break;
}
}
享受你的游戏。
概述
在本章中,你学习了如何使用精灵表来管理你的资产,创建像素完美的游戏,并检测滑动。你还创建了一个名为 Cocosban 的精彩益智游戏。
如果你注意到了,玩家和箱子的移动是通过让资产跳转到目的地来实现的。你为什么不添加一个缓动效果来创建平滑的移动呢?这将是你的一大成就。
此外,没有检查玩家是否完成了关卡。完成关卡没有箱子在箱子目标之外。试着开发它。
然后,跟随我通过一条充满音乐的小径;我们将为我们的游戏添加音效!
第五章。成为音乐大师
尽管你可能认为你在阅读这本书时创建的游戏是完整的,但由于一个简单的原因,它们缺少某种氛围:它们是哑的。
没有声音,没有背景音乐,什么都没有。一个完整、完全抛光的游戏必须有背景音乐和音效,这就是你将在本章中学到的。
跟随我,你将能够:
-
为你的游戏添加音效
-
为你的游戏添加背景音乐
-
循环声音
-
开始和停止声音
-
改变音乐和音效的音量
此外,由于你学得越多越好,你还将看到如何创建选项菜单。
现在,上网选择一些优秀的音乐和音效。
选择声音
让我们猜测发生了什么。你找到了那首令人难以置信的歌曲,它是电子音乐和重金属音乐的混合,你认为它非常适合你的太空射击游戏。你下载了它,并享受了 4 分 56 秒的蓝光质量声音。
此外,在几分钟内,你至少找到了三首也适合你游戏的歌。让我们把它们都添加进去,就像在 GTA 系列游戏中做游戏内广播一样。
等等!玩家正在浏览器中运行你的游戏,可能是在任何免费 Wi-Fi 区域之外,下载速度也不是很高。除非你的名字是 Lady Gaga,而且那首歌是你的最新热门单曲,否则你不可能让他们等待半小时来下载一首歌。
人们想要按下播放按钮并玩你的游戏;记住,你是在制作游戏,而不是几秒钟内的声音,所以请明智地选择你的声音,它们不能比游戏本身更大。
在音质和大小之间找到良好的平衡,直到你找到一个好的妥协点,并且在选择背景音乐时,一个短循环比一个长调要好得多。
此外,记住不同的浏览器在不同的操作系统上读取不同的声音类型,所以你应该提供三种不同格式的相同声音:MP3
、WAV
和OGG
。
注意
关于声音优化和转换的讨论超出了本书的范围,但我建议你使用我用来编辑游戏中声音的免费软件Audacity,可在audacity.sourceforge.net/
找到。
预加载声音
预加载声音与预加载图形资源完全相同。在assets
文件夹中,有两个 mp3 文件:loop.mp3
,这是一个用作背景音乐的短循环,而bang.mp3
则是一个 Uzi 音效。记住,在你的最终项目中,你将不得不包括WAV
和OGG
文件,以确保在不同设备和不同浏览器之间实现最大的兼容性。
loadassets.js
文件将包含预加载声音的数组:
var gameResources = [
"assets/bang.mp3",
"assets/loop.mp3"
];
现在,让我们创建一个可以播放声音和音乐的菜单。
创建声音菜单
创建菜单有几种方法,其中最有趣的是创建每个菜单项的图形资源,然后添加触摸或鼠标监听器,并以您应该已经知道的方式处理整个事情。
这次,您将看到一些新内容:Cocos2d-JS 内置的 Menu
类。
这是 gameScript.js
的内容:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
this.audioEngine = cc.audioEngine;
var playSoundMenu = new cc.MenuItemFont.create("Play Sound effect",this.playSound,this);
playSoundMenu.setPosition(new cc.Point(0,350));
var playBGMusicMenu = new cc.MenuItemFont.create("Play BG music",this.playBGMusic,this);
playBGMusicMenu.setPosition(new cc.Point(0,300));
var stopBGMusicMenu = new cc.MenuItemFont.create("Stop BG music",this.stopBGMusic,this);
stopBGMusicMenu.setPosition(new cc.Point(0,250));
var musicUpMenu = new cc.MenuItemFont.create("Music volume Up",this.musicUp,this);
musicUpMenu.setPosition(new cc.Point(0,200));
var musicDownMenu = new cc.MenuItemFont.create("Music volume Down",this.musicDown,this);
musicDownMenu.setPosition(new cc.Point(0,150));
var effectsUpMenu = new cc.MenuItemFont.create("Effects volume Up",this.effectsUp,this);
effectsUpMenu.setPosition(new cc.Point(0,100));
var effectsDownMenu = new cc.MenuItemFont.create("Effects volume Down",this.effectsDown,this);
effectsDownMenu.setPosition(new cc.Point(0,50));
var menu = cc.Menu.create(playSoundMenu,playBGMusicMenu,stopBGMusicMenu,musicUpMenu,musicDownMenu,effectsUpMenu,effectsDownMenu);
menu.setPosition(new cc.Point(160,40));
this.addChild(menu);
}
});
这确实有很多内容,但学习起来并不复杂:gameScene
变量声明与之前的项目相同,而游戏声明与以下代码行不同:
this.audioEngine = cc.audioEngine;
这将允许您初始化音频引擎,您会发现很多:确切地说,有七个菜单项声明,就像这样:
var playSoundMenu = new cc.MenuItemFont.create("Play Sound effect",this.playSound,this);
playSoundMenu.setPosition(new cc.Point(0,350));
cc.MenuItemFont.create
函数在点击时创建一个具有缩放效果的文本菜单项。
这三个参数分别代表要显示的文本、要运行的回调函数以及运行回调的目标。
所有的七个菜单项都是用相同的方式创建的,并且使用您已经知道的 setPosition
方法进行定位。
一旦所有这些项都被创建,您就可以使用以下代码片段将它们转换成一个实际的菜单:
var menu = cc.Menu.create(playSoundMenu,playBGMusicMenu,stopBGMusicMenu,musicUpMenu,musicDownMenu,effectsUpMenu,effectsDownMenu);
menu.setPosition(new cc.Point(160,40));
this.addChild(menu);
Menu.create
函数包含了您刚刚创建的所有菜单项,并且像往常一样使用 addChild
和 setPosition
方法添加和定位到舞台。
运行项目,您将看到以下截图所示的内容:
点击或触摸菜单项以查看缩放效果,尽管目前没有任何操作发生,因为回调函数仍然需要被创建。
管理音乐和音效
是时候创建所有回调函数了,让我们扩展 game
类声明的内容:
var game = cc.Layer.extend({
init:function () {
// same as before
},
playSound:function(){
this.audioEngine.playEffect("assets/bang.mp3");
},
playBGMusic:function(){
if(!this.audioEngine.isMusicPlaying()){
this.audioEngine.playMusic("assets/loop.mp3",true);
}
},
stopBGMusic:function(){
if(this.audioEngine.isMusicPlaying()){
this.audioEngine.stopMusic();
}
},
musicUp:function(){
this.audioEngine.setMusicVolume(this.audioEngine.getMusicVolume()+0.1);
},
musicDown:function(){
this.audioEngine.setMusicVolume(this.audioEngine.getMusicVolume()-0.1);
},
effectsUp:function(){
this.audioEngine.setEffectsVolume(this.audioEngine.getEffectsVolume()+0.1);
},
effectsDown:function(){
this.audioEngine.setEffectsVolume(this.audioEngine.getEffectsVolume()-0.1);
}
});
现在,如果您测试项目,您将能够播放和停止声音,以及调整音乐和音效的音量。
让我们逐一查看所有函数:
playSound:function(){
this.audioEngine.playEffect("assets/bang.mp3");
}
playEffect method simply plays a sound effect.
playBGMusic:function(){
if(!this.audioEngine.isMusicPlaying()){
this.audioEngine.playMusic("assets/loop.mp3",true);
}
}
当音乐没有播放时,使用 playMusic
方法播放音乐。第二个参数表示循环播放。您可以通过 isMusicPlaying
方法查看音乐是否正在播放:
stopBGMusic:function(){
if(this.audioEngine.isMusicPlaying()){
this.audioEngine.stopMusic();
}
}
应用之前使用过的相同概念,如果音乐已经在播放,您可以使用 stopMusic
方法停止它:
musicUp:function(){
this.audioEngine.setMusicVolume(this.audioEngine.getMusicVolume()+0.1);
}
getMusicVolume
和 setMusicVolume
方法分别使用从 0(无音量)到 1(全音量)的值获取和设置音乐音量:
musicDown:function(){
this.audioEngine.setMusicVolume(this.audioEngine.getMusicVolume()-0.1);
}
以下概念应用于 getEffectsVolume
和 setEffectsVolume
:
effectsUp:function(){
this.audioEngine.setEffectsVolume(this.audioEngine.getEffectsVolume()+0.1);
}
effectsDown:function(){
this.audioEngine.setEffectsVolume(this.audioEngine.getEffectsVolume()-0.1);
}
此外,这也是您如何使用 Cocos2d-JS 管理声音的方法。
概述
感谢您在本章中学到的知识,您的游戏现在将包含背景音乐和音效。
为什么不将声音添加到您在前几章中创建的游戏中呢?那么,准备好吧,因为我们将要把交互性提升到一个全新的水平!
第六章 使用虚拟垫控制游戏
在制作跨平台游戏时,需要考虑的一个重要因素是玩家将如何控制主要角色。你游戏将运行的大多数设备都没有键盘或鼠标,尽管越来越多的便携式设备现在支持垫子,但你的游戏也必须在没有垫子的情况下可玩。
在本章中,我将向您展示在任意设备上创建虚拟垫的三种最流行的方法。除此之外,你还将学习如何:
-
滚动大图像以给出无限背景的印象
-
安排事件
-
控制帧率
-
检查精灵之间的碰撞
-
创建一个简单的粒子系统
-
手动移动精灵或使用动作
因此,首先要做的是看看成功游戏是如何让玩家通过虚拟垫与他们互动的。
虚拟垫概述
创建虚拟垫最古老、最简单且高度不建议的方式是在屏幕上放置方向按钮,并根据玩家按下的按钮来控制角色。
我在制作游戏可滑动操作之前,也在我的 Sokoban 游戏的第一个版本中使用了这种虚拟垫,正如我在 Cocosban 游戏制作过程中向您展示的那样。
Sokoban 游戏
在这个游戏中,你通过点击或轻触箭头按钮的精确位置来移动角色。它可以适应节奏较慢的解谜游戏,但在节奏较快的街机游戏中则无法玩。
正因如此,著名的平台游戏如 Mikey Shorts 使用幽灵按钮。幽灵按钮像正常按钮一样工作,但可感区域比图标本身大得多。
虽然按钮的大小并不比我在 Sokoban 游戏中使用的大,但可感区域覆盖了整个屏幕:实际红色按钮的可感区域是覆盖红色按钮图标的整个屏幕列,蓝色按钮的可感区域是覆盖蓝色按钮图标的整个屏幕列,以此类推。
处理虚拟垫的另一种方式是使用虚拟模拟垫。尽管控制 Mikey Shorts 的方式是数字的,这意味着一个按钮只能被按下或释放;有时,游戏需要像以下 Grand Theft Auto 系列截图所示的更精确的动作:
在屏幕的左下角,你可以看到一个虚拟模拟垫。垫子最初是通过触摸屏幕激活的,然后你将垫子从原始位置拖得越远,角色行走或奔跑的速度就越快。
我想提到的另一种创建虚拟垫的方式是 VVVVVV 游戏中使用的,它不显示任何图标。以下是一个 VVVVVV 游戏的截图:
你只需通过连续拖动,或者拖动并按住设备上的手指来左右移动角色,我必须说这比旧的虚拟摇杆要好得多,因为你可以虚拟地使用设备上的任何位置来移动。最重要的是,没有必须跨越的任何原点来改变方向:无论你将手指从开始触摸的点移动多远,一旦你将手指向相反方向移动,角色就会向另一个方向行走。
现在,除了我说过已经过时的第一个例子之外,你将看到如何使用虚拟摇杆创建所有这些控制角色的方法。
首先来看看游戏
我们正在制作一个小游戏来测试我们的虚拟摇杆:一个横幅游戏,购物车周围有下落的炸弹和草莓,试图抓住草莓同时避开炸弹?这听起来疯狂吗?是的。
这是我们的assets
文件夹的内容:
整个游戏的制作与无尽太空跑者的制作非常相似,因此没有必要谈论你应该已经知道的代码。
这是main.js
的内容:
cc.game.onStart = function(){
var screenSize = cc.view.getFrameSize();
cc.view.setDesignResolutionSize(480, 320, cc.ResolutionPolicy.SHOW_ALL);
cc.LoaderScene.preload(gameResources, function () {
cc.director.runScene(new gameScene());
}, this);
};
cc.game.run();
只需查看分辨率策略,就可以使游戏在横幅模式下运行。
这是loadassets.js
的内容:
var gameResources = [
"assets/bomb.png",
"assets/cart.png",
"assets/strawberry.png",
"assets/leftbutton.png",
"assets/rightbutton.png"
];
如所说,gamescript.js
的内容与无尽太空跑者的内容非常相似。
首先,让我们看看最终结果:
我们在舞台的底部中央有一个购物车,每秒都有水果和炸弹随机轨迹下落。
正如我已经告诉你的,没有什么新东西!你应该知道构建这个游戏所需的一切。这是gamescript.js
:首先,你需要两个全局变量来处理购物车本身以及将要填充水果和炸弹的层:
var itemsLayer;
var cart;
然后按常规定义gameScene
:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
game
类声明包含了游戏本身的精髓:
var game = cc.Layer.extend({
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(backgroundLayer);
itemsLayer = cc.Layer.create()
this.addChild(itemsLayer)
topLayer = cc.Layer.create()
this.addChild(topLayer)
cart = cc.Sprite.create("assets/cart.png");
topLayer.addChild(cart,0);
cart.setPosition(240,24);
this.schedule(this.addItem,1);
},
addItem:function(){
var item = new Item();
itemsLayer.addChild(item,1);
},
removeItem:function(item){
itemsLayer.removeChild(item);
}
});
这看起来像有很多代码,但没有什么新东西:我们只是添加了一个背景渐变,然后是两层:一层用于水果和炸弹,另一层用于购物车,最后添加了购物车本身。为了创建水果和炸弹,我们使用schedule
方法,该方法每秒调用一次addItem
函数来创建一个新的Item
类实例,而removeItem
函数将在水果和炸弹飞出屏幕后将其移除。
这与空间无尽跑者的创建非常相似,同样适用于在Item
声明中创建水果和炸弹:
var Item = cc.Sprite.extend({
ctor:function() {
this._super();
if(Math.random()<0.5){
this.initWithFile("assets/bomb.png");
this.isBomb=true;
}
else{
this.initWithFile("assets/strawberry.png");
this.isBomb=false;
}
},
onEnter:function() {
this._super();
this.setPosition(Math.random()*400+40,350);
var moveAction = cc.MoveTo.create(8, new cc.Point(Math.random()*400+40,-50));
this.runAction(moveAction);
this.scheduleUpdate();
},
update:function(dt){
if(this.getPosition().y<35 && this.getPosition().y>30 && Math.abs(this.getPosition().x-cart.getPosition().x)<10 && !this.isBomb){
gameLayer.removeItem(this);
console.log("FRUIT");
}
if(this.getPosition().y<35 && Math.abs(this.getPosition().x-cart.getPosition().x)<25 && this.isBomb){
gameLayer.removeItem(this);
console.log("BOMB");
}
if(this.getPosition().y<-30){
gameLayer.removeItem(this)
}
}
});
再次,有很多代码,但其中大部分是纯 JavaScript,与 Cocos2d-JS 无关。但让我们看看它:
ctor:function() {
this._super();
if(Math.random()<0.5){
this.initWithFile("assets/bomb.png");
this.isBomb=true;
}
else{
this.initWithFile("assets/strawberry.png");
this.isBomb=false;
}
}
你如何决定当前物品是水果还是炸弹?只需简单地画一个随机数,然后根据其值,使用炸弹或水果图像。isBomb
自定义属性将告诉我们它是炸弹(true
)还是水果(false
):
onEnter:function() {
this._super();
this.setPosition(Math.random()*400+40,350);
var moveAction = cc.MoveTo.create(8, new cc.Point(Math.random()*400+40,-50));
this.runAction(moveAction);
this.scheduleUpdate();
}
当它需要放置在舞台上时,我们将其放置在屏幕顶部的随机水平位置,并创建一个 tween 将其移动到屏幕底部的不同随机水平位置。这与无尽跑酷中的小行星移动非常相似。
update:function(dt){
if(this.getPosition().y<35 && this.getPosition().y>30 && Math.abs(this.getPosition().x-cart.getPosition().x)<10 && !this.isBomb){
gameLayer.removeItem(this);
console.log("FRUIT");
}
if(this.getPosition().y<35 && Math.abs(this.getPosition().x-cart.getPosition().x)<25 && this.isBomb){
gameLayer.removeItem(this);
console.log("BOMB");
}
if(this.getPosition().y<-30){
gameLayer.removeItem(this)
}
}
每一帧都会调用update
函数,检查三个条件:
-
如果项目是水果并且它非常接近小车,那么我们移除项目,并向控制台输出一些文本以获取调试信息,显示玩家击中了水果。
-
如果项目是炸弹并且它很近(不像水果那样近),但接近小车,那么我们移除项目,并向控制台输出一些文本以获取调试信息,显示玩家击中了炸弹。这是一个难度很高的游戏,因为被炸弹击中比收集水果更容易。
-
如果项目(无论是什么类型的物品)位于舞台底部之外,我们需要将其移除。
游戏就到这里。现在,你需要让玩家以前面提到的方式控制小车。
使用幽灵按钮控制小车
要使用幽灵按钮控制小车,首先,你必须在屏幕上放置按钮,正如所说的,它将只作为一个假按钮,因为舞台的整个左右区域将代表实际的按钮。
你需要在脚本中添加一些全局变量来处理左右按钮以及水平速度:
var itemsLayer;
var cart;
var xSpeed = 0;
var left;
var right;
xSpeed
变量表示小车的水平速度,而left
和right
变量将被分配给左右箭头按钮。
现在,init
函数需要放置按钮,设置touch
监听器,并在每一帧安排更新:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(backgroundLayer);
itemsLayer = cc.Layer.create()
this.addChild(itemsLayer)
topLayer = cc.Layer.create()
this.addChild(topLayer)
cart = cc.Sprite.create("assets/cart.png");
topLayer.addChild(cart,0);
cart.setPosition(240,24);
left = cc.Sprite.create("assets/leftbutton.png");
topLayer.addChild(left,0);
left.setPosition(40,160)
left.setOpacity(128)
right = cc.Sprite.create("assets/rightbutton.png");
topLayer.addChild(right,0);
right.setPosition(440,160);
right.setOpacity(128)
this.schedule(this.addItem,1);
cc.eventManager.addListener(touchListener, this);
this.scheduleUpdate();
}
我希望你能检查左右箭头按钮是否放置在topLayer
上,并且它们的透明度设置为半透明。同时,看看我们将要创建的监听器变量的名称:touchListener
。
此外,这是touchListener
的声明:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
if(touch.getLocation().x < 240){
xSpeed = -2;
left.setOpacity(255);
right.setOpacity(128);
}
else{
xSpeed = 2;
right.setOpacity(255);
left.setOpacity(128);
}
return true;
},
onTouchEnded:function (touch, event) {
xSpeed = 0;
left.setOpacity(128);
right.setOpacity(128);
}
})
这是一个类似于我们之前遇到过的触摸事件。让我们更仔细地看看触发的事件:
onTouchBegan: function (touch, event) {
if(touch.getLocation().x < 240){
xSpeed = -2;
left.setOpacity(255);
right.setOpacity(128);
}
else{
xSpeed = 2;
right.setOpacity(255);
left.setOpacity(128);
}
return true;
}
当玩家触摸处理幽灵按钮的屏幕时,我们只需要检查屏幕的左侧或右侧是否被触摸,相应地设置xSpeed
以及打开或关闭速度,并将相应的箭头按钮设置为全透明或半透明。
如果你想让 Cocos2d-JS 能够检查玩家何时停止触摸屏幕,函数必须返回true
。为了实现这一点,请添加以下代码片段:
onTouchEnded:function (touch, event) {
xSpeed = 0;
left.setOpacity(128);
right.setOpacity(128);
}
当玩家停止触摸屏幕时,将xSpeed
设置回零,并关闭两个按钮。
现在,你只需要在game
类的update
函数中移动小车:
update:function(dt){
if(xSpeed>0){
cart.setFlippedX(true);
}
if(xSpeed<0){
cart.setFlippedX(false);
}
cart.setPosition(cart.getPosition().x+xSpeed,cart.getPosition().y);
}
实际上没有必要解释任何事情,因为你只是通过xSpeed
像素移动小车;只需看看setFlippedX
方法,当小车向右移动时,它会水平翻转小车。
运行游戏,你将看到以下截图所示的内容:
在屏幕上的任何位置触摸,根据你触摸的屏幕位置将购物车移动到左边或右边。这就是幽灵按钮的全部内容。现在,让我们看看如何使用虚拟板控制游戏。
使用虚拟板控制购物车
要用虚拟板控制游戏,首先需要有一个虚拟板。因此,我们需要更改assets
文件夹中的某些图像:
左右按钮已被移除,并由虚拟板图像替代,因此loadassets.js
进行了如下更改:
var gameResources = [
"assets/bomb.png",
"assets/cart.png",
"assets/strawberry.png",
"assets/touchorigin.png",
"assets/touchend.png"
];
显然,游戏的大部分内容保持不变,因为你只是改变了控制购物车的方式。你需要稍微更改全局变量:
var itemsLayer;
var cart;
var xSpeed = 0;
var touchOrigin;
var touching = false;
var touchEnd;
在游戏相关变量保持不变的情况下,我添加了两个新变量,分别称为touchOrigin
和touchEnd
,它们将处理虚拟板的触摸。之前用于处理按钮的变量也已删除。一个名为touching
的布尔变量将告诉我们玩家是否在触摸屏幕。
现在,game
类的init
函数也需要一些更改:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0,0,0,255), cc.color(0x46,0x82,0xB4,255));
this.addChild(backgroundLayer);
itemsLayer = cc.Layer.create()
this.addChild(itemsLayer)
topLayer = cc.Layer.create()
this.addChild(topLayer)
cart = cc.Sprite.create("assets/cart.png");
topLayer.addChild(cart,0);
cart.setPosition(240,24);
this.schedule(this.addItem,1);
cc.eventManager.addListener(touchListener, this);
this.scheduleUpdate();
}
基本上,所有关于左右按钮的行都已经删除,但脚本的主体在touchListener
声明中:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
touchOrigin = cc.Sprite.create("assets/touchorigin.png");
topLayer.addChild(touchOrigin,0);
touchOrigin.setPosition(touch.getLocation().x,touch.getLocation().y);
touchEnd = cc.Sprite.create("assets/touchend.png");
topLayer.addChild(touchEnd,0);
touchEnd.setPosition(touch.getLocation().x,touch.getLocation().y);
touching = true;
return true;
},
onTouchMoved: function (touch, event) {
touchEnd.setPosition(touch.getLocation().x,touchEnd.getPosition().y);
},
onTouchEnded:function (touch, event) {
touching = false;
topLayer.removeChild(touchOrigin);
topLayer.removeChild(touchEnd);
}
})
我首先想让你看到的是三个事件:
-
onTouchBegan
:此事件将两个虚拟板精灵放置在触摸位置,并将布尔变量touching
设置为true
。 -
onTouchMoved
:此事件将touchEnd
精灵更新到当前触摸位置。正如其名所示,此事件在玩家在屏幕上移动手指时触发。 -
onTouchEnded
:此事件移除两个虚拟板精灵,并将布尔变量touching
设置为false
。
很容易猜到我是如何在game
类的update
函数中移动购物车的。如果touching
变量为true
,购物车速度就是touchEnd
和touchOrigin
函数的x坐标之间的差异:
update:function(dt){
if(touching){
xSpeed = (touchEnd.getPosition().x-touchOrigin.getPosition().x)/50;
if(xSpeed>0){
cart.setFlippedX(true);
}
if(xSpeed<0){
cart.setFlippedX(false);
}
cart.setPosition(cart.getPosition().x+xSpeed,cart.getPosition().y);
}
}
作为一款类比板,touchEnd
和touchOrigin
函数的x坐标之间的差异越大,购物车移动的速度就越快。我将这个差异除以50
以保持游戏的可玩性;否则,游戏会移动得太快。
测试游戏并玩。
拖动屏幕以适当的速度移动购物车。对于类比虚拟板来说,这也是全部。
仅用手指控制购物车
尽管你一直在用手指控制购物车,但整个章节中你都有运动的视觉反馈。在本章中我将要展示的最后一种移动购物车的方法不提供任何视觉反馈,但在仅限于一个轴(如你现在所做的那样)的游戏中效果很好。
首先,你不需要任何与游戏直接相关的图形资源,所以我们的loadassets.js
文件比以往任何时候都要小:
var gameResources = [
"assets/bomb.png",
"assets/cart.png",
"assets/strawberry.png"
];
现在,这些更改甚至比你从幽灵按钮创建虚拟键盘时所做的更改还要小:你只需要稍微改变全局变量:
var itemsLayer;
var xSpeed = 0;
var cart;
var detectedX;
var savedX;
var touching=false;
detectedX
和savedX
变量将存储当前和最后保存的手指或鼠标在水平位置。
touchListener
的内容比以前简单得多,因为你不需要管理图像:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
touching = true;
detectedX = touch.getLocation().x;
savedX = detectedX
return true;
},
onTouchMoved: function (touch, event) {
detectedX = touch.getLocation().x;
},
onTouchEnded:function (touch, event) {
touching = false;
}
})
除了像之前那样设置touching
布尔变量为true
或false
之外,你还可以通过detectedX
和savedX
来存储初始和当前的触摸水平坐标。
game
类的update
函数负责处理其余部分:
update:function(dt){
if(touching){
var deltaX = savedX - detectedX
if(deltaX>0){
xSpeed = -2;
}
if(deltaX<0){
xSpeed = 2;
}
savedX = detectedX;
if(xSpeed>0){
cart.setFlippedX(true);
}
if(xSpeed<0){
cart.setFlippedX(false);
}
cart.setPosition(cart.getPosition().x+xSpeed,cart.getPosition().y);
}
}
当玩家触摸屏幕时,通过比较当前和最后保存的水平触摸坐标之间的差异来判断购物车是否需要向左或向右移动。然后,将最后保存的水平触摸坐标更新为当前的横向触摸坐标,以便在触发onTouchMoved
事件时再次改变。
测试游戏并来回移动你的手指,你会看到当你的手指改变方向时,购物车会立即改变方向。
摘要
恭喜!这是一个艰难且漫长的章节,因为它解释了三种不同的控制游戏的方法。现在,取决于你选择哪种方式更适合你的每款游戏,所以为什么不写下你最喜欢的移动游戏列表,并思考你会使用哪种方式来控制玩家呢?
现在,让我们继续到下一章,在那里你将遇到真实的物理。
第七章。使用 Box2D 引擎为您的游戏添加物理
如果你问我休闲游戏最大的革命是什么,毫无疑问我会说是物理引擎。许多畅销的休闲游戏,如蜡笔物理、图腾破坏者、城堡粉碎、愤怒的小鸟、小翅膀,仅举几个例子,都使用了物理引擎来添加一种没有这些引擎就无法实现的现实行为。
在 2D 物理引擎中,Box2D 是最受欢迎的,最初是用 C++编写的,后来移植到了包括 JavaScript 在内的所有主要语言。
Cocos2d-JS 支持 Box2D,本章将涵盖创建物理游戏,包括以下概念:
-
配置和设置 Cocos2d-JS 以将 Box2D 引擎添加到您的游戏中
-
创建物理世界
-
为世界添加真实的重力
-
通过组合刚体、形状和固定装置来创建物理对象
-
创建材料
-
创建静态对象
-
创建动态对象
-
将精灵附加到物理对象上
-
使用鼠标/手指选择物理对象
-
销毁物理对象
-
检查物体之间的碰撞
-
运行物理模拟
这有很多东西,不是吗?
到了本章的结尾,您将拥有一个著名物理游戏的可玩关卡。
在开始之前
仅用几页来学习 Box2D 是不可能的。您需要一整本书来开始掌握它。
小贴士
要深入了解 Box2D,您可以在www.packtpub.com/game-development/box2d-flash-games
找到我的书,《Box2D for Flash Games》。
总之,这一章将为您提供添加物理到游戏的骨架。尽管经验丰富的 Box2D 用户可能会发现碰撞检测等一些概念介绍得不够完美,但最终它还是可行的,这正是本章的真正目的:为您提供开始学习 Box2D 并将其包含到 Cocos2d-JS 项目中的知识。
将 Box2D 引擎添加到您的项目中
最好的游戏是 Totem Destroyer,您可以在这里找到它:armorgames.com/play/1871/totem-destroyer
。
您必须通过点击/轻触砖块来摧毁它们,同时注意不要让图腾掉到地上,否则游戏结束。并非所有砖块都可以被摧毁。在下面的屏幕截图所示的关卡中,深色砖块无法被摧毁:
尽管游戏玩法相当简单,但它包含了一些高级物理概念,例如碰撞检测和如何选择物理体。
我们将构建这个关卡;因此,像往常一样,我们首先需要处理的是资源文件夹的内容:
这就是loadassets.js
的内容:
var gameResources = [
"assets/brick1x1.png",
"assets/brick2x1.png",
"assets/brick3x1.png",
"assets/brick4x1.png",
"assets/brick4x2.png",
"assets/ground.png",
"assets/totem.png"
];
为了尽可能快地加载,我们在前几章中使用的基本 Cocos2d-JS 源代码没有包含任何物理引擎。
为了让 Cocos2d-JS 与 Box2D 一起工作,我们必须加载另一个名为external
的模块,我们将在project.json
文件中定义它。
{
"debugMode" : 0,
"showFPS" : false,
"frameRate" : 60,
"id" : "gameCanvas",
"renderMode" : 0,
"engineDir":"cocos2d-html5/",
"modules" : ["cocos2d","external"],
"jsList" : [
"src/loadassets.js",
"src/gamescript.js"
]
}
现在,引擎知道我们将使用 Box2D;因此,我们可以专注于游戏本身。
配置物理世界
从现在开始,所有的脚本都将按照惯例写在gamescript.js
中,所以请准备好编写你的第一个 Cocos2d-JS Box2D 脚本。
一些魔法从前两行开始,它们声明了全局变量:
var world;
var worldScale = 30;
在这里,world
变量将代表我们在其中设置游戏的物理世界,它将包括我们将会发现的自己的重力和其他属性。然而,首先我想说几句关于worldScale
的话。
Box2D 是一个使用现实世界测量单位的真实物理引擎。这样,你将在 Box2D 世界中创建的一切都将用米来衡量。如果你创建一个边长为 2 米的盒子,这意味着它是两米。
另一方面,浏览器有自己的单位,即像素。你可以有一个 480 像素宽的游戏,但你永远不会找到一个两米宽的游戏。
因此,我们需要找到像素和米之间的比率。在几乎每一个项目中,1 米=30 像素的设置都很好,并允许我们在像素中思考和操作,而不必关心 Box2D 的内部单位。
gameScene
类的声明没有改变:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
如往常一样,有趣的部分可以在game
声明中找到:
var game = cc.Layer.extend({
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
var gravity = new Box2D.Common.Math.b2Vec2(0, -10)
world = new Box2D.Dynamics.b2World(gravity, true);
this.scheduleUpdate();
},
update:function(dt){
world.Step(dt,10,10);
console.log(world);
}
});
一旦运行项目,在你的控制台中,你应该会看到几个b2World的实例。
这意味着我们的 Box2D 物理世界正在运行;让我们看看发生了什么。
首先,我们添加了一个渐变背景层:
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
然后,我们已经知道物理世界有重力。以下是定义重力的方法:
var gravity = new Box2D.Common.Math.b2Vec2(0, -10);
总而言之,现实世界中的重力可以用一个向量来表示,地球的重力可以用一个向量(0,9.81)来表示,其中 9.81 用米每秒平方表示,这是在地球表面附近坠落物体的平均加速度。
在 Box2D 中,b2Vec2
类型用于存储向量;虽然我们可以将 9.81 近似为 10,但重力向量的y值为什么是-10,这相当不清楚。负重力?
让我来解释一下:你已经知道 Cocos2d-JS 的坐标原点位于舞台的左下角;因此,只要你从底部向上移动,你的 y 坐标就会增加。另一方面,Box2D 以相反的方式工作:只要一个物理体向下坠落,它的y坐标就会增加,将重力设置为(0,10)会使 Cocos2d-JS 精灵飞走而不是坠落。
这就是为什么我们需要反转重力。在 Box2D 中,内部世界体将飞走,但我们将在舞台上看到相同的物体坠落。
现在,我们终于可以创建世界了:
world = new Box2D.Dynamics.b2World(gravity, true);
如你所见,世界有两个参数:我们之前创建的gravity
变量和一个布尔标志,用于确定身体是否可以休眠。通常,为了节省 CPU 时间,一段时间内没有受到打击且不受力的影响的物理身体会被休眠。这意味着它们仍然存在于 Box2D 世界中,尽管它们的每个帧的位置不会更新,直到由于某些事件(如碰撞或施加到它们上的力)而醒来。
剩下的行应该对你来说已经很清晰了。我们正在使游戏能够安排更新函数在每个帧执行:
this.scheduleUpdate();
当我们调用scheduleUpdate
时,我们还需要一个update
函数,在这种情况下,它只包含:
world.Step(dt,10,10)
Step
方法将模拟推进一定的时间,dt
在这种情况下,为了尽可能准确,而其他两个参数分别代表速度和位置迭代。
这两个参数是必需的,因为大多数 Box2D 代码都是用于一个称为约束求解器的操作,这是一个一次解决模拟中所有约束的算法。虽然单个约束可以很容易地解决,但当更多的约束介入时,解决其中一个意味着稍微干扰其他约束。这就是为什么我们需要更多的迭代才能有一个准确的模拟。官方 Box2D 文档建议将速度设置为 8,位置设置为 3,尽管我通常将两者都设置为 10,并且在制作简单游戏时没有遇到任何问题。
现在,是时候构建图腾了。
向世界添加身体
在 Box2D 世界中,一个物理对象被称为身体。因此,我们将看到如何向世界添加一个身体。此外,由于我们“图腾破坏者”游戏中的所有身体都是盒子,我们将定义一个函数来创建一个身体,并对其进行定制以适应我们的需求。
我们将从结尾开始,调用一个我们尚未编写的函数,只是为了看看我们需要创建任何用于“图腾破坏者”的身体的全部参数。
因此,游戏的init
函数将被修改成这样:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
var gravity = new Box2D.Common.Math.b2Vec2(0, -10)
world = new Box2D.Dynamics.b2World(gravity, true);
this.scheduleUpdate();
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
}
我们的定制函数被称为addBody
,根据参数的数量将执行很多事情。让我们看看以下参数:
-
240
:这是身体水平中心,以像素为单位。 -
10
:这是身体垂直中心,以像素为单位。 -
480
:这是身体的宽度,以像素为单位。 -
20
:这是身体的高度,以像素为单位。 -
false
:这个布尔值决定了身体是动态的还是静态的。我们正在构建两种类型的身体,动态身体,它们会受到重力等力的作用并响应碰撞,以及静态身体,它们不能被移动。这将是一个静态身体。 -
"assets/ground.png"
:这些是要绑定到身体上的图形资源。 -
"ground"
:这是身体类型。我们称之为ground
,因为它将代表地面。
换句话说,我们在舞台底部创建了一个静态身体,它将代表地面。
现在,是时候看看如何使用 Cocos2d-JS 和 Box2D 创建和配置一个身体了。将addBody
函数添加到game
类中:
addBody: function(posX,posY,width,height,isDynamic,spriteImage,type){
var fixtureDef = new Box2D.Dynamics.b2FixtureDef;
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
fixtureDef.shape = new Box2D.Collision.Shapes.b2PolygonShape;
fixtureDef.shape.SetAsBox(0.5*width/worldScale,0.5*height/worldScale);
var bodyDef = new Box2D.Dynamics.b2BodyDef;
if(isDynamic){
bodyDef.type = Box2D.Dynamics.b2Body.b2_dynamicBody;
}
else{
bodyDef.type = Box2D.Dynamics.b2Body.b2_staticBody;
}
bodyDef.position.Set(posX/worldScale,posY/worldScale);
var userSprite = cc.Sprite.create(spriteImage);
this.addChild(userSprite, 0);
userSprite.setPosition(posX,posY);
bodyDef.userData = {
type: type,
asset: userSprite
}
var body = world.CreateBody(bodyDef)
body.CreateFixture(fixtureDef);
}
这里有很多新内容,所以我们将逐行查看。首先,我们必须创建一个固定件:
var fixtureDef = new Box2D.Dynamics.b2FixtureDef;
将固定件视为一个关系,它是一个物理演员(body)与其形状之间的关系,该形状决定了物体的外观——如盒子、圆形等。
固定件还通过这三个属性决定了身体的材料:
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
density
属性影响身体的重量,friction
决定了身体如何相互滑动,而restitution
用于查看身体如何弹跳。
现在,是时候创建与固定件相连的形状了:
fixtureDef.shape = new Box2D.Collision.Shapes.b2PolygonShape;
fixtureDef.shape.SetAsBox(0.5*width/worldScale,0.5*height/worldScale);
SetAsBox
方法根据 Box2D 接受的宽度的一半和高度创建一个盒子。所以,如果你想有一个 30 米宽的盒子,你必须将其宽度设置为300.5。如前所述,我们谈论的是像素,所以我们还必须将给定的宽度除以worldScale
。
一旦我们有了形状和固定件,就是时候关注物理身体了:
var bodyDef = new Box2D.Dynamics.b2BodyDef;
现在,我们可以确定身体是静态的还是动态的。在我们的游戏中,只有地板将是一个静态身体。相应的代码如下:
if(isDynamic){
bodyDef.type = Box2D.Dynamics.b2Body.b2_dynamicBody;
}
else{
bodyDef.type = Box2D.Dynamics.b2Body.b2_staticBody;
}
type
属性将决定身体是静态的还是动态的。现在,我们有了形状、身体和固定件;为什么我们不把这个身体放在世界的某个地方呢?请看下面的代码片段:
bodyDef.position.Set(posX/worldScale,posY/worldScale);
这可以通过position
属性来完成。别忘了像素到米的转换。
现在是 Box2D 最困难的部分。大多数试图学习 Box2D 的人在需要将图形资产附加到身体上时都会失败。主要原因在于,Box2D 不允许你将精灵附加到身体上。用 72 像素的字体打印出来。你必须手动将精灵放置在舞台上,并随着世界的更新手动移动它们。
让我们添加这个精灵:
var userSprite = cc.Sprite.create(spriteImage);
this.addChild(userSprite, 0);
userSprite.setPosition(posX,posY);
这很简单,因为我们只是以与第一章开始以来相同的方式添加了一个精灵。无论如何,我们必须以某种方式告诉 Box2D 这个精灵属于我们刚刚创建的身体。
太棒了!有一个userData
属性,我们可以用它来存储任何类型的自定义身体信息:
bodyDef.userData = {
type: type,
asset: userSprite
}
在这种情况下,我定义了一个对象,其中包含要链接的精灵和我们要处理的身体类型——在这种情况下,它将是ground
,根据传递给addBody
函数的参数。
最后,我们准备将身体附加到世界中,也就是说,将我们的身体定义——b2BodyDef
是一个身体定义——转换成物理身体:
var body = world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
CreateBody
方法将从身体定义创建一个真实的物理身体,而CreateFixture
方法将固定件及其形状附加到身体上。
最后,我们可以运行游戏,以下是你应该看到的图像:
最后,我们将有一个坚固的地板,而且还有一个函数将允许我们快速创建整个图腾。
让我们添加这些行:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
var gravity = new Box2D.Common.Math.b2Vec2(0, -10)
world = new Box2D.Dynamics.b2World(gravity, true);
this.scheduleUpdate();
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
this.addBody(204,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png", "destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png", "destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
}
我们只需正确调用addBody
函数,就可以用每个砖块定义的自己的图形资产、位置、大小和属性来构建我们的图腾。
现在启动游戏:
那就对了!我们的图腾现在站在地板上,准备被摧毁。
随着世界变化更新精灵位置
不幸的是,我们的图腾仍然只是一堆静态的精灵。是的,我们将其附加到了一个物体上,但世界发生变化时会发生什么呢?
尝试移除图腾的左脚,方法如下:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
var gravity = new Box2D.Common.Math.b2Vec2(0, -10)
world = new Box2D.Dynamics.b2World(gravity, true);
this.scheduleUpdate();
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
//this.addBody(204,32,24,24,true,"assets/brick1x1.png", "destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png","destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
}
我只是注释了一行;让我们看看会发生什么:
这显然是错误的。图腾应该掉下来!
这是因为我们在创建物体时正确地放置了精灵,但自那时起,精灵的位置尚未更新。
结果是,无论物体发生什么变化,精灵都保持在相同的位置。还记得吗?Box2D 不允许将精灵附加到物体上。教训学到了。
我们必须在update
函数中手动移动精灵:
update:function(dt){
world.Step(dt,10,10);
for (var b = world.GetBodyList(); b; b = b.GetNext()) {
if (b.GetUserData() != null) {
var mySprite = b.GetUserData().asset;
mySprite.setPosition(b.GetPosition().x * worldScale, b.GetPosition().y * worldScale);
mySprite.setRotation(-1 * cc.radiansToDegrees (b.GetAngle()));
}
}
}
现在运行游戏:
最后,图腾掉下来了!让我们看看发生了什么:
for (var b = world.GetBodyList(); b; b = b.GetNext()) {
这个循环遍历世界中放置的所有物体。
if (b.GetUserData() != null) {
b
现在是我们的当前物体,我们将查看是否将其用户数据中设置了某些内容:
var mySprite = b.GetUserData().asset;
mySprite.setPosition(b.GetPosition().x * worldScale, b.GetPosition().y * worldScale);
mySprite.setRotation(-1 * cc.radiansToDegrees (b.GetAngle()));
一旦我们知道用户数据中有什么东西,就像这个例子一样,我们知道它是我们创建来构建图腾或地面的物体之一。你还记得我们在用户数据中创建了一个对象吗?mySprite
变量将存储我们插入此对象中的精灵。
GetPosition
方法返回物体的位置;因此,我们可以更新精灵的位置——记住从米到像素的转换——而getAngle
返回物体的旋转。
这样我们就可以手动更新所有附加到 Box2D 世界物体的精灵。
选择和摧毁世界物体
正如名字“图腾破坏者”所暗示的,你应该能够摧毁图腾。首先,取消注释之前注释的行,以便恢复图腾的左脚,然后当玩家触摸/点击它们时,我们就可以准备摧毁砖块了。
一切从触摸开始,所以我们必须通过首先将监听器添加到游戏的init
函数来管理它:
init:function () {
// same as before
cc.eventManager.addListener(touchListener, this);
}
然后创建listener
变量本身:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: function (touch, event) {
var worldPoint = new Box2D.Common.Math.b2Vec2(touch.getLocation().x/worldScale,touch.getLocation().y/worldScale);
for (var b = world.GetBodyList(); b; b = b.GetNext()) {
if (b.GetUserData() != null && b.GetUserData().type=="destroyable") {
for(var f = b.GetFixtureList();f; f=f.GetNext()){
if(f.TestPoint(worldPoint)){
gameLayer.removeChild(b.GetUserData().asset)
world.DestroyBody(b);
}
}
}
}
}
});
让我们看看我们添加了什么。首先,我们必须获取点击/触摸坐标并将它们转换为 Box2D 世界坐标;这意味着将像素转换为米,并将坐标放入一个b2Vec2
变量中:
var worldPoint = new Box2D.Common.Math.b2Vec2(touch.getLocation().x/worldScale,touch.getLocation().y/worldScale);
然后,我们将以更新精灵位置时相同的方式遍历所有物体:
for (var b = world.GetBodyList(); b; b = b.GetNext()) {
并非所有物体都可以被摧毁:例如,地面和深色砖块不能被摧毁,所以我们必须确保我们只尝试摧毁我们用destroyable
标记的用户数据中的砖块:
if (b.GetUserData() != null && b.GetUserData().type=="destroyable") {
一旦我们知道一块砖可以被摧毁,我们就必须遍历它的所有固定件,看看其中是否有任何一个包含玩家点击/触摸的点。这就是我们将如何遍历一个物体的所有固定件:
for (var f = b.GetFixtureList();f; f=f.GetNext()){
一旦 f
代表当前纹理,TestPoint
方法将返回 true
如果传递给它的点位于固定件内部:
if(f.TestPoint(worldPoint)){
到这个时候,我们确信玩家触摸了 b
主体,我们可以在移除精灵后摧毁它。记住:Box2D 不允许你将精灵附加到主体上。这个代码片段如下所示:
gameLayer.removeChild(b.GetUserData().asset);
world.DestroyBody(b);
DestroyBody
方法从世界中移除一个主体。
运行游戏并触摸一些主体,然后你将能够摧毁那些轻的主体。
现在游戏已经准备好被玩,我们只需要检查偶像是否触摸到地面。这是一个游戏结束事件,所以它非常重要。
检查主体之间的碰撞
为了完成原型,我们需要检查偶像是否触摸到地面。根据你到目前为止对 Box2D 的了解,最简单的方法是持续扫描偶像的碰撞,并检查它碰撞到的主体中是否有地面。
我们需要在 update
函数中添加一些行:
update:function(dt){
world.Step(dt,10,10);
for (var b = world.GetBodyList(); b; b = b.GetNext()) {
if (b.GetUserData() != null) {
var mySprite = b.GetUserData().asset;
mySprite.setPosition(b.GetPosition().x * worldScale, b.GetPosition().y * worldScale);
mySprite.setRotation(-1 * cc.radiansToDegrees (b.GetAngle()));
if(b.GetUserData().type=="totem"){
for(var c = b.GetContactList(); c; c = c.m_next){
if(c.other.GetUserData() && c.other.GetUserData().type=="ground"){
console.log("Oh no!!!!");
}
}
}
}
}
}
在我们遍历主体和固定件的方式中,我们可以使用以下代码行遍历接触:
for(var c = b.GetContactList(); c; c = c.m_next){
对于每个 c
接触,我们通过 other
属性检查与之接触的主体;如果它的类型是 ground
,我们在控制台输出一条消息。整个过程只有在当前主体是偶像时才会进行。
运行游戏并让偶像触摸地面。你将在控制台日志中看到几个 Oh no!!!! 的实例。最终,我们有一个工作的图腾摧毁者原型!!
摘要
构建一个图腾摧毁者关卡是一个巨大的成就,因为现在你能够构建一个像最受欢迎的浏览器游戏之一那样的跨平台游戏;所有这些都归功于 Box2D。
你为什么不通过设定一个目标——比如 移除四个方块——来改进它呢?当这个目标达成后,等待几秒钟看看偶像是否保持在原位而没有掉到地上,就像在原始游戏中那样?
第八章。使用 Chipmunk2D 引擎给你的游戏添加物理
在上一章中,你看到了如何使用 Box2D 给你的游戏添加物理。然而,Box2D 并不是 Cocos2d-JS 支持的唯一物理引擎;你还可以使用 Chipmunk2D 引擎在你的游戏中添加物理特性。
所以主要问题是:你应该使用 Box2D 还是 Chipmunk2D 来给你的游戏添加物理?没有正确答案。只需使用你感觉最舒适的一个。
因此,在本章中,我将向你展示如何使用 Chipmunk2D 创建相同的 Totem Destroyer 游戏,突出两个物理引擎之间的相似之处和不同之处。
正如你在上一章中看到的,现在是时候学习了:
-
配置 Cocos2d-JS 以将 Chipmunk2D 引擎添加到你的游戏中
-
创建具有重力的物理空间
-
将刚体和形状组合以创建物理对象
-
创建材料
-
创建静态对象
-
创建动态对象
-
将精灵附加到物理对象上
-
使用鼠标或手指选择物理对象
-
销毁物理对象
-
检查物体之间的碰撞
-
运行物理模拟
-
使用调试绘图来测试你的项目
我假设你对上一章中讨论的基本 Box2D 概念相当熟悉,所以我在创建游戏时会快速进行。
将 Chipmunk2D 引擎添加到你的项目中
由于我们将创建与上一章相同的游戏,我建议你将你的项目复制到一个新文件夹中,因为我们将在上一章中已经编写的代码中重用大部分。所有图形资产都不会改变,所以只需不要触摸assets
文件夹。对于loadassets.js
、main.js
和project.json
文件也是如此。
所以基本上,我们唯一要更改的文件是gamescript.js
。准备好深入 Chipmunk2D 的世界。
一个没有物理的物理游戏
既然我们已经构建了 Totem Destroyer 游戏原型,我们可以从中移除所有物理部分,只留下骨架,在那里我们将构建新的物理引擎。
gamescript.js
去物理版本如下:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
var game = cc.Layer.extend({
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
// create physics world somehow
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
this.addBody(204,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png","destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
this.scheduleUpdate();
cc.eventManager.addListener(touchListener, this);
},
addBody: function(posX,posY,width,height,isDynamic,spriteImage,type){
// create the physics body somehow
},
update:function(dt){
// update the world somehow
}
});
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
onTouchBegan: function (touch, event) {
// destroy a physics body somehow
}
})
现在,一切准备就绪,可以将 Chimpunk2D 物理引擎注入到游戏中。让我们从头开始,创建物理世界。
配置物理空间
看看标题。它说配置物理空间。我使用space而不是world,因为 Chipmunk2D 称space为 Box2D 所称的world。
世界和空间代表同一件事:物理驱动事件发生的地方。
虽然 Chipmunk2D 称之为space,但我们将继续使用名为world的变量,以尽可能多地与 Box2D 代码保持相似。这是你看到两个引擎相似之处和不同之处的最佳方式。
按照以下方式更改游戏声明中的init
函数:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
world = new cp.Space();
world.gravity = cp.v(0, -100);
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
this.addBody(204,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png","destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
this.scheduleUpdate();
cc.eventManager.addListener(touchListener, this);
}
此外,在脚本的最开始创建world
全局变量:
var world;
让我们看看当执行以下行时会发生什么:
world = new cp.Space();
cp.Space
方法创建 Chipmunk2D 空间;到现在你应该知道这与 Box2D 世界相同:
world.gravity = cp.v(0, -100);
gravity
属性使用向量设置 world gravity
。cp.v
是 Chipmunk2D 表示向量的方式,与 Box2D 使用 b2Vec2
的方式相同。它有一个水平和垂直分量,要模拟地球重力,可以使用 (0,-100)
。
与 Box2D 不同,Chipmunk2D 不使用现实世界的测量单位,所以请期待使用像素而不是米。
向空间添加物体
我们已经有了带有所有必需参数的 addBody
函数,所以是时候定义它了:
addBody: function(posX,posY,width,height,isDynamic,spriteImage,type){
if(isDynamic){
var body = new cp.Body(1,cp.momentForBox(1,width,height));
}
else{
var body = new cp.Body(Infinity,Infinity);
}
body.setPos(cp.v(posX,posY));
if(isDynamic){
world.addBody(body);
}
var shape = new cp.BoxShape(body, width, height);
shape.setFriction(1);
shape.setElasticity(0);
shape.name=type;
world.addShape(shape);
}
这就是 Box2D 和 Chipmunk2D 之间的大差异开始显现的地方。因此,我们将逐行解释 addBody
函数:
if(isDynamic) {
var body = new cp.Body(1,cp.momentForBox(1,width,height));
}
else{
var body = new cp.Body(Infinity,Infinity);
}
我们有两种创建物体的方式,无论它是静态的还是动态的。两者都使用 cp.Body
方法,其参数是质量和惯性矩。惯性矩是刚体质量属性,它决定了围绕旋转轴所需的扭矩以实现所需的角加速度。
注意
更多信息,请访问维基百科上的文章 en.wikipedia.org/wiki/Moment_of_inertia
,它解释得非常清楚。
当一个物体是动态的,我会将其质量设置为 1
,但它可以是任何正的有限数,惯性矩是质量、宽度和高度通过 momentForBox
方法计算的结果,这个方法为我们做了繁重的工作。
因此,一个质量为 1 的盒子可以这样声明:
var body = new cp.Body(1,cp.momentForBox(1,width,height));
当声明一个质量为 15 的盒子时,可以这样替换 1 为 15:
var body = new cp.Body(15,cp.momentForBox(15,width,height));
小贴士
记住,对于动态物体,质量可以设置为任何正数。
另一方面,当处理静态物体时,你必须将质量和惯性矩都设置为无限大,JavaScript 使用无限大来表示。
一旦创建了物体,你需要给它一个空间中的位置:
body.setPos(cp.v(posX,posY));
setPos
方法使用像素坐标将其放置在空间中。正如你所看到的,cp.v
参数是你调用 addBody
函数时设置的像素坐标,无需进行单位转换。
如果你记得,在 Box2D 章节中,你需要将米转换为像素。然而,Chipmunk2D 直接在像素中工作。
现在,是时候将物体添加到空间中了:
if(isDynamic){
world.addBody(body);
}
addBody
方法将一个物体添加到空间中。你可能想知道为什么我只在它是动态的情况下将其添加到空间中。一旦一个物体被定义为具有无限质量和惯性矩的静态物体,除非你计划在游戏过程中手动移动它(例如一个移动的平台,这与我们的固体地面不同),否则不需要将其添加到空间中,因为你只会添加它的碰撞形状。
什么是一个物体的碰撞形状?你即将发现它:
var shape = new cp.BoxShape(body, width, height);
大概和 Box2D 一样,Chipmunk2D 使用物体和形状工作,其中物体代表抽象的物理实体,而形状是附加到物体上的实际物理物质。在 Box2D 中,我们使用固定件作为物体和形状之间的粘合剂,而在 Chipmunk2D 中,这并不是必要的:我们可以直接创建一个形状并将其附加到物体上。
让我们创建形状然后:
var shape = new cp.BoxShape(body, width, height);
shape.setFriction(1);
shape.setElasticity(0);
shape.name=type;
world.addShape(shape);
cp.BoxShape
方法创建形状,设置宽度、高度,并将形状附加到setFriction
和setElasticity
定义的形状材料上,这被称为弹性,类似于 Box2D 的恢复系数。我还为形状赋予了一个名称;然后addShape
方法将形状添加到空间中。
现在,所有这些形状和物体都应该准备好由 Chipmunk2D 空间处理,所以是时候看看如何运行模拟了。
更新 Chipmunk2D 空间和使用 debug draw
要更新 Chipmunk2D 空间,只需在更新函数中调用step
方法:
update:function(dt){
world.step(dt);
}
这将使模拟前进dt
时间。
好的,现在运行项目,你会看到只有背景渐变。我们遗漏了什么吗?
与 Box2D 类似,Chipmunk2D 不绘制空间;它只是计算它,并留给我们渲染它的任务。
无论如何,为了快速简单的测试,有一个名为 debug draw 的功能(也包含在 Box2D 中),尽管我没有向你展示如何减少页面数量,这允许你在没有将实际图形资产附加到物体上时渲染空间。
这样修改init
函数:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
world = new cp.Space();
world.gravity = cp.v(0, -100);
var debugDraw = cc.PhysicsDebugNode.create(world);
debugDraw.setVisible(true);
this.addChild(debugDraw);
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
this.addBody(204,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png","destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
this.scheduleUpdate();
cc.eventManager.addListener(touchListener, this);
}
这三条线将使用cc.PhysicsDebugNode.create
方法创建一个 debug draw 层,稍后将其添加到舞台中。
现在运行项目:
此外,这里是我们用 debug draw 渲染的动态图腾碎片和静态背景。PhysicsDebug
将遍历空间中的形状和约束,并以默认颜色绘制它们。现在,我们可以在游戏完成后继续添加功能,并添加实际的图形资产。这将节省开发时间,因为如果某些东西没有按预期工作,我们可以使用 debug draw 来查看 Chipmunk2D 物体的位置是否与图形资产的位置匹配。
选择和摧毁空间物体
玩家必须能够摧毁某些物体:通过点击或轻触名为destroyable
的物体来摧毁它们。因此,这是完整的touchListener
声明:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
onTouchBegan: function (touch, event) {
for(var i=shapeArray.length-1;i>=0;i--){
if(shapeArray[i].pointQuery(cp.v(touch.getLocation().x,touch.getLocation().y))!=undefined){
if(shapeArray[i].name=="destroyable"){
world.removeBody(shapeArray[i].getBody())
world.removeShape(shapeArray[i])
shapeArray.splice(i,1);
}
}
}
}
})
在评论它之前,我将向你解释另一种遍历空间中所有这些物体或形状的方法。
你还记得 Box2D 中的物体选择吗?我们使用GetBodyList()
函数遍历所有世界物体。这是其中一种方法。然而,还有其他方法;既然我想尽可能多地展示功能,这次我们将遍历形状而不使用任何 Chipmunk2D 专有函数。
我们可以添加另一个全局变量,称为shapeArray
,一个空数组:
var world;
var shapeArray=[];
然后,在 addBody
函数中,一旦我们将形状添加到空间中,我们就将其追加到 shapeArray
:
addBody: function(posX,posY,width,height,isDynamic,spriteImage,type){
if(isDynamic){
var body = new cp.Body(1,cp.momentForBox(1,width,height));
}
else{
var body = new cp.Body(Infinity,Infinity);
}
body.setPos(cp.v(posX,posY));
if(isDynamic){
world.addBody(body);
}
var shape = new cp.BoxShape(body, width, height);
shape.setFriction(1);
shape.setElasticity(0);
shape.name=type;
world.addShape(shape);
shapeArray.push(shape);
}
一旦我们有了 shapeArray
中的所有形状,就很容易通过循环遍历它们,并使用 pointQuery
(其参数是一个具有舞台坐标的向量)来查看点击或触摸的点是否在它们之中。如果它不返回 undefined
,则意味着该点在给定的形状内。
然后,removeBody
和 removeShape
空间方法分别删除形状和物体。记得在删除东西时手动修改 shapeArray
。
想看看这行不行?只需运行项目并点击可摧毁的砖块:
小心!这是掉落的砖块!
这让我想起了两件事。首先,这不是解决关卡的最佳方式。其次,我们必须检测偶像何时撞击地面。
检查物体之间的碰撞
在上一章中,为了检测碰撞,我们遍历了偶像接触点以查看何时撞击地面。
Box2D 和 Chipmunk2D 都有更有趣的方式来检查碰撞,因为它们处理碰撞监听器。
将高亮行添加到 init
函数中:
init:function () {
this._super();
var backgroundLayer = cc.LayerGradient.create(cc.color(0xdf,0x9f,0x83,255), cc.color(0xfa,0xf7,0x9f,255));
this.addChild(backgroundLayer);
world = new cp.Space();
world.gravity = cp.v(0, -100);
this._debugNode = cc.PhysicsDebugNode.create(world);
this._debugNode.setVisible( true );
this.addChild( this._debugNode );
this.scheduleUpdate();
this.addBody(240,10,480,20,false,"assets/ground.png","ground");
this.addBody(204,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(276,32,24,24,true,"assets/brick1x1.png","destroyable");
this.addBody(240,56,96,24,true,"assets/brick4x1.png","destroyable");
this.addBody(240,80,48,24,true,"assets/brick2x1.png","solid");
this.addBody(228,104,72,24,true,"assets/brick3x1.png","destroyable");
this.addBody(240,140,96,48,true,"assets/brick4x2.png","solid");
this.addBody(240,188,24,48,true,"assets/totem.png","totem");
cc.eventManager.addListener(touchListener, this);
world.setDefaultCollisionHandler (this.collisionBegin,null,null,null);
}
只用一行代码,我们就进入了碰撞监听器的世界。可以使用以下不同类型的监听器:
-
setDefaultCollisionHandler
: 每当碰撞更新时,此方法将调用四个函数。在 Chipmunk2D 以及 Box2D 中,碰撞有四种状态:-
begin
: 此方法定义脚本意识到两个形状接触的时间。 -
preSolve
: 在解决碰撞之前调用此方法。解决碰撞意味着根据碰撞本身更新形状和物体。 -
postSolve
: 在解决碰撞后调用此方法。 -
separate
: 当碰撞不再存在时,即这两个形状不再接触时,会调用此方法。
-
我们只需要检查碰撞何时开始;这就是为什么我将 collisionBegin
作为第一个参数传递,将其他参数留为 null
。collisionBegin
函数非常简单:
collisionBegin : function (arbiter, space ) {
if((arbiter.a.name=="totem" && arbiter.b.name=="ground") || (arbiter.b.name=="totem" && arbiter.a.name=="ground")){
console.log("Oh no!!!!");
}
return true;
}
我只是检查第一个形状:arbiter.a
是否被称作 totem
,第二个形状:arbiter.b
是否被称作 ground
或反之,以输出控制台信息。
你还必须返回 true
,否则碰撞将被忽略。
运行项目,当图腾以这种方式接触地面时:
你将看到这个:
哦不!!!
最后,我们完成了 Totem Destroyer 原型的所有游戏机制。我们只需将我们的图形资源添加到游戏中。
你注意到吗?我们通过添加图形资源来完成项目,而在上一章中,我们是先添加它们的。这是我喜欢编程的原因之一。你的选择是无限的。
使用你自己的图形资源
就像在上一章中一样,当我们添加身体时,我们会添加图形,然后根据其身体位置和旋转更新它们的位置和旋转。
首先,更新addBody
函数:
addBody: function(posX,posY,width,height,isDynamic,spriteImage,type){
if(isDynamic){
var body = new cp.Body(1,cp.momentForBox(1,width,height));
}
else{
var body = new cp.Body(Infinity,Infinity);
}
body.setPos(cp.v(posX,posY));
var bodySprite = cc.Sprite.create(spriteImage);
gameLayer.addChild(bodySprite,0);
bodySprite.setPosition(posX,posY);
if(isDynamic){
world.addBody(body);
}
var shape = new cp.BoxShape(body, width, height);
shape.setFriction(1);
shape.setElasticity(0);
shape.name=type;
shape.image=bodySprite;
world.addShape(shape);
shapeArray.push(shape);
}
这与我们在 Box2D 中看到的方式相同:精灵被添加到游戏中,并保存在自定义形状属性中——在这种情况下,image
。
要在update
函数中更新精灵的位置,我们需要遍历所有形状:
update:function(dt){
world.step(dt);
for(var i=shapeArray.length-1;i>=0;i--){
shapeArray[i].image.x=shapeArray[i].body.p.x
shapeArray[i].image.y=shapeArray[i].body.p.y
var angle = Math.atan2(-shapeArray[i].body.rot.y,shapeArray[i].body.rot.x);
shapeArray[i].image.rotation= angle*57.2957795;
}
}
我们遍历我们的自定义变量shapeArray
,并根据其身体位置和旋转更新每个形状图像。虽然使用p
属性获取身体位置非常简单,但 Chipmunk2D 不返回以度或弧度为单位的角度,而是以向量形式返回;你可以使用rot
属性获取其位置。这就是为什么我使用atan2
方法从向量中获取角度;然后我将它乘以57.2957795
将弧度转换为度。
同时,别忘了在移除其身体时手动移除精灵:
var touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
onTouchBegan: function (touch, event) {
for(var i=shapeArray.length-1;i>=0;i--){
if(shapeArray[i].pointQuery(cp.v(touch.getLocation().x,touch.getLocation().y))!=undefined){
if(shapeArray[i].name=="destroyable"){
gameLayer.removeChild(shapeArray[i].image);
world.removeBody(shapeArray[i].getBody())
world.removeShape(shapeArray[i])
shapeArray.splice(i,1);
}
}
}
}
})
运行项目并查看您的自定义图形效果:
到这个时候,你可以移除调试绘图图形;这让你有了使用两个不同物理引擎制作的相同游戏。
摘要
让我恭喜你;你不仅制作了一个图腾破坏者游戏,而且你还能够使用两个不同的物理引擎来制作它。并不是所有的开发者都能做到这一点。现在,将你在 Box2D 游戏中添加的相同改进添加到游戏中,因为你已经改进了游戏,不是吗?让我们开始最后一章,在那里你将在几分钟内创建一个大片游戏。
第九章。创建您自己的大片游戏 - 一个完整的匹配 3 游戏
这是这本书的最后一章,我真心希望您阅读这本书的乐趣和我写作的乐趣一样。当我为这本书制定初步大纲时,我计划将最后一章奉献给像 Candy Crush Saga 或 Farm Heroes Saga 这样的匹配 3 游戏。
我后来意识到,关于这些游戏在网络上有很多教程。因此,我决定向大家展示一些新内容,即 Dungeon Raid 游戏(可在itunes.apple.com/us/app/dungeon-raid/id403090531
找到)的引擎,我将其改编成 Globez(可在www.mindjolt.com/globez.html
找到),这是一个被数百万次玩过的游戏。
在制作这个游戏的过程中,你不仅会使用这本书中描述的大多数概念来创建一个真实游戏引擎,你还会学习 Cocos2d-JS 绘图 API 的基础知识。
仔细遵循步骤;这是一个完整的游戏引擎,有很多事情要做。
设置游戏
由于这是一个没有物理的基本游戏,我们不需要包含外部库;因此,project.json
返回其原始内容:
{
"debugMode" : 0,
"showFPS" : false,
"frameRate" : 60,
"id" : "gameCanvas",
"renderMode" : 0,
"engineDir":"cocos2d-html5/",
"modules" : ["cocos2d"],
"jsList" : [
"src/loadassets.js",
"src/gamescript.js"
]
}
此外,main.js
的内容基本上和以前一样:
cc.game.onStart = function(){
var screenSize = cc.view.getFrameSize();
cc.view.setDesignResolutionSize(300, 300, cc.ResolutionPolicy.SHOW_ALL);
cc.LoaderScene.preload(gameResources, function () {
cc.director.runScene(new gameScene());
}, this);
};
cc.game.run();
只看分辨率:300x300 是主要游戏区域。现在,我们只关注主要游戏区域,相信我,你会有足够的任务要做!
loadassets.js
正在加载使用TexturePacker创建的精灵表:
var gameResources = [
"assets/globes.png",
"assets/globes.plist",
];
globes.png
只是一个包含所有彩色地球仪的单个文件:
globes.plist
以这种方式定义了各种图像,每个颜色名称都分配了一个key
节点。例如:
<key>purple</key>
<dict>
<key>frame</key>
<string>{{2,2},{46,46}}</string>
<key>offset</key>
<string>{0,0}</string>
<key>rotated</key>
<false/>
<key>sourceColorRect</key>
<string>{{2,2},{46,46}}</string>
<key>sourceSize</key>
<string>{50,50}</string>
</dict>
现在我们完成了设置,我们可以开始编写游戏本身了。
创建游戏板
我们首先要做的是在gamescript.js
中创建游戏板,我们将在上面玩游戏。我们试图使引擎尽可能可定制,所以我们从一些全局变量开始。更改其中大多数变量将导致游戏玩法快速改变。如下所示:
var fieldSize = 6;
var tileTypes = ["red", "green", "blue", "grey", "yellow"];
var tileSize = 50;
var tileArray = [];
var globezLayer;
-
fieldSize
:这个变量是场地尺寸的宽度和高度,以瓦片为单位。这意味着我们将在一个 6 x 6 瓦片场地上玩游戏。 -
tileTypes
:这是一个数组,包含了在globes.plist
文件中定义的精灵键。我只使用了五种不同类型的地球仪,因为我喜欢游戏提供制作大型组合的机会。你可以选择你想要的颜色数量;只需记住,游戏中颜色越多,游戏难度就越高。 -
tileSize
:这个变量是一个瓦片的尺寸,以像素为单位。 -
tileArray
:这是一个将包含所有地球仪对象的数组。 -
globezLayer
:这个变量将是放置地球仪瓦片的层。
gameScene
的定义没有改变:
var gameScene = cc.Scene.extend({
onEnter:function () {
this._super();
gameLayer = new game();
gameLayer.init();
this.addChild(gameLayer);
}
});
让我们看看游戏定义,脚本的精髓:
var game = cc.Layer.extend({
init:function () {
this._super();
cc.spriteFrameCache.addSpriteFrames("assets/globes.plist", "assets/globes.png");
var backgroundLayer = cc.LayerGradient.create(cc.color(0x00,0x22,0x22,255), cc.color(0x22,0x00,0x44,255));
this.addChild(backgroundLayer);
globezLayer = cc.Layer.create();
// new cc.layer() can also be used
this.addChild(globezLayer)
this.createLevel();
},
createLevel: function(){
// do something
}
});
这里没有什么新内容;我们加载精灵图集,创建并放置一个背景层,创建并放置将包含所有 globez 的层,并调用createLevel
函数。
让我们在createLevel
中添加 globe 创建:
createLevel: function(){
for(var i = 0; i < fieldSize; i ++){
tileArray[i] = [];
for(var j = 0;j < fieldSize; j ++){
this.addTile(i, j);
}
}
},
addTile:function(row,col){
// do something
}
这里我们只是在根据fieldSize
数字的条目数构建一个名为tileArray
的两维数组。给定i和j的值,addTile
函数会导致在游戏场中创建这样的瓦片,它接受该瓦片在游戏场中的行和列。
让我们看看addTile
来了解如何设置游戏场:
addTile:function(row,col){
var randomTile = Math.floor(Math.random()*tileTypes.length);
var spriteFrame = cc.spriteFrameCache.getSpriteFrame(tileTypes[randomTile]);
var sprite = cc.Sprite.createWithSpriteFrame(spriteFrame);
// new cc.Sprite(spriteFrame) can also be used
sprite.val = randomTile;
sprite.picked = false;
globezLayer.addChild(sprite,0);
sprite.setPosition(col*tileSize+tileSize/2,row*tileSize+tileSize/2);
tileArray[row][col] = sprite;
}
在这个时候,你可以测试项目并看看会发生什么:
你注意到了吗?每次运行游戏时,你都会得到一个不同、随机的游戏场。
查看addTile
函数将让你了解发生了什么:
var randomTile = Math.floor(Math.random()*tileTypes.length);
首先,生成一个介于零和允许的瓦片类型数量减一之间的随机数。在这种情况下,是从零到五。
var spriteFrame = cc.spriteFrameCache.getSpriteFrame(tileTypes[randomTile]);
从精灵图集中,我们将选择与tileTypes[randomTile]
具有相同键的精灵:
var sprite = cc.Sprite.createWithSpriteFrame(spriteFrame);
最后,从其分配的帧开始创建精灵:
sprite.val = randomTile;
sprite.picked = false;
我为我们的小精灵添加了两个自定义属性:
-
val
:这个变量代表由randomTile
变量定义的瓦片的值 -
picked
:这是一个布尔变量,表示瓦片是否已经被玩家选中
然后,精灵被添加到globezLayer
层:
globezLayer.addChild(sprite,0);
现在,我们必须用所有这些 globez 做些事情。
选择和取消选择第一个 globe
当我们想要玩家与游戏互动时,需要做的第一件事是什么?是的,添加一个监听器。我们将将其添加到游戏的init
函数中:
init:function () {
this._super();
cc.spriteFrameCache.addSpriteFrames("assets/globes.plist", "assets/globes.png");
var backgroundLayer = cc.LayerGradient.create(cc.color(0x00,0x22,0x22,255), cc.color(0x22,0x00,0x44,255));
this.addChild(backgroundLayer);
globezLayer = cc.Layer.create();
this.addChild(globezLayer)
this.createLevel();
cc.eventManager.addListener(touchListener, this);
}
这些都是你已经遇到的概念;我只是将它们组合起来以创建一个游戏。所以,你现在应该知道我们将要声明touchListener
;但首先,让我添加两个额外的全局变量来跟踪我将要选择的瓦片和颜色:
var fieldSize = 6;
var tileTypes = ["red","green","blue","grey","yellow"];
var tileSize = 50;
var tileArray = [];
var globezLayer;
var startColor = null;
var visitedTiles = [];
visitedTiles
是一个数组,它将存储玩家捡起的瓦片,而startColor
是第一个被选中的瓦片的颜色。我们以null
开始,因为没有选择颜色。
现在,让我们转到touchListener
的创建:
var touchListener = cc.EventListener.create({
event: cc.EventListener.MOUSE,
onMouseDown: function (event) {
var pickedRow = Math.floor(event._y / tileSize);
var pickedCol = Math.floor(event._x / tileSize);
tileArray[pickedRow][pickedCol].setOpacity(128);
tileArray[pickedRow][pickedCol].picked = true;
startColor = tileArray[pickedRow][pickedCol].val;
visitedTiles.push({
row: pickedRow,
col: pickedCol
});
},
onMouseUp: function(event){
startColor=null;
for(i = 0; i < visitedTiles.length; i ++){
tileArray[visitedTiles[i].row][visitedTiles[i].col].setOpacity(255);
tileArray[visitedTiles[i].row][visitedTiles[i].col].picked=false;
}
}
});
看起来代码很多,但别担心,它真的很简单。检查以下代码行:
event: cc.EventListener.MOUSE
这次,我们将使用鼠标进行操作,但显然,如果你想的话也可以使用触摸。假设你应该能够使用两种方式来控制游戏。使用鼠标,我们必须处理两个事件,onMouseDown
和onMouseUp
:
onMouseDown: function (event) {
var pickedRow = Math.floor(event._y / tileSize);
var pickedCol = Math.floor(event._x / tileSize);
tileArray[pickedRow][pickedCol].setOpacity(128);
tileArray[pickedRow][pickedCol].picked = true;
startColor = tileArray[pickedRow][pickedCol].val;
visitedTiles.push({
row: pickedRow,
col: pickedCol
});
}
当鼠标按下时,pickedRow
和 pickedCol
变量根据点击坐标和 tileSize
获取鼠标选择的行和列的索引。一旦我知道我选择的 globe 的行和列,我可以通过将它的不透明度设置为 128 来使它半透明——记住在 Cocos2d-JS 中,不透明度范围从 0 到 255,使用 setOpacity
方法。我还将 globe 的 picked
值设置为 true
,因为我实际上已经选择了它,而且由于它是第一个我选择的 globe,我还需要将 startColor
设置为 globe 的颜色。从现在起,我们只需要选择相同颜色的 globez。
最后但同样重要的是,我们需要用新选中的 globe 更新 visitedTiles
数组——在这个例子中,它被添加为一个对象。
目前,onMouseUp
非常简单,尽管它将成为整个游戏中最复杂的函数。它如下所示:
onMouseUp: function(event){
startColor=null;
for(i = 0; i < visitedTiles.length; i ++){
tileArray[visitedTiles[i].row][visitedTiles[i].col].setOpacity(255);
tileArray[visitedTiles[i].row][visitedTiles[i].col].picked=false;
}
visitedTiles = [];
}
没什么好说的;一旦玩家释放鼠标,startColor
方法需要重置为 null
,并且 visitedTiles
数组中的每个 globe 都必须设置为完全不透明,picked
属性设置为 false
。有了空的 visitedTiles
数组,我们就准备好等待下一个玩家的选择了。
测试游戏并尝试选择和释放一个 globe:
如你所见,当你选择一个 globe 时,它会变成半透明。当你释放它时,它会恢复为完全不透明。
让我们制作我们的第一个链。
制作 globez 链条
游戏玩法很简单:你必须水平、垂直或对角线连接尽可能多的 globez。你也可以回溯。
让我们看看如何连接 globez。首先,不过,让我先向你介绍一个将在所有你的绘制匹配游戏中非常重要的变量:tolerance
。查看以下代码片段:
var fieldSize = 6;
var tileTypes = ["red","green","blue","grey","yellow"];
var tileSize = 50;
var tileArray = [];
var globezLayer;
var visitedTiles = [];
var startColor = null;
var tolerance = 400;
看看下面的截图:
假设玩家想要从下到上连接三个绿色 globez。当你用手指在小型表面上绘制,比如在手机上,也许是在火车上,你并不那么精确。所以,在左侧,我们可以看到如果我们在一个基于瓦片的游戏中检测到玩家移动会发生什么。不那么精确的绘制会触碰到五个不同的 globez,导致非法移动,这会让人感到沮丧。在右侧,使用容差,我们只有在瓦片中心附近检测到玩家移动。看出了区别吗?玩家只触碰到三个 globez,按照要求执行了合法移动。
我将中心到距离的平方——内白圆的半径——称为 tolerance
,在这个例子中,我将它设置为 20 像素 * 20 像素 = 400。
当以下条件满足时,我们可以说我们有一个合法的移动:
-
我们处于容差区域内
-
当前 globe 还没有被选中——
picked
属性是false
-
当前 globe 与最后一个选中的 globe 相邻
-
当前 globe 与第一个选中的 globe 颜色相同
转换为 Cocos2d-JS,这意味着 onMouseMove
函数将包含:
onMouseMove: function(event){
if(startColor!=null){
var currentRow = Math.floor(event._y / tileSize);
var currentCol = Math.floor(event._x / tileSize);
var centerX = currentCol * tileSize + tileSize / 2;
var centerY = currentRow * tileSize + tileSize / 2;
var distX = event._x - centerX;
var distY = event._y - centerY;
if(distX * distX + distY * distY < tolerance){
if(!tileArray[currentRow][currentCol].picked){
if(Math.abs(currentRow - visitedTiles[visitedTiles.length - 1].row) <= 1 && Math.abs(currentCol -visitedTiles[visitedTiles.length -1].col) <= 1){
if(tileArray[currentRow][currentCol].val==startColor){
tileArray[currentRow][currentCol].setOpacity(128);
tileArray[currentRow][currentCol].picked=true;
visitedTiles.push({
row:currentRow,
col:currentCol
});
}
}
}
}
}
}
看起来代码很多,但这只是前面提到的四个条件的表示。我想指出以下这一行:
if(distX * distX + distY * distY < tolerance){ … }
在这里,我正在应用勾股定理而不使用平方根,以节省 CPU 时间。
测试脚本,看看会发生什么:
现在,即使你的绘制不够精确,你也能选择 globez。现在,如果你改变主意,想要回溯并尝试另一条路线怎么办?
回溯
当你将鼠标移回倒数第二个 globe 时,你可以回溯你的选择。在这种情况下,最后一个 globe 将从 visitedTiles
数组中移除,并且 picked
属性和透明度都恢复到默认值:true
和 255
。
要检查回溯,你必须检查以下条件:
-
我们处于一个容差区域
-
当前 globe 已经被选中——
picked
属性为true
-
当前 globe 是
visitedTiles
数组的倒数第二个条目
这只是 onMouseMove
代码中的一个小改动:
onMouseMove: function(event){
if(startColor!=null){
// same as before
if(distX * distX + distY * distY < tolerance){
if(!tileArray[currentRow][currentCol].picked){
// same as before
}
else{
if(visitedTiles.length>=2 && currentRow == visitedTiles[visitedTiles.length - 2].row && currentCol == visitedTiles[visitedTiles.length - 2].col){
tileArray[visitedTiles[visitedTiles.length - 1].row][visitedTiles[visitedTiles.length - 1].col].setOpacity(255);
tileArray[visitedTiles[visitedTiles.length - 1].row][visitedTiles[visitedTiles.length - 1].col].picked=false;
visitedTiles.pop();
}
}
}
}
}
现在测试你的游戏,并尝试回溯。查看以下截图:
你看到这个了吗?现在,你可以改变主意,选择另一条路线。
现在还没结束,尽管我们已经完全处理了玩家的移动。
移除 globez
一句古老的谚语说,如果你不能移除物品,这不是一场匹配游戏。而且,这是正确的!一旦你选择了 globez,一旦你释放鼠标,你必须能够移除它们。
移除 globez 非常简单:一旦你知道 visitedTiles
数组至少有三个项目,只需从舞台和 tileArray
数组中移除这些项目。
这样修改 onMouseUp
:
onMouseUp: function(event){
startColor=null;
for(i = 0; i < visitedTiles.length; i ++){
if(visitedTiles.length<3){
tileArray[visitedTiles[i].row][visitedTiles[i].col].setOpacity(255);
tileArray[visitedTiles[i].row][visitedTiles[i].col].picked=false;
}
else{
globezLayer.removeChild (tileArray[visitedTiles[i].row][visitedTiles[i].col]);
tileArray[visitedTiles[i].row][visitedTiles[i].col]=null;
}
}
visitedTiles = [];
}
在使用 removeChild
从舞台物理移除已移除的 globez 后,在 tileArray
数组中将它们设置为 null
将在我们要补充棋盘时很有用。
现在,尝试游戏:
你看到了吗?我们移除了 globez。现在,游戏完成了。等等。不。一旦移除 globez,一些 globez 必须坠落,并且更多的 globez 必须从屏幕顶部出现以填充舞台。我们将使用缓动使它们轻轻坠落。
制作 globez 坠落
一旦移除 globez,你需要检查是否有空隙在它们下面,并相应地使它们坠落。
记住,与大多数其他语言不同,Cocos2d-JS 将原点 (0,0) 坐标设置在舞台的左下角,所以最低行是行零。
我们需要大量编辑 onMouseUp
:
onMouseUp: function(event){
startColor=null;
for(i = 0; i < visitedTiles.length; i ++){
if(visitedTiles.length<3){
tileArray[visitedTiles[i].row][visitedTiles[i].col].setOpacity(255);
tileArray[visitedTiles[i].row][visitedTiles[i].col].picked=false;
}
else{
globezLayer.removeChild(tileArray[visitedTiles[i].row][visitedTiles[i].col]);
tileArray[visitedTiles[i].row][visitedTiles[i].col]=null;
}
}
if(visitedTiles.length>=3){
for(i = 1; i < fieldSize; i ++){
for(j = 0; j < fieldSize; j ++){
if(tileArray[i][j] != null){
var holesBelow = 0;
for(var k = i - 1; k >= 0; k --){
if(tileArray[k][j] == null){
holesBelow++;
}
}
if(holesBelow>0){
var moveAction = cc.MoveTo.create(0.5, new cc.Point(tileArray[i][j].x,tileArray[i][j].y-holesBelow*tileSize));
// cc,moveTo() can also be used
tileArray[i][j].runAction(moveAction);
tileArray[i - holesBelow][j] = tileArray[i][j];
tileArray[i][j] = null;
}
}
}
}
}
visitedTiles = [];
}
运行脚本并查看会发生什么:
一旦绿色 globez 被移除,上面的 globez 会坠落。
让我们分析一下代码:
if(visitedTiles.length>=3){
一切都取决于我们是否选择了超过三个 globez。否则,不会有 globez 被移除,也就没有必要检查空位:
for(i = 1; i < fieldSize; i ++){
我们从 1
开始循环所有行,即倒数第二行,到 fieldsize
-1 行,这是最顶部的行。
for(j = 0; j < fieldSize; j ++){
对于列我们也做同样的事情,但这次我们扫描它们。
if(tileArray[i][j] != null){
如果在给定位置有一个 globe,那么是时候计算它下面的空位数量了。
var holesBelow = 0;
holesBelow
变量将跟踪地球下方的空位。
for(var k = i - 1; k >= 0; k --){
从当前行向下到第一行,我们必须计算空位数量。
if(tileArray[k][j] == null){
当其 tileArray
值为 null 时,我们找到空位。
holesBelow++;
在这种情况下,我们增加 holesBelow
变量:
if(holesBelow>0){
一旦循环完成,我们检查是否有洞:
var moveAction = cc.MoveTo.create(0.5, new cc.Point(tileArray[i][j].x,tileArray[i][j].y-holesBelow*tileSize));
tileArray[i][j].runAction(moveAction);
然后,相应地移动地球。
tileArray[i - holesBelow][j] = tileArray[i][j];
tileArray[i][j] = null;
最后,我们可以更新 tileArray
以注册地球的新位置。
现在我们成功地让 globez 下落,只剩下最后一件事要做——创建新的 globez 来再次填充舞台。
创建新的 globez
创建新的 globez 与创建 globez 下落有相同的概念。对于每一列,我们计算空位的数量;这个数量是我们必须创建的 globez 的数量。
为了创建平滑的外观,每个 globe 都将在舞台顶部之外创建,并通过动画缓动将其放置在正确的位置。
这是我们最后一次需要修改 onMouseUp
,保证!
onMouseUp: function(event){
// same as before
if(visitedTiles.length>=3){
// same as before
for(i = 0; i < fieldSize; i ++){
for(j = fieldSize-1; j>=0; j --){
if(tileArray[j][i] != null){
break;
}
}
var missingGlobes = fieldSize-1-j;
if(missingGlobes>0){
for(j=0;j<missingGlobes;j++){
this.fallTile(fieldSize-j-1,i,missingGlobes-j)
}
}
}
}
visitedTiles = [];
}
这是扫描空位并调用 fallTile
方法以创建新瓷砖的代码,该瓷砖具有目标行、目标列和下落高度。我们使用下落高度来创建一个平滑的缓动到地球最终位置。
下面是 fallTile
的定义:
fallTile:function(row,col,height){
var randomTile = Math.floor(Math.random()*tileTypes.length);
var spriteFrame = cc.spriteFrameCache.getSpriteFrame(tileTypes[randomTile]);
var sprite = cc.Sprite.createWithSpriteFrame(spriteFrame);
sprite.val = randomTile;
sprite.picked = false;
globezLayer.addChild(sprite,0);
sprite.setPosition(col*tileSize+tileSize/2,(fieldSize+height)*tileSize);
var moveAction = cc.MoveTo.create(0.5, new cc.Point(col*tileSize+tileSize/2,row*tileSize+tileSize/2));
sprite.runAction(moveAction);
tileArray[row][col] = sprite;
}
它真的很像几页前创建的 addTile
方法——这是一段漫长的旅程,不是吗?最终 Globez 成功了:
一旦你移除了一些 globez,新的 globez 将从顶部落下。
奖励 - 使用绘图 API 进行视觉反馈
正如承诺的那样,我们将使用绘图 API 为我们用鼠标绘制的路径提供视觉反馈。
首先,让我们创建一个新的全局变量来存储我们将绘制玩家移动的层。它被称为 arrowsLayer
:
var fieldSize = 6;
var tileTypes = ["red","green","blue","grey","yellow"];
var tileSize = 50;
var tileArray = [];
var globezLayer;
var arrowsLayer;
var visitedTiles = [];
var startColor = null;
var tolerance = 400;
我们将在 init
函数中在 globezLayer
之后创建和添加 arrowLayer
:
init:function () {
this._super();
cc.spriteFrameCache.addSpriteFrames("assets/globes.plist", "assets/globes.png");
var backgroundLayer = cc.LayerGradient.create(cc.color(0x00,0x22,0x22,255), cc.color(0x22,0x00,0x44,255));
this.addChild(backgroundLayer);
globezLayer = cc.Layer.create();
this.addChild(globezLayer)
arrowsLayer = cc.DrawNode.create();
// new cc.DrawNode() can also be used
this.addChild(arrowsLayer);
this.createLevel();
cc.eventManager.addListener(touchListener, this);
}
我们可以绘制的实体是 DrawNode
。
无论发生什么,当我们释放鼠标时,我们将使用 clear
方法清除绘图区域:
onMouseUp: function(event){
arrowsLayer.clear();
// same as before
}
现在,我们知道如何清除一个绘图节点,并且我们必须看看如何在其中绘制线条。当玩家执行合法移动时,添加一个新的方法调用,无论你是选择一个新的地球还是回溯:
onMouseMove: function(event){
if(startColor!=null){
// same as before
if(distX * distX + distY * distY < tolerance){
// same as before
this.drawPath();
}
}
}
现在唯一要做的就是创建 touchListener
监听器的 drawPath
方法:
drawPath:function(){
arrowsLayer.clear();
if(visitedTiles.length>0){
for(var i=1;i<visitedTiles.length;i++){
arrowsLayer.drawSegment(new cc.Point(visitedTiles[i-1].col*tileSize+tileSize/2,visitedTiles[i-1].row*tileSize+tileSize/2),new cc.Point(visitedTiles[i].col*tileSize+tileSize/2,visitedTiles[i].row*tileSize+tileSize/2), 4,cc.color(255, 255, 255, 255));
}
}
}
如您所见,我遍历 visitedTiles
数组,并使用 drawSegment
方法从第一个 cc.Point
参数绘制到第二个。
最后,你的游戏完成了。真的!!
看看你是如何用鼠标绘制线条,连接你选择的各个 Globez。
现在去哪里
通常,每个章节都以一个总结标题结束;无论如何,这次我认为你不需要总结。我的意思是,你已经制作了几个游戏,从注意力集中到推箱子,从无尽跑酷到 Globez。
首先,我想感谢你阅读整本书,我希望你阅读它的时候和我写作它的时候一样享受。
虽然你创建了几个游戏,但这只是进入跨平台游戏开发漫长旅程的开始。
首先,你应该通过添加声音、计分系统和一些其他功能来完善游戏,我相信你现在阅读这些最后几页时已经知道如何添加了。
然后,我只是想指出三个你可能觉得有用的网站,如果你想要深入研究跨平台 HTML5 开发。
保护你的代码
由于你的游戏是用 JavaScript 编写的,任何人都可以通过直接查看你页面的 HTML 来查看你的代码。有几个工具可以混淆你的代码,使其难以阅读——或者至少让代码偷窥者感到非常困难。我选择了其中两个:
-
JavaScript 混淆器 (
javascriptobfuscator.com/
):这是一个在线免费工具,用于混淆你的代码;只需复制并粘贴你的代码或上传小文件,它就会为你完成艰苦的工作。 -
JScrambler (
jscrambler.com
):这是我目前使用并推荐的。它为你提供了很多混淆级别、移动游戏优化、网站锁定、到期日期等功能。只需拖放你的项目并下载受保护的版本。
将你的游戏作为原生应用移植到移动设备上
一旦你的游戏在每个浏览器上运行,你可能会想将其转换为原生移动应用,以尝试征服新市场。有三个出色的工具允许你从 HTML、CSS 和 JavaScript 创建移动应用:
-
Cocos2D 专有 JSB API (
www.cocos2d-x.org/wiki/Basic_usage_of_JSB_API
):这是将 C++绑定到 JavaScript 的官方 API。从 Cocos2d-iphone 或 Cocos2d-x 项目开始,你可以让所有图形、渲染和物理代码以原生方式运行,而游戏逻辑则以 JavaScript 运行。 -
PhoneGap (
phonegap.com/
):这个应用既可以从你的电脑使用原生 SDK 运行,也可以从云端运行。PhoneGap 编译你的 HTML5 游戏,创建原生应用,准备在苹果应用商店等市场发布。我用它创建了 BWBan 的 iOS 版本 (itunes.apple.com/us/app/bwban/id783208885?mt=8
),从一个 HTML5 游戏开始。 -
CocoonJS (
www.ludei.com/cocoonjs/
): 这个平台为你提供了一个平台来测试、加速、部署和货币化你的 HTML5 应用和游戏,在所有移动设备上都有许多有趣的功能,帮助你更快地交付优秀的网络产品。
发布你的游戏
制作游戏很有趣,但让人们玩你的游戏更有趣。以下是两个顶级游戏门户,你应该上传你的游戏以获得播放、评分、评论和反馈:
-
Newgrounds (
www.newgrounds.com/
): 这个网页游戏门户收集了来自世界各地开发者的最佳独立音频、网络电影和游戏。 -
Kongregate (
www.kongregate.com/
): 这是我的最爱网页游戏门户;它还提供有趣的收益分成。
许可你的游戏
游戏发行商一直在寻找高质量的游戏,并愿意支付你许可他们使用和定制你的游戏。不幸的是,与它们取得联系并获得足够的关注并不容易。幸运的是,有一个服务为你做了这项艰苦的工作,向数百位潜在买家展示你的游戏:
- FGL (
www.fgl.com/
): 这是 HTML5、Android、iOS、Unity 和 Flash 行业领先的分发和货币化服务。多亏了 FGL,我获得了几个赞助。我强烈推荐与他们合作。
保持最新
HTML5 游戏市场是一个几乎每天都在变化的新市场。你需要保持最新,以了解新闻和趋势。这里有一个我强烈推荐的论坛:
- HTML5GameDevs (
www.html5gamedevs.com/
): 这是顶级 HTML5 游戏开发论坛,有文章、游戏发布、框架、演示、视频、教程、博客文章等。
很明显,我的博客www.emanueleferonato.com/
几乎每天都有新闻和教程更新。
最后,你可以参考官方 Cocos2d-JS 网站www.cocos2d-x.org/wiki/Cocos2d-JS
,在那里你可以找到所有新发布和正在开发的功能。
概述
在最后一章中,你使用这本书阅读过程中学到的几乎所有功能创建了一个完整的 Match-3 游戏原型。现在,你应该能够从头开始创建自己的游戏,将你的想法从纸笔转移到现代网络浏览器和移动设备上。