HTML5游戏开发示例全绝不原创的飞龙|汽车网页游戏_汽车_生活大百科
本书将向您展示如何使用最新的HTML5和CSS3网络标准来构建纸牌游戏,绘画游戏,物理游戏,甚至是通过网络的多人游戏。通过本书,您将通过清晰系统的教程构建六个示例游戏。
本书分为九章,每章专注于一个主题。我们将创建六个游戏,并具体学习如何绘制游戏对象,对它们进行动画处理,添加音频,连接玩家,并使用Box2D物理引擎构建物理游戏。
第二章,开始DOM游戏开发,通过在DOM和jQuery中创建传统的乒乓球游戏,启动游戏开发之旅。
第三章,在CSS3中构建记忆匹配游戏,介绍了CSS3的新功能,并讨论了如何在DOM和CSS3中创建记忆卡匹配游戏。
第四章,使用Canvas和绘图API构建解开游戏,介绍了一种在网页中绘制游戏并与之交互的新方法,使用新的Canvas元素。它还演示了如何使用Canvas构建解谜游戏。
第五章,构建Canvas游戏大师班,扩展了解开游戏,展示了如何使用Canvas绘制渐变和图像。它还讨论了精灵表动画和多层管理。
第六章,为您的游戏添加声音效果,通过使用“音频”元素向游戏添加声音效果和背景音乐。它讨论了网络浏览器之间的音频格式能力,并在本章末创建了一个键盘驱动的音乐游戏。
第七章,使用本地存储存储游戏数据,扩展了CSS3记忆匹配游戏,以演示如何使用新的本地存储API来存储和恢复游戏进度和最佳记录。
第八章,使用WebSockets构建多人画图猜词游戏,讨论了新的WebSocketsAPI,它允许浏览器与套接字服务器建立持久连接。这允许多个玩家实时一起玩游戏。本章末创建了一个画图猜词游戏。
您需要最新的现代网络浏览器,一个良好的文本编辑器,以及基本的HTML,CSS和JavaScript知识。
本书适用于具有HTML、CSS和JavaScript基本理解,并希望创建在浏览器上运行的Canvas或基于DOM的游戏的游戏设计师。
在本书中,您会经常看到几个标题。
为了清晰地说明如何完成一个过程或任务,我们使用:
指示通常需要一些额外的解释,以便理解,因此它们后面会跟着:
这个标题解释了您刚刚完成的任务或指示的工作原理。
您还会在本书中找到一些其他学习辅助工具,包括:
这些是简短的多项选择问题,旨在帮助您测试自己的理解。
这些设置了实际的挑战,并为您提供了尝试所学内容的想法。
您还会发现一些文本样式,用于区分不同类型的信息。以下是一些样式的示例,以及它们的含义解释。
文本中的代码单词显示如下:“我们将从index.html开始我们的HTML5游戏开发之旅。”
代码块设置如下:
functionsetupLevelData(){varnotes=audiogame.leveldata.split(";");//storethetotalnumberofdotsaudiogame.totalDotsCount=notes.length;for(variinnotes){varnote=notes[i].split(",");vartime=parseFloat(note[0]);varline=parseInt(note[1]);varmusicNote=newMusicNote(time,line);audiogame.musicNotes.push(musicNote);}}任何命令行输入或输出都以如下形式书写:
$./configure$sudomakeinstall新术语和重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会出现在文本中,就像这样:“您将获得多用户草图板的介绍页面。右键单击启动实验选项,然后选择在新窗口中打开链接”。
警告或重要说明会出现在这样的框中。
提示和技巧会出现在这样。
层叠样式表(CSS)定义了网页的视觉呈现方式。它为所有HTML元素和它们的状态(如悬停和激活)定义样式。
JavaScript是网页的逻辑控制器。它使网页动态化,并在页面和用户之间提供客户端交互。它通过文档对象模型(DOM)访问HTML。它通过应用不同的CSS样式来重新设计HTML元素。
这三个功能为我们带来了新的游戏市场,HTML5游戏。有了它们的新力量,我们可以使用HTML5元素、CSS3属性和JavaScript设计游戏在浏览器中玩耍。
在本章中,我们将:
所以让我们开始吧。
HTML5和CSS3中引入了许多新功能。在我们开始创建游戏之前,让我们概览一下这些新功能,看看我们如何使用它们来创建游戏。
Canvas是HTML5元素,提供低级别的绘制形状和位图操作功能。我们可以将Canvas元素想象成一个动态图像标签。传统的
标签显示静态图像。无论图像是动态生成的还是静态加载自服务器,图像都是静态的,不会改变。我们可以将
标签更改为另一个图像源,或者对图像应用样式,但我们无法修改图像的位图上下文本身。
另一方面,Canvas就像是一个客户端动态的
标签。我们可以在其中加载图像,在其中绘制形状,并通过JavaScript与之交互。
背景音乐和音效通常是游戏设计中的重要元素。HTML5通过audio标签提供了本地音频支持。由于这一功能,我们不需要专有的FlashPlayer来播放HTML5游戏中的音效。我们将在第六章讨论audio标签的用法,使用HTML5音频元素构建音乐游戏。
地理位置让网页获取用户计算机的纬度和经度。多年前,当每个人都在使用台式电脑上网时,这个功能可能并不那么有用。我们并不需要用户的道路级别的位置精度。我们可以通过分析IP地址获得大致位置。
如今,越来越多的用户使用强大的智能手机上网。Webkit和其他现代移动浏览器都在每个人的口袋里。地理位置让我们设计移动应用程序和游戏,以便使用位置信息。
WebGL通过在Web浏览器中提供一组3D图形API来扩展Canvas元素。该API遵循OpenGLES2.0的标准。WebGL为3DHTML5游戏提供了一个真正的3D渲染场所。然而,在撰写本书时,并非所有浏览器都原生支持WebGL。目前,只有MozillaFirefox4、GoogleChrome和WebKit浏览器的夜间构建版本原生支持它。
为WebGL创建游戏的技术与通常的HTML5游戏开发有很大不同。在WebGL中创建游戏需要处理3D模型,并使用类似于OpenGL的API。因此,本书不会讨论WebGL游戏开发。
HTML5为Web浏览器提供了持久的数据存储解决方案。
本地存储可以持久地存储键值对数据。即使浏览器终止,数据仍然存在。此外,数据不仅限于只能由创建它的浏览器访问。它对具有相同域的所有浏览器实例都是可用的。由于本地存储,我们可以在Web浏览器中轻松地本地保存游戏状态,如进度和获得成就。
HTML5还提供了WebSQL数据库。这是一个客户端关系数据库,目前受Safari、Chrome和Opera支持。通过数据库存储,我们不仅可以存储键值对数据,还可以支持SQL查询的复杂关系结构。
本地存储和WebSQL数据库对我们在创建游戏时本地保存游戏状态非常有用。
除了本地存储,一些其他存储方法现在也得到了Web浏览器的支持。这些包括WebSQL数据库和IndexedDB。这些方法支持根据条件查询存储的数据,因此更适合支持复杂的数据结构。
通过缓存清单,我们可以将所有游戏图形、游戏控制JavaScript文件、CSS样式表和HTML文件本地存储。我们可以将我们的HTML5游戏打包成桌面或移动设备上的离线游戏。玩家甚至可以在飞行模式下玩游戏。
CSS是演示层,HTML是内容层。它定义了HTML的外观。在使用HTML5创建游戏时,尤其是基于DOM的游戏,我们不能错过CSS。我们可能纯粹使用JavaScript来创建和设计带有Canvas元素的游戏。但是在创建基于DOM的HTML5游戏时,我们需要CSS。因此,让我们看看CSS3中有什么新内容,以及如何使用新属性来创建游戏。
新的CSS3属性让我们可以以不同的方式在DOM中进行动画,而不是直接在Canvas绘图板上绘制和交互。这使得可以制作更复杂的基于DOM的浏览器游戏。
传统上,当我们对元素应用新样式时,样式会立即更改。CSS3过渡在目标元素的样式更改期间应用插值。
例如,我们这里有一个蓝色的框,当我们鼠标悬停时想要将其变为红色。我们将使用以下代码片段:
HTML:
a.box{-webkit-transition:all5slinear;}提示下载本书的示例代码
以下屏幕截图显示了应用过渡的框悬停效果:
由于CSS3规范仍处于草案阶段,尚未确定,因此来自不同浏览器供应商的实现可能与W3C规范有一些细微差异。因此,浏览器供应商倾向于使用供应商前缀来实现其CSS3属性,以防止冲突。
我将在大多数示例中只使用-webkit-前缀,以防止在书中放置太多相似的行。更重要的是理解概念,而不是在这里阅读带有不同供应商前缀的相同规则。
CSS3变换让我们可以缩放元素,旋转元素和平移它们的位置。CSS3变换分为2D和3D。
我们可以用translate重新定位一个元素:
-webkit-transform:translate(x,y);或者使用缩放变换来缩放元素:
-webkit-transform:scale(1.1);我们还可以使用CSS3变换来缩放和旋转元素,并结合其他变换:
a.box{-webkit-transition:all0.5slinear;-webkit-transform:translate(100px,50px);}a.box:hover{-webkit-transform:translate(100px,50px)scale(1.1)rotate(30deg);}以下屏幕截图显示了当我们鼠标悬停时CSS3变换效果:
CSS3动画是更进一步的一步。我们可以定义动画的关键帧。每个关键帧包含应在该时刻更改的一组属性。这就像一组应用于目标元素的CSS3过渡的序列。
我们探索了HTML5和CSS3的一些关键新功能。有了这些功能,我们可以在浏览器上创建HTML5游戏。但是为什么我们需要这样做呢?创建HTML5游戏有什么好处呢?
在现代浏览器中原生支持所有这些功能,我们不需要用户预先安装任何第三方插件才能进行游戏。这些插件不是标准的。它们是专有的,通常需要额外的插件安装,我们可能无法安装。
在传统的游戏设计中,我们在一个边界框内构建游戏。我们在电视上玩视频游戏。我们在网页浏览器中玩Flash游戏,有一个矩形边界。
由于HTML5和CSS3的新功能,我们现在可以在浏览器中创建整个游戏。我们可以控制DOM中的每个元素。我们可以使用CSS3对每个文档对象进行动画处理。我们有Canvas来动态绘制和与之交互。我们有音频元素来处理背景音乐和音效。我们还有本地存储来保存游戏数据和WebSocket来创建实时多人游戏。大多数现代浏览器已经支持这些功能。现在是时候制作HTML5游戏了。
通过观察使用不同技术制作的其他HTML5游戏,我们有机会研究不同HTML5游戏的表现。
来自瑞典的网页设计师Erik创建了一个有趣的书签。它是一个适用于任何网页的类似小行星的游戏。是的,任何网页。它展示了与任何网页进行交互的一种异常方式。它在您正在阅读的网站上创建了一个飞机。然后您可以使用箭头键驾驶飞机,并使用空格键发射子弹。有趣的是,子弹会摧毁页面上的HTML元素。您的目标是摧毁您选择的网页上的所有东西。这个书签是打破通常浏览器游戏界限的又一个例子。它告诉我们,在设计HTML5游戏时,我们可以打破常规思维。
以下的截图显示了飞机摧毁网页内容的情况:
谷歌演示了第一人称射击游戏Quake2的WebGLHTML5移植版。玩家可以使用WSAD键四处移动,并用鼠标射击敌人。玩家甚至可以通过WebSocket实时进行多人游戏。据谷歌称,HTML5Quake2的每秒帧数可以达到60帧。
AvesEngine是由dextrose开发的HTML5游戏开发框架。它为游戏开发者提供了工具和API,用于构建自己的等距浏览器游戏世界和地图编辑器。从官方演示视频中捕获的以下截图显示了它是如何创建等距世界的:
AvesEngine的视频演示可在YouTube上通过以下链接观看:
这些例子只是其中的一部分。以下网站提供了由他人创建的HTML5游戏的更新:
在接下来的章节中,我们将构建六款游戏。我们将首先创建一个基于DOM的乒乓球游戏,可以由同一台机器上的两名玩家进行游戏。然后我们将创建一个带有CSS3动画的记忆匹配游戏。之后,我们将使用Canvas创建一个解开谜题的游戏。接下来,我们将使用音频元素创建一个音乐游戏。然后,我们将使用WebSocket创建一个多人绘画和猜谜游戏。最后,我们将使用Box2DJavaScript端口创建一个物理汽车游戏的原型。以下截图是我们将在第三章中构建的记忆匹配游戏的截图,在CSS3中构建记忆匹配游戏
在本章中,我们学到了关于HTML5游戏的基本信息。
具体来说,我们涵盖了:
现在我们已经了解了一些关于HTML5游戏的背景信息,我们准备在下一章中创建我们的第一个基于DOM的JavaScript驱动游戏。
在第一章“介绍HTML5游戏”中,我们已经对整本书要学习的内容有了一个概念。从本章开始,我们将经历许多通过实践学习的部分,并且我们将在每个部分专注于一个主题。在深入研究尖端的CSS3动画和HTML5Canvas游戏之前,让我们从传统的基于DOM的游戏开发开始。在本章中,我们将用一些基本技术热身。
以下屏幕截图显示了本章结束后我们将获得的游戏。这是一个由两名玩家同时使用一个键盘玩的乒乓球游戏:
所以,让我们开始制作我们的乒乓球。
开发HTML5游戏的环境类似于设计网站。我们需要具有所需插件的Web浏览器和一个好的文本编辑器。哪个文本编辑器好是一个永无止境的争论。每个文本编辑器都有其自身的优势,所以只需选择您喜欢的即可。对于浏览器,我们将需要一个支持最新HTML5、CSS3规范并为我们提供方便的调试工具的现代浏览器。
每个网站、网页和HTML5游戏都以默认的HTML文档开始。此外,文档以基本的HTML代码开始。我们将从index.html开始我们的HTML5游戏开发之旅。
我们将从头开始创建我们的HTML5乒乓球游戏。这可能听起来好像我们要自己准备所有的东西。幸运的是,至少我们可以使用一个JavaScript库来帮助我们。jQuery是我们将在整本书中使用的JavaScript库。它将帮助我们简化我们的JavaScript逻辑:
我们刚刚创建了一个基本的带有jQuery的HTML5页面,并确保jQuery已正确加载。
在HTML5中,DOCTYPE和meta标签被简化了。
简化也适用于meta标签。现在我们可以使用以下简短的行来定义HTML的字符集:
页眉和页脚HTML5带来了许多新功能和改进,其中之一就是语义。HTML5添加了新元素来改进语义。我们刚刚使用了两个,header和footer。Header为部分或整个页面提供了标题介绍。因此,我们将h1标题放在header内。Footer与其名称相同,包含了部分或整个页面的页脚信息。
语义HTML意味着标记本身提供了有意义的信息,而不仅仅定义了视觉外观。
我们将JavaScript代码放在所有页面内容之后和
标签之前。之所以将代码放在那里而不是放在
部分内,是有原因的。
通常,浏览器会从顶部到底部加载内容并呈现它们。如果将JavaScript代码放在head部分,那么直到所有JavaScript代码加载完毕,文档的内容才会被加载。实际上,如果浏览器在页面中间加载JavaScript代码,所有呈现和加载都将被阻塞。这就是为什么我们希望尽可能将JavaScript代码放在底部的原因。这样,我们可以以更高的性能提供内容。
在撰写本书时,最新的jQuery版本是1.4.4。这就是为什么我们代码示例中的jQuery文件被命名为jquery-1.4.4.min.js。这个版本号会有所不同,但使用方式应该是相同的,除非jQuery发生了没有向后兼容的重大变化。
我们需要确保页面在执行我们的JavaScript代码之前已经准备就绪。否则,当我们尝试访问尚未加载的元素时,可能会出现错误。jQuery为我们提供了一种在页面准备就绪后执行代码的方法。以下是代码:
jQuery(document).ready(function(){//codehere.});实际上,我们刚刚使用的是以下代码:
$(function(){//codehere.});$符号是jQuery的快捷方式。当我们调用$(something)时,实际上是在调用jQuery(something)。
$(function_callback)是ready事件的另一个快捷方式。
这与以下内容相同:
$(document).ready(function_callback);同样,与以下内容相同:
jQuery(document).ready(function_callback);快速测验a.在
标签之后d.在
标签之前
我们已经准备好了准备工作,现在是设置乒乓球游戏的时候了。
我们在乒乓球比赛中放了两个球拍和一个球。我们还使用jQuery来初始化两个球拍的位置。
jQuery是一个为了轻松浏览DOM元素、操作它们、处理事件和创建异步远程调用而设计的JavaScript库。
它包含两个主要部分:选择和修改。选择使用CSS选择器语法在网页中选择所有匹配的元素。修改操作修改所选元素,例如添加、删除子元素或样式。使用jQuery通常意味着将选择和修改操作链接在一起。
例如,以下代码选择所有具有box类的元素并设置CSS属性:
$(".box").css({"top":"100px","left":"200px"});理解基本的jQuery选择器jQuery是关于选择元素并对其执行操作。我们需要一种方法来在整个DOM树中选择我们需要的元素。jQuery借用了CSS的选择器。选择器提供一组模式来匹配元素。以下表列出了我们在本书中将使用的最常见和有用的选择器:
jQuerycss是一个用于获取和设置所选元素的CSS属性的函数。
这是如何使用css函数的一般定义:
.css(propertyName).css(propertyName,value).css(map)css函数接受以下表中列出的几种类型的参数:
使用jQuery而不是纯JavaScript有几个优点,如下所示:
我们用jQuery初始化了球拍游戏元素。我们将进行一个实验,看看如何使用jQuery来放置游戏元素。
让我们用网格背景检查一下我们的乒乓球游戏元素:
我们通过放置一个名为pixel_grid.jpg的图像来开始示例。这是我为了方便调试而创建的图像。图像被分成小网格。每个10x10的网格形成一个100x100像素的大块。通过将这个图像作为DIV的背景,我们放置了一个标尺,使我们能够测量其子DIV在屏幕上的位置。
当一个DOM节点被设置为absolute位置时,left和top属性可以被视为坐标。我们可以将left/top属性视为X/Y坐标,Y正方向向下。以下图表显示了它们之间的关系。左侧是实际的CSS值,右侧是我们在编程游戏时的坐标系:
默认情况下,left和top属性是指网页的左上角。当这个DOM节点的任何父节点都显式设置了position样式时,这个参考点就会不同。left和top属性的参考点变成了那个父节点的左上角。
这就是为什么我们需要将游乐场设置为相对位置,所有游戏元素都在绝对位置内。我们示例中的以下代码片段显示了它们的位置值:
#playground{position:relative;}#ball{position:absolute;}.paddle{position:absolute;}小测验a.$("#header")
b.$(".header")
c.$("header")
d.$(header)
这本书是关于游戏开发的。我们可以将游戏开发看作是以下循环:
在之前的章节中,我们学会了如何用CSS和jQuery显示游戏对象。接下来我们需要在游戏中获取玩家的输入。在本章中我们将讨论键盘输入。
我们将创建一个传统的乒乓球游戏。左右两侧有两个球拍。球放在操场的中间。玩家可以通过使用w和s键来控制左球拍的上下移动,使用箭头上和下键来控制右球拍。我们将专注于键盘输入,将球的移动留到后面的部分:
让我们看看我们刚刚使用的HTML代码。HTML页面包含页眉、页脚信息和一个ID为game的DIV。游戏节点包含一个名为playground的子节点。playground包含三个子节点,两个球拍和一个球。
我们通常通过准备一个结构良好的HTML层次结构来开始HTML5游戏开发。HTML层次结构帮助我们将类似的游戏对象(即一些DIV)分组在一起。这有点像在AdobeFlash中将资产分组到电影剪辑中,如果你以前用过它制作动画的话。我们也可以将其视为游戏对象的图层,以便我们可以轻松地选择和样式化它们。
键盘上的每个键都被分配一个数字。通过获取该数字,我们可以找出按下了哪个键。我们监听jQuery的keydown事件监听器。事件触发时,event对象包含键码。我们可以通过调用which函数来获取按下键的键码。
您可以尝试在keydown事件监听器中添加一个控制台日志函数,并观察每个键的表示整数:
$(document).keydown(function(e){console.log(e.which);keyboardinputkeyboardinputkeycode});使常量更易读在我们的例子中,我们使用键码来检查玩家是否按下我们感兴趣的键。以箭头上键为例。它的键码是38。我们可以简单地将键码与数字直接进行比较,如下所示:
$(document).keydown(function(e){switch(e.which){case38://dosomethingwhenpressedarrow-up}}然而,这并不是一种推荐的做法,因为它使游戏代码更难以维护。想象一下,如果以后我们想要将动作从箭头上键映射到另一个键。我们可能不确定38是否表示箭头上。相反,我们可以使用以下代码为常量赋予一个有意义的名称:
在大多数情况下,我们通过使用格式如100px来将左侧和顶部的CSS样式应用于DOM元素。在设置属性时,我们指定单位。当获取属性的值时也是一样的。当我们调用$("#paddleA").css("top")时,我们得到的值是100px而不是100。这在我们想要对该值进行算术运算时会给我们带来问题。
在这个例子中,我们想通过将球拍的top属性设置为其当前位置减去五个像素来将球拍移动到上方。假设球拍A现在的top属性设置为100px。如果我们使用以下表达式来添加五个像素,它会失败并返回100px5:
$("#paddleA").css("top")+5这是因为JavaScript执行了css函数并得到了"100px"。然后它将"5"附加到"100px"字符串上。
在进行任何数学运算之前,我们需要一种方法来转换"100px"字符串。
JavaScript为我们提供了parseInt函数。
这是如何使用parseInt函数的一般定义:
parseInt(string,radix)parseInt函数需要一个必需参数和一个可选参数:
你还应该知道,你可以通过直接在控制台窗口中输入JavaScript表达式来执行JavaScript表达式。控制台窗口是GoogleChrome开发者工具中的一个工具。(其他浏览器中也有类似的工具)。我们可以通过点击扳手图标|工具|开发者工具|控制台来打开控制台。
这是一个方便的方法,在开发过程中,当你不确定一个简单表达式是否有效时,可以快速测试一下。以下截图测试了两个parseInt表达式的返回值:
在控制台面板中直接执行JavaScript表达式
有时将字符串转换为整数可能会很棘手。你知道10秒20的parseInt结果是什么吗?10x10和$20.5呢?
现在是时候打开控制台面板,尝试将一些字符串转换为数字。
我们现在正在编写更复杂的逻辑代码。在开发者工具的控制台上保持警惕是一个好习惯。如果代码中包含任何错误或警告,错误消息将会出现在那里。它报告发现的任何错误以及包含错误的代码行。在测试HTML5游戏时,保持控制台窗口打开非常有用和重要。我曾经看到很多人因为代码不起作用而束手无策。原因是他们有拼写错误或语法错误,而他们在与代码搏斗数小时后才检查控制台窗口。
以下截图显示了html5games.pingpong.js文件的第25行存在错误。错误消息是赋值中的无效左侧。检查代码后,我发现我在设置jQuery中的CSStop属性时错误地使用了等号(=):
以前的输入方法只允许一次输入。键盘输入也不太顺畅。现在想象一下,两个玩家一起玩乒乓球游戏。他们无法很好地控制球拍,因为他们的输入会干扰对方。在本节中,我们将修改我们的代码,使其支持多个键盘输入。
我们将使用另一种方法来处理按键按下事件。这种方法会更加顺畅,并支持同时进行多个输入:
vara=0;varb="xyz";functionsomething(){varc=1;}由于全局变量在整个文档中都可用,如果我们将不同的JavaScript库集成到网页中,可能会增加变量名冲突的可能性。作为良好的实践,我们应该将所有使用的全局变量放入一个对象中。
varpingpong={}pingpong.pressedKeys=[];将来,我们可能需要更多的全局变量,我们将把它们全部放在pingpong对象中。这样可以将名称冲突的机会减少到只有一个名称,pingpong。
按下的键存储在数组中,我们有一个定时器定期循环和检查数组。这可以通过JavaScript中的setInterval函数来实现。
以下是setInterval函数的一般定义:
setInterval(expression,milliseconds)setInterval接受两个必需的参数:
在游戏循环中,我们将执行几个常见的事情:
在游戏循环中实际执行的内容因不同类型的游戏而异,但目的是相同的。游戏循环定期执行,以帮助游戏平稳运行。
现在想象一下,我们可以使小红球在操场上移动。当它击中球拍时,球会弹开。当球通过球拍并击中球拍后面的操场边缘时,玩家将失去得分。所有这些操作都是通过jQuery在HTML页面中操纵DIV的位置。要完成这个乒乓球游戏,我们的下一步是移动球。
我们刚刚学习并使用了setInterval函数来创建一个定时器。我们将使用定时器每30毫秒移动球一点。当球击中操场边缘时,我们还将改变球运动的方向。现在让球动起来:
pingpong.ball={speed:5,x:150,y:100,directionX:1,directionY:1}functiongameloop(){moveBall();movePaddles();}functionmoveBall(){//referenceusefulvariablesvarplaygroundHeight=parseInt($("#playground").height());varplaygroundWidth=parseInt($("#playground").width());varball=pingpong.ball;//checkplaygroundboundary//checkbottomedgeif(ball.y+ball.speed*ball.directionY>playgroundHeight){ball.directionY=-1;}//checktopedgeif(ball.y+ball.speed*ball.directionY<0){ball.directionY=1;}//checkrightedgeif(ball.x+ball.speed*ball.directionX>playgroundWidth){ball.directionX=-1;}//checkleftedgeif(ball.x+ball.speed*ball.directionX<0){ball.directionX=1;}ball.x+=ball.speed*ball.directionX;ball.y+=ball.speed*ball.directionY;//checkmovingpaddlehere,later.//actuallymovetheballwithspeedanddirection$("#ball").css({"left":ball.x,"top":ball.y});}刚刚发生了什么?我们刚刚成功地使球在操场上移动。我们有一个循环,每30毫秒运行一次常规游戏逻辑。在游戏循环中,我们每次移动球五个像素。
球的三个属性是速度和方向X/Y。速度定义了球在每一步中移动多少像素。方向X/Y要么是1,要么是-1。我们用以下方程移动球:
new_ball_x=ball_x_position+speed*direction_xnew_ball_y=ball_y_position+speed*direction_y方向值乘以移动。当方向为1时,球向轴的正方向移动。当方向为-1时,球向负方向移动。通过切换X和Y方向,我们可以使球在四个方向上移动。
我们将球的X和Y与操场DIV元素的四个边缘进行比较。这将检查球的下一个位置是否超出边界,然后我们在1和-1之间切换方向以创建弹跳效果。
在上一节中移动球时,我们已经检查了操场的边界。现在我们可以用键盘控制球拍并观察球在操场上移动。现在还缺少什么?我们无法与球互动。我们可以控制球拍,但球却像它们不存在一样穿过它们。这是因为我们错过了球拍和移动球之间的碰撞检测。
我们将使用类似的方法来检查碰撞的边界:
我们已经修改了球的检查,使其在与球拍重叠时弹开。此外,当击中左右边缘时,我们将球重新定位到操场的中心。
让我们看看如何检查球和左球拍之间的碰撞。
首先,我们检查球的X位置是否小于左球拍的右边缘。右边缘是left值加上球拍的width。
然后,我们检查球的Y位置是否在球拍的顶部边缘和底部边缘之间。顶部边缘是top值,底部边缘是top值加上球拍的height。
如果球的位置通过了两个检查,我们就会将球弹开。这就是我们检查它的方式,这只是一个基本的碰撞检测。
我们通过检查它们的位置和宽度/高度来确定这两个对象是否重叠。这种类型的碰撞检测对于矩形对象效果很好,但对于圆形和其他形状则不太好。以下截图说明了问题。以下图中显示的碰撞区域是误报。它们的边界框碰撞了,但实际形状并没有重叠。
对于特殊形状,我们将需要更高级的碰撞检测技术,我们将在后面讨论。
我们检查球拍的三个边缘,以确定球是否与它们重叠。如果你玩游戏并仔细观察球的弹跳,你会发现现在它并不完美。球可能会在球拍后面弹跳。思考原因,并修改代码以实现更好的球和球拍的碰撞检测。
我们在前面的部分实现了基本的游戏机制。我们的乒乓球游戏现在缺少一个计分板,可以显示两名玩家的得分。我们讨论了如何使用jQuery来修改所选元素的CSS样式。我们是否也可以用jQuery来改变所选元素的内容?是的,我们可以。
我们将创建一个基于文本的计分板,并在任一玩家得分时更新得分:
我们刚刚使用了另一个常见的jQuery函数:html()来动态改变游戏内容。
html()函数获取或更新所选元素的HTML内容。以下是html()函数的一般定义:
.html().html(htmlString)当我们使用html()函数时,如果没有参数,它会返回第一个匹配元素的HTML内容。如果带有参数使用,它会将HTML内容设置为所有匹配元素的给定HTML字符串。
例如,提供以下HTML结构:
MynameisMakzan.
Mypet'snameis以下两个jQuery调用都返回Makzan:
$("#myname").html();//returnsMakzan$(".name").html();//returnsMakzan然而,在下面的jQuery调用中,它将所有匹配的元素设置为给定的HTML内容:
$(".name").html("Mr.Mystery")执行jQuery命令会产生以下HTML结果:
MynameisMr.Mystery
Mypet'snameisMr.Mystery
英雄尝试赢得比赛我们现在有了得分。看看你是否可以修改游戏,使其在任何玩家得到10分后停止。然后显示一个获胜消息。
您可能还想尝试对游戏进行样式设置,使其更具吸引力。给记分牌和游乐场添加一些图像背景怎么样?用两个守门员角色替换球拍?
在本章中,我们学到了许多关于使用HTML5和JavaScript创建简单乒乓球游戏的基本技术。
我们还讨论了如何创建游戏循环并移动球和球拍。
现在我们已经通过创建一个简单的基于DOM的游戏来热身,我们准备使用CSS3的新功能创建更高级的基于DOM的游戏。在下一章中,我们将创建具有CSS3动画、过渡和变换的游戏。
CSS3引入了许多令人兴奋的功能。在本章中,我们将探索并使用其中一些功能来创建匹配记忆游戏。CSS3样式显示游戏对象的外观和动画,而jQuery库帮助我们定义游戏逻辑。
所以让我们继续吧。
让我们看一些例子来理解它。
在这个例子中,我们将在网页上放置两张扑克牌,并将它们转换到不同的位置、比例和旋转。我们将通过设置过渡来缓和变换:
我们刚刚通过使用CSS3过渡来创建了两个动画效果,以调整transform属性。
请注意,新的CSS3过渡和变换属性尚未最终确定。Web浏览器支持这些起草但稳定的属性,并带有供应商前缀。在我们的示例中,为了支持Chrome和Safari,我们使用了-webkit-前缀。我们可以在代码中使用其他前缀来支持其他浏览器,例如为Mozilla使用-moz-,为Opera使用-o-。
这是CSS变换的用法:
transform:transform-function1transform-function2;transform属性的参数是函数。有两组函数,2Dtransform函数和3D。CSStransform函数旨在移动、缩放、旋转和扭曲目标DOM元素。以下显示了变换函数的用法。
2Drotate函数按给定的正参数顺时针旋转元素,并按给定的负参数逆时针旋转:
rotate(angle)translate函数通过给定的X和Y位移移动元素:
translate(tx,ty)我们可以通过调用translateX和translateY函数来独立地沿X或Y轴进行平移,如下所示:
translateX(number)translateY(number)scale函数按给定的sx,sy向量缩放元素。如果我们只传递第一个参数,那么sy将与sx的值相同:
scale(sx,sy)此外,我们可以独立地按如下方式缩放X和Y轴:
scaleX(number)scaleY(number)3D变换函数3D旋转功能通过给定的[x,y,z]单位向量在3D空间中旋转元素。例如,我们可以使用rotate3d(0,1,0,60deg)将Y轴旋转60度:
rotate3d(x,y,z,angle)我们还可以通过调用以下方便的函数仅旋转一个轴:
rotateX(angle)rotateY(angle)rotateZ(angle)与2Dtranslate函数类似,translate3d允许我们在所有三个轴上移动元素:
translate3d(tx,ty,tz)translateX(tx)translateY(ty)translateZ(tz)此外,scale3d在3D空间中缩放元素:
scale3d(sx,sy,sz)scaleX(sx)scaleY(sy)scaleZ(sz)我们刚讨论的transform函数是常见的,我们会多次使用它们。还有一些其他未讨论的transform函数。它们是matrix,skew和perspective。
CSS3中有大量新功能。过渡模块是其中之一,对我们在游戏设计中影响最大。
什么是CSS3过渡?W3C用一句话解释了它:
这是transition属性的用法:
transition:opacity0.3s,background-color0.5s我们还可以使用以下属性单独定义每个过渡属性:
transition-property,transition-duration,transition-timing-function和transition-delay。
CSS3模块
根据W3C,CSS3不同于CSS2.1,因为CSS2.1只有一个规范。CSS3分为不同的模块。每个模块都会单独进行审查。例如,有过渡模块,2D/3D变换模块和弹性盒布局模块。
将规范分成模块的原因是因为CSS3的每个部分的工作进度不同。一些CSS3功能相当稳定,例如边框半径,而有些尚未定型。通过将整个规范分成不同的部分,它允许浏览器供应商支持稳定的模块。在这种情况下,缓慢的功能不会减慢整个规范。CSS3规范的目标是标准化网页设计中最常见的视觉用法,而这个模块符合这个目标。
我们已经翻译,缩放和旋转了扑克牌。在示例中尝试更改不同的值怎么样?rotate3d函数中有三个轴。如果我们旋转其他轴会发生什么?通过自己尝试代码来熟悉变换和过渡模块。
现在想象一下,我们不仅仅是移动纸牌,而且还想翻转卡片元素,就像我们翻转真正的纸牌一样。通过使用rotationtransform函数,现在可以创建翻牌效果。
当我们点击纸牌时,我们将开始一个新项目并创建一个翻牌效果:
我们已经创建了一个通过鼠标单击切换的翻牌效果。该示例利用了几个CSS变换属性和JavaScript来处理鼠标单击事件。
当鼠标单击卡片时,我们将card-flipped类应用于卡片元素。第二次单击时,我们希望删除已应用的card-flipped样式,以便卡片再次翻转。这称为切换类样式。
jQuery为我们提供了一个方便的函数,名为toggleClass,可以根据类是否应用来自动添加或删除类。
要使用该函数,我们只需将要切换的类作为参数传递。
例如,以下代码向具有IDcard1的元素添加或删除card-flipped类:
$("#card1").toggleClass("card-flipped");toggleClass函数接受一次切换多个类。我们可以传递多个类名,并用空格分隔它们。以下是同时切换两个类的示例:
$("#card1").toggleClass("card-flippedscale-up");通过z-index控制重叠元素的可见性通常,网页中的所有元素都是分布和呈现而不重叠的。设计游戏是另一回事。我们总是需要处理重叠的元素并有意隐藏它们(或其中的一部分)。Z-index是CSS2.1属性,帮助我们控制多个重叠元素时的可见性行为。
在这个例子中,每张卡片有两个面,正面和背面。两个面放在完全相同的位置。它们彼此重叠。Z-index属性定义了哪个元素在顶部,哪个在后面。具有较高z-index的元素在较低z-index的元素前面。当它们重叠时,具有较高z-index的元素将覆盖具有较低z-index的元素。以下截图演示了z-index的行为:
在翻牌示例中,我们交换了两个面的z-index,以确保对应的面在正常状态和翻转状态下都在另一个面的上方。以下代码显示了交换。
在正常状态下,正面的z-index较高:
.front{z-index:10;}.back{z-index:8;}在翻转状态下,正面的z-index变为低于背面的z-index。现在背面覆盖了正面:
.card-flipped.front{z-index:8;}.card-flipped.back{z-index:10;}介绍CSS透视属性CSS3让我们能够以3D形式呈现元素。我们已经能够在3D空间中转换元素。perspective属性定义了3D透视视图的外观。您可以将值视为您观察对象的距离。您越近,观察对象的透视失真就越大。
在撰写本书时,只有Safari支持3D透视功能。Chrome支持3D变换,但不支持perspective属性。因此,在Safari中效果最佳,在Chrome中效果也可以接受。
以下两个3D立方体演示了不同的透视值如何改变元素的透视视图:
您可以在Safari中输入以下地址查看此实验:
立方体是通过将六个面放在一起,并对每个面应用3D变换来创建的。它使用了我们讨论过的技术。尝试创建一个立方体并尝试使用perspective属性进行实验。
以下网页对创建CSS3立方体进行了全面的解释,并讨论了通过键盘控制立方体的旋转:
在引入backface-visibility之前,页面上的所有元素都向访问者展示它们的正面。实际上,元素的正面或背面没有概念,因为这是唯一的选择。而CSS3引入了三个轴的旋转,我们可以旋转一个元素,使其正面朝后。试着看看你的手掌并旋转你的手腕,你的手掌转过来,你看到了手掌的背面。这也发生在旋转的元素上。
CSS3引入了一个名为backface-visibility的属性,用于定义我们是否可以看到元素的背面。默认情况下,它是可见的。以下截图演示了backface-visibility属性的两种不同行为。
在撰写本书时,只有AppleSafari支持backface-visibility属性。
我们已经学习了一些CSS基本技术。让我们用这些技术制作一个游戏。我们将制作一款纸牌游戏。纸牌游戏利用变换来翻转纸牌,过渡来移动纸牌,JavaScript来控制逻辑,以及一个名为自定义数据属性的新HTML5功能。别担心,我们将逐步讨论每个组件。
在翻牌示例中,我们使用了两种不同的扑克牌图形。现在我们准备整副扑克牌的图形。尽管我们在匹配游戏中只使用了六张扑克牌,但我们准备整副扑克牌,这样我们可以在可能创建的其他扑克牌游戏中重复使用这些图形。
一副牌有52张,我们还有一张背面的图形。与使用53个单独的文件不同,将单独的图形放入一个大的精灵表文件是一个好的做法。精灵表这个术语来自于一种旧的计算机图形技术,它将一个图形纹理加载到内存中并显示部分图形。
将图形放入一个文件的另一个好处是避免文件格式头的开销。53张图像精灵表的大小小于每个文件中带有文件头的53张不同图像的总和。
图形已准备就绪,然后我们需要在游戏区域上设置一个静态页面,并在游戏区域上准备和放置游戏对象。这样以后添加游戏逻辑和交互会更容易:
在将复杂的游戏逻辑添加到我们的匹配游戏之前,让我们准备HTML游戏结构并准备所有CSS样式:
我们在HTML中创建了游戏结构,并对HTML元素应用了样式。我们还使用jQuery在网页加载和准备好后在游戏区域创建了12张卡片。翻转和移除卡片的样式也已准备好,并可以在稍后使用游戏逻辑应用到卡片上。
由于我们为每张卡片使用绝对定位,我们需要自己将卡片对齐到4x3的瓷砖中。在JavaScript逻辑中,我们通过循环每张卡片并通过计算循环索引来对齐它:
$("#cards").children().each(function(index){//alignthecardstobe4x3ourselves.$(this).css({"left":($(this).width()+20)*(index%4),"top":($(this).height()+20)*Math.floor(index/4)});});JavaScript中的“%”是模运算符,它返回除法后剩下的余数。余数用于在循环卡片时获取列数。以下图表显示了行/列关系与索引号:
另一方面,除法用于获取行数,以便我们可以将卡片定位在相应的行上。
以索引3为例,3%4是3。所以索引3的卡片在第三列。而3/4是0,所以它在第一行。
让我们选择另一个数字来看看公式是如何工作的。让我们看看索引8。8%4是0,它在第一列。8/4是2,所以它在第三行。
在我们的HTML结构中,我们只有一张卡片,在结果中,我们有12张卡片。这是因为我们在jQuery中使用了clone函数来克隆卡片元素。克隆目标元素后,我们调用appendTo函数将克隆的卡片元素附加为卡片元素的子元素:
$(".card:first-child").clone().appendTo("#cards");使用子筛选器在jQuery中选择元素的第一个子元素当我们选择卡片元素并克隆它时,我们使用了以下选择器:
$(".card:first-child"):first-child是一个子筛选器,选择给定父元素的第一个子元素。
除了:first-child,我们还可以使用:last-child选择最后一个子元素。
我们将卡片DIV放在游戏元素的中心。CSS3灵活的盒子布局模块引入了一种实现垂直居中对齐的简单方法。由于这个模块仍在进行中,我们需要应用浏览器供应商前缀。我们将以Webkit为例:
display:-webkit-box;-webkit-box-pack:center;-webkit-box-align:center;灵活盒模块定义了元素在其容器中有额外空间时的对齐方式。我们可以通过使用display,一个CSS2属性,值为box,一个新的CSS3属性值,将元素设置为灵活盒容器的行为。
box-pack和box-align是两个属性,用于定义它如何在水平和垂直方向上对齐并使用额外的空间。我们可以通过将这两个属性都设置为center来使元素居中。
CSS精灵表是一个包含许多单独图形的大图像。大的精灵表图像被应用为元素的背景图像。我们可以通过移动固定宽度和高度元素的背景位置来剪裁每个图形。
我们的牌组图像包含总共53个图形。为了方便演示背景位置,让我们假设我们有一张包含三张卡片图像的图像,如下面的截图:
使用CSS精灵和背景位置
在CSS样式中,我们将卡片元素设置为80像素宽,120像素高,背景图像设置为大牌组图像。如果我们想要左上角的图形,我们将背景位置的X和Y都应用为0。如果我们想要第二个图形,我们将背景图像移动到左80像素。这意味着将X位置设置为-80像素,Y设置为0。由于我们有固定的宽度和高度,只有裁剪的80x120区域显示背景图像。以下截图中的矩形显示了可视区域:
现在让我们想象手中拿着一副真正的牌组并设置匹配游戏。
我们首先在手中洗牌,然后将每张卡片背面朝上放在桌子上。为了更容易玩游戏,我们将卡片排成4x3的数组。现在游戏已经准备好了。
现在我们要开始玩游戏了。我们拿起一张卡片并翻转它使其正面朝上。然后我们拿起另一张并将其朝上。之后,我们有两种可能的操作。如果它们是相同的图案,我们就把这两张卡片拿走。否则,我们将它们再次放回背面,就好像我们没有触摸过它们一样。游戏将继续,直到我们配对所有卡片并将它们全部拿走。
在我们脑海中有了逐步的情景之后,代码流程将会更加清晰。实际上,这个例子中的代码与我们玩真正的牌组的过程完全相同。我们只需要将人类语言替换为JavaScript代码。
在上一个示例中,我们准备了游戏环境,并决定了游戏逻辑与玩真正的牌组相同。现在是时候编写JavaScript逻辑了:
我们编写了CSS3匹配游戏的游戏逻辑。该逻辑为玩牌添加了鼠标点击交互,并控制了图案检查的流程。
在播放淡出过渡后,我们移除成对的卡片。我们可以通过使用TransitionEnd事件来安排在过渡结束后执行的函数。以下是我们代码示例中的代码片段,它向成对的卡片添加了card-removed类来开始过渡。然后,它绑定了TransitionEnd事件以在DOM中完全移除卡片。此外,请注意webkit供应商前缀,因为它尚未最终确定:
$(".card-flipped").removeClass("card-flipped").addClass("card-removed");$(".card-removed").bind("webkitTransitionEnd",removeTookCards);延迟执行翻牌的代码游戏逻辑流程的设计方式与玩一副真正的牌相同。一个很大的区别是我们使用了几个setTimeout函数来延迟代码的执行。当点击第二张卡时,我们在以下代码示例片段中安排checkPattern函数在0.7秒后执行:
JavaScript中没有内置的数组随机化函数。我们必须自己编写。幸运的是,我们可以从内置的数组排序函数中获得帮助。
以下是sort函数的用法:
sort(compare_function);sort函数接受一个可选参数。
这里的诀窍是我们使用了compare函数,该函数返回-0.5到0.5之间的随机数:
anArray.sort(shuffle);functionshuffle(a,b){return0.5-Math.random();}通过在compare函数中返回一个随机数,sort函数以不一致的方式对相同的数组进行排序。换句话说,我们正在洗牌数组。
来自Mozilla开发者网络的以下链接提供了关于使用sort函数的详细解释和示例:
我们可以通过使用自定义数据属性将自定义数据存储在DOM元素内。我们可以使用data-前缀创建自定义属性名称并为其分配一个值。
例如,我们可以在以下代码中将自定义数据嵌入到列表元素中:
Ping-PongMatchingGame这是HTML5规范中提出的一个新功能。根据W3C的说法,自定义数据属性旨在存储页面或应用程序私有的自定义数据,对于这些数据没有更合适的属性或元素。
W3C还指出,这个自定义数据属性是“用于网站自己的脚本使用,而不是用于公开可用的元数据的通用扩展机制。”
我们正在编写我们的匹配游戏并嵌入我们自己的数据到卡片元素中,因此,自定义数据属性符合我们的使用方式。
我们使用自定义属性来存储每张卡片内的卡片模式,因此我们可以通过比较模式值在JavaScript中检查两张翻转的卡片是否匹配。此外,该模式也用于将扑克牌样式化为相应的图形:
b.我们可能希望在第三方游戏门户网站中访问自定义数据属性。
c.我们可能希望在每个玩家的DOM元素中存储一个data-score属性,以便在我们的网页中对排名进行排序。
d.我们可以在每个玩家的DOM元素中创建一个ranking属性来存储排名数据。
在匹配游戏示例中,我们使用了jQuery库中的attr函数来访问我们的自定义数据:
pattern=$(this).attr("data-pattern");attr函数返回给定属性名称的值。例如,我们可以通过调用以下代码获取所有a标签中的链接:
Data函数旨在将自定义数据嵌入到HTML元素的jQuery对象中。它是在HTML5自定义数据属性之前设计的。
以下是data函数的用法:
.data(key).data(key,value)data函数接受两种类型的函数:
为了支持HTML5自定义数据属性,jQuery扩展了data函数,使其能够访问HTML代码中定义的自定义数据。
以下代码解释了我们如何使用data函数。
给定以下HTML代码:
我们可以通过调用jQuery中的data函数访问data-custom-name属性:
$("#target").data("customName")它将返回"HTML5Games"。
以下哪两个jQuery语句读取自定义分数数据并返回100?
a.$("#game").attr("data-score");
b.$("#game").attr("score");
c.$("#game").data("data-score");
d.$("#game").data("score");
我们已经创建了CSS3匹配游戏。这里缺少什么?游戏逻辑没有检查游戏是否结束。尝试在游戏结束时添加你赢了文本。您还可以使用本章讨论的技术来为文本添加动画。
这种CSS3纸牌方法适用于创建纸牌游戏。卡片上有两面适合翻转。过渡适合移动卡片。通过移动和翻转,我们可以定义玩法规则并充分利用纸牌游戏。
您能否使用纸牌图形和翻转技术创建另一个游戏?比如扑克?
多年来,我们一直在使用有限的字体来设计网页。我们无法使用任何我们想要的字体,因为浏览器从访问者的本地机器加载字体。我们无法控制并确保访问者拥有我们想要的字体。
尽管我们可以将网络字体嵌入到InternetExplorer5中,但格式受限,我们必须等到浏览器供应商支持嵌入最常见的TrueType字体格式。
想象一下,我们可以通过嵌入不同样式的网络字体来控制游戏的情绪。我们可以使用我们想要的字体设计游戏,并更好地控制游戏的吸引力。让我们尝试将网络字体嵌入到我们的匹配记忆游戏中。
Google字体目录是一个列出可免费使用的网络字体的网络字体服务。我们将嵌入从Google字体目录中选择的网络字体:
我们刚刚用一种非常见的网络字体为我们的游戏添加了样式。该字体是通过谷歌字体目录托管和交付的。
除了使用字体目录,我们还可以使用@fontface来嵌入我们的字体文件。以下链接提供了一种可靠的方法来嵌入字体:
在嵌入之前检查字体许可证
选择不同的字体交付服务
在本章中,我们学习了使用不同的CSS3新属性来创建游戏。
现在我们已经学会了如何使用CSS3功能创建基于DOM的HTML5游戏,我们将在下一章中探索另一种创建HTML5游戏的方法,即使用新的Canvas标签和绘图API。
HTML5中一个突出的新功能是Canvas元素。我们可以将画布元素视为一个动态区域,可以使用脚本在上面绘制图形和形状。
网站中的图像多年来一直是静态的。有动画gif,但它无法与访问者进行交互。画布是动态的。我们可以通过JavaScript绘图API动态绘制和修改画布中的上下文。我们还可以向画布添加交互,从而制作游戏。
在过去的两章中,我们已经讨论了基于DOM的游戏开发与CSS3和一些HTML5功能。在接下来的两章中,我们将专注于使用新的HTML5功能来创建游戏。在本章中,我们将介绍一个核心功能,即画布,以及一些基本的绘图技术。
在本章中,我们将涵盖以下主题:
Untangle解谜游戏是一个玩家被给予一些连接的圆的游戏。这些线可能会相交,玩家需要拖动圆圈,使得没有线再相交。
以下截图预览了我们将通过本章实现的游戏:
所以让我们从头开始制作我们的画布游戏。
W3C社区表示画布元素和绘图功能是:
画布元素包含用于绘制的上下文,实际的图形和形状是由JavaScript绘图API绘制的。
让我们从基本形状——圆开始在画布上绘制。
我们刚刚在上面创建了一个简单的带有圆圈的画布上下文。
画布元素本身没有太多设置。我们设置了画布的宽度和高度,就像我们固定了真实绘图纸的尺寸一样。此外,我们为画布分配了一个ID属性,以便在JavaScript中更容易地引用它:
Sorry,yourwebbrowserdoesnotsupportCanvascontent.当Web浏览器不支持画布时放置回退内容并非所有的Web浏览器都支持画布元素。特别是那些古老的版本。Canvas元素提供了一种简单的方法来提供回退内容,如果不支持画布元素。在画布的开放和关闭标记内的任何内容都是回退内容。如果Web浏览器支持该元素,则此内容将被隐藏。不支持画布的浏览器将显示该回退内容。在回退内容中提供有用的信息是一个好的做法。例如,如果画布的目的是动态图片,我们可以考虑在那里放置一个
的替代内容。或者我们还可以为访问者提供一些链接,以便轻松升级他们的浏览器。
在这个例子中,我们在画布元素内提供了一个句子。这个句子对于支持画布元素的任何浏览器都是隐藏的。如果他们的浏览器不支持新的HTML5画布功能,它将显示给访问者。以下截图显示了旧版本的InternetExplorer显示回退内容,而不是绘制画布元素:
没有绘制圆的圆函数。画布绘图API提供了一个绘制不同弧的函数,包括圆。弧函数接受以下参数
弧函数中使用的角度参数是弧度,而不是度。如果您熟悉度角,您可能需要在将值放入弧函数之前将度转换为弧度。我们可以使用以下公式转换角度单位:
radians=π/180xdegrees以下图表包含了一些常见的角度值,分别以度和弧度为单位。图表还指示了角度值的位置,以便我们在绘制画布中的弧时轻松选择起始角度和结束角度参数。
为了更清楚地绘制具有起始角度和结束角度的不同弧,让我们绘制一些弧。
让我们通过给出不同的起始和结束角度来对arc函数进行一些实验:
我们在弧函数中使用了不同的startAngle和endAngle参数来绘制六种不同的弧形状。这些弧形状演示了弧函数的工作原理。
让我们回顾一下度和弧度的关系圆,并看一下顶部的半圆。顶部的半圆从角度0开始,到角度π结束,弧是逆时针绘制的。如果我们看一下圆,它看起来像下面的图表:
发生了什么?
如果我们从210度开始,到120度结束,顺时针方向,我们将得到以下弧:
a.ctx.arc(300,250,50,Math.PI*3/2,Math.PI/2,true);
b.ctx.arc(300,250,50,Math.PI*3/2,Math.PI/2);
c.ctx.arc(300,250,50,Math.PI*3/2,0,true);
d.ctx.arc(300,250,50,Math.PI*3/2,0);
当我们调用弧函数或其他路径绘制函数时,我们并没有立即在画布上绘制路径。相反,我们将其添加到路径列表中。这些路径直到我们执行绘图命令才会被绘制。
有两个绘制执行命令。一个用于填充路径,另一个用于绘制描边。
我们通过调用fill函数填充路径,并通过调用stroke函数绘制路径的描边,这在绘制线条时会用到:
ctx.fill();为每种样式开始一个路径fill和stroke函数填充和绘制画布上的路径,但不清除路径列表。以以下代码片段为例。在用红色填充我们的圆之后,我们添加其他圆并用绿色填充。代码的结果是两个圆都被绿色填充,而不仅仅是新圆被绿色填充:
varcanvas=document.getElementById('game');varctx=canvas.getContext('2d');ctx.fillStyle="red";ctx.arc(100,100,50,0,Math.PI*2,true);ctx.fill();ctx.arc(210,100,50,0,Math.PI*2,true);ctx.fillStyle="green";ctx.fill();这是因为在调用第二个fill命令时,画布中的路径列表包含两个圆。因此,fill命令会用绿色填充两个圆,并覆盖红色圆。
为了解决这个问题,我们希望确保每次绘制新形状时都调用beginPath。
beginPath清空路径列表,所以下次调用fill和stroke命令时,它只会应用于beginPath之后的所有路径。
我们刚刚讨论了一个代码片段,我们打算用红色绘制两个圆,另一个用绿色。结果代码绘制出来的两个圆都是绿色的。我们如何向代码添加beginPath命令,以便正确绘制一个红色圆和一个绿色圆?
closePath函数将从最新路径的最后一个点绘制一条直线到路径的第一个点。这是关闭路径。如果我们只打算填充路径而不打算绘制描边轮廓,closePath函数不会影响结果。以下屏幕截图比较了在半圆上调用closePath和不调用closePath的结果:
a.是的,我们需要closePath函数。
b.不,它不在乎我们是否有closePath函数。
绘制圆形是一个常见的函数,我们将经常使用它。最好创建一个绘制圆形的函数,而不是现在输入几行代码。
让我们为绘制圆形创建一个函数,并在画布上绘制一些圆圈:
绘制圆形的代码在页面加载和准备就绪后执行。我们使用循环在画布上随机绘制了几个圆圈。
在游戏开发中,我们经常使用random函数。我们可能希望随机召唤一个怪物让玩家战斗,我们可能希望玩家取得进展时随机掉落奖励,我们可能希望随机数成为掷骰子的结果。在这段代码中,我们随机放置圆圈在画布上。
要在JavaScript中生成一个随机数,我们使用Math.random()函数。
random函数中没有参数。它总是返回一个介于0和1之间的浮点数。这个数字大于或等于0,小于1。
有两种常见的使用random函数的方式。一种方式是在给定范围内生成随机数。另一种方式是生成真或假值
当我们开发基于DOM的游戏时,比如我们在前几章中构建的游戏,我们经常将游戏对象放入DIV元素中,并在代码逻辑中稍后访问它们。在基于画布的游戏开发中情况就不同了。
为了在画布上绘制游戏对象后访问它们,我们需要自己记住它们的状态。比如现在我们想知道有多少个圆被绘制了,它们在哪里,我们需要一个数组来存储它们的位置。
functionCircle(x,y,radius){this.x=x;this.y=y;this.radius=radius;}varuntangleGame={circles:[]};$(function(){varcanvas=document.getElementById('game');varctx=canvas.getContext('2d');varcircleRadius=10;varwidth=canvas.width;varheight=canvas.height;//random5circlesvarcirclesCount=5;for(vari=0;iJavaScript是面向对象编程语言。我们可以为我们的使用定义一些对象结构。Circle对象为我们提供了一个数据结构,可以轻松存储一组x和y位置以及半径。
在定义Circle对象之后,我们可以通过以下代码创建一个新的Circle实例,具有x、y和半径值:
varcircle1=newCircle(100,200,10);注意有关面向对象编程JavaScript的更详细用法,请阅读以下链接中的MozillaDeveloperCenter:
我们在画布上随机画了几个圆。它们是相同风格和相同大小的。我们如何随机绘制圆的大小?并用不同的颜色填充圆?尝试修改代码并使用绘图API进行操作。
现在我们这里有几个圆,怎么样用线连接它们?让我们在每个圆之间画一条直线。
有一些绘制API供我们绘制和设置线条样式
通常我们使用moveTo和lineTo对来绘制线条。就像在现实世界中,我们在纸上移动笔到线条的起始点并放下笔来绘制一条线。然后,继续绘制另一条线或在绘制之前移动到其他位置。这正是我们在画布上绘制线条的流程。
到目前为止,我们已经展示了我们可以根据逻辑动态在画布中绘制形状。游戏开发中还有一个缺失的部分,那就是输入。
现在想象一下,我们可以在画布上拖动圆圈,连接的线条会跟随圆圈移动。在这一部分,我们将在画布上添加鼠标事件,使我们的圆圈可拖动。
我们在jQuery的ready函数中设置了三个鼠标事件监听器。它们是鼠标按下、移动和松开事件。
我们可以通过鼠标事件中的layerX和layerY属性获取相对于元素的鼠标光标位置。以下是我们在代码示例中使用的代码片段。||0是为了在layerX或layerY未定义时使结果为0:
varmouseX=e.layerX||0;varmouseY=e.layerY||0;请注意,我们需要显式设置元素的位置属性,以便获取正确的layerX和layerY属性。
在讨论了基于DOM开发和基于画布开发之间的区别之后,我们不能直接监听画布中任何绘制形状的鼠标事件。这是不可能的。我们不能监视画布中任何绘制形状的事件。我们只能获取画布元素的鼠标事件,并计算画布的相对位置。然后根据鼠标位置改变游戏对象的状态,最后在画布上重新绘制它。
我们如何知道我们点击了一个圆?
我们可以使用点在圆内的公式。这是为了检查圆的中心点与鼠标位置之间的距离。当距离小于圆的半径时,鼠标点击了圆。
我们使用以下公式来计算两点之间的距离:
Distance=(x2-x1)2+(y2-y1)2以下图表显示了当中心点与鼠标光标之间的距离小于半径时,光标在圆内的情况:
我们使用的以下代码解释了如何在鼠标按下事件处理程序中应用距离检查来知道鼠标光标是否在圆内:
if(Math.pow(mouseX-circleX,2)+Math.pow(mouseY-circleY,2)a.是的
b.不
a.点的坐标小于圆的中心点的坐标。
b.点与圆的中心之间的距离小于圆的半径。
c.点的x坐标小于圆的半径。
d.点与圆的中心之间的距离大于圆的半径。
在第二章《使用基于DOM的游戏开发入门》中,我们讨论了游戏循环的方法。在第二章的乒乓球游戏中,游戏循环操作键盘输入并更新基于DOM的游戏对象的位置。
在这里,游戏循环用于重新绘制画布以呈现后来的游戏状态。如果我们在改变状态后不重新绘制画布,比如圆的位置,我们将看不到它。
这就像是在电视上刷新图像。电视每秒刷新屏幕12次。我们也会每秒重新绘制画布场景。在每次重绘中,我们根据当前圆的位置在画布上绘制游戏状态。
当我们拖动圆时,我们重新绘制画布。问题是画布上已经绘制的形状不会自动消失。我们将继续向画布添加新路径,最终搞乱画布上的一切。如果我们在每次重绘时不清除画布,将会发生以下截图中的情况:
清除画布
由于我们已经在JavaScript中保存了所有游戏状态,我们可以安全地清除整个画布,并根据最新的游戏状态绘制更新的线条和圆。要清除画布,我们使用画布绘制API提供的clearRect函数。clearRect函数通过提供一个矩形裁剪区域来清除矩形区域。它接受以下参数作为裁剪区域:
ctx.clearRect(x,context.clearRect(x,y,width,height)
x和y设置了要清除的区域的左上位置。width和height定义了要清除的区域大小。要清除整个画布,我们可以将(0,0)作为左上位置,并将画布的宽度和高度提供给clearRect函数。以下代码清除了整个画布上的所有绘制内容:
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);小测验a.是
b.否
ctx.clearRect(0,0,ctx.canvas.width,0);a.是
我们在画布上有可拖动的圆圈和连接的线条。一些线相交,而另一些则不相交。现在想象我们想要区分相交的线。我们需要一些数学公式来检查它们,并加粗这些相交的线。
让我们增加这些相交线的粗细,这样我们就可以在画布中区分它们:
我们刚刚在现有的拖动圆圈示例中添加了线相交检查代码。线相交代码涉及一些数学公式,以获得两条线的交点,并检查该点是否在我们提供的线段内。让我们看看数学部分,看看它是如何工作的。
根据我们从几何学中学到的相交方程,对于一般形式中的两条给定线,我们可以得到交点。
一般形式是什么?在我们的代码中,我们有线段的起点和终点的x和y坐标。这是一个线段,因为在数学中它只是线的一部分。线的一般形式由Ax+By=C表示。
以下图表解释了一般形式上的线段:
我们可以通过以下方程将具有点1的线段转换为x1,y1和具有点2的线段转换为x2,y2的一般形式:
A=y2-y1B=x1-x2C=A*x1+B*y2现在我们有一个线方程AX+BY=C,其中A,B,C是已知的,X和Y是未知的。
我们正在检查两条相交的线。我们可以将两条线都转换为一般形式,并得到两条线方程:
Line1:A1X+B1Y=C1Line2:A2X+B2Y=C2通过将两个一般形式方程放在一起,X和Y是两个未知的变量。然后我们可以解这两个方程,得到X和Y的交点。
如果A1*B2-A2*B1为零,则两条线是平行的,没有交点。否则,我们可以使用以下方程得到交点:
X=(B2*C1B1*C2)/(A1*B2A2*B1)Y=(A1*C2A2*C1)/(A1*B2A2*B1)一般形式的交点只能说明两条线不相互平行,并且将在某一点相交。它并不保证交点在两条线段上。
以下图表显示了交点和给定线段的两种可能结果。在左图中,交点不在两条线段之间,在这种情况下,两条线段互不相交。在右侧图中,点在两条线段之间,因此这两条线段相互相交:
因此,我们需要另一个名为isInBetween的函数来确定提供的值是否在开始和结束值之间。然后我们使用这个函数来检查方程的交点是否在我们正在检查的两条线段之间。
在获得线条相交的结果后,我们绘制粗线以指示那些相交的线条。
现在我们已经创建了一个交互画布,我们可以拖动圆圈和连接圆圈的线条与其他线条相交。我们来玩个游戏吧?有一些预定义的圆圈和线条,我们的目标是拖动圆圈,使没有线条与其他线条相交。这就是所谓的解开谜题游戏。
让我们在我们的线交点代码中添加游戏逻辑:
UntanglePuzzleGameinCanvas
谜题0,完成度:0%
我们已经在我们的画布中添加了游戏逻辑,以便我们可以玩我们在整章中创建的圆圈拖动代码。
让我们回顾一下我们添加到untangleGame对象的变量。以下表格列出了这些变量的描述和用法:
在每个级别中,我们有解谜游戏中圆圈的初始位置。级别数据被设计为对象数组。每个对象包含每个级别的数据。在每个级别数据中,有三个属性:级别编号、圆圈和连接圆圈的线。下表显示了每个级别数据中的属性:
在每个级别数据都以我们自定义的结构定义好之后
当没有线条相互交叉时,级别完成。我们遍历每条线,并查看有多少条线是细线。细线意味着它们没有与其他线条相交。我们可以使用细线与所有线条的比率来得到级别完成的百分比:
varprogress=0;for(variinuntangleGame.lines){if(untangleGame.lines[i].thickness==untangleGame.thinLineThickness){progress++;}}varprogressPercentage=Math.floor(progress/untangleGame.lines.length*100);当进度达到100%时,我们可以简单地确定级别已经完成:
if($("#progress").html()=="100"){//levelcomplete,levelupcode}显示当前级别和完成进度在画布游戏下方有一句话描述当前级别的状态和进度。它用于向玩家显示游戏状态,让他们知道他们在游戏中取得了进展:
Puzzle0,Completeness:0%
我们使用了我们在第二章中讨论的jQueryHTML函数,开始DOM游戏开发,来更新完成进度。
$("#progress").html(progressPercentage);Haveagohero在示例解谜游戏中,我们只定义了三个级别。只有三个级别是不够有趣的。要不要给游戏添加更多级别?如果你想不出级别,可以在互联网上搜索类似的解谜游戏,获取一些级别设计的灵感。
在本章中,我们学到了很多关于绘制形状和与新的HTML5画布元素和绘图API交互的知识。
现在我们已经学习了关于画布和绘图API中的基本绘图功能,可以使用它们在画布中创建一个解谜游戏。我们准备学习一些高级的画布绘图技术。在下一章中,我们将使用更多的画布绘图API来增强我们的解谜游戏,比如绘制文本、绘制图像和绘制渐变。
在上一章中,我们探索了一些基本的画布上下文绘图API,并创建了一个名为Untangle的游戏。在本章中,我们将通过使用其他一些上下文绘图API来增强游戏。
以下截图是我们将通过本章构建的最终结果的预览。它是一个基于Canvas的Untangle游戏,带有动画游戏指南和一些细微的细节:
所以让我们开始吧...
在上一章中,我们介绍了填充纯色。Canvas在填充形状时可以做得更多。我们可以用线性渐变和径向渐变填充形状。
让我们改进一下我们现在的纯黑色背景。如何从上到下绘制一个渐变呢?
我们刚刚用线性渐变颜色填充了一个矩形。要填充线性渐变颜色,我们只需要设置渐变的起点和终点。然后在它们之间添加几个颜色停止。
以下是我们如何使用线性渐变函数的方式:
createLinearGradient(x1,y1,x2,y2);参数定义x1渐变的起点。y1x2渐变的终点。y2在渐变颜色中添加颜色停止仅仅拥有起点和终点是不够的。我们还需要定义我们使用的颜色以及它如何应用到渐变中。这在渐变中被称为颜色停止。我们可以使用以下gradient函数向渐变中添加一个颜色停止:
addColorStop(position,color);参数定义讨论位置0到1之间的浮点数。位置0表示颜色停在起点,1表示它停在终点。0到1之间的任何数字表示它停在起点和终点之间。例如,0.5表示一半,0.33表示离起点30%。颜色那个颜色停止的颜色样式。颜色样式与CSS颜色样式的语法相同。我们可以使用HEX表达式,如#FFDDAA。或其他颜色样式,如RGBA颜色名称。下面的截图显示了线性渐变设置和结果绘制之间的并排比较。起点和终点定义了渐变的范围和角度。颜色停止定义了颜色在渐变范围之间的混合方式:
添加带不透明度的颜色停止
我们可以使用RGBA函数为颜色停止设置不透明度值。以下代码告诉渐变从红色开始,不透明度为一半:
gradient.addColorStop(0,"rgba(255,0,0,0.5)");
Canvas绘图API中有两种渐变类型。我们刚刚使用的是线性渐变。另一种是径向渐变。径向渐变从一个圆到另一个圆填充渐变。
想象一下,我们现在将我们拖动的圆填充为径向渐变。我们将把实心黄色圆改为白黄渐变:
functiondrawCircle(ctx,x,y){//preparetheradialgradientsfillstylevarcircle_gradient=ctx.createRadialGradient(x-3,y-3,1,x,y,untangleGame.circleRadius);circle_gradient.addColorStop(0,"#fff");circle_gradient.addColorStop(1,"#cc0");ctx.fillStyle=circle_gradient;//drawthepathctx.beginPath();ctx.arc(x,y,untangleGame.circleRadius,0,Math.PI*2,true);ctx.closePath();//actuallyfillthecirclepathctx.fill();}在下面的屏幕截图中,我将绘图放大到200%,以更好地演示圆形中的径向渐变:
我们通过填充径向渐变使拖动圆看起来更真实。
以下是我们创建径向渐变的方法:
createRadialGradient(x1,y1,r1,x2,y2,r2);参数定义x1,y1画布坐标中起始圆的中心x和y。r1起始圆的半径。x2,y2画布坐标中结束圆的中心x和y。r2结束圆的半径。下面的屏幕截图显示了径向渐变设置和画布中的最终结果之间的并排比较:
径向渐变将颜色从起始圆到结束圆进行混合。在这个渐变圆中,起始圆是中心的小圆,结束圆是最外面的圆。有三个颜色停止点。白色在起始和结束圆处停止;另一种深色在离起始圆90%的地方停止。
我们向渐变中添加颜色停止点来定义颜色的混合方式。如果我们忘记向渐变中添加任何颜色停止点并填充一个矩形会发生什么?如果我们只定义一个颜色停止点会怎样?尝试实验颜色停止点设置。
在径向渐变示例中,小的起始圆在较大的结束圆内。如果起始圆比结束圆大会发生什么?如果起始圆不在结束圆内会怎么样?也就是说,如果两个圆不重叠会发生什么?
现在想象一下,我们想直接在画布内显示进度级别。画布为我们提供了在画布内绘制文本的方法。
我们刚刚在基于画布的游戏中绘制了标题和级别进度文本。我们使用fillText函数在画布中绘制文本。以下表格显示了我们如何使用该函数:
fillText(string,x,y);参数定义string我们要绘制的文本。x文本绘制的x坐标。y文本绘制的y坐标。这是绘制文本的基本设置。还有几个绘图上下文属性需要设置文本绘制。
请注意,画布中的文本绘制被视为位图图像数据。这意味着访问者无法选择文本;搜索引擎无法索引文本;我们无法搜索它们。因此,我们应该仔细考虑是否要在画布中绘制文本,还是直接将它们放在DOM中。
a.左对齐,底部基线。
b.居中对齐,字母基线。
c.右对齐,底部基线。
d.居中对齐,中间基线。
a.在画布中绘制逼真的书籍,包括所有文本和翻页效果。
b.将所有文本和内容放在DOM中,并在画布中绘制逼真的翻页效果。
在上一章的记忆匹配游戏中,我们使用了自定义字体。自定义字体嵌入也适用于画布。让我们在画布中的Untangle游戏中进行一个绘制自定义字体的实验。
让我们用手写风格字体绘制画布文本:
我们刚刚选择了一个网络字体,并将其嵌入到画布中绘制文本时。这表明我们可以像其他DOM元素一样为画布中填充的文本设置字体系列。
有时,不同字体系列的文本宽度会有所不同,尽管它们具有相同的字数。在这种情况下,我们可以使用measureText函数来获取我们绘制的文本的宽度。以下链接到Mozilla开发者网络解释了我们如何使用该函数:
我们已经在画布内绘制了一些文本。那么绘制图像呢?是的。在画布中绘制图像和图像处理是画布具有的一个重要功能。
我们将在游戏中绘制一个黑板背景:
我们刚刚在画布元素内绘制了一幅图像。
在画布上绘制图像有两种常见的方法。我们可以引用现有的img标签,也可以在JavaScript中动态加载图像。
这是我们在画布中引用现有图像标签的方式。
假设我们在HTML中有以下img标签:
我们可以使用以下JavaScript代码在画布中绘制图像:
varimg=document.getElementById('board');context.drawImage(img,x,y);这是另一个加载图像的代码片段,而不将img标签附加到DOM中。如果我们在JavaScript中加载图像,我们需要确保图像在绘制到画布上之前已加载。因此,我们在图像的onload事件之后绘制图像:
varboard=newImage();board.onload=function(){context.drawImage(board,x,y);images,insidecanvasimages,insidecanvasdrawing}board.src="images/board.png";提示设置onload事件处理程序和分配图像src时的顺序很重要
当我们将src属性分配给图像并且如果图像被浏览器缓存,一些浏览器会立即触发onload事件。如果我们在分配src属性后放置onload事件处理程序,我们可能会错过它,因为它是在我们设置事件处理程序之前触发的。
在我们的示例中,我们使用了后一种方法。我们创建了一个Image对象并加载了背景。当图像加载完成时,我们启动游戏循环,从而开始游戏。
加载图像时我们还应该处理的另一个事件是onerror事件。当我们访问额外的网络数据时,这是特别有用的。我们有以下代码片段来检查我们示例中的错误:
untangleGame.background.onerror=function(){console.log("Errorloadingtheimage.");}试一试现在加载错误只在控制台中显示消息。玩家通常不会查看控制台。设计一个警报对话框或其他方法来告诉玩家游戏未能加载游戏资源,如何?
有三种在画布中绘制图像的行为。我们可以在给定的坐标上绘制图像而不进行任何修改,我们还可以在给定的坐标上绘制具有缩放因子的图像,或者我们甚至可以裁剪图像并仅绘制裁剪区域。
drawImage函数接受几个参数:
我们已经用渐变和图像增强了画布游戏。在继续之前,让我们装饰一下画布游戏的网页。
我们将建立一个居中对齐的布局,带有一个游戏标题:
我们刚刚装饰了包含基于画布的游戏的网页。虽然我们的游戏是基于画布绘制的,但这并不限制我们用图形和CSS样式装饰整个网页。
画布元素的默认背景
画布元素的默认背景是透明的。如果我们不设置画布的任何背景CSS样式,它将是透明的。当我们的绘图不是矩形时,这是有用的。在这个例子中,纹理布局背景显示在画布区域内。
a.将背景颜色设置为#ffffff。
b.什么也不做。默认情况下是透明的。
我们在第三章“在CSS3中构建记忆匹配游戏”中首次使用了精灵表图像,用于显示一副扑克牌。
在images文件夹中有一个名为guide_sprite.png的图形文件。这是一个包含动画每一步的游戏指南图形。
让我们用动画将这个指南画到我们的游戏中:
在使用drawImage上下文函数时,我们可以只绘制图像的一部分区域。
以下截图逐步演示了动画的过程。矩形是裁剪区域。我们使用一个名为guideFrame的变量来控制显示哪一帧。每帧的宽度为80。因此,我们通过将宽度和当前帧数相乘来获得裁剪区域的x位置:
varnextFrameX=untangleGame.guideFrame*80;ctx.drawImage(untangleGame.guide,nextFrameX,0,80,130,325,130,80,130);guideFrame变量每500米通过以下guideNextFrame函数进行更新:
在开发游戏时,制作精灵动画是一种常用的技术。在传统视频游戏中使用精灵动画有一些好处。这些原因可能不适用于网页游戏开发,但我们在使用精灵表动画时有其他好处:
它通常用于角色动画。以下截图是我在名为邻居的HTML5游戏中使用的愤怒猫的精灵动画:
在这个例子中,我们通过裁剪帧并自行设置定时器来构建精灵表动画。当处理大量动画时,我们可能希望使用一些第三方精灵动画插件或创建自己的画布精灵动画,以更好地重用和管理逻辑代码。
精灵动画是HTML5游戏开发中的重要主题,有许多在线资源讨论这个主题。以下链接是其中一些:
现在所有的东西都绘制到上下文中,它没有其他状态来区分已绘制的项目。我们可以将画布游戏分成不同的图层,并编写逻辑来控制和绘制每个图层。
我们将把Untangle游戏分成四个图层:
现在总共有四个画布。每个画布负责一个图层。图层分为背景、游戏指导线、游戏本身和显示级别进度的用户界面。
默认情况下,画布和其他元素一样,是依次排列的。为了重叠所有画布以构建图层效果,我们对它们应用了absolute位置。
以下截图显示了我们游戏中现在设置的四个层。默认情况下,后添加的DOM位于之前添加的DOM之上。因此,bg画布位于底部,ui位于顶部:
我们正在创建一个基于画布的游戏,但我们并不局限于只使用画布绘图API。级别进度信息现在在另一个ID为ui的画布中。在这个示例中,我们混合了我们在第三章中讨论的CSS技术,在CSS3中构建记忆匹配游戏。
当我们在画布上拖动圆圈时,它们可能会重叠在级别信息上。在绘制UI画布层时,我们会检查是否有任何圆圈的坐标过低并且重叠在文本上。然后我们会淡化UI画布的CSS不透明度,这样就不会分散玩家对圆圈的注意力。
在玩家升级后,我们还会淡出指南动画。这是通过将整个guide画布淡出到CSS过渡缓和为0不透明度来实现的。由于guide画布只负责该动画,隐藏该画布不会影响其他元素:
if(untangleGame.currentLevel==1){$("#guide").addClass('fadeout');}提示只清除改变的区域以提高画布性能
我们可以使用clear函数来清除画布上下文的一部分。这将提高性能,因为它避免了每次重新绘制整个画布上下文。这是通过标记自上次绘制以来状态发生变化的上下文的“脏”区域来实现的。
在我们的示例中的指南画布层,我们可以考虑只清除精灵表图像绘制的区域,而不是整个画布。
在简单的画布示例中,我们可能看不到明显的差异,但是当我们有一个包含许多精灵图像动画和复杂形状绘制的复杂画布游戏时,它有助于提高性能。
当玩家进入第2级时,我们会淡出指南。当玩家拖动任何圆圈时,我们如何淡出指南动画?我们怎么做?
在本章中,我们学到了很多关于在画布中绘制渐变、文本和图像的知识。
在这本书中我们没有提到的一件事是画布中的位图操作。画布上下文是一个位图数据,我们可以在每个像素上应用操作。例如,我们可以在画布上绘制图像并对图像应用类似于Photoshop的滤镜。我们不会在书中涵盖这个内容,因为图像处理是一个高级话题,而且应用可能与游戏开发无关。
现在我们已经学会了在画布中构建游戏并为游戏对象制作动画,比如游戏角色,我们准备在下一章为我们的游戏添加音频组件和音效。
我们将在第九章中回到基于画布的游戏,
在本章中,我们将学习以下主题:
以下截图显示了我们将通过本章创建的最终结果:
所以,让我们开始吧。
在之前的章节中,我们在Untangle游戏示例中有几种鼠标交互。现在想象一下,我们希望在鼠标交互时有声音效果。这要求我们指示游戏使用哪个音频文件。我们将使用audio标签在按钮上创建声音效果。
我们将从代码包中提供的代码示例开始。我们将有类似以下截图所示的文件夹结构:
我们刚刚创建了一个基本的HTML5游戏布局,其中播放按钮放置在页面中间。JavaScript文件处理按钮的鼠标悬停和点击,并播放相应的声音效果。
使用audio标签的最简单方法是提供一个源文件。以下代码片段显示了如何定义音频元素:
提示在音频标签中显示回退内容
audio标签是HTML5规范中新引入的。我们可以在audio标签内放置回退内容,例如Flash电影来播放音频。以下来自HTML5Rocks的链接显示了如何使用具有Flash回退的audio标签的快速指南:
除了设置audio标签的源文件外,我们还可以使用几个属性来进行额外的控制。以下表格显示了我们可以为音频元素设置的属性:
以下屏幕截图显示Chrome显示控件:
我们可以通过调用getElementById函数来获取音频元素的引用。然后,我们通过调用play函数来播放它。以下代码播放buttonactive音频:
暂停声音与播放按钮类似,我们也可以通过使用pause函数暂停音频元素的播放。以下代码暂停buttonactive音频元素:
注意没有stop函数来停止音频元素。相反,我们可以暂停音频并将元素的currentTime属性重置为零。以下代码显示了如何停止音频元素:
document.getElementById("buttonactive").pause();
document.getElementById("buttonactive").currentTime=0;
我们还可以设置音频元素的音量。音量必须在0和1之间。我们可以将音量设置为0来静音,将其设置为1来达到最大音量。以下代码片段将buttonactive音频的音量设置为30%:
使用jQueryhover事件jQuery提供了一个hover函数来定义当我们鼠标悬停和移出DOM元素时的行为。以下是我们如何使用hover函数:
.hover(function1,function2);参数讨论function1当鼠标移入时执行该函数。function2这是可选的。当鼠标移出时执行该函数。当未提供此函数时,移出行为与function1相同。在以下代码中,当鼠标移动时,我们播放鼠标悬停音效,并在鼠标移出时暂停音效:
维基百科在以下网址包含了有关Ogg格式的详细解释:
Ogg是一个开源标准,可以免费使用。有许多支持它的音乐播放器和转换器。我们将使用名为Audacity的免费软件将我们的MP3文件转换为Ogg格式:
在撰写本书时,Audacity1.3beta版本已发布,导出布局发生了变化。单击文件|导出…,并在导出对话框中选择Ogg格式。
我们刚刚将一个MP3格式的音效转换为Ogg格式,以使音频在不支持MP3格式的浏览器中工作。
以下表格显示了撰写本书时最受欢迎的网络浏览器支持的音频格式:
a.使用stop函数
b.使用pause函数并将currentTime重置为0
c.将currentTime重置为0
现在想象一下,我们不仅播放音效,还使用audio标签播放整首歌曲。随着歌曲的播放,有一些音乐点向下移动,作为音乐的可视化。
首先,我们将在画布上绘制一些路径作为音乐播放的背景。
我们创建了一个画布,在这个音乐游戏示例中,我们介绍了HTML5游戏中的基本场景管理。
在HTML5中创建场景类似于在上一章中创建图层。它是一个包含多个子元素的DOM元素。所有子元素都是绝对定位的。我们的示例中现在有两个场景。以下代码片段显示了整个游戏中可能的场景结构,包括游戏结束场景、信用场景和排行榜场景:
以下截图显示了场景在网页中放置在同一位置。这与图层结构非常相似。不同之处在于我们将通过显示和隐藏每个场景来控制场景:
如果您曾经玩过舞动革命、吉他英雄或触觉复仇游戏,那么您可能熟悉音乐点向下或向上移动,玩家在音乐点移动到正确位置时击中音乐点。以下截图展示了触觉复仇游戏:
我们将在画布中以类似的音乐可视化方式播放audio标签中的歌曲。
执行以下步骤:
我们刚刚构建了一个完全功能的音乐游戏,这是基本的播放功能。它播放旋律和基础部分的歌曲,并有一些音乐点向下移动。
audiogame.musicNotes=[];audiogame.leveldata="1.592,3;1.984,2;2.466,1;2.949,2;4.022,3;";functionsetupLevelData(){varnotes=audiogame.leveldata.split(";");for(variinnotes){varnote=notes[i].split(",");vartime=parseFloat(note[0]);varline=parseInt(note[1]);varmusicNote=newMusicNote(time,line);audiogame.musicNotes.push(musicNote);}}级别数据字符串由键盘记录,我们将在本章后面讨论录制。
在这里,级别数据只包含几个音符。在代码包中,有完整歌曲的整个级别数据。
JavaScriptparseInt函数有一个可选的第二个参数。它定义要解析的数字的基数。默认情况下,它使用十进制,但当字符串以零开头时,parseInt将解析字符串为八进制。例如,parseInt("010")返回结果8而不是10。如果我们想要十进制数,那么我们可以使用parseInt("010",10)来指定基数。
//startinggamevardate=newDate();audiogame.startingTime=date.getTime();//sometimelatervardate=newDate();varelapsedTime=(date.getTime()-audiogame.startingTime)/1000;以下截图显示了前面的代码片段在控制台中运行:
当点在灰线上时,音乐点应该与歌曲相匹配。音乐点从游戏顶部出现并向下移动到灰线。我们延迟音乐播放以等待点从上到下移动。在这个例子中大约是3.55秒,所以我们延迟音乐播放3.55秒。
当点被创建时,它被放置在给定的距离处。每次gameloop函数执行时,我们将所有点的距离减少2.5。距离存储在每个代表它距离灰线有多远的dot对象中:
for(variinaudiogame.dots){audiogame.dots[i].distance-=2.5;}点的y位置由灰线减去距离计算如下:
//drawthedotctx.save();varx=ctx.canvas.width/2-100if(audiogame.dots[i].line==2){x=ctx.canvas.width/2;}elseif(audiogame.dots[i].line==3){x=ctx.canvas.width/2+100;}ctx.translate(x,ctx.canvas.height-80-audiogame.dots[i].distance);ctx.drawImage(audiogame.dotImage,-audiogame.dotImage.width/2,-audiogame.dotImage.height/2);以下截图显示了灰线和每个点之间的距离。当距离为零时,它恰好在灰线上:
现在我们有一个游戏场景正在播放我们的歌曲。但是,它覆盖了我们用播放按钮制作的菜单场景。现在想象一下,我们打开游戏时,播放按钮被显示,然后我们点击按钮,游戏场景滑入并开始播放音乐。
我们将默认隐藏游戏场景,并在点击播放按钮后显示它:
我们刚刚在菜单场景和游戏场景之间创建了一个过渡。
点击播放按钮时,游戏场景从顶部滑入。这种场景过渡效果是通过CSS3过渡来实现的。游戏场景的位置最初是放置在负的顶部数值上。然后我们通过过渡将顶部位置从负值改变为零,这样它就从顶部动画到正确的位置。
使滑动效果生效的另一重要事项是将场景的父DIV的溢出设置为隐藏。如果没有隐藏的溢出,即使顶部位置为负值,游戏场景也是可见的。因此,将场景的父DIV设置为隐藏的溢出是很重要的。
以下截图展示了游戏场景的滑入过渡。#gameDIV是菜单场景和游戏场景的父级。当我们添加.show-scene类时,游戏场景从顶部移动,将顶部值设置为0并进行过渡:
当显示游戏时,我们为场景过渡创建了一个滑入效果。通过使用JavaScript和CSS3,我们可以创造许多不同的场景过渡效果。尝试制作自己的过渡效果,比如淡入、从右侧推入,甚至是带有3D旋转的翻转效果。
现在我们可以点击播放按钮。音乐游戏滑入并播放带有音符下落的歌曲。接下来,我们将为音乐音符添加交互。因此,我们将添加键盘事件来控制三条线击中音乐音符。
我们刚刚为我们的音乐游戏添加了键盘交互。击打键时会有发光动画。当在正确时刻按下正确的键时,音乐点会消失。
我们使用J,K和L键来击打游戏中的三条音乐线。J键控制左线,K键控制中线,L键控制右线。
还有一个指示,显示我们刚刚击中了音乐线。这是通过在灰线和音乐线的交叉点放置以下图像来实现的:
然后,我们可以使用以下jQuery代码来控制击中指示图形的显示和隐藏:
$(document).keydown(function(e){varline=e.which-73;$('#hit-line-'+line).removeClass('hide');$('#hit-line-'+line).addClass('show');});$(document).keyup(function(e){varline=e.which-73;$('#hit-line-'+line).removeClass('show');$('#hit-line-'+line).addClass('hide');});J,K和L键控制音乐线1到3。由于J,K和L的键码分别为74、75和76,我们可以通过将键码减去73来知道它是哪条线。
如果音符几乎在灰色水平线上,距离接近零。这有助于我们确定音符是否击中了灰线。通过检查按键按下事件和音符距离,我们可以确定是否成功击中了音符。以下代码片段显示了当距离在20像素内时,我们认为音符被击中:
$(document).keydown(function(e){varline=e.which-73;$('#hit-line-'+line).removeClass('hide');$('#hit-line-'+line).addClass('show');//ourtargetisJ(74),K(75),L(76)varhitLine=e.which-73;//checkifhitamusicnotedotfor(variinaudiogame.dots){if(hitLine==audiogame.dots[i].line&&Math.abs(audiogame.dots[i].distance)<20){//removethehitdotfromthedotsarrayaudiogame.dots.splice(i,1);}}});坚定决心,我们在击中时移除音乐点。错过的点仍然会穿过灰线并向底部移动。这创造了一个基本的游戏玩法,玩家必须在歌曲播放时在正确的时刻正确击中所有音乐点。
我们在音乐点被击中时(因此不再绘制)从数组中删除音乐点数据。要从数组中删除一个元素,我们使用splice函数。以下代码行从给定索引处的数组中删除一个元素:
array.splice(index,1);splice函数有点棘手。这是因为它允许我们在数组中添加或删除元素。然后,它会将删除的元素作为另一个数组返回。听起来很复杂。因此,我们将进行一些实验。
我们将在Web浏览器中打开JavaScript控制台,对splice函数进行一些测试:
我们刚刚创建了一个数组,并尝试使用splice函数添加和删除元素。请注意,splice数组会返回另一个包含已删除元素的数组。
以下是我们如何使用splice函数:
array.splice(index,length,element1,element2,…,elementN);以下表格显示了我们如何使用这些参数:
以下是Mozilla开发者网络链接,讨论了splice函数的不同用法:
在类似的商业音乐游戏中,当玩家击中或错过音乐点时会显示一些字。我们如何将这个功能添加到我们的游戏中?
我们已经为游戏创建了基本的交互。我们可以进一步改进游戏,通过添加旋律音量反馈来使表演更加逼真,并计算表演的成功率。
现在想象我们正在表演音乐。我们击中音乐点演奏旋律。如果我们错过了其中任何一个,那么我们就无法演奏好,旋律就会消失。
我们将存储一些游戏统计数据,并用它来调整旋律音量。我们将继续进行JavaScript文件:
我们想要移除点,要么是在它掉落到底部边界下方时,要么是在玩家击中它时。游戏循环在游戏画布上显示点列表中的所有点。我们可以通过从点数组中移除其数据来移除点图形。
我们使用以下的splice函数来移除数组中目标索引处的条目:
audiogame.dots.splice(index,1);存储最近五次结果中的成功次数在我们的游戏中,我们需要存储最近五次结果中的成功次数以计算成功率。我们可以通过使用一个代表这个的计数器来实现。当成功击中一个点时,计数器增加1,但当玩家未能击中一个点时,计数器减少1。
如果我们将计数器限制在一个范围内,比如在我们的例子中是0到5,那么计数器就代表了最近几次结果中的成功次数。
在上一章中,我们讨论了如何在Untangle游戏中显示游戏进度。我们能否在音乐游戏中应用类似的技术?我们有玩家在游戏过程中的成功百分比。在游戏顶部显示为百分比条形图如何?
游戏依赖级别数据进行播放。如果没有级别数据,回放可视化将无法工作。如果回放可视化不起作用,我们也无法进行播放。那么我们如何记录级别数据呢?
我们刚刚为游戏添加了录音功能。现在我们可以录制我们的音符。我们可以通过将audiogame.isRecordMode变量设置为true和false来切换录制模式和播放模式。
varcurrentTime=audiogame.melody.currentTime.toFixed(3);varnote=newMusicNote(currentTime,e.which-73);audiogame.musicNotes.push(note);我们还捕获了分号键,以便将所有记录的MusicNote数据打印成字符串。字符串遵循time,line;time,line的格式,因此我们可以直接复制打印的字符串,并将其粘贴为级别数据以进行播放。
我们现在可以玩游戏了,但是游戏结束时没有指示。现在想象一下,当游戏完成时,我们想知道我们玩得有多好。我们将捕获旋律结束信号,并显示游戏的成功率。
我们刚刚监听了音频元素的ended事件,并用处理程序函数处理了它。
音频元素中还有许多其他事件。以下表格列出了一些常用的音频事件:
这里我们列出了一些常用事件;您可以在Mozilla开发者中心的以下网址上查看完整的音频事件列表:
在我们的音乐游戏中,当游戏结束时,我们在控制台中打印出成功率。当游戏结束时,添加一个游戏结束场景,并在游戏结束时显示它会怎样?在显示游戏结束场景时,使用动画过渡也是不错的。
在本章中,我们学到了如何使用HTML5音频元素,并制作了一个音乐游戏。
具体来说,我们涵盖了以下主题:
我们还讨论了管理场景和动画过渡。
我们已经学习了如何在HTML5游戏中添加音乐和音效。现在我们准备在下一章中通过添加排行榜来存储游戏得分,构建一个更完整的游戏。
本地存储是HTML5的一个新规范。它允许网站在浏览器中本地存储信息,并在以后访问存储的数据。这是游戏开发中的一个有用功能,因为我们可以将其用作内存插槽,在Web浏览器中本地保存任何游戏数据。
我们将在我们在第三章构建CSS3记忆匹配游戏中构建的游戏中添加游戏数据存储。除了存储和加载游戏数据,我们还将使用纯CSS3样式向玩家通知打破记录的好消息,通过一个漂亮的3D丝带。
以下截图显示了我们将通过本章创建的最终结果。那么,让我们开始吧:
还记得我们在第三章中制作的CSS3记忆匹配游戏吗?现在想象一下我们已经发布了我们的游戏,玩家们正在努力表现得很好。
我们想告诉玩家他们是否比上次玩得更好或更差。我们将保存最新的分数,并通过比较分数来告知玩家这次是否比上次更好。
当他们表现更好时,他们可能会感到自豪。这可能使他们上瘾,他们可能会继续努力获得更高的分数。
我们将继续使用第三章中制作的记忆匹配游戏的代码。执行以下步骤:
我们刚刚建立了一个基本的得分系统,用于比较玩家的得分和上次的得分。
我们可以使用localStorage对象的setItem函数来存储数据。以下表格显示了该函数的用法:
localStorage.setItem("last-elapsed-time",matchingGame.elapsedTime);与setItem相辅相成,我们可以通过以下方式使用getItem函数获取存储的数据:
localStorage.getItem(key);该函数返回给定键的存储值。当尝试获取一个不存在的键时,它会返回null。这可以用来检查我们是否为特定键存储了任何数据。
本地存储以键值对的形式存储数据。键和值都是字符串。如果我们保存数字、布尔值或任何类型的数据而不是字符串,那么在保存时它会将值转换为字符串。
通常,当我们从本地存储加载保存的值时会出现问题。加载的值是一个字符串,而不管我们保存的类型是什么。在使用之前,我们需要明确将值解析为正确的类型。
例如,如果我们将一个浮点数保存到本地存储中,那么在加载时我们需要使用parseFloat函数。以下代码片段显示了如何使用parseFloat来检索存储的浮点数:
varscore=13.234;localStorage.setItem("game-score",score);//result:stored"13.234".vargameScore=localStorage.getItem("game-score");//result:get"13.234"intogameScore;gameScore=parseFloat(gameScore);//result:13.234floatingvalue在前面的代码片段中,如果我们忘记将gameScore从字符串转换为浮点数,操作可能是不正确的。例如,如果我们在没有使用parseFloat函数的情况下将gameScore增加1,结果将是13.2341而不是14.234。因此,请确保将值从本地存储转换为其正确的类型。
本地存储的大小限制
对于每个域通过localStorage存储的数据都有大小限制。这个大小限制在不同的浏览器中可能略有不同。通常,大小限制为5MB。如果超过了限制,那么当向localStorage设置键值时,浏览器会抛出QUOTA_EXCEEDED_ERR异常。
除了使用setItem和getItem函数外,我们还可以将localStorage对象视为关联数组,并通过使用方括号访问存储的条目。
例如,我们可以用后一种版本替换以下代码:
使用setItem和getItem:
localStorage.setItem("last-elapsed-time",elapsedTime);varlastElapsedTime=localStorage.getItem("last-elapsed-time");访问localStorage的方式如下:
我们将所有游戏数据打包到一个对象中并进行存储。
Mozilla开发者网络提供了使用Date对象的详细参考,网址如下:
我们在第四章,使用Canvas和DrawingAPI构建Untangle游戏中使用JSON表示游戏级别数据。
JSON.stringify(anyObject);通常,我们只使用stringify函数的第一个参数。这是我们要编码为字符串的对象。以下代码片段演示了编码为JavaScript对象的结果:
varjsObj={};jsObj.testArray=[1,2,3,4,5];jsObj.name='CSS3MatchingGame';jsObj.date='8May,2011';JSON.stringify(jsObj);//result:{"testArray":[1,2,3,4,5],"name":"CSS3MatchingGame","date":"8May,2011"}注意stringify方法可以很好地将具有数据结构的对象解析为字符串。但是,它无法将任何对象转换为字符串。例如,如果我们尝试将DOM元素传递给它,它将返回错误。如果我们传递一个日期对象,它将返回表示日期的字符串。或者,它将删除解析对象的所有方法定义。
JSON的完整形式是JavaScript对象表示法。从名称上我们知道它使用JavaScript的语法来表示对象。因此,将JSON格式的字符串解析回JavaScript对象非常容易。
以下代码片段显示了我们如何在JSON对象中使用解析函数:
JSON.parse(jsonFormattedString);我们可以在WebInspector中打开控制台来测试JSONJavaScript函数。以下屏幕截图显示了我们刚刚讨论的代码片段在编码对象和解析它们时的运行结果:
在我们将某些内容保存在本地存储后,我们可能想知道在编写加载部分之前究竟保存了什么。我们可以使用WebInspector中的存储面板来检查我们保存了什么。它列出了同一域下保存的所有键值对。以下屏幕截图显示了我们保存了last-score,值为{"savedTime":"23/2/201119:27:02","score":23}。
该值是我们用于将对象编码为JSON的JSON.stringify函数的结果。您也可以尝试直接将对象保存到本地存储中:
假设我们想通过通知玩家他们打破了与上次得分相比的新纪录来鼓励他们。我们想在上面显示一个带有NewRecord文本的缎带。由于新的CSS3属性,我们可以完全在CSS中创建缎带效果。
当玩家打破上次得分时,我们将创建一个新的记录缎带并显示它。因此,请执行以下步骤:
我们刚刚以纯CSS3样式创建了一个丝带效果,并借助JavaScript来显示和隐藏它。丝带由一个小三角形叠加在一个矩形上组成,如下面的屏幕截图所示:
现在,我们如何在CSS中创建一个三角形?我们可以通过将宽度和高度都设置为0,并只绘制一个边框来创建一个三角形。然后,三角形的大小由边框宽度决定。以下代码是我们在新记录丝带中使用的三角形CSS:
.triangle{position:relative;height:0px;width:0;left:-5px;top:-32px;border-style:solid;border-width:6px;border-color:transparent#882011transparenttransparent;z-index:-1;}注意以下PVMGarage网站提供了关于纯CSS3丝带使用的详细解释:
每次游戏结束时,它会将最后得分与当前得分进行比较。然后,它保存当前得分。
如何修改代码以保存最高分并在打破最高分时显示新记录丝带?
我们通过添加游戏结束画面和存储游戏记录来增强了我们的CSS3记忆匹配游戏。现在想象一下,玩家正在进行游戏,然后意外关闭了Web浏览器。一旦玩家再次打开游戏,游戏将从头开始,玩家正在玩的游戏将丢失。通过本地存储,我们可以将整个游戏数据编码为JSON并存储起来。这样,玩家可以稍后恢复他们的游戏。
我们将把游戏数据打包到一个对象中,并在每秒保存到本地存储中。
我们将继续使用我们的CSS3记忆匹配游戏:
最后,我们在每秒钟将savingObject保存在localStorage中。该对象使用我们在本章前面使用的stringify函数进行JSON编码。然后,我们通过解析来自本地存储的JSON字符串来重新创建游戏。
当游戏结束时,我们需要删除保存的记录。否则,新游戏将无法开始。本地存储提供了一个remoteItem函数来删除特定记录。
以下是我们如何使用该函数来删除具有给定键的记录:
localStorage.removeItem(key);提示如果要删除所有存储的记录,可以使用localStorage.clear()函数。
我们在savingObject中克隆了洗过的牌组,这样我们就可以在恢复游戏时使用牌组的顺序来重新创建卡片。但是,我们不能通过将数组分配给另一个变量来复制数组。以下代码无法将数组A复制到数组B:
vara=[1,2,3,4,5];varb=a;a.pop();//result://a:[1,2,3,4]//b:[1,2,3,4]slice函数提供了一种简单的方法来克隆只包含基本类型元素的数组。只要数组不包含另一个数组或对象作为元素,我们就可以使用slice函数来克隆数组。以下代码成功地将数组A克隆到B:
vara=[1,2,3,4,5];varb=a.slice();a.pop();//result://a:[1,2,3,4]//b:[1,2,3,4,5]slice函数通常用于通过从现有数组中选择一系列元素来创建一个新数组。当使用slice函数而没有任何参数时,它会克隆整个数组。Mozilla开发者网络在以下URL提供了有关slice函数的详细用法:
我们已保存了游戏进度,但尚未编写恢复游戏的逻辑。所以,让我们继续进行恢复部分。
我们刚刚通过解析整个游戏状态的保存JSON字符串完成了游戏加载部分。
if(savedObject!=undefined){matchingGame.savingObject.removedCards=savedObject.removedCards;//findthosecardsandremovethem.for(variinmatchingGame.savingObject.removedCards){$(".card[data-card-index="+matchingGame.savingObject.removedCards[i]+"]").remove();}}提示使用存储事件跟踪存储更改
有时,我们可能想要监听localStorage的变化。我们可以通过监听storage事件来实现。当localStorage中的任何内容发生变化时,该事件将被触发。来自DiveintoHTML5的以下链接提供了关于如何使用该事件的详细讨论:
考虑以下每个陈述是否为真:
在本章中,我们学到了如何使用本地存储在Web浏览器中保存游戏数据。
我们还使用纯CSS3样式创建了一个漂亮的3D丝带作为新记录徽章。
现在我们已经学会了如何通过使用本地存储来改进我们以前的游戏,我们准备进入一个名为WebSocket的高级功能,它可以在实时互动中连接玩家。
在之前的章节中,我们构建了几个本地单人游戏。在本章中,我们将借助WebSockets构建一个多人游戏。WebSockets使我们能够创建基于事件的服务器-客户端架构。所有连接的浏览器之间传递的消息都是即时的。我们将结合Canvas绘图、JSON数据打包和在之前章节中学到的几种技术来构建绘画和猜词游戏。
以下屏幕截图显示了我们将在本章中创建的绘画和猜词游戏:
在我们开始构建WebSockets示例之前,我们将看一下现有的多用户绘图板示例。这个示例让我们知道如何使用WebSockets服务器立即在浏览器之间发送数据。
浏览器使用WebSockets的能力
在撰写本书时,只有苹果Safari和GoogleChrome支持WebSocketsAPI。MozillaFirefox和Opera因协议上的潜在安全问题而放弃了对WebSockets的支持。GoogleChrome也计划在安全漏洞修复之前放弃WebSockets。
Mozilla的以下链接解释了他们为什么禁用了WebSockets:
我们刚刚看到浏览器如何实时连接在一起。我们在绘图板上画了些东西,所有其他连接的用户都可以看到这些图画。此外,我们也可以看到其他人正在画什么。
该示例是使用HTML5WebSockets功能与后端服务器制作的,以向所有连接的浏览器广播绘图数据。
绘画部分是建立在Canvas上的,我们已经在第四章,使用Canvas和绘图API构建Untangle游戏中介绍过。WebSocketAPI使浏览器能够与服务器建立持久连接。后端是一个名为node.js的基于事件的服务器,我们将在本章中安装和使用。
HTML5的WebSockets提供了一个客户端API,用于将浏览器连接到后端服务器。该服务器必须支持WebSockets协议,以保持连接持久。
在这一部分,我们将下载并安装一个名为Node.JS的服务器,我们可以在上面安装一个WebSockets模块。
Node.JS在Linux和Mac上可以直接使用。以下链接提供了一个安装程序,用于在Windows上安装Node.JS:
$./configure$sudomakeinstall使用sudomakeinstall命令以root权限安装Node.JS,并以root访问权限安装所需的第三方库。以下链接讨论了如何在不使用sudo的情况下安装Node.JS:
$node--versionv0.5.0-pre刚刚发生了什么?我们刚刚下载并安装了Node.JS服务器。我们还下载了node.js服务器的WebSockets库。通过本章的示例,我们将在此服务器和WebSockets库的基础上构建服务器逻辑。
Node.js服务器安装在Unix或Linux操作系统上运行良好。但是,在Windows上安装和运行node.js服务器需要更多步骤。以下链接显示了如何在Windows上安装node.js服务器:
我们刚刚安装了带有WebSockets库的node.js服务器。现在,我们将构建一些内容来测试WebSockets。现在想象一下,我们需要一个服务器来接受浏览器的连接,然后向所有用户广播连接计数。
varws=require(__dirname+'/lib/ws/server');varserver=ws.createServer();server.addListener("connection",function(conn){//initstuffonconnectionconsole.log("Aconnectionestablishedwithid",conn.id);varmessage="Welcome"+conn.id+"joiningtheparty.Totalconnection:"+server.manager.length;server.broadcast(message);});server.listen(8000);console.log("WebSocketserverisrunning.");console.log("Listeningtoport8000.");nodeserver.js$nodeserver.jsWebSocketserverisrunning.Listeningtoport8000.刚刚发生了什么?我们刚刚创建了一个简单的服务器逻辑,初始化了WebSockets库,并监听了连接事件。
在Node.JS中,不同的功能被打包到模块中。当我们需要特定模块中的功能时,我们使用require进行加载。我们加载WebSockets模块,然后在服务器逻辑中使用以下代码初始化服务器:
varws=require(__dirname+'/lib/ws/server');varserver=ws.createServer();__dirname表示正在执行的服务器JavaScript文件的当前目录。我们将lib文件夹放在服务器逻辑文件的同一文件夹下。因此,WebSockets服务器位于当前目录|lib|ws|server。
最后,我们需要为服务器分配一个端口来监听以下代码:
server.listen(8000);在上述代码片段中,8000是客户端连接到此服务器的端口号。我们可以选择不同的端口号,但必须确保所选的端口号不会与其他常见服务器服务重叠。
为了获取有关node.js服务器的全局范围对象和变量的更多信息,请访问以下链接的官方文档:
node.js服务器是基于事件的。这意味着大多数逻辑是在触发某个事件时执行的。我们在示例中使用的以下代码监听connection事件并处理它:
server.addListener("connection",function(conn){console.log("Aconnectionestablishedwithid",conn.id);…});connection事件带有一个连接参数。我们在连接实例中有一个id属性,我们可以用它来区分每个连接的客户端。
以下表列出了两个常用的服务器事件:
我们可以通过访问服务器管理器来获取WebSocketsnode.js服务器中连接的客户端数。我们可以使用以下代码获取计数:
vartotalConnectedClients=server.manager.length;向所有连接的浏览器广播消息一旦服务器收到新的connection事件,我们就会向所有客户端广播连接的更新计数。向客户端广播消息很容易。我们只需要在server实例中使用string参数调用broadcast函数。
以下代码片段向所有连接的浏览器广播服务器消息:
varmessage="amessagefromserver";server.broadcast(message);创建一个连接到WebSocket服务器并获取总连接数的客户端我们在上一个示例中构建了服务器,现在我们将构建一个客户端,连接到我们的WebSocket服务器并从服务器接收消息。该消息将包含来自服务器的总连接计数。
我们刚刚构建了一个客户端,它与我们在上一节中构建的服务器建立了WebSockets连接。然后,客户端将从服务器接收的任何消息打印到检查器中的控制台面板中。
在支持WebSockets的任何浏览器中,我们可以通过使用以下代码创建一个新的WebSocket实例来建立连接:
varsocket=newWebSocket(url);url参数是一个带有WebSocketsURL的字符串。在我们的示例中,我们正在本地运行我们的服务器。因此,我们使用的URL是ws://127.0.0.1:8000,其中8000表示我们正在连接的服务器的端口号。这是8000,因为当我们构建服务器端逻辑时,服务器正在监听端口8000。
与服务器类似,客户端端有几个WebSockets事件。以下表格列出了我们将用于处理WebSockets的事件:
我们现在知道有多少浏览器连接。假设我们想要构建一个聊天室,用户可以在各自的浏览器中输入消息,并立即将消息广播给所有连接的用户。
我们将让用户输入消息,然后将消息发送到node.js服务器。然后服务器将消息转发到所有连接的浏览器。一旦浏览器接收到消息,它就会在聊天区域显示出来。在这种情况下,用户一旦加载网页就连接到即时聊天室。
我们刚刚通过添加一个输入文本字段来扩展了我们的连接示例,让用户在其中输入一些文本并将其发送出去。文本作为消息发送到WebSockets服务器。然后服务器将在终端中打印接收到的消息。
为了从客户端向服务器发送消息,我们在WebSocket实例中调用以下send方法:
websocketGame.socket.send(message);在我们的示例中,以下代码片段从输入文本字段中获取消息并将其发送到服务器:
varmessage=$("#chat-input").val();websocketGame.socket.send(message);在服务器端接收消息在服务器端,我们需要处理刚刚从客户端发送的消息。在WebSocketnode.js库中的连接实例中有一个名为message的事件。我们可以监听连接消息事件以接收来自每个客户端连接的消息。
以下代码片段显示了我们如何使用消息事件监听器在服务器终端上打印消息和唯一连接ID:
conn.addListener("message",function(message){console.log("Gotdata'"+message+"'fromconnection"+conn.id);});注意在服务器和客户端之间发送和接收消息时,只接受字符串。我们不能直接发送对象。但是,我们可以在传输之前将数据转换为JSON格式的字符串。我们将在本章后面展示发送数据对象的示例。
在上一个示例中,服务器可以接收来自浏览器的消息。但是,服务器除了在终端中打印接收到的消息之外,什么也不做。因此,我们将向服务器添加一些逻辑,以广播消息。
这是我们之前示例的延伸。我们讨论了服务器如何向所有连接的客户端广播连接计数。我们还讨论了客户端如何向服务器发送消息。在这个例子中,我们将这两种技术结合起来,让服务器将接收到的消息广播给所有连接的用户。
如果您曾经使用服务器端语言和数据库构建过网页聊天室,那么您可能会想知道WebSocket实现和传统实现之间有什么区别。
下图显示了客户端和服务器之间的请求。它显示了许多无用的请求被发送,但服务器在没有新数据的情况下响应客户端:
还有一种更好的轮询方法叫做长轮询。客户端向服务器发送请求并等待响应。与传统的轮询方法不同,服务器不会以“没有更新”的方式响应,直到有需要推送给服务器的内容。在这种方法中,服务器可以在有更新时向客户端推送内容。一旦客户端从服务器收到响应,它会创建另一个请求并等待下一个服务器通知。下面的图显示了长轮询方法,客户端请求更新,服务器只在有更新时响应:
在WebSockets方法中,请求的数量远少于轮询方法。这是因为客户端和服务器之间的连接是持久的。一旦建立连接,只有在有任何更新时才会从客户端或服务器端发送请求。例如,当客户端想要向服务器更新某些内容时,客户端向服务器发送消息。服务器也只在需要通知客户端数据更新时才向客户端发送消息。在连接期间不会发送其他无用的请求。因此,利用的带宽更少。以下图显示了WebSockets方法:
使用基于事件的WebSockets方法实现多用户聊天室的好处是什么?这些好处如何使消息传递如此即时?
假设我们想要一个共享的素描本。任何人都可以在素描本上画东西,所有其他人都可以查看,就像我们在本章开头玩的素描本示例一样。我们学习了如何在客户端和服务器之间传递消息。我们将进一步发送绘图数据。
在处理数据发送和服务器处理之前,让我们专注于制作一个绘图白板。我们将使用画布来构建一个本地绘图素描本。
我们刚刚创建了一个本地绘图板。这就像一个白板,玩家可以通过拖动鼠标在画布上绘图。但是,绘图数据尚未发送到服务器;所有绘图只在本地显示。
画线函数与我们在第四章中使用的相同。我们还使用相同的代码来获取鼠标相对于画布元素的位置。但是,鼠标事件的逻辑与第四章不同。
当我们在计算机上画东西时,通常意味着我们点击画布并拖动鼠标(或笔)。直到鼠标按钮松开为止才画线。然后,用户再次点击另一个地方并拖动以绘制线条。
在我们的示例中,我们有一个名为isDrawing的布尔标志,用于指示用户是否正在绘图。isDrawing标志默认为false。当鼠标按钮按下时,我们将标志设置为true。当鼠标移动时,我们在鼠标按钮按下时的移动点和上一个点之间画一条线。然后,当鼠标按钮松开时,我们再次将isDrawing标志设置为false。
这就是绘图逻辑的工作方式。
我们能否通过添加颜色支持来修改绘图画板?再加上五个按钮,分别是红色、蓝色、绿色、黑色和白色?玩家可以在绘图时选择颜色。
我们将进一步通过将我们的绘图数据发送到服务器,并让服务器将绘图广播到所有连接的浏览器。
我们刚刚构建了一个多用户绘图画板。这类似于我们在本章开头尝试的绘图画板。我们通过发送一个复杂的数据对象作为消息,扩展了构建聊天室时所学到的内容。
为了正确地在服务器和客户端之间传递多个数据,我们必须定义一个数据对象,服务器和客户端都能理解。
数据对象中有几个属性。以下表格列出了这些属性以及我们为什么需要它们:
此外,我们在客户端和服务器端都定义了以下常量。这些常量是用于dataType属性的:
//ContantsLINE_SEGMENT:0,CHAT_MESSAGE:1,有了这些常量,我们可以通过以下可读的代码来比较dataType,而不是使用无意义的整数:
if(data.dataType==websocketGame.CHAT_MESSAGE){…}将绘图线数据打包成JSON进行广播在上一章中,当将JavaScript对象存储到本地存储中时,我们使用了JSON.stringify函数将其转换为JSON格式的字符串。现在,我们需要在服务器和客户端之间以字符串格式发送数据。我们使用了相同的方法将绘画线条数据打包成对象,并将其作为JSON字符串发送。
以下代码片段显示了我们如何在客户端打包线段数据并以JSON格式的字符串发送到服务器:
//sendthelinesegmenttoservervardata={};data.dataType=websocketGame.LINE_SEGMENT;data.startX=startX;data.startY=startY;data.endX=mouseX;data.endY=mouseY;websocketGame.socket.send(JSON.stringify(data));在从其他客户端接收到绘画线条后重新创建它们JSON解析通常成对出现,与stringify一起使用。当我们从服务器接收到消息时,我们必须将其解析为JavaScript对象。以下是客户端上的代码,它解析数据并根据数据更新聊天历史或绘制线条:
vardata=JSON.parse(e.data);if(data.dataType==websocketGame.CHAT_MESSAGE){$("#chat-history").append("
"+data.sender+"said:"+data.message+"");}elseif(data.dataType==websocketGame.LINE_SEGMENT){drawLine(ctx,data.startX,data.startY,data.endX,data.endY,1);}构建多人绘画和猜词游戏在本章的早些时候,我们构建了一个即时聊天室。此外,我们刚刚构建了一个多用户草图本。那么,如何将这两种技术结合起来构建一个绘画和猜词游戏呢?绘画和猜词游戏是一种游戏,其中一个玩家被给予一个词来绘制。所有其他玩家不知道这个词,并根据绘画猜测这个词。绘画者和正确猜测词语的玩家将获得积分。
我们将按照以下方式实现绘画和猜词游戏的游戏流程:
我们刚刚在WebSockets和Canvas中创建了一个多人绘画和猜词游戏。游戏和多用户草图本之间的主要区别在于,服务器现在控制游戏流程,而不是让所有用户绘制。
控制多人游戏的游戏流程比单人游戏要困难得多。我们可以简单地使用几个变量来控制单人游戏的游戏流程,但是我们必须使用消息传递来通知每个玩家特定的更新游戏流程。
//ConstantsvarLINE_SEGMENT=0;varCHAT_MESSAGE=1;varGAME_LOGIC=2;游戏流程中有几种状态。在游戏开始之前,连接的玩家正在等待游戏开始。一旦有足够的连接进行多人游戏,服务器向所有玩家发送游戏逻辑消息,通知他们开始游戏。
当游戏结束时,服务器向所有玩家发送游戏结束状态。然后,游戏结束,游戏逻辑暂停,直到有玩家点击重新开始按钮。一旦重新开始按钮被点击,客户端向服务器发送游戏重新开始状态,指示服务器准备新游戏。然后,游戏重新开始。
//ConstantforgamelogicstatevarWAITING_TO_START=0;varGAME_START=1;varGAME_OVER=2;varGAME_RESTART=3;服务器端的以下代码保存了一个指示哪个玩家轮到的索引:
varplayerTurn=0;发送到玩家(轮到他的回合)的数据与发送到其他玩家的数据不同。其他玩家只收到一个游戏开始信号的数据:
vargameLogicData1={};gameLogicData1.dataType=GAME_LOGIC;gameLogicData1.gameState=GAME_START;gameLogicData1.isPlayerTurn=false;另一方面,玩家(轮到他画画)收到以下包含单词信息的数据:
vargameLogicData2={};gameLogicData2.dataType=GAME_LOGIC;gameLogicData2.gameState=GAME_START;gameLogicData2.answer=currentAnswer;gameLogicData2.isPlayerTurn=true;在服务器端枚举连接的客户端我们可以使用servermanager类中的forEach方法枚举所有连接的客户端。以下代码显示了用法。它循环遍历每个连接,并调用给定的callback函数,如下所示:
server.manager.forEach(function);例如,以下代码片段在服务器终端上打印所有连接的ID:
server.manager.forEach(function(connection){console.log("Thisisconnection",connection.id);}}在服务器端向特定连接发送消息在我们之前的示例中,我们使用广播向所有连接的客户端发送消息。除了向每个人发送消息,我们可以使用send方法将消息发送到特定的连接,如下所示:
server.send(connectionID,message);send方法需要两个参数。connectionID是目标连接的唯一ID,message是我们要发送的字符串。
在我们从画画和猜图游戏中提取的以下代码中,我们向现在必须画画的玩家的浏览器发送特殊数据。我们使用forEach函数循环遍历连接,并检查连接是否轮到画画。然后,我们打包答案并将这些数据发送给目标连接,如下所示:
server.manager.forEach(function(connection){if(index==playerTurn){vargameLogicData2={};gameLogicData2.dataType=GAME_LOGIC;gameLogicData2.gameState=GAME_START;gameLogicData2.answer=currentAnswer;gameLogicData2.isPlayerTurn=true;server.send(connection.id,JSON.stringify(gameLogicData2));}index++;});改进游戏我们刚刚创建了一个可玩的多人游戏。但是,还有很多需要改进的地方。在接下来的几节中,我们列出了游戏中的两个可能的改进。
在游戏中,画画者画线,其他玩家猜图。现在,想象两个玩家在玩,第三个玩家加入。由于没有任何地方存储绘制的线条,第三个玩家无法看到画画者画了什么。这意味着第三个玩家必须等到游戏结束才能玩。
我们如何让晚加入的玩家继续游戏而不丢失那些绘制的线条?我们如何为新连接的玩家重建绘图?在服务器上存储当前游戏的所有绘图数据怎么样?
服务器端的答案检查与currentAnswer变量比较消息,以确定玩家是否猜对。如果情况不匹配,答案将被视为不正确。当答案是“apples”时,玩家猜“apple”时被告知错误,这看起来很奇怪。
我们如何改进答案检查机制?如果使用不同的大小写或者相似的单词来改进答案检查逻辑,会怎么样?
游戏逻辑基本上已经完成,游戏已经可以玩了。但是,我们忘记了装饰游戏以使其看起来更吸引人。我们将使用CSS样式来装饰我们的猜画游戏。
我们刚刚为我们的游戏应用了样式,并嵌入了一个来自GoogleFontDirectory的字体,看起来像是涂鸦文本。画布现在被设计成更像是一个带有粗边框和微妙阴影的画布。
在这一章中,我们学到了很多关于将浏览器连接到WebSockets的知识。一个浏览器的消息和事件会几乎实时地广播到另一个浏览器。
具体来说,我们:
现在我们已经学会了如何构建一个多人游戏,我们准备在下一章中借助物理引擎来构建物理游戏。
以下屏幕截图显示了本章结束时我们将获得的内容。这是一个汽车游戏,玩家将汽车移向目的地点:
现在,假设我们想创建一个汽车游戏。我们对汽车施加力使其向前移动。汽车在坡道上移动,然后飞过空中。之后,汽车落在目的地坡道上,游戏结束。物理世界的每个部分的每次碰撞都会影响这一运动。如果我们必须从头开始制作这个游戏,那么我们至少要计算每个部分的速度和角度。幸运的是,物理库帮助我们处理所有这些物理问题。我们所要做的就是创建物理模型并在画布中呈现它。
我们已经导入了必要的JavaScript文件。值得记住的是,如果您以后想使用此基础创建另一个物理游戏,Box2DJS建议按照完全相同的顺序复制JavaScript导入代码,因为文件之间存在依赖关系。
//theglobalobjectthatcontainsthevariableneededforthecargame.varcarGame={}varcanvas;varctx;varcanvasWidth;varcanvasHeight;$(function(){carGame.world=createWorld();console.log("Theworldiscreated.",carGame.world);//getthereferenceofthecontextcanvas=document.getElementById('game');ctx=canvas.getContext('2d');canvasWidth=parseInt(canvas.width);canvasHeight=parseInt(canvas.height);});functioncreateWorld(){//setthesizeoftheworldvarworldAABB=newb2AABB();worldAABB.minVertex.Set(-4000,-4000);worldAABB.maxVertex.Set(4000,4000);//Definethegravityvargravity=newb2Vec2(0,300);//settoignoresleepingobjectvardoSleep=false;//finallycreatetheworldwiththesize,gravity,andsleepobjectparameter.varworld=newb2World(worldAABB,gravity,doSleep);returnworld;}我们还没有在画布中呈现物理世界。这就是为什么我们在页面上只看到一个空白画布。但是,我们已经在控制台日志中打印了新创建的世界。以下屏幕截图显示了控制台跟踪带有许多以m_开头的属性的世界对象。这些是世界的物理状态:
我们刚刚安装了Box2DJavaScript库,并创建了一个空世界来测试安装。
b2World是Box2D环境中的核心类。我们所有的物理实体,包括地面和汽车,都是在这个世界中创建的。以下代码显示了如何创建一个世界:
varworld=newb2World(worldAABB,gravity,doSleep);b2World类需要三个参数来初始化,这些参数在下表中列出并附有描述:
在物理世界中,我们需要很多边界区域。我们需要的第一个边界是世界边界。世界边界内的所有物体都将被计算,而边界外的物体将被销毁。
我们可以将b2AABB视为具有最低边界点和最高边界点的矩形。以下代码片段显示了如何使用b2AABB类。minVertex是边界的左上角点,而maxVertex是右下角点。以下世界定义了一个8000x8000的世界:
varworldAABB=newb2AABB();worldAABB.minVertex.Set(-4000,-4000);worldAABB.maxVertex.Set(4000,4000);注意Box2D数学模型中的单位与我们在计算机世界中通常使用的不同。长度单位是米,而不是像素。此外,旋转单位是弧度。
我们必须定义世界的重力。重力由b2Vec2定义。b2Vec2是一个1x2矩阵的向量。我们可以将其视为X和Y轴的向量。因此,以下代码定义了向下300个单位的重力:
vargravity=newb2Vec2(0,300);设置Box2D忽略休眠的物体休眠的物体是一个不再移动或改变状态的动态物体。
物理库计算世界中所有物体的数学数据和碰撞。当世界中有更多物体需要在每一帧中计算时,性能会变慢。在创建物理世界时,我们需要设置库来忽略休眠的物体或计算所有物体。
在我们的游戏中,只有很少的物体,所以性能还不是问题。此外,如果以后我们创建的物体进入空闲或休眠状态,我们将无法再与它们交互。因此,在本例中,我们将此标志设置为false。
在撰写本书时,只有GoogleChrome可以在画布中流畅运行Box2DJavaScript库。因此,建议在GoogleChrome中测试游戏,直到其他网络浏览器可以流畅运行为止。
现在世界是空的。如果我们要放置物体,那些物体将会掉下来,最终离开我们的视线。现在假设我们想在世界中创建一个静态地面物体,以便物体可以站在那里。我们可以在Box2D中做到这一点。
functioncreateGround(){//boxshapedefinitionvargroundSd=newb2BoxDef();groundSd.extents.Set(250,25);groundSd.restitution=0.4;//bodydefinitionwiththegivenshapewejustcreated.vargroundBd=newb2BodyDef();groundBd.AddShape(groundSd);groundBd.position.Set(250,370);varbody=carGame.world.CreateBody(groundBd);returnbody;}createGround();刚才发生了什么?我们已经使用形状和物体定义创建了一个地面物体。这是一个我们将经常使用的常见过程,用来在世界中创建不同类型的物体。因此,让我们详细了解一下我们是如何做到的。
形状定义了几何数据。在Box2D的JavaScript端口中,形状还定义了密度、摩擦和恢复等材料属性。形状可以是圆形、矩形或多边形。在前面的示例中使用的以下代码定义了一个框形状定义。在框形状中,我们必须通过设置extents属性来定义框的大小。extents属性接受两个参数:半宽和半高。这是一个半值,因此形状的最终面积是该值的四倍:
//boxshapedefinitionvargroundSd=newb2BoxDef();groundSd.extents.Set(250,25);groundSd.restitution=0.4;创建一个物体在定义形状之后,我们可以使用给定的形状定义创建一个物体定义。然后,我们设置物体的初始位置,最后要求世界实例根据我们的物体定义创建一个物体。下面的代码显示了我们如何在世界中创建一个物体,给定形状定义:
vargroundBd=newb2BodyDef();groundBd.AddShape(groundSd);groundBd.position.Set(250,370);varbody=carGame.world.CreateBody(groundBd);没有质量的物体被视为静态物体,或固定物体。这些物体是不可移动的,不会与其他静态物体发生碰撞。因此,这些物体可以用作地面或墙壁,成为关卡环境。另一方面,动态物体将根据重力移动并与其他物体发生碰撞。我们稍后将创建一个动态箱子物体。
我们已经创建了一个地面,但它只存在于数学模型中。我们在画布上看不到任何东西,因为我们还没有在上面画任何东西。为了展示物理世界的样子,我们必须根据物理世界画一些东西。
我们刚刚创建了一个函数,用于将世界中的每个形状绘制为带有深绿色轮廓的框。
以下代码显示了我们如何循环遍历世界中的每个形状进行绘制:
现在我们将看一下drawShape函数。
在每个形状上,我们想在画布中绘制对象的轮廓。在绘制任何东西之前,我们将线条样式设置为深绿色。然后,我们检查形状是圆形、矩形框还是多边形。如果是圆形,我们就使用极坐标来绘制给定形状的半径的圆。如果是多边形,我们就按照以下方式绘制多边形的每一条边:
functiondrawShape(shape,context){context.strokeStyle='#003300';context.beginPath();switch(shape.m_type){caseb2Shape.e_circleShape://Drawthecircleincanvasbasesonthephysicsobjectshapebreak;caseb2Shape.e_polyShape://Drawthepolygonincanvasbasesonthephysicsobjectshapebreak;}context.stroke();}在物理世界中创建一个动态框现在想象我们把一个箱子放入世界中。箱子从空中掉下来,最后撞到地面。箱子会弹起一点,最后停在地面上。这与我们在上一节中创建的不同。在上一节中,我们创建了一个静态地面,它是不可移动的,不会受到重力的影响。现在我们将创建一个动态框。
我们刚刚在世界中创建了一个动态物体。与不可移动的地面物体相比,这个箱子受到重力的影响,并且在碰撞过程中速度会发生变化。当一个物体包含有质量或密度的形状时,它是一个动态物体。否则,它是静态的。因此,我们为我们的箱子定义了一个密度。Box2D会使它成为动态的,并根据密度和物体的大小自动计算质量。
恢复值在0和1之间。在我们的情况下,箱子掉在地面上。当地面和箱子的恢复值都为0时,箱子根本不会弹跳。当箱子或地面中的一个恢复值为1时,碰撞是完全弹性的。
当两个物体发生碰撞时,碰撞的恢复值是两个物体的恢复值中的最大值。因此,如果一个恢复值为0.4的箱子掉在恢复值为0.6的地面上,这次碰撞会使用0.6来计算弹跳速度。
在Box2D物理世界中,所有计算都是按照系统化的迭代进行的。世界根据当前步骤计算所有事物的物理变换。当我们将“步骤”移动到下一个级别时,世界会根据新状态再次进行计算。
step函数类似于我们在第二章,使用基于DOM的游戏开发入门中的gameloop函数。它定期执行以计算游戏的新状态。
现在我们在游戏中有一个箱子。现在想象我们创建两个圆形的车轮。然后,我们将拥有汽车的基本组件,车身和车轮。
我们将通过以下步骤向世界中添加两个圆:
在模拟物理世界时,箱子和车轮都会掉下来并相互碰撞以及与地面碰撞。
创建圆形物体类似于创建方形物体。唯一的区别是我们使用CircleDef类而不是方形形状定义。在圆形定义中,我们使用radius属性而不是extents属性来定义圆的大小。
我们已经准备好了汽车箱体和两个轮子箱体。我们离制作汽车只差一步。现在想象我们有一种胶水可以把车轮粘在车身上。然后,汽车和轮子就不会再分开,我们就会有一辆车。我们可以使用关节来实现这一点。在本节中,我们将使用joint将车轮和车身粘在一起。
关节对于在两个身体之间(或者在一个身体和世界之间)添加约束很有用。有许多种类型的关节,我们在这个例子中使用的是旋转关节。
旋转关节使用一个公共锚点将两个身体粘在一起。然后,这两个身体被粘在一起,只允许基于公共锚点旋转。下面截图的左侧显示了两个身体是如何连接的。在我们的代码示例中,我们将锚点设置为轮子的中心点。下面截图的右侧显示了我们如何设置关节。轮子因为旋转原点在中心而旋转。这种设置使得汽车和轮子看起来很真实:
还有其他类型的关节,它们以不同的方式很有用。关节在创建游戏环境中很有用,因为有几种类型的关节,每种关节类型都值得一试,你应该考虑如何使用它们。以下链接是Box2D手册,解释了每种类型的关节以及我们如何在不同的环境设置中使用它们:
现在我们已经准备好了汽车。让我们用键盘移动它。
我们刚刚创建了与我们的汽车车身的交互。我们可以通过按下Z和X键来左右移动汽车。现在游戏似乎变得有趣起来了。
我们可以通过调用ApplyForce函数向任何身体施加力。以下代码显示了该函数的用法:
body.ApplyForce(force,point);这个函数接受两个参数,列在下表中:
除了ApplyForce函数,我们还可以使用ApplyImpulse函数移动任何物体。这两个函数都可以移动物体,但它们的移动方式不同。如果我们想改变物体的瞬时速度,那么我们可以在物体上使用ApplyImpulse一次,将速度改变为目标值。另一方面,我们需要不断地对物体施加力以增加速度。
例如,我们想要增加汽车的速度,就像踩油门一样。在这种情况下,我们对汽车施加力。如果我们正在创建一个需要启动球的球类游戏,我们可以使用ApplyImpulse函数向球体添加一个瞬时冲量。
你能想到另一种情况吗,我们需要对物体施加力或冲量吗?
现在我们可以移动汽车。然而,环境还不够有趣。现在想象一下,有一些坡道供汽车跳跃,两个平台之间有一个间隙,玩家必须飞过汽车。使用不同的坡道设置玩起来会更有趣。
我们刚刚将地面箱子创建代码封装到一个函数中,这样我们就可以轻松地创建一组地面物体。这些地面物体构成了游戏的级别环境。
此外,这是我们第一次旋转物体。我们使用rotation属性设置物体的旋转,该属性以弧度值为参数。大多数人可能习惯于度单位;我们可以使用以下公式从度获取弧度值:
groundBd.rotation=degree*Math.PI/180;通过设置箱子的旋转,我们可以在游戏中设置不同坡度的坡道。
现在我们已经设置了一个坡道,并且可以在环境中玩汽车。如何使用不同类型的连接器来设置游乐场?例如,使用滑轮连接器作为升降机怎么样?另一方面,包括一个带有中心连接器的动态板怎么样?
Box2D物理库会自动计算所有碰撞。现在想象一下,我们设置了一个地面物体作为目的地。玩家成功将汽车移动到目的地时获胜。由于Box2D已经计算了所有碰撞,我们所要做的就是获取检测到的碰撞列表,并确定我们的汽车是否撞到了目的地地面。
我们刚刚通过检查碰撞联系人创建了游戏获胜逻辑。当汽车成功到达目的地地面物体时,玩家获胜。
在每个步骤中,Box2D计算所有碰撞并将它们放入world实例中的contactlist中。我们可以使用carGame.world.GetContactList()函数获取联系人列表。返回的联系人列表是一个链接列表。我们可以通过以下for循环遍历整个链接列表:
for(varcn=carGame.world.GetContactList();cn!=null;cn=cn.GetNext()){//Wehaveshape1andshape2ofeachcontactnode.//cn.GetShape1();//cn.GetShape2();}当我们获得碰撞的形状时,我们检查该形状的主体是否是汽车或目的地主体。由于汽车形状可能在形状1或形状2中,gamewinWall也是如此,我们使用以下代码来检查两种组合:
varbody1=cn.GetShape1().GetBody();varbody2=cn.GetShape2().GetBody();if((body1==carGame.car&&body2==carGame.gamewinWall)||(body2==carGame.car&&body1==carGame.gamewinWall)){console.log("LevelPassed!");}试试看英雄我们在第七章,使用本地存储存储游戏数据中创建了一个游戏结束对话框。在这里使用该技术创建一个对话框,显示玩家通过了级别,怎么样?当我们向游戏添加不同的级别设置时,它也将作为级别过渡的工具。
您可能已经尝试在上一个示例中多次刷新页面,以使汽车成功跳到目的地。现在想象一下,我们可以按键重新初始化世界。然后,我们可以按照试错的方法直到成功。
我们将R键指定为游戏的重新启动键:
carGame.world=createWorld();试试看英雄创建游戏结束墙现在重新启动游戏的唯一方法是按重新启动键。在世界底部创建一个地面,检查任何下落的汽车怎么样?当汽车掉落并撞到底部地面时,我们知道玩家失败了,然后重新开始游戏。
现在想象一下,当完成每个游戏时,我们可以升级到下一个环境设置。对于每个级别,我们将需要几个环境设置。
我们将重构我们的代码以支持从级别数据结构加载静态地面物体。让我们通过以下步骤来完成它:
我们刚刚创建了一个数据结构来存储关卡。然后,我们根据给定的关卡号创建了游戏,并使用关卡数据构建了世界。
每个关卡数据都是一个对象数组。每个对象包含世界中每个地面物体的属性。这包括基本属性,如位置、大小和旋转。还有一个名为type的属性。它定义了物体是普通的箱子物体、汽车数据,还是获胜的目的地地面:
carGame.levels[0]=[{"type":"car","x":50,"y":210,"fuel":20},{"type":"box","x":250,"y":270,"width":250,"height":25,"rotation":0},{"type":"box","x":500,"y":250,"width":65,"height":15,"rotation":-10},{"type":"box","x":600,"y":225,"width":80,"height":15,"rotation":-20},{"type":"box","x":950,"y":225,"width":80,"height":15,"rotation":20},{"type":"box","x":1100,"y":250,"width":100,"height":15,"rotation":0},{"type":"win","x":1200,"y":215,"width":15,"height":25,"rotation":0}];在创建世界时,我们使用以下代码循环遍历关卡数组中的所有对象。然后根据类型创建汽车和地面物体,并引用游戏获胜的地面:
for(vari=0;i我们已经创建了一个至少可以玩几个关卡的游戏。然而,它们只是一些轮廓框。我们甚至无法区分游戏中的目的地和其他地面物体。现在想象一下,目的地是一个赛车旗,有一辆汽车图形来代表它。这将使游戏目的更加清晰。
#asset{position:absolute;top:-99999px;}functioncreateGround(x,y,width,height,rotation,type){//boxshapedefinitionvargroundSd=newb2BoxDef();groundSd.extents.Set(width,height);groundSd.restitution=0.4;if(type=="win"){groundSd.userData=document.getElementById('flag');}…}vargroundBody=createGround(obj.x,obj.y,obj.width,obj.height,obj.rotation,obj.type);//thecarboxdefinitionvarboxSd=newb2BoxDef();boxSd.density=1.0;boxSd.friction=1.5;boxSd.restitution=.4;boxSd.extents.Set(40,20);boxSd.userData=document.getElementById('bus');我们曾经通过jQuery的$(selector)方法获取元素的引用。jQuery选择器返回一个带有额外jQuery数据包装的元素对象数组。如果我们想要获取原始文档元素引用,那么我们可以使用document.getElementById方法或$(selector).get(0)。由于$(selector)返回一个数组,get(0)给出列表中的第一个原始文档元素
我们现在以最少的图形呈现我们的游戏。至少,玩家可以轻松知道他们在控制什么,以及他们应该去哪里。
Box2D库使用画布来渲染物理世界。因此,我们学到的所有关于画布的技术都可以应用在这里。在第五章,构建一个CanvasGamesMasterclass中,我们学习了使用drawImage函数在画布中显示图像。我们使用这种技术在物理世界的画布上绘制旗帜图形。
我们有一个图像标签列表,引用了游戏中需要的图形资源。然而,我们不想显示这些图像标签,它们只是用于加载和引用。我们通过以下CSS样式将这些资源图像标签隐藏在HTML边界之外。我们不使用display:none,因为我们无法获取根本没有显示的元素的宽度和高度。我们需要宽度和高度来正确定位物理世界中的图形:
#asset{position:absolute;top:-99999px;}根据其物理体的状态在每帧绘制图形从Box2D绘制只是用于开发,然后我们用我们的图形替换它。
以下代码检查形状是否分配了用户数据。在我们的示例中,用户数据用于引用该图形资源的image标签。我们获取图像标签并将其传递给画布上下文的drawImage函数进行绘制。
Box2D中的所有盒形和圆形形状的原点都在中心。然而,在画布中绘制图像需要左上角点。因此,我们有x/y坐标和左上角x/y点的偏移量,这是图像宽度和高度的负一半:
if(s.GetUserData()!=undefined){//theuserdatacontainsthereferencetotheimagevarimg=s.GetUserData();//thexandyoftheimage.//Wehavetosubstractthehalfwidth/heightvarx=s.GetPosition().x;vary=s.GetPosition().y;vartopleftX=-$(img).width()/2;vartopleftY=-$(img).height()/2;context.save();context.translate(x,y);context.rotate(s.GetBody().GetRotation());context.drawImage(img,topleftX,topleftY);context.restore();}在画布中旋转和平移图像我们使用drawImage函数直接绘制图像与坐标。然而,在这里情况不同。我们需要旋转绘制的图像。这是通过在绘制之前旋转上下文,然后在绘制后恢复旋转来完成的。我们可以通过保存上下文状态,平移它,旋转它,然后调用restore函数来实现这一点。以下代码显示了我们如何在给定位置和旋转角度绘制图像。topleftX和topleftY是从图像中心原点到左上角点的偏移距离:
context.save();context.translate(x,y);context.rotate(s.GetBody().GetRotation());context.drawImage(img,topleftX,topleftY);context.restore();提示我们不需要使物理体积与其图形完全相同。例如,如果我们有一个圆形的鸡,我们可以通过一个球体来在物理世界中表示它。使用简单的物理体可以大大提高性能。
我们已经学会了使用CSS3过渡来为记分牌添加动画。将它应用到这个汽车游戏怎么样?此外,怎么样给汽车添加一些引擎声音?尝试应用我们通过这本书学到的知识,为玩家提供完整的游戏体验。
我们刚刚用更多的图形装饰了我们的游戏。我们还为每个级别环境绘制了背景图像。以下截图说明了视觉地面如何表示逻辑物理框。与汽车和获胜旗帜不同,地面图形与物理地面无关。它只是一个背景图像,其图形位于各自的位置。我们可以使用这种方法,因为这些框永远不会移动:
然后,我们可以为每个级别准备几种CSS样式,类名中带有级别编号,例如.gamebg_level_1和.gamebg_level_2。通过将每个类与每个级别的背景链接起来,我们可以在切换级别时更改背景,如下代码所示:
$('#game').removeClass().addClass('gamebg_level'+level);添加燃料以在施加力时增加约束现在我们通过提供有限的燃料来限制玩家的输入。当玩家对汽车施加力时,燃料会减少。我们使用以下keydown逻辑来减少燃料并在燃料耗尽时阻止额外的力量:
case88://xkeytoapplyforcetowardsrightif(carGame.fuel>0){varforce=newb2Vec2(10000000,0);carGame.car.ApplyForce(force,carGame.car.GetCenterPosition());carGame.fuel--;$(".fuel-value").width(carGame.fuel/carGame.fuelMax*100+'%');}在CSS3进度条中呈现剩余燃料在我们的游戏中,我们将剩余燃料呈现为进度条。进度条实际上是另一个DIV内部的DIV。以下标记显示了进度条的结构。外部DIV定义了最大值,内部DIV显示了实际值:
以下截图说明了进度条的结构:
有了这个结构,我们可以通过将宽度设置为百分比值来显示特定的进度。我们使用以下代码根据燃料的百分比来更新进度条:
$(".fuel-value").width(carGame.fuel/carGame.fuelMax*100+'%');这是设置进度条并使用宽度样式控制的基本逻辑。此外,我们给进度条的背景添加了漂亮的渐变,如下截图所示:
这是在样式表中完成的,使用以下CSS3渐变背景定义:
.progressbar{background:-webkit-gradient(linear,lefttop,leftbottom,color-stop(0%,#8C906F),color-stop(48%,#8C906F),color-stop(51%,#323721),color-stop(54%,#55624F),color-stop(100%,#55624F));}.progressbar.fuel-value{background:-webkit-gradient(linear,lefttop,leftbottom,color-stop(0%,#A8D751),color-stop(48%,#A8D751),color-stop(51%,#275606),color-stop(54%,#4A8A49),color-stop(100%,#4A8A49));}总结在本章中,我们学到了如何使用Box2D物理引擎在画布中创建汽车冒险游戏。
我们还讨论了添加燃料条以限制玩家的输入,增加游戏乐趣。
我们现在已经学会了使用Box2D物理库来创建基于画布的物理游戏。
我们通过九章讨论了使用CSS3和JavaScript制作HTML5游戏的不同方面。我们学会了在DOM中构建传统的乒乓球游戏,在CSS3中构建卡片匹配游戏,并在画布中创建了一个解谜游戏。然后,我们探索了向游戏添加声音,并围绕它创建了一个迷你钢琴音乐游戏。接下来,我们讨论了使用本地存储保存和加载游戏状态。此外,我们尝试使用WebSockets构建了一个实时多人游戏。最后,在本章中,我们创建了一个带有物理引擎的汽车游戏。
在整本书中,我们构建了不同类型的游戏,并学习了一些制作HTML5游戏所需的基本技术。下一步是继续开发自己的游戏。为了帮助开发自己的游戏,有一些资源可以提供帮助。以下列表提供了一些HTML5游戏开发的有用链接:
THE END
1.汽车小游戏,汽车小游戏大全,4399汽车小游戏全集,4399小游戏4399汽车小游戏大全收录了国内外汽车类小游戏、双人汽车小游戏、汽车驾驶小游戏、汽车总动员小游戏、汽车小游戏下载。好玩就拉朋友们一起来玩吧!https://www.4399.com/special/167.htm
2.环球汽车网环球汽车网(简称环网)成立于2008年5月6日,以打造“最具互动力的汽车网站”为目标,通过真实有趣的报道、为消费者提供丰富多样的汽车产品信息及实用性、技术性、权威性的最新资讯!http://www.huanqiuauto.com/
3.汽车汽车游戏汽车小游戏汽车模拟驾驶游戏汽车小游戏大全7k7k汽车小游戏大全收录了大量精品汽车游戏、双人汽车游戏、汽车模拟驾驶、汽车总动员、汽车找不同、乐高汽车等许许多多的不同类型汽车小游戏。好玩就拉朋友们一起来玩吧!https://www.7k7k.com/tag/333/
4.汽车俱乐部网页游戏懂车帝提供汽车俱乐部 网页游戏的详细内容,懂车帝是一个汽车资讯平台,懂车更懂你。我们提供最新汽车报价,汽车图片,汽车价格大全,行情、评测、导购等内容,看车选车买车就上懂车帝。https://www.dongchedi.com/tag/pgc/11164133
5.网页汽车小游戏大全aiwan91游戏网页汽车小游戏大全,为玩家提供最新的网页汽车小游戏大全,最全的网页汽车小游戏大全,最客观的网页汽车小游戏大全,邀您激情体验,玩热门网页游戏,就上aiwan91游戏平台!http://www.aiwan91.com/s19707/
6.大众大众汽车报价图片大众新款车型大众:网易汽车提供最新大众新闻资讯,大众车型报价,大众汽车图片,大众视频,大众全国经销商等,大众在线询价,预约试驾,团购买车,尽在网易汽车。https://product.auto.163.com/brand/1698.html
7.八月全新网页游戏,科技盛宴的极致体验介绍这款网页游戏集成了最前沿的技术,以全新的姿态展现在玩家面前,采用最新的游戏引擎技术,画面精美绝伦,仿佛身临其境,独特的游戏世界观和丰富的剧情设计,让您沉浸其中,无法自拔。 功能多样,引领游戏体验新纪元 1、实时语音交流:游戏内引入了先进的实时语音技术,让您与好友轻松沟通,共同挑战游戏中的难关,实时的交流互动,https://www.evtms.cn/post/61215.html
8.HTML5汽车赛道飙车游戏免费源码下载网页游戏代码下载文章浏览阅读1w次,点赞5次,收藏8次。HTML5汽车赛道飙车游戏 HTML5汽车赛道飙车游戏代码免费下载:https://download.csdn.net/download/qq_44273429/14017244简介:H5精品短跑赛车俱乐部游戏,赛车游戏源代码。游戏介绍:鼠标,键盘左右键,控制赛车方向,https://blog.csdn.net/qq_44273429/article/details/112152684
9.汽车使命汽车使命官方网站,我们为您提供关于汽车使命游戏各方面详尽的游戏资讯.包括: 汽车使命新手指南,汽车使命游戏截图,汽车使命游戏攻略心得等.同时还有汽车使命官方网站的最新动态.http://www.07073.com/qcsm/
10.汽车网页游戏好玩的汽车网页游戏排行榜87pk汽车网页游戏大全,为您提供最全的汽车网页游戏资料库、好玩的汽车网页游戏排行榜以及汽车网页游戏开服表。找网页游戏,就上87pk!http://www.87pk.com/game/index_0_269_331_0_1.html
11.爱游戏(ayx)(官方)网站/网页版登录入口/IOS/安卓通用版/手机2024-12-05 04:42:01「百科/秒懂百科」【 爱游戏网页版官方入口】支持:32/64bi系统类型:(官方)官方网站IOS/Android通用版/手机APP(2024APP下载)《爱游戏网页版官方入口》是一款MOBA手游,游戏无符文系统,拥有丰富的阴阳术,多样的赛事,可以给予玩家超棒的竞技游戏体验,欢迎前来下载爽玩。 http://www.czwltjx.cn/vjq/detail/ppzfnasz.html
12.汽车之家在线玩汽车之家网页版点击即玩汽车之家,《汽车之家》是时下一款简单易上手的数字小游戏。汽车之家HTML5手机小游戏免费在线玩,无需下载 点击即玩!https://u.ali213.net/detail/70.html
13.爱游戏(ayx)(官方)网站/网页版登录入口/手机版usd【ayx爱游戏体育官方网页入口】APP下载(2024好运连连)系统类型:ayx爱游戏体育官方网页入口官方版正版下载-ayx爱游戏体育官方网页入口标准版V.7.5.39天天签送彩金!APP,现在下载,安装苹果/安卓通用版,新用户还送新人礼包是http://www.yuelaohunlian.com/
14.www.jxmzxx.com/appnews/442646.html{得益于在电动汽车电池领域的卓越创新性成就,宁德时代从100家参选企业中脱颖而出,、.. 数据里看影响力 亚洲已成为中国网络文学传播最广泛地区 5年前,刚上大一的夏梓轩遭遇车祸,这名曾经积极阳光的大男孩余生只能在轮椅上度过,不仅学业被迫中断,就业也成了问题。-——。 http://www.jxmzxx.com/appnews/442646.html
15.营销活动策划方案(通用15篇)与业主有一个面对面的接触机会,把安全汽车生活的理念带给业主,就是倡导业主在享受汽车带来乐趣的同时,更能过上安全的汽车生活。此次活动结合了趣味安全游戏,直观现场演示,把枯燥的安全知识生动地传达给了活动中的每一个人。 6、汽车日常保养讲座。 7、网上车市、供车、改装等咨询活动。https://www.cnfla.com/huodongfangan/3277786.html
标签之前
b.在
元素内
c.在