HTML5-游戏开发示例-全-

HTML5 游戏开发示例(全)

原文:zh.annas-archive.org/md5/4F48ABC6F07BFC08A9422C3E7897B7CC

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

HTML5 承诺成为在线游戏的热门新平台。HTML5 游戏可以在计算机,智能手机和平板电脑上运行,包括 iPhone 和 iPad。成为第一批今天构建 HTML5 游戏的开发人员,并为明天做好准备!

本书将向您展示如何使用最新的 HTML5 和 CSS3 网络标准来构建纸牌游戏,绘画游戏,物理游戏,甚至是通过网络的多人游戏。通过本书,您将通过清晰系统的教程构建六个示例游戏。

HTML5,CSS3 和相关的 JavaScript API 是网络中的最新热门话题。这些标准为我们带来了新的游戏市场,HTML5 游戏。借助它们的新力量,我们可以使用 HTML5 元素,CSS3 属性和 JavaScript 设计在浏览器中玩的游戏。

本书分为九章,每章专注于一个主题。我们将创建六个游戏,并具体学习如何绘制游戏对象,对它们进行动画处理,添加音频,连接玩家,并使用 Box2D 物理引擎构建物理游戏。

本书涵盖了什么

第一章,介绍 HTML5 游戏,介绍了 HTML5,CSS3 和相关 JavaScript API 的新功能。它还演示了我们可以使用这些功能制作什么游戏以及它的好处。

第二章,开始 DOM 游戏开发,通过在 DOM 和 jQuery 中创建传统的乒乓球游戏,启动游戏开发之旅。

第三章,在 CSS3 中构建记忆匹配游戏,介绍了 CSS3 的新功能,并讨论了如何在 DOM 和 CSS3 中创建记忆卡匹配游戏。

第四章,使用 Canvas 和绘图 API 构建解开游戏,介绍了一种在网页中绘制游戏并与之交互的新方法,使用新的 Canvas 元素。它还演示了如何使用 Canvas 构建解谜游戏。

第五章,构建 Canvas 游戏大师班,扩展了解开游戏,展示了如何使用 Canvas 绘制渐变和图像。它还讨论了精灵表动画和多层管理。

第六章,为您的游戏添加声音效果,通过使用“音频”元素向游戏添加声音效果和背景音乐。它讨论了网络浏览器之间的音频格式能力,并在本章末创建了一个键盘驱动的音乐游戏。

第七章,使用本地存储存储游戏数据,扩展了 CSS3 记忆匹配游戏,以演示如何使用新的本地存储 API 来存储和恢复游戏进度和最佳记录。

第八章,使用 WebSockets 构建多人画图猜词游戏,讨论了新的 WebSockets API,它允许浏览器与套接字服务器建立持久连接。这允许多个玩家实时一起玩游戏。本章末创建了一个画图猜词游戏。

第九章,使用 Box2D 和 Canvas 构建物理汽车游戏,教授如何将著名的物理引擎 Box2D 集成到我们的 Canvas 游戏中。它讨论了如何创建物理实体,施加力,将它们连接在一起,将图形与物理相关联,并最终创建一个平台卡车游戏。

本书需要什么

您需要最新的现代网络浏览器,一个良好的文本编辑器,以及基本的 HTML,CSS 和 JavaScript 知识。

这本书适合谁

本书适用于具有 HTML、CSS 和 JavaScript 基本理解,并希望创建在浏览器上运行的 Canvas 或基于 DOM 的游戏的游戏设计师。

约定

在本书中,您会经常看到几个标题。

为了清晰地说明如何完成一个过程或任务,我们使用:

行动时间标题

  1. 动作 1

  2. 动作 2

  3. 动作 3

指示通常需要一些额外的解释,以便理解,因此它们后面会跟着:

刚刚发生了什么?

这个标题解释了您刚刚完成的任务或指示的工作原理。

您还会在本书中找到一些其他学习辅助工具,包括:

弹出测验标题

这些是简短的多项选择问题,旨在帮助您测试自己的理解。

试试看英雄标题

这些设置了实际的挑战,并为您提供了尝试所学内容的想法。

您还会发现一些文本样式,用于区分不同类型的信息。以下是一些样式的示例,以及它们的含义解释。

文本中的代码单词显示如下:“我们将从index.html开始我们的 HTML5 游戏开发之旅。”

代码块设置如下:

// starting game
var date = new Date();
audiogame.startingTime = date.getTime();
// some time later
var date = new Date();
var elapsedTime = (date.getTime() - audiogame.startingTime)/1000;

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

function setupLevelData()
{
var notes = audiogame.leveldata.split(";");
// store the total number of dots
audiogame.totalDotsCount = notes.length;
for(var i in notes)
{
var note = notes[i].split(",");
var time = parseFloat(note[0]);
var line = parseInt(note[1]);
var musicNote = new MusicNote(time,line);
audiogame.musicNotes.push(musicNote);
}
}

任何命令行输入或输出都以如下形式书写:

$ ./configure
$ sudo make install

新术语重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会出现在文本中,就像这样:“您将获得多用户草图板的介绍页面。右键单击启动实验选项,然后选择在新窗口中打开链接”。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:介绍 HTML5 游戏

超文本标记语言 HTML 在过去几十年中一直在塑造互联网。它定义了内容在网页中的结构以及相关页面之间的链接。HTML 从 2 版到 HTML 4.1 版再到 XHTML 1.1 版不断发展。由于这些网络应用程序和社交网络应用程序,HTML 现在正在向 HTML5 迈进。

层叠样式表CSS)定义了网页的视觉呈现方式。它为所有 HTML 元素和它们的状态(如悬停和激活)定义样式。

JavaScript 是网页的逻辑控制器。它使网页动态化,并在页面和用户之间提供客户端交互。它通过文档对象模型DOM)访问 HTML。它通过应用不同的 CSS 样式来重新设计 HTML 元素。

这三个功能为我们带来了新的游戏市场,HTML5 游戏。有了它们的新力量,我们可以使用 HTML5 元素、CSS3 属性和 JavaScript 设计游戏在浏览器中玩耍。

在本章中,我们将:

  • 发现 HTML5 中的新功能

  • 讨论让我们对 HTML5 和 CSS3 如此兴奋的原因

  • 看看其他人如何使用 HTML5 进行游戏设计

  • 预览我们将在后面章节中构建的游戏

所以让我们开始吧。

在 HTML5 中发现新功能

HTML5 和 CSS3 中引入了许多新功能。在我们开始创建游戏之前,让我们概览一下这些新功能,看看我们如何使用它们来创建游戏。

画布

Canvas是 HTML5 元素,提供低级别的绘制形状和位图操作功能。我们可以将 Canvas 元素想象成一个动态图像标签。传统的<img>标签显示静态图像。无论图像是动态生成的还是静态加载自服务器,图像都是静态的,不会改变。我们可以将<img>标签更改为另一个图像源,或者对图像应用样式,但我们无法修改图像的位图上下文本身。

另一方面,Canvas 就像是一个客户端动态的<img>标签。我们可以在其中加载图像,在其中绘制形状,并通过 JavaScript 与之交互。

Canvas 在 HTML5 游戏开发中扮演着重要角色。这是本书的主要关注点之一。

音频

背景音乐和音效通常是游戏设计中的重要元素。HTML5 通过audio标签提供了本地音频支持。由于这一功能,我们不需要专有的 Flash Player 来播放 HTML5 游戏中的音效。我们将在第六章讨论audio标签的用法,使用 HTML5 音频元素构建音乐游戏

地理位置

地理位置让网页获取用户计算机的纬度和经度。多年前,当每个人都在使用台式电脑上网时,这个功能可能并不那么有用。我们并不需要用户的道路级别的位置精度。我们可以通过分析 IP 地址获得大致位置。

如今,越来越多的用户使用强大的智能手机上网。Webkit 和其他现代移动浏览器都在每个人的口袋里。地理位置让我们设计移动应用程序和游戏,以便使用位置信息。

基于位置的服务已经在一些社交网络应用程序中使用,例如 foursquare (foursquare.com)和 Gowalla (gowalla.com)。这种基于位置的社交社区的成功创造了使用位置服务与我们的智能手机的趋势。

WebGL

WebGL 通过在 Web 浏览器中提供一组 3D 图形 API 来扩展 Canvas 元素。该 API 遵循 OpenGL ES 2.0 的标准。WebGL 为 3D HTML5 游戏提供了一个真正的 3D 渲染场所。然而,在撰写本书时,并非所有浏览器都原生支持 WebGL。目前,只有 Mozilla Firefox 4、Google Chrome 和 WebKit 浏览器的夜间构建版本原生支持它。

为 WebGL 创建游戏的技术与通常的 HTML5 游戏开发有很大不同。在 WebGL 中创建游戏需要处理 3D 模型,并使用类似于 OpenGL 的 API。因此,本书不会讨论 WebGL 游戏开发。

来自 Google Body(bodybrowser.googlelabs.com)的以下屏幕截图演示了他们如何使用 WebGL 显示一个响应用户输入的 3D 人体。

WebGL

提示

LearningWebGL(learnwebgl.com)提供了一系列关于开始使用 WebGL 的教程。如果您想要学习更多关于使用它的知识,这是一个很好的起点。

WebSocket

WebSocket 是 HTML5 规范的一部分,用于将网页连接到套接字服务器。它为浏览器和服务器之间提供了事件驱动的连接。这意味着客户端不需要每隔一段时间轮询服务器以获取新数据。只要有数据更新,服务器就会将更新推送到浏览器。这个功能的一个好处是游戏玩家几乎可以实时互动。当一个玩家做了什么并将数据发送到服务器时,服务器将向每个其他连接的浏览器广播一个事件,以确认玩家刚刚做了什么。这创造了创建多人 HTML5 游戏的可能性。

注意

由于安全问题,Mozilla Firefox 和 Opera 现在暂时禁用了 WebSocket。Safari 和 Chrome 也可能在问题解决之前放弃对 WebSocket 的支持。您可以通过访问以下链接了解更多关于这个问题的信息:hacks.mozilla.org/2010/12/websockets-disabled-in-firefox-4/

本地存储

HTML5 为 Web 浏览器提供了持久的数据存储解决方案。

本地存储可以持久地存储键值对数据。即使浏览器终止,数据仍然存在。此外,数据不仅限于只能由创建它的浏览器访问。它对具有相同域的所有浏览器实例都是可用的。由于本地存储,我们可以在 Web 浏览器中轻松地本地保存游戏状态,如进度和获得成就。

HTML5 还提供了 Web SQL 数据库。这是一个客户端关系数据库,目前受 Safari、Chrome 和 Opera 支持。通过数据库存储,我们不仅可以存储键值对数据,还可以支持 SQL 查询的复杂关系结构。

本地存储和 Web SQL 数据库对我们在创建游戏时本地保存游戏状态非常有用。

除了本地存储,一些其他存储方法现在也得到了 Web 浏览器的支持。这些包括 Web SQL 数据库和 IndexedDB。这些方法支持根据条件查询存储的数据,因此更适合支持复杂的数据结构。

您可以在 Mozilla 的以下链接中找到更多关于使用 Web SQL 数据库和 IndexedDB 的信息:hacks.mozilla.org/2010/06/comparing-indexeddb-and-webdatabase/

离线应用程序

通常我们需要互联网连接来浏览网页。有时我们可以浏览缓存的离线网页。这些缓存的离线网页通常会很快过期。通过 HTML5 引入的下一个离线应用程序,我们可以声明我们的缓存清单。这是一个文件列表,将在没有互联网连接时存储以供以后访问。

通过缓存清单,我们可以将所有游戏图形、游戏控制 JavaScript 文件、CSS 样式表和 HTML 文件本地存储。我们可以将我们的 HTML5 游戏打包成桌面或移动设备上的离线游戏。玩家甚至可以在飞行模式下玩游戏。

来自 Pie Guy 游戏(mrgan.com/pieguy)的以下屏幕截图显示了 iPhone 上的 HTML5 游戏,没有互联网连接。请注意离线状态的小飞机符号:

离线应用

发现 CSS3 中的新功能

CSS 是演示层,HTML 是内容层。它定义了 HTML 的外观。在使用 HTML5 创建游戏时,尤其是基于 DOM 的游戏,我们不能错过 CSS。我们可能纯粹使用 JavaScript 来创建和设计带有 Canvas 元素的游戏。但是在创建基于 DOM 的 HTML5 游戏时,我们需要 CSS。因此,让我们看看 CSS3 中有什么新内容,以及如何使用新属性来创建游戏。

新的 CSS3 属性让我们可以以不同的方式在 DOM 中进行动画,而不是直接在 Canvas 绘图板上绘制和交互。这使得可以制作更复杂的基于 DOM 的浏览器游戏。

CSS3 过渡

传统上,当我们对元素应用新样式时,样式会立即更改。CSS3 过渡在目标元素的样式更改期间应用插值。

例如,我们这里有一个蓝色的框,当我们鼠标悬停时想要将其变为红色。我们将使用以下代码片段:

HTML:

<a href="#" class="box"></a>

CSS:

a.box {
display:block;
width: 100px;
height: 100px;
background: #00f; /* blue */
border: 1px solid #000;
}
a.box:hover {
background: #f00;
}

当我们鼠标悬停时,框立即变为红色。应用了 CSS3 过渡后,我们可以使用特定持续时间和缓动值来插值样式:

a.box {
-webkit-transition: all 5s linear;
}

提示

下载本书的示例代码

您可以从您在www.PacktPub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便直接将文件发送到您的电子邮件。

以下屏幕截图显示了应用过渡的框悬停效果:

CSS3 过渡

注意

由于 CSS3 规范仍处于草案阶段,尚未确定,因此来自不同浏览器供应商的实现可能与 W3C 规范有一些细微差异。因此,浏览器供应商倾向于使用供应商前缀来实现其 CSS3 属性,以防止冲突。

Safari 和 Chrome 使用-webkit-前缀。Opera 使用-o-前缀。Firefox 使用-moz-前缀,IE 使用-ms-前缀。现在声明 CSS3 属性,例如 box-shadow,可能有点复杂,需要为几个浏览器编写几行相同的规则。我们可以期望在该属性规范确定后,前缀将被消除。

我将在大多数示例中只使用-webkit-前缀,以防止在书中放置太多相似的行。更重要的是理解概念,而不是在这里阅读带有不同供应商前缀的相同规则。

CSS3 变换

CSS3 变换让我们可以缩放元素,旋转元素和平移它们的位置。CSS3 变换分为 2D 和 3D。

我们可以用 translate 重新定位一个元素:

-webkit-transform: translate(x,y);

或者使用缩放变换来缩放元素:

-webkit-transform: scale(1.1);

我们还可以使用 CSS3 变换来缩放和旋转元素,并结合其他变换:

a.box {
-webkit-transition: all 0.5s linear;
-webkit-transform: translate(100px,50px);
}
a.box:hover {
-webkit-transform: translate(100px,50px) scale(1.1) rotate(30deg);
}

以下屏幕截图显示了当我们鼠标悬停时 CSS3 变换效果:

CSS3 变换

CSS3 变换 3D 进一步将空间扩展到三个轴,目前仅在 Safari 和移动 Safari 上有效。来自WebKit.org的以下屏幕截图显示了当我们鼠标悬停时 3D 卡片翻转效果:

CSS3 变换

CSS3 动画

CSS3 过渡是一种动画类型。它声明了元素的两种样式之间的插值动画。

CSS3 动画是更进一步的一步。我们可以定义动画的关键帧。每个关键帧包含应在该时刻更改的一组属性。这就像一组应用于目标元素的 CSS3 过渡的序列。

AT-AT Walker (anthonycalzadilla.com/css3-ATAT/index-bones.html) 展示了使用 CSS3 动画关键帧、变换和过渡创建骨骼动画的演示:

CSS3 动画

学习更多 HTML5 和 CSS3 新功能的细节

来自 Google 的 HTML5Rocks(html5rocks.com)提供了一个关于新 HTML5 元素和 CSS3 属性的坚实的快速入门指南。

苹果还展示了在其基于 WebKit 的浏览器中使用 HTML5 可以有多么吸引人(apple.com/html5)。

CSS3 Info(www.css3.info)是一个提供最新 CSS3 新闻的博客。这是一个获取最新 CSS3 规范状态、兼容列表和基本 CSS3 代码的好地方。

创建 HTML5 游戏的好处

我们探索了 HTML5 和 CSS3 的一些关键新功能。有了这些功能,我们可以在浏览器上创建 HTML5 游戏。但是为什么我们需要这样做呢?创建 HTML5 游戏有什么好处呢?

不需要第三方插件

在现代浏览器中原生支持所有这些功能,我们不需要用户预先安装任何第三方插件才能进行游戏。这些插件不是标准的。它们是专有的,通常需要额外的插件安装,我们可能无法安装。

支持 iOS 设备而无需插件

全球数百万的苹果 iOS 设备不支持 Flash Player 等第三方插件。无论苹果出于什么原因不允许 Flash Player 在他们的移动 Safari 上运行,HTML5 和相关的 Web 标准是他们在浏览器中得到的。我们可以通过创建为移动设备优化的 HTML5 游戏来触及这一用户群体。

打破传统浏览器游戏的界限

在传统的游戏设计中,我们在一个边界框内构建游戏。我们在电视上玩视频游戏。我们在网页浏览器中玩 Flash 游戏,有一个矩形边界。

有了创意,我们不再受限于矩形游戏舞台。我们可以玩弄所有页面元素,甚至可以使用许多浏览器窗口来组成一个游戏。此外,我们甚至可以只使用 URL 栏来创建一个游戏(probablyinteractive.com/url-hunter)。这可能听起来有些混乱,但这是因为还没有多少网页这样做。

Photojojo(photojojo.com/store/awesomeness/cell-phone-lenses)是一个在线摄影商店,在其商店页面上提供了一个有趣的彩蛋功能。页面上有一个带有标题“不要拉”的开关按钮。当用户点击它时,一个橙色的手臂从顶部出现,帧逐帧地进行动画处理。它像一块布一样抓住网页并将整个页面向上拉,创建一个有趣的向下滚动效果。这不是一个游戏,但足够有趣,可以展示我们如何打破界限。

打破传统浏览器游戏的界限

这里有另一个例子,名为 Twitch(reas.com/twitch/),来自 Chrome 实验。这是一个迷你游戏集合,玩家必须将球从起点运送到终点。有趣的是,每个迷你游戏都是一个小型浏览器窗口。当球到达该迷你游戏的目的地时,它会被转移到新创建的迷你游戏浏览器中继续旅程。以下截图显示了 Twitch 的整个地图以及各个网页浏览器:

打破传统浏览器游戏的界限

构建 HTML5 游戏

由于 HTML5 和 CSS3 的新功能,我们现在可以在浏览器中创建整个游戏。我们可以控制 DOM 中的每个元素。我们可以使用 CSS3 对每个文档对象进行动画处理。我们有 Canvas 来动态绘制和与之交互。我们有音频元素来处理背景音乐和音效。我们还有本地存储来保存游戏数据和 WebSocket 来创建实时多人游戏。大多数现代浏览器已经支持这些功能。现在是时候制作 HTML5 游戏了。

其他人正在玩的 HTML5 游戏

通过观察使用不同技术制作的其他 HTML5 游戏,我们有机会研究不同 HTML5 游戏的表现。

匹配游戏

匹配游戏 (10k.aneventapart.com/Uploads/300/) 展示了一个美丽的匹配游戏,使用了 CSS3 动画和其他视觉增强效果。当您按下 3D 样式的 CSS 按钮时,游戏开始。卡片在背面和正面使用 3D 旋转翻转。正面的图案是从在线画廊动态获取的。

匹配游戏

Sinuous

Sinuous (10k.aneventapart.com/Uploads/83/),10K Apart 的获胜者,向我们展示了一个简单的游戏想法如何通过适当的实现让人上瘾。玩家用鼠标控制空间中的大点。目标是移动点以避开飞来的彗星。听起来很简单,但绝对让人上瘾,是一个“再试一次”的游戏。这个游戏是用 Canvas 标签创建的。玩家还可以在他们的支持 webkit 的移动设备上玩这个游戏,比如 iPhone、iPad 和 Android。

Sinuous

类似于小行星的书签

来自瑞典的网页设计师 Erik 创建了一个有趣的书签。它是一个适用于任何网页的类似小行星的游戏。是的,任何网页。它展示了与任何网页进行交互的一种异常方式。它在您正在阅读的网站上创建了一个飞机。然后您可以使用箭头键驾驶飞机,并使用空格键发射子弹。有趣的是,子弹会摧毁页面上的 HTML 元素。您的目标是摧毁您选择的网页上的所有东西。这个书签是打破通常浏览器游戏界限的又一个例子。它告诉我们,在设计 HTML5 游戏时,我们可以打破常规思维。

该书签可以在以下网址安装:erkie.github.com/

以下的截图显示了飞机摧毁网页内容的情况:

类似于小行星的书签

Quake 2

谷歌演示了第一人称射击游戏 Quake 2 的 WebGL HTML5 移植版。玩家可以使用 WSAD 键四处移动,并用鼠标射击敌人。玩家甚至可以通过 WebSocket 实时进行多人游戏。据谷歌称,HTML5 Quake 2 的每秒帧数可以达到 60 帧。

Quake 2

Quake 2 移植版可以在 Google Code 上找到:code.google.com/p/quake2-gwt-port/

RumpeTroll

RumpeTroll (rumpetroll.com/) 是 HTML5 社区的一个实验,每个人都可以通过 WebSocket 连接在一起。我们可以给我们的生物取名字,并通过鼠标点击四处移动。我们还可以输入任何内容开始聊天。此外,由于 WebSocket 插入,我们可以实时看到其他人在做什么。

RumpeTroll

Scrabb.ly

Scrabb.ly (scrabb.ly) 是一个多人游戏,赢得了 Node.js Knockout 比赛的人气奖。它使用 HTML5 WebSocket 将用户连接在一起。这个在线棋盘游戏是基于 DOM 的,由 JavaScript 驱动。

Scrabb.ly

注意

Node.js (http://nodejs.org) 是一个事件驱动的服务器端 JavaScript。它可以用作连接并发 WebSocket 客户端的服务器。

Aves Engine

Aves Engine 是由 dextrose 开发的 HTML5 游戏开发框架。它为游戏开发者提供了工具和 API,用于构建自己的等距浏览器游戏世界和地图编辑器。从官方演示视频中捕获的以下截图显示了它是如何创建等距世界的:

Aves Engine

该引擎还负责 2.5 维等距坐标系统、碰撞检测和其他基本的虚拟世界功能。这个游戏引擎甚至在 iPad 和 iPhone 等移动设备上运行良好。Aves Engine 自首次亮相以来就引起了很多关注,现在已被大型社交游戏公司 Zynga Game Network Inc 收购。

Aves Engine 的视频演示可在 YouTube 上通过以下链接观看:

tinyurl.com/dextrose-aves-engine-sneak

浏览更多 HTML5 游戏

这些例子只是其中的一部分。以下网站提供了由他人创建的 HTML5 游戏的更新:

  • Canvas Demo (canvasdemo.com) 收集了一系列使用 HTML5 Canvas 标签的应用程序和游戏。它还提供了大量 Canvas 教程资源。这是学习 Canvas 的好地方。

  • HTML5 游戏 (html5games.com) 收集了许多 HTML5 游戏,并将它们组织成不同的类别。

  • Mozilla Labs 在 2011 年初举办了一个 HTML5 游戏设计比赛,许多优秀的游戏被提交到比赛中。比赛现在已经结束,所有参赛作品的列表在以下链接:gaming.mozillalabs.com/games/

  • HTML5 Game Jam (www.html5gamejam.com/games) 是一个 HTML5 活动,该网站列出了一系列有趣的 HTML5 游戏,还提供了一些有用的资源。

我们将在本书中创建的内容

在接下来的章节中,我们将构建六款游戏。我们将首先创建一个基于 DOM 的乒乓球游戏,可以由同一台机器上的两名玩家进行游戏。然后我们将创建一个带有 CSS3 动画的记忆匹配游戏。之后,我们将使用 Canvas 创建一个解开谜题的游戏。接下来,我们将使用音频元素创建一个音乐游戏。然后,我们将使用 WebSocket 创建一个多人绘画和猜谜游戏。最后,我们将使用 Box2D JavaScript 端口创建一个物理汽车游戏的原型。以下截图是我们将在第三章中构建的记忆匹配游戏的截图,在 CSS3 中构建记忆匹配游戏

我们将在本书中创建的内容

摘要

在本章中,我们学到了关于 HTML5 游戏的基本信息。

具体来说,我们涵盖了:

  • 来自 HTML5 和 CSS3 的新功能。我们已经初步了解了我们将在后续章节中使用的技术。Canvas、音频、CSS 动画等更多新功能被介绍了。我们将有许多新功能可以使用。

  • 创建 HTML5 游戏的好处。我们讨论了为什么要创建 HTML5 游戏。我们想要满足网络标准,满足移动设备,并打破游戏的边界。

  • 其他人正在玩的 HTML5 游戏。我们列出了使用我们将使用的不同技术创建的几款现有 HTML5 游戏。在创建我们自己的游戏之前,我们可以测试这些游戏。

  • 我们还预览了本书中将要构建的游戏。

现在我们已经了解了一些关于 HTML5 游戏的背景信息,我们准备在下一章中创建我们的第一个基于 DOM 的 JavaScript 驱动游戏。

第二章:开始使用基于 DOM 的游戏开发

在第一章“介绍 HTML5 游戏”中,我们已经对整本书要学习的内容有了一个概念。从本章开始,我们将经历许多通过实践学习的部分,并且我们将在每个部分专注于一个主题。在深入研究尖端的 CSS3 动画和 HTML5 Canvas 游戏之前,让我们从传统的基于 DOM 的游戏开发开始。在本章中,我们将用一些基本技术热身。

在本章中,我们将:

  • 准备开发工具

  • 设置我们的第一个游戏-乒乓球

  • 使用 jQuery JavaScript 库学习基本定位

  • 获取键盘输入

  • 使用记分的乒乓球游戏

以下屏幕截图显示了本章结束后我们将获得的游戏。这是一个由两名玩家同时使用一个键盘玩的乒乓球游戏:

开始使用基于 DOM 的游戏开发

所以,让我们开始制作我们的乒乓球。

准备开发环境

开发 HTML5 游戏的环境类似于设计网站。我们需要具有所需插件的 Web 浏览器和一个好的文本编辑器。哪个文本编辑器好是一个永无止境的争论。每个文本编辑器都有其自身的优势,所以只需选择您喜欢的即可。对于浏览器,我们将需要一个支持最新 HTML5、CSS3 规范并为我们提供方便的调试工具的现代浏览器。

现在互联网上有几种现代浏览器选择。它们是苹果 Safari(apple.com/safari/)、Google Chrome(www.google.com/chrome/)、Mozilla Firefox(mozilla.com/firefox/)和 Opera(opera.com)。这些浏览器支持我们在整本书中讨论的大多数功能。我们将使用 Google Chrome 来演示本书中的大多数示例,因为它在 CSS3 过渡和 Canvas 上运行速度快且流畅。

为基于 DOM 的游戏准备 HTML 文档

每个网站、网页和 HTML5 游戏都以默认的 HTML 文档开始。此外,文档以基本的 HTML 代码开始。我们将从index.html开始我们的 HTML5 游戏开发之旅。

安装 jQuery 库的操作时间

我们将从头开始创建我们的 HTML5 乒乓球游戏。这可能听起来好像我们要自己准备所有的东西。幸运的是,至少我们可以使用一个 JavaScript 库来帮助我们。jQuery是我们将在整本书中使用的JavaScript 库。它将帮助我们简化我们的 JavaScript 逻辑:

  1. 创建一个名为pingpong的新文件夹。

  2. pingpong目录中创建一个名为js的新文件夹。

  3. 现在是时候下载 jQuery 库了。转到jquery.com/

  4. 选择生产并单击下载 jQuery

  5. jquery-1.4.4.min.js保存在我们在步骤 2 中创建的js文件夹中。

  6. 创建一个名为index.html的新文档,并将其保存在第一个游戏文件夹中。

  7. 在文本编辑器中打开index.html并插入一个空的 HTML 模板:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ping Pong</title>
</head>
<body>
<header>
<h1>Ping Pong</h1>
</header>
<footer>
This is an example of creating a Ping Pong Game.
</footer>
</body>
</html>

  1. 通过在 body 标签关闭之前添加以下行来包含 jQuery JavaScript 文件:
<script src="img/jquery-1.4.4.min.js"></script>

  1. 最后,我们必须确保 jQuery 已成功加载。我们将在 body 标签关闭之前并在 jQuery 之后放置以下代码:
<script>
$(function(){
alert("Welcome to the Ping Pong battle.");
});
</script>

  1. 保存index.html并在浏览器中打开它。我们应该看到以下警报窗口显示我们的文本。这意味着我们的 jQuery 已正确设置:

安装 jQuery 库的操作时间

刚刚发生了什么?

我们刚刚创建了一个基本的带有 jQuery 的 HTML5 页面,并确保 jQuery 已正确加载。

新的 HTML5 doctype

在 HTML5 中,DOCTYPEmeta标签被简化了。

在 HTML4.01 中,我们声明 doctype 的代码如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

这是一行很长的代码,对吧?在 HTML5 中,doctype 声明不能更简单:

<!DOCTYPE html>

我们甚至没有在声明中使用 HTML 版本。这意味着 HTML5 将支持以前 HTML 版本的所有现有内容。未来的 HTML 版本也将支持 HTML5 的现有内容。

简化也适用于meta标签。现在我们可以使用以下简短的行来定义 HTML 的字符集:

<meta charset=utf-8>

页眉和页脚

HTML5 带来了许多新功能和改进,其中之一就是语义。HTML5 添加了新元素来改进语义。我们刚刚使用了两个,headerfooter。Header 为部分或整个页面提供了标题介绍。因此,我们将h1标题放在 header 内。Footer 与其名称相同,包含了部分或整个页面的页脚信息。

注意

语义 HTML 意味着标记本身提供了有意义的信息,而不仅仅定义了视觉外观。

放置 JavaScript 代码的最佳实践

我们将 JavaScript 代码放在所有页面内容之后和</body>标签之前。之所以将代码放在那里而不是放在<head></head>部分内,是有原因的。

通常,浏览器会从顶部到底部加载内容并呈现它们。如果将 JavaScript 代码放在head部分,那么直到所有 JavaScript 代码加载完毕,文档的内容才会被加载。实际上,如果浏览器在页面中间加载 JavaScript 代码,所有呈现和加载都将被阻塞。这就是为什么我们希望尽可能将 JavaScript 代码放在底部的原因。这样,我们可以以更高的性能提供内容。

在撰写本书时,最新的 jQuery 版本是 1.4.4。这就是为什么我们代码示例中的 jQuery 文件被命名为jquery-1.4.4.min.js。这个版本号会有所不同,但使用方式应该是相同的,除非 jQuery 发生了没有向后兼容的重大变化。

在页面准备就绪后运行我们的代码

我们需要确保页面在执行我们的 JavaScript 代码之前已经准备就绪。否则,当我们尝试访问尚未加载的元素时,可能会出现错误。jQuery 为我们提供了一种在页面准备就绪后执行代码的方法。以下是代码:

jQuery(document).ready(function(){
// code here.
});

实际上,我们刚刚使用的是以下代码:

$(function(){
// code here.
});

$符号是 jQuery 的快捷方式。当我们调用$(something)时,实际上是在调用jQuery(something)

$(function_callback)ready事件的另一个快捷方式。

这与以下内容相同:

$(document).ready(function_callback);

同样,与以下内容相同:

jQuery(document).ready(function_callback);

快速测验

  1. 哪里是放置 JavaScript 代码的最佳位置?

a. 在<head>标签之前

b. 在<head></head>元素内

c. 在<body>标签之后

d. 在</body>标签之前

设置乒乓球游戏元素

我们已经准备好了准备工作,现在是设置乒乓球游戏的时候了。

行动时间 将乒乓球游戏元素放入 DOM

  1. 我们将从 jQuery 安装示例继续。在文本编辑器中打开index.html

  2. 然后,在 body 中创建以下游乐场和 DIV 节点中的游戏对象。游乐场内有两个挡板和一个球。此外,游乐场位于游戏内:

<div id="game">
<div id="playground">
<div id="paddleA" class="paddle"></div>
<div id="paddleB" class="paddle"></div>
<div id="ball"></div>
</div>
</div>

  1. 我们现在已经准备好了游戏对象的结构,现在是给它们应用样式的时候了。将以下样式放在head元素内:
<style>
#playground{
background: #e0ffe0;
width: 400px;
height: 200px;
position: relative;
overflow: hidden;
}
#ball {
background: #fbb;
position: absolute;
width: 20px;
height: 20px;
left: 150px;
top: 100px;
border-radius: 10px;
}
.paddle {
background: #bbf;
left: 50px;
top: 70px;
position: absolute;
width: 30px;
height: 70px;
}
#paddleB {
left: 320px;
}
</style>

  1. 在最后一节中,我们将我们的 JavaScript 逻辑放在了 jQuery 包含之后。随着代码的不断增长,我们将把它放在一个单独的文件中。因此,在js文件夹中创建一个名为html5games.pingpong.js的文件。

  2. 我们准备了 JavaScript 文件。现在是将它们链接到我们的 HTML 文件的时候了。在index.html中的</body>标签之前放入以下代码:

<script src="img/jquery-1.4.4.js"></script>
<script src="img/html5games.pingpong.js"></script>

  1. 我们将把游戏逻辑放在html5games.pingpong.js中。我们现在唯一的逻辑是以下挡板的初始化代码:
// code inside $(function(){} will run after the DOM is loaded and ready
$(function(){
$("#paddleB").css("top", "20px");
$("#paddleA").css("top", "60px");
});

  1. 我们将在浏览器中测试设置。在浏览器中打开index.html文件,我们应该看到与以下截图类似的屏幕:

进行操作将乒乓球游戏元素放置在 DOM 中

刚刚发生了什么?

我们在乒乓球比赛中放了两个球拍和一个球。我们还使用 jQuery 来初始化两个球拍的位置。

介绍 jQuery

jQuery 是一个为了轻松浏览 DOM 元素、操作它们、处理事件和创建异步远程调用而设计的 JavaScript 库。

它包含两个主要部分:选择修改。选择使用 CSS 选择器语法在网页中选择所有匹配的元素。修改操作修改所选元素,例如添加、删除子元素或样式。使用 jQuery 通常意味着将选择和修改操作链接在一起。

它包含两个主要部分:选择修改。选择使用 CSS 选择器语法在网页中选择所有匹配的元素。修改操作修改所选元素,例如添加、删除子元素或样式。使用 jQuery 通常意味着将选择和修改操作链接在一起。

例如,以下代码选择所有具有box类的元素并设置 CSS 属性:

$(".box").css({"top":"100px","left":"200px"});

理解基本的 jQuery 选择器

jQuery 是关于选择元素并对其执行操作。我们需要一种方法来在整个 DOM 树中选择我们需要的元素。jQuery 借用了 CSS 的选择器。选择器提供一组模式来匹配元素。以下表列出了我们在本书中将使用的最常见和有用的选择器:

选择器模式 含义 示例
\(("Element") | 选择具有给定标签名称的所有元素 | `\)("p")选择所有的 p 标签。$("body")`选择 body 标签。
\(("#id") | 选择具有给定属性 ID 的元素 | 提供以下代码:**<div id="box1"></div>**<div id="box2"></div>`\)("#box1")`选择突出显示的元素。
\((".className") | 选择具有给定类属性的所有元素 | 提供以下代码:**<div class="apple"></div>****<div class="apple"></div>**<div class="orange"></div><div class="banana"></div>`\)(".apple")`选择具有设置为 apple 的类的突出显示的元素。
\(("selector1, selector2, selectorN") | 选择与给定选择器匹配的所有元素 | 提供以下代码:**<div class="apple"></div>****<div class="apple"></div>****<div class="orange"></div>**<div class="banana"></div>`\)(".apple, .orange")`选择设置为 apple 或 orange 的突出显示的元素。

理解 jQuery CSS 函数

jQuery css是一个用于获取和设置所选元素的 CSS 属性的函数。

这是如何使用css函数的一般定义:

.css(propertyName)
.css(propertyName, value)
.css(map)

css函数接受以下表中列出的几种类型的参数:

函数类型 参数定义 讨论
.css(propertyName) propertyName是 CSS 属性 该函数返回所选元素的给定 CSS 属性的值。例如,以下代码返回body元素的background-color属性的值:$("body").css("background-color")它只会读取值,而不会修改属性值。
.css(propertyName, value) propertyName是 CSS 属性,value是要设置的值 该函数将给定的 CSS 属性修改为给定的值。例如,以下代码将所有具有box类的元素的背景颜色设置为红色:$(".box").css("background-color","#ff0000")
.css(map) map是要更新的属性-值对集合 此函数用于同时将多个 CSS 属性设置为相同的选定元素。例如,以下代码将 ID 为box1的选定元素的左侧和顶部 CSS 属性都设置为:$("#box1").css({"left" : "40px","top" : "100px"})

使用 jQuery 的好处

使用 jQuery 而不是纯 JavaScript 有几个优点,如下所示:

  • 使用 jQuery 需要更短的代码来选择 DOM 节点并修改它们

  • 更短的代码导致更清晰的代码阅读,这在通常包含大量代码的游戏开发中非常重要

  • 编写更短的代码可以提高开发速度

  • 使用 jQuery 库使得代码能够支持所有主要浏览器,无需额外的调整;jQuery 包装了纯 JavaScript 代码,并且自己处理跨浏览器的能力

使用 jQuery 在 DOM 中操作游戏元素

我们用 jQuery 初始化了球拍游戏元素。我们将进行一个实验,看看如何使用 jQuery 来放置游戏元素。

行动时间 使用 jQuery 改变元素的位置

让我们用网格背景检查一下我们的乒乓球游戏元素:

  1. 我们将继续我们的乒乓球示例。

  2. 我准备了一个网格图像。从以下 URL 下载pixel_grid.jpg图像:

gamedesign.cc/html5games/pixel_grid.jpg

  1. 在示例目录中创建一个名为images的文件夹。

  2. pixel_grid.jpg放入 images 文件夹中。这个图像可以帮助我们稍后检查像素位移。

  3. 接下来,在文本编辑器中打开index.html文件。

  4. 修改playground DIV 的background属性,包括像下面这样的像素网格图像:

#playground{
background: #e0ffe0 url(images/pixel_grid.jpg);
width: 400px;
height: 200px;
position: relative;
overflow: hidden;
}

  1. 现在在 web 浏览器中打开index.html,我们应该有以下的截图。游戏元素叠加在网格图像的顶部,所以我们可以看到元素的放置位置:

行动时间 使用 jQuery 改变元素的位置

刚刚发生了什么?

我们通过放置一个名为pixel_grid.jpg的图像来开始示例。这是我为了方便调试而创建的图像。图像被分成小网格。每个 10 x 10 的网格形成一个 100 x 100 像素的大块。通过将这个图像作为 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;
}

小测验

  1. 使用哪个 jQuery 选择器,如果你想选择所有的标题元素?

a. $("#header")

b. $(".header")

c. $("header")

d. $(header)

从玩家那里获取键盘输入

这本书是关于游戏开发的。我们可以将游戏开发看作是以下循环:

  1. 游戏状态被可视化显示。

  2. 玩家输入他们的命令。

  3. 游戏根据玩家的输入在设计好的游戏机制下运行。

  4. 再次从步骤 1 开始循环该过程。

在之前的章节中,我们学会了如何用 CSS 和 jQuery 显示游戏对象。接下来我们需要在游戏中获取玩家的输入。在本章中我们将讨论键盘输入。

行动时间 通过键盘输入移动 DOM 对象

我们将创建一个传统的乒乓球游戏。左右两侧有两个球拍。球放在操场的中间。玩家可以通过使用ws键来控制左球拍的上下移动,使用箭头上键来控制右球拍。我们将专注于键盘输入,将球的移动留到后面的部分:

  1. 让我们继续进行pingpong目录。

  2. 打开html5games.pingpong.js文件,其中包含我们的游戏逻辑。我们现在唯一的逻辑是监听按键按下事件并移动相应的球拍上下。用以下代码替换文件中的内容:

var KEY = {
UP: 38,
DOWN: 40,
W: 87,
S: 83
}
$(function(){
// listen to the key down event
$(document).keydown(function(e){
switch(e.which){
case KEY.UP: // arrow-up
// get the current paddle B's top value in Int type
var top = parseInt($("#paddleB").css("top"));
// move the paddle B up 5 pixels
$("#paddleB").css("top",top-5);
break;
case KEY.DOWN: // arrow-down
var top = parseInt($("#paddleB").css("top"));
// move the paddle B down 5 pixels
$("#paddleB").css("top",top+5);
break;
case KEY.W: // w
var top = parseInt($("#paddleA").css("top"));
// move the paddle A up 5 pixels
$("#paddleA").css("top",top-5);
break;
case KEY.S: // s
var top = parseInt($("#paddleA").css("top"));
// move the paddle A drown 5 pixels
$("#paddleA").css("top",top+5);
break;
}
});
keyboard inputkeyboard inputDOM objects, moving});

  1. 让我们测试游戏的球拍控制。在 Google Chrome 中打开index.html。尝试按下w键、s键和箭头上。两个球拍应该能够根据输入向上或向下移动,但现在它们不能同时移动。

行动时间 通过键盘输入移动 DOM 对象

刚刚发生了什么?

让我们看看我们刚刚使用的 HTML 代码。HTML 页面包含页眉、页脚信息和一个 ID 为game的 DIV。游戏节点包含一个名为 playground 的子节点。playground 包含三个子节点,两个球拍和一个球。

我们通常通过准备一个结构良好的 HTML 层次结构来开始 HTML5 游戏开发。HTML 层次结构帮助我们将类似的游戏对象(即一些 DIV)分组在一起。这有点像在 Adobe Flash 中将资产分组到电影剪辑中,如果你以前用过它制作动画的话。我们也可以将其视为游戏对象的图层,以便我们可以轻松地选择和样式化它们。

理解键码

键盘上的每个键都被分配一个数字。通过获取该数字,我们可以找出按下了哪个键。我们监听 jQuery 的keydown事件监听器。事件触发时,event对象包含键码。我们可以通过调用which函数来获取按下键的键码。

您可以尝试在keydown事件监听器中添加一个控制台日志函数,并观察每个键的表示整数:

$(document).keydown(function(e){
console.log(e.which);
keyboard inputkeyboard inputkey code});

使常量更易读

在我们的例子中,我们使用键码来检查玩家是否按下我们感兴趣的键。以箭头上键为例。它的键码是 38。我们可以简单地将键码与数字直接进行比较,如下所示:

$(document).keydown(function(e){
switch(e.which){
case 38:
// do something when pressed arrow-up
}
}

然而,这并不是一种推荐的做法,因为它使游戏代码更难以维护。想象一下,如果以后我们想要将动作从箭头上键映射到另一个键。我们可能不确定 38 是否表示箭头上。相反,我们可以使用以下代码为常量赋予一个有意义的名称:

var KEY = {
UP: 38,
DOWN: 40,
W: 87,
S: 83
}
// listen to the key down event
$(document).keydown(function(e){
switch(e.which){
case KEY.UP:
// do something when pressed arrow-up
}
}

通过给 38 命名为KEY.UP,我们可以确保代码块与箭头上键相关联,因此在维护游戏时我们可以毫无疑问地进行修改。

使用 parseInt 函数将字符串转换为数字

在大多数情况下,我们通过使用格式如100px来将左侧和顶部的 CSS 样式应用于 DOM 元素。在设置属性时,我们指定单位。当获取属性的值时也是一样的。当我们调用$("#paddleA").css("top")时,我们得到的值是100px而不是100。这在我们想要对该值进行算术运算时会给我们带来问题。

在大多数情况下,我们通过使用格式如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函数需要一个必需参数和一个可选参数:

参数 定义 讨论
字符串 要解析的字符串 该函数解析字符串的第一个数字。如果给定的字符串无法转换为数字,则返回NaN,即非数字。它将默认以十六进制解析以"0x"开头的字符串。以下代码是例子:parseInt("100px")返回 100。parseInt("5cm")返回 5。parseInt("0xF")返回 15。
基数 可选。用于指示要使用的数字系统的数字 第二个参数强制parseInt函数解析给定数字系统中的字符串。例如:parseInt("0x10")返回 16parseInt("0x10",10)返回 0parseInt("FF",16)返回 255

在控制台面板中直接执行 JavaScript 表达式

你还应该知道,你可以通过直接在控制台窗口中输入 JavaScript 表达式来执行 JavaScript 表达式。控制台窗口是 Google Chrome 开发者工具中的一个工具。(其他浏览器中也有类似的工具)。我们可以通过点击扳手图标 | 工具 | 开发者工具 | 控制台来打开控制台。

这是一个方便的方法,在开发过程中,当你不确定一个简单表达式是否有效时,可以快速测试一下。以下截图测试了两个parseInt表达式的返回值:

在控制台面板中直接执行 JavaScript 表达式

试试吧

有时将字符串转换为整数可能会很棘手。你知道10 秒 20parseInt结果是什么吗?10x10$20.5呢?

现在是时候打开控制台面板,尝试将一些字符串转换为数字。

检查控制台窗口

我们现在正在编写更复杂的逻辑代码。在开发者工具的控制台上保持警惕是一个好习惯。如果代码中包含任何错误或警告,错误消息将会出现在那里。它报告发现的任何错误以及包含错误的代码行。在测试 HTML5 游戏时,保持控制台窗口打开非常有用和重要。我曾经看到很多人因为代码不起作用而束手无策。原因是他们有拼写错误或语法错误,而他们在与代码搏斗数小时后才检查控制台窗口。

以下截图显示了html5games.pingpong.js文件的第 25 行存在错误。错误消息是赋值中的无效左侧。检查代码后,我发现我在设置 jQuery 中的 CSS top属性时错误地使用了等号(=):

$("#paddleA").css("top"=top+5);
// instead of the correct code:
// $("#paddleA").css("top", top+5);

检查控制台窗口

支持多个玩家的键盘输入

以前的输入方法只允许一次输入。键盘输入也不太顺畅。现在想象一下,两个玩家一起玩乒乓球游戏。他们无法很好地控制球拍,因为他们的输入会干扰对方。在本节中,我们将修改我们的代码,使其支持多个键盘输入。

行动时间 使用另一种方法监听键盘输入

我们将使用另一种方法来处理按键按下事件。这种方法会更加顺畅,并支持同时进行多个输入:

  1. 打开我们在上一节中使用的html5games.pingpong.js

  2. 删除我们在那里编写的所有代码。从头开始会更简单。

  3. 我们需要一个全局变量数组来存储按键的状态。在打开的 JavaScript 文件中输入以下代码:

var pingpong = {}
pingpong.pressedKeys = [];

  1. 接下来的事情是页面加载并准备就绪后执行代码。它将监听并标记按下的键。将以下代码放在我们刚刚编写的两行代码之后的 JavaScript 文件中:
$(function(){
// set interval to call gameloop every 30 milliseconds
pingpong.timer = setInterval(gameloop,30);
// mark down what key is down and up into an array called "pressedKeys"
$(document).keydown(function(e){
pingpong.pressedKeys[e.which] = true;
});
$(document).keyup(function(e){
pingpong.pressedKeys[e.which] = false;
});
});

  1. 我们已经存储了按下的键。我们缺少的是实际移动挡板。我们设置了一个定时器来连续调用一个移动挡板的函数。将以下代码粘贴到html5games.pingpong.js文件中:
function gameloop() {
movePaddles();
}
function movePaddles() {
// use our custom timer to continuously check if a key is pressed.
if (pingpong.pressedKeys[KEY.UP]) { // arrow-up
// move the paddle B up 5 pixels
var top = parseInt($("#paddleB").css("top"));
$("#paddleB").css("top",top-5);
}
if (pingpong.pressedKeys[KEY.DOWN]) { // arrow-down
// move the paddle B down 5 pixels
var top = parseInt($("#paddleB").css("top"));
$("#paddleB").css("top",top+5);
}
if (pingpong.pressedKeys[KEY.W]) { // w
// move the paddle A up 5 pixels
var top = parseInt($("#paddleA").css("top"));
$("#paddleA").css("top",top-5);
}
if (pingpong.pressedKeys[KEY.S]) { // s
// move the paddle A down 5 pixels
var top = parseInt($("#paddleA").css("top"));
$("#paddleA").css("top",top+5);
}
}

  1. 让我们测试一下我们刚刚编写的代码。保存所有文件,然后在 Web 浏览器中打开index.html

  2. 尝试按键控制两个挡板。两个挡板应该平稳移动,并且同时响应,没有中断。

刚刚发生了什么?

我们使用了另一种方法来捕获键盘输入。我们不是在检测到按键按下后立即执行动作,而是存储哪些键被按下,哪些没有。然后,我们使用 JavaScript 间隔每 30 毫秒检查按下的键。这种方法使我们能够同时知道当时按下的所有键,因此我们可以同时移动两个挡板。

更好地声明全局变量

全局变量是可以在整个文档中全局访问的变量。在任何函数外声明的变量都是全局变量。例如,在以下示例代码片段中,ab是全局变量,而c是一个局部变量,只存在于函数内部:

var a = 0;
var b = "xyz";
function something(){
var c = 1;
}

由于全局变量在整个文档中都可用,如果我们将不同的 JavaScript 库集成到网页中,可能会增加变量名冲突的可能性。作为良好的实践,我们应该将所有使用的全局变量放入一个对象中。

行动时间部分,我们有一个全局数组来存储所有按下的键。我们不仅将这个数组放在全局范围内,而是创建了一个名为pingpong的全局对象,并将数组放在其中:

var pingpong = {}
pingpong.pressedKeys = [];

将来,我们可能需要更多的全局变量,我们将把它们全部放在pingpong对象中。这样可以将名称冲突的机会减少到只有一个名称,pingpong

使用 setInterval 函数创建 JavaScript 定时器

按下的键存储在数组中,我们有一个定时器定期循环和检查数组。这可以通过 JavaScript 中的setInterval函数来实现。

以下是setInterval函数的一般定义:

setInterval(expression, milliseconds)

setInterval接受两个必需的参数:

参数 定义 讨论
表达式 要执行的函数回调或代码表达式 表达式可以是函数回调的引用或内联代码表达式。内联代码表达式需要引号,而函数回调的引用则不需要。例如,以下代码每 100 毫秒调用hello函数:setInterval(hello,100);以下代码每 100 毫秒调用带参数的hi函数:setInterval("hi('Makzan')",100);
毫秒 表达式每次执行之间的持续时间,以毫秒为单位 时间间隔的单位是毫秒。因此,将其设置为 1000 意味着每秒运行一次表达式。

理解游戏循环

我们有一个定时器,每 30 毫秒执行一些与游戏相关的代码,因此这段代码每秒执行 33.3 次。在游戏开发中,这称为游戏循环

在游戏循环中,我们将执行几个常见的事情:

  • 处理用户输入,我们刚刚做了

  • 更新游戏对象的状态,包括位置和外观

  • 检查游戏结束

在游戏循环中实际执行的内容因不同类型的游戏而异,但目的是相同的。游戏循环定期执行,以帮助游戏平稳运行。

使用 JavaScript 间隔移动 DOM 对象

现在想象一下,我们可以使小红球在操场上移动。当它击中球拍时,球会弹开。当球通过球拍并击中球拍后面的操场边缘时,玩家将失去得分。所有这些操作都是通过 jQuery 在 HTML 页面中操纵 DIV 的位置。要完成这个乒乓球游戏,我们的下一步是移动球。

用 JavaScript 间隔移动球的时间

我们刚刚学习并使用了setInterval函数来创建一个定时器。我们将使用定时器每 30 毫秒移动球一点。当球击中操场边缘时,我们还将改变球运动的方向。现在让球动起来:

  1. 我们将使用我们上一个示例,监听多个键盘输入,作为起点。

  2. 在文本编辑器中打开html5games.pingpong.js文件。

  3. 我们现在正在移动球,我们需要全局存储球的状态。我们将把与球相关的变量放在pingpong对象中:

pingpong.ball = {
speed: 5,
x: 150,
y: 100,
directionX: 1,
directionY: 1
}

  1. 在每个游戏循环中,我们都会移动球拍。现在我们也会移动球。在gameloop函数中添加一个moveBall函数调用:
function gameloop() {
moveBall();
movePaddles();
}

  1. 是时候定义moveBall函数了。该函数分为四个部分,它获取当前球的位置,检查操场的边界,在击中边界时改变球的方向,并在所有这些计算之后实际移动球。让我们把以下moveBall函数定义放在 JavaScript 文件中:
function moveBall() {
// reference useful variables
var playgroundHeight = parseInt($("#playground").height());
var playgroundWidth = parseInt($("#playground").width());
var ball = pingpong.ball;
// check playground boundary
// check bottom edge
if (ball.y + ball.speed*ball.directionY > playgroundHeight)
{
ball.directionY = -1;
}
// check top edge
if (ball.y + ball.speed*ball.directionY < 0)
{
ball.directionY = 1;
}
// check right edge
if (ball.x + ball.speed*ball.directionX > playgroundWidth)
{
ball.directionX = -1;
}
// check left edge
if (ball.x + ball.speed*ball.directionX < 0)
{
ball.directionX = 1;
}
ball.x += ball.speed * ball.directionX;
ball.y += ball.speed * ball.directionY;
// check moving paddle here, later.
// actually move the ball with speed and direction
$("#ball").css({
"left" : ball.x,
"top" : ball.y
});
}

  1. 我们已经准备好了每 30 毫秒移动一次球的代码。保存所有文件并在 Google Chrome 中打开index.html进行测试。

  2. 球拍的工作方式与上一个示例中的相同,球应该在操场上移动。

刚刚发生了什么?

我们刚刚成功地使球在操场上移动。我们有一个循环,每 30 毫秒运行一次常规游戏逻辑。在游戏循环中,我们每次移动球五个像素。

球的三个属性是速度和方向 X/Y。速度定义了球在每一步中移动多少像素。方向 X/Y 要么是 1,要么是-1。我们用以下方程移动球:

new_ball_x = ball_x_position + speed * direction_x
new_ball_y = ball_y_position + speed * direction_y

方向值乘以移动。当方向为 1 时,球向轴的正方向移动。当方向为-1 时,球向负方向移动。通过切换 X 和 Y 方向,我们可以使球在四个方向上移动。

我们将球的 X 和 Y 与操场 DIV 元素的四个边缘进行比较。这将检查球的下一个位置是否超出边界,然后我们在 1 和-1 之间切换方向以创建弹跳效果。

开始碰撞检测

在上一节中移动球时,我们已经检查了操场的边界。现在我们可以用键盘控制球拍并观察球在操场上移动。现在还缺少什么?我们无法与球互动。我们可以控制球拍,但球却像它们不存在一样穿过它们。这是因为我们错过了球拍和移动球之间的碰撞检测。

与球拍击球的时间

我们将使用类似的方法来检查碰撞的边界:

  1. 打开我们在上一节中使用的html5games.pingpong.js文件。

  2. moveball函数中,我们已经预留了放置碰撞检测代码的位置。找到带有// check moving paddle here的行。

  3. 让我们把以下代码放在那里。该代码检查球是否与任一球拍重叠,并在它们重叠时将球弹开:

// check left paddle
var paddleAX = parseInt($("#paddleA").css("left"))+parseInt($("#paddleA").css("width"));
var paddleAYBottom = parseInt($("#paddleA").css("top"))+parseInt($("#paddleA").css("height"));
var paddleAYTop = parseInt($("#paddleA").css("top"));
if (ball.x + ball.speed*ball.directionX < paddleAX)
{
if (ball.y + ball.speed*ball.directionY <= paddleAYBottom &&
ball.y + ball.speed*ball.directionY >= paddleAYTop)
{
ball.directionX = 1;
}
}
// check right paddle
var paddleBX = parseInt($("#paddleB").css("left"));
var paddleBYBottom = parseInt($("#paddleB").css("top"))+parseInt($("#paddleB").css("height"));
var paddleBYTop = parseInt($("#paddleB").css("top"));
if (ball.x + ball.speed*ball.directionX >= paddleBX)
{
if (ball.y + ball.speed*ball.directionY <= paddleBYBottom &&
ball.y + ball.speed*ball.directionY >= paddleBYTop)
{
ball.directionX = -1;
}
}

  1. 当球击中操场的左侧或右侧边缘后,我们还需要将球重置在中间区域。删除check rightcheck left代码部分中的弹球代码,并粘贴以下代码:
// check right edge
if (ball.x +ball.speed*ball.directionX > playgroundWidth)
{
// player B lost.
// reset the ball;
ball.x = 250;
ball.y = 100;
$("#ball").css({
"left": ball.x,
"top" : ball.y
});
ball.directionX = -1;
}
// check left edge
if (ball.x + ball.speed*ball.directionX < 0)
{
// player A lost.
// reset the ball;
ball.x = 150;
ball.y = 100;
$("#ball").css({
"left": ball.x,
"top" : ball.y
});
ball.directionX = 1;
}

  1. 在浏览器中测试游戏,现在球在击中左右球拍后会弹开。当击中左右边缘时,它也会重置到操场的中心。

进行操作,用球拍击球

刚刚发生了什么?

我们已经修改了球的检查,使其在与球拍重叠时弹开。此外,当击中左右边缘时,我们将球重新定位到操场的中心。

让我们看看如何检查球和左球拍之间的碰撞。

首先,我们检查球的 X 位置是否小于左球拍的右边缘。右边缘是left值加上球拍的width

刚刚发生了什么?

然后,我们检查球的 Y 位置是否在球拍的顶部边缘和底部边缘之间。顶部边缘是top值,底部边缘是top值加上球拍的height

刚刚发生了什么?

如果球的位置通过了两个检查,我们就会将球弹开。这就是我们检查它的方式,这只是一个基本的碰撞检测。

我们通过检查它们的位置和宽度/高度来确定这两个对象是否重叠。这种类型的碰撞检测对于矩形对象效果很好,但对于圆形和其他形状则不太好。以下截图说明了问题。以下图中显示的碰撞区域是误报。它们的边界框碰撞了,但实际形状并没有重叠。

刚刚发生了什么?

对于特殊形状,我们将需要更高级的碰撞检测技术,我们将在后面讨论。

试试吧

我们检查球拍的三个边缘,以确定球是否与它们重叠。如果你玩游戏并仔细观察球的弹跳,你会发现现在它并不完美。球可能会在球拍后面弹跳。思考原因,并修改代码以实现更好的球和球拍的碰撞检测。

在 HTML 中动态显示文本

我们在前面的部分实现了基本的游戏机制。我们的乒乓球游戏现在缺少一个计分板,可以显示两名玩家的得分。我们讨论了如何使用 jQuery 来修改所选元素的 CSS 样式。我们是否也可以用 jQuery 来改变所选元素的内容?是的,我们可以。

进行操作,显示两名玩家的得分

我们将创建一个基于文本的计分板,并在任一玩家得分时更新得分:

  1. 我们正在改进我们现有的游戏,所以我们将上一个示例作为起点。

  2. 在文本编辑器中打开index.html。我们将添加计分板的 DOM 元素。

  3. index.htmlgame DIV 内部之前添加以下 HTML 代码:

<div id="scoreboard">
<div class="score">Player A : <span id="scoreA">0</span></div>
<div class="score">Player B : <span id="scoreB">0</span></div>
</div>

  1. 让我们转到 JavaScript 部分。打开html5games.pingpong.js文件。

  2. 我们需要两个全局变量来存储玩家的得分。在pingpong全局对象内添加他们的得分变量:

var pingpong = {
scoreA : 0, // score for player A
scoreB : 0 // score for player B
}

  1. 我们有一个地方来检查玩家 B 是否输掉了比赛。我们在那里增加了玩家 A 的得分,并用以下代码更新了计分板:
// player B lost.
pingpong.scoreA++;
$("#scoreA").html(pingpong.scoreA);

  1. 我们在第 6 步中有类似的代码,用于在玩家 A 输掉比赛时更新玩家 B 的得分:
// player A lost.
pingpong.scoreB++;
$("#scoreB").html(pingpong.scoreB);

  1. 现在是测试我们最新代码的时候了。在 Web 浏览器中打开index.html。尝试通过控制两个球拍来玩游戏,并失去一些分数。计分板应该正确计分:

进行操作,显示两名玩家的得分

刚刚发生了什么?

我们刚刚使用了另一个常见的 jQuery 函数:html()来动态改变游戏内容。

html()函数获取或更新所选元素的 HTML 内容。以下是html()函数的一般定义:

.html()
.html(htmlString)

当我们使用html()函数时,如果没有参数,它会返回第一个匹配元素的 HTML 内容。如果带有参数使用,它会将 HTML 内容设置为所有匹配元素的给定 HTML 字符串。

例如,提供以下 HTML 结构:

<p>My name is <span id="myname" class="name">Makzan</span>.</p>
<p>My pet's name is <span id="pet" class="name">

以下两个 jQuery 调用都返回 Makzan:

$("#myname").html(); // returns Makzan
$(".name").html(); // returns Makzan

然而,在下面的 jQuery 调用中,它将所有匹配的元素设置为给定的 HTML 内容:

$(".name").html("<small>Mr. Mystery</small>")

执行 jQuery 命令会产生以下 HTML 结果:

<p>My name is <span id="myname" class="name"><small>Mr. Mystery</small></span></p>
<p>My pet's name is <span id="pet" class="name"><small>Mr. Mystery</small></span></p>

英雄尝试赢得比赛

我们现在有了得分。看看你是否可以修改游戏,使其在任何玩家得到 10 分后停止。然后显示一个获胜消息。

您可能还想尝试对游戏进行样式设置,使其更具吸引力。给记分牌和游乐场添加一些图像背景怎么样?用两个守门员角色替换球拍?

摘要

在本章中,我们学到了许多关于使用 HTML5 和 JavaScript 创建简单乒乓球游戏的基本技术。

具体来说,我们涵盖了:

  • 创建我们的第一个 HTML5 游戏——乒乓球

  • 使用 jQuery 操作 DOM 对象

  • 获取支持多个按键按下的键盘输入

  • 检测与边界框的碰撞

我们还讨论了如何创建游戏循环并移动球和球拍。

现在我们已经通过创建一个简单的基于 DOM 的游戏来热身,我们准备使用 CSS3 的新功能创建更高级的基于 DOM 的游戏。在下一章中,我们将创建具有 CSS3 动画、过渡和变换的游戏。

第三章:在 CSS3 中构建记忆匹配游戏

CSS3 引入了许多令人兴奋的功能。在本章中,我们将探索并使用其中一些功能来创建匹配记忆游戏。CSS3 样式显示游戏对象的外观和动画,而 jQuery 库帮助我们定义游戏逻辑。

在本章中,我们将:

  • 使用动画转换扑克牌

  • 使用新的 CSS3 属性翻转扑克牌

  • 创建整个记忆匹配游戏

  • 并将自定义网络字体嵌入我们的游戏

所以让我们继续吧。

使用 CSS3 过渡移动游戏对象

第一章,介绍 HTML5 游戏中,我们曾经在概述新的 CSS3 功能时,简要了解了 CSS3 过渡模块和变换模块。我们经常希望通过缓和属性来使游戏对象动画化。过渡是为此目的设计的 CSS 属性。想象一下,我们在网页上有一张扑克牌,想要在五秒内将其移动到另一个位置。我们必须使用 JavaScript,设置计时器,并编写自己的函数来每隔几毫秒更改位置。通过使用transition属性,我们只需要指定起始和结束样式以及持续时间。浏览器会自动进行缓和和中间动画。

让我们看一些例子来理解它。

移动扑克牌的时间

在这个例子中,我们将在网页上放置两张扑克牌,并将它们转换到不同的位置、比例和旋转。我们将通过设置过渡来缓和变换:

  1. 在以下层次结构中创建一个新文件夹,其中包含三个文件。现在,css3transition.cssindex.html为空,我们将稍后添加代码。jquery-1.6.min.js是我们在上一章中使用的 jQuery 库。移动扑克牌的时间

  2. 在这个例子中,我们使用了两张扑克牌图像。这些图像可以在代码包中找到,或者您可以从gamedesign.cc/html5games/css3-basic-transition/images/AK.pnggamedesign.cc/html5games/css3-basic-transition/images/AQ.png下载。

  3. 创建一个名为images的新文件夹,并将两张卡片图像放在其中。

  4. 接下来要做的是编写 HTML,其中包含两个卡片 DIV 元素。当页面加载时,我们将为这两个卡片元素应用 CSS 过渡样式:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Getting Familiar with CSS3 Transition</title>
<link rel="stylesheet" href="css/css3transition.css" />
</head>
<body>
<header>
<h1>Getting Familiar with CSS3 Transition</h1>
</header>
<section id="game">
<div id="cards">
<div id="card1" class="card cardAK"></div>
<div id="card2" class="card cardAQ"></div>
</div> <!-- #cards -->
</section> <!-- #game -->
<footer>
<p>This is an example of transitioning cards.</p>
</footer>
<script src="img/jquery-1.6.min.js"></script>
<script>
$(function(){
$("#card1").addClass("moveAndScale");
$("#card2").addClass("rotateRight");
});
</script>
CSS3 transition moduleCSS3 transition moduleplaying card, moving</body>
</html>

  1. 是时候通过 CSS 定义扑克牌的视觉样式了。它包含基本的 CSS 2.1 属性和 CSS3 新属性。新的 CSS3 属性已经突出显示:
body {
background: #aaa;
}
/* defines styles for each card */
.card {
width: 80px;
height: 120px;
margin: 20px;
background: #efefef;
position: absolute;
-webkit-transition: all 1s linear;
}
/* set the card to corresponding playing card graphics */
.cardAK {
background: url(../images/AK.png);
}
.cardAQ {
background: url(../images/AQ.png);
}
/* rotate the applied DOM element 90 degree */
.rotateRight {
-webkit-transform: rotate3d(0,0,1,90deg);
}
/* move and scale up the applied DOM element */
.moveAndScale {
-webkit-transform: translate3d(150px,150px,0) scale3d(1.5, 1.5, 1);
}

  1. 让我们保存所有文件,并在浏览器中打开index.html。两张卡应该如下截图所示进行动画:

移动扑克牌的时间

发生了什么?

我们刚刚通过使用 CSS3 过渡来创建了两个动画效果,以调整transform属性。

注意

请注意,新的 CSS3 过渡和变换属性尚未最终确定。Web 浏览器支持这些起草但稳定的属性,并带有供应商前缀。在我们的示例中,为了支持 Chrome 和 Safari,我们使用了-webkit-前缀。我们可以在代码中使用其他前缀来支持其他浏览器,例如为 Mozilla 使用-moz-,为 Opera 使用-o-

这是 CSS 变换的用法:

transform: transform-function1 transform-function2;

transform属性的参数是函数。有两组函数,2Dtransform函数和 3D。CSS transform函数旨在移动、缩放、旋转和扭曲目标 DOM 元素。以下显示了变换函数的用法。

2D 变换函数

2Drotate函数按给定的正参数顺时针旋转元素,并按给定的负参数逆时针旋转:

rotate(angle)

translate 函数通过给定的 X 和 Y 位移移动元素:

translate (tx, ty)

我们可以通过调用translateXtranslateY函数来独立地沿 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)

与 2D translate函数类似,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,skewperspective

如果您想找到最新的 CSS 变换工作规范,可以访问 W3C 网站,网址如下。CSS 2D 变换模块(dev.w3.org/csswg/css3-3d-transforms/)和 3D 变换模块(www.w3.org/TR/css3-2d-transforms/)。

通过使用 CSS3 过渡来缓慢改变样式

CSS3 中有大量新功能。过渡模块是其中之一,对我们在游戏设计中影响最大。

什么是CSS3 过渡?W3C 用一句话解释了它:

CSS 过渡允许 CSS 值中的属性更改在指定的持续时间内平滑发生。

通常,当我们改变元素的任何属性时,属性会立即更新为新值。过渡会减慢更改过程。它在给定的持续时间内创建从旧值到新值的平滑过渡。

这是transition属性的用法:

transition: property_name duration timing_function delay.

参数 定义
property_name 过渡应用的属性名称。可以设置为all
Duration 过渡所需的持续时间。
Timing_function timing函数定义了开始值和结束值之间的插值。默认值是ease。通常我们会使用ease, ease-in, ease-outlinear
Delay 延迟参数延迟了给定秒数的过渡开始。

我们可以在一行中放置几个transition属性。例如,以下代码在 0.3 秒内过渡不透明度,0.5 秒内过渡背景颜色:

transition: opacity 0.3s, background-color 0.5s

我们还可以使用以下属性单独定义每个过渡属性:

transition-property,transition-duration,transition-timing-functiontransition-delay

提示

CSS3 模块

根据 W3C,CSS3 不同于 CSS 2.1,因为 CSS 2.1 只有一个规范。CSS3 分为不同的模块。每个模块都会单独进行审查。例如,有过渡模块,2D/3D 变换模块和弹性盒布局模块。

将规范分成模块的原因是因为 CSS3 的每个部分的工作进度不同。一些 CSS3 功能相当稳定,例如边框半径,而有些尚未定型。通过将整个规范分成不同的部分,它允许浏览器供应商支持稳定的模块。在这种情况下,缓慢的功能不会减慢整个规范。CSS3 规范的目标是标准化网页设计中最常见的视觉用法,而这个模块符合这个目标。

试试看

我们已经翻译,缩放和旋转了扑克牌。在示例中尝试更改不同的值怎么样?rotate3d函数中有三个轴。如果我们旋转其他轴会发生什么?通过自己尝试代码来熟悉变换和过渡模块。

创建翻转卡片效果

现在想象一下,我们不仅仅是移动纸牌,而且还想翻转卡片元素,就像我们翻转真正的纸牌一样。通过使用rotation transform函数,现在可以创建翻牌效果。

使用 CSS3 翻牌的时间

当我们点击纸牌时,我们将开始一个新项目并创建一个翻牌效果:

  1. 让我们继续我们之前的代码示例。

  2. 卡片现在包含两个面,一个正面和一个背面。将以下代码替换为 HTML 中的body标签:

<section id="game">
<div id="cards">
<div class="card">
<div class="face front"></div>
<div class="face back cardAK"></div>
</div> <!-- .card -->
<div class="card">
<div class="face front"></div>
<div class="face back cardAQ"></div>
</div> <!-- .card -->
</div> <!-- #cards -->
</section> <!-- #game -->
<script src="img/jquery-1.6.min.js"></script>

  1. 然后将 CSS 外部链接更改为css3flip.css文件:
<link rel="stylesheet" href="css/css3flip.css" />

  1. 现在让我们将样式添加到css3flip.css中:
#game {
background: #9c9;
padding: 5px;
}
/* Define the 3D perspective view and dimension of each card. */
.card {
-webkit-perspective: 600;
width: 80px;
height: 120px;
}

  1. 每张卡上有两个面。我们将晚些时候旋转面。因此,我们通过 CSS3 的transition属性定义了面的过渡方式。我们还隐藏了背面的可见性。我们稍后会详细看一下这个属性:
.face {
border-radius: 10px;
width: 100%;
height: 100%;
position: absolute;
-webkit-transition: all .3s;
-webkit-backface-visibility: hidden;
}

  1. 现在是为每个单独的面样式。正面的 z-index 比背面高:
.front {
background: #966;
z-index: 10;
}
.back {
background: #eaa;
-webkit-transform: rotate3d(0,1,0,-180deg);
z-index: 8;
}

  1. 当我们翻转卡片时,我们将正面旋转到背面,背面旋转到正面。我们还交换了正面和背面的 z-index:
.card-flipped .front {
-webkit-transform: rotate3d(0,1,0,180deg);
z-index: 8;
}
.card-flipped .back {
-webkit-transform: rotate3d(0,1,0,0deg);
z-index: 10;
}
.cardAK {
background: url(../images/AK.png);
}
.cardAQ {
background: url(../images/AQ.png);
}

  1. 接下来,我们将在加载 jQuery 库后添加逻辑,以在单击卡片时切换卡片翻转状态:
<script>
$(function(){
$("#cards").children().each(function(index) {
// listen the click event on each card DIV element.
$(this).click(function() {
// add the class "card-flipped".
// the browser will animate the styles between current state and card-flipped state.
$(this).toggleClass("card-flipped");
});
});
});
</script>

  1. 现在样式和脚本都准备好了。让我们保存所有文件并在 Web 浏览器中预览。单击纸牌翻转它,再次单击翻转回来。

使用 CSS3 翻牌的时间

刚才发生了什么?

我们已经创建了一个通过鼠标单击切换的翻牌效果。该示例利用了几个 CSS 变换属性和 JavaScript 来处理鼠标单击事件。

使用 jQuery toggleClass 函数切换类

当鼠标单击卡片时,我们将card-flipped类应用于卡片元素。第二次单击时,我们希望删除已应用的card-flipped样式,以便卡片再次翻转。这称为切换类样式。

jQuery 为我们提供了一个方便的函数,名为toggleClass,可以根据类是否应用来自动添加或删除类。

要使用该函数,我们只需将要切换的类作为参数传递。

例如,以下代码向具有 IDcard1的元素添加或删除card-flipped类:

$("#card1").toggleClass("card-flipped");

toggleClass函数接受一次切换多个类。我们可以传递多个类名,并用空格分隔它们。以下是同时切换两个类的示例:

$("#card1").toggleClass("card-flipped scale-up");

通过 z-index 控制重叠元素的可见性

通常,网页中的所有元素都是分布和呈现而不重叠的。设计游戏是另一回事。我们总是需要处理重叠的元素并有意隐藏它们(或其中的一部分)。Z-index是 CSS 2.1 属性,帮助我们控制多个重叠元素时的可见性行为。

在这个例子中,每张卡片有两个面,正面和背面。两个面放在完全相同的位置。它们彼此重叠。Z-index属性定义了哪个元素在顶部,哪个在后面。具有较高 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 立方体演示了不同的透视值如何改变元素的透视视图:

介绍 CSS 透视属性

您可以在 Safari 中输入以下地址查看此实验:

gamedesign.cc/html5games/perspective-cube/

Have a go hero

立方体是通过将六个面放在一起,并对每个面应用 3D 变换来创建的。它使用了我们讨论过的技术。尝试创建一个立方体并尝试使用perspective属性进行实验。

以下网页对创建 CSS3 立方体进行了全面的解释,并讨论了通过键盘控制立方体的旋转:

www.paulrhayes.com/2009-07/animated-css3-cube-interface-using-3d-transforms/

介绍 backface-visibility

在引入backface-visibility之前,页面上的所有元素都向访问者展示它们的正面。实际上,元素的正面或背面没有概念,因为这是唯一的选择。而 CSS3 引入了三个轴的旋转,我们可以旋转一个元素,使其正面朝后。试着看看你的手掌并旋转你的手腕,你的手掌转过来,你看到了手掌的背面。这也发生在旋转的元素上。

CSS3 引入了一个名为backface-visibility的属性,用于定义我们是否可以看到元素的背面。默认情况下,它是可见的。以下截图演示了backface-visibility属性的两种不同行为。

注意

在撰写本书时,只有 Apple Safari 支持backface-visibility属性。

介绍 backface-visibility

注意

您可以在官方 Webkit 博客上阅读有关 CSS 3D 变换中不同属性和函数的更详细信息:webkit.org/blog/386/3d-transforms/

创建一款卡片匹配记忆游戏

我们已经学习了一些 CSS 基本技术。让我们用这些技术制作一个游戏。我们将制作一款纸牌游戏。纸牌游戏利用变换来翻转纸牌,过渡来移动纸牌,JavaScript 来控制逻辑,以及一个名为自定义数据属性的新 HTML5 功能。别担心,我们将逐步讨论每个组件。

下载扑克牌的精灵表

在翻牌示例中,我们使用了两种不同的扑克牌图形。现在我们准备整副扑克牌的图形。尽管我们在匹配游戏中只使用了六张扑克牌,但我们准备整副扑克牌,这样我们可以在可能创建的其他扑克牌游戏中重复使用这些图形。

一副牌有 52 张,我们还有一张背面的图形。与使用 53 个单独的文件不同,将单独的图形放入一个大的精灵表文件是一个好的做法。精灵表这个术语来自于一种旧的计算机图形技术,它将一个图形纹理加载到内存中并显示部分图形。

使用大型精灵表而不是分离的图像文件的一个好处是可以减少HTTP 请求的数量。 当浏览器加载网页时,它会创建一个新的 HTTP 请求来加载每个外部资源,包括 JavaScript 文件,CSS 文件和图像。 为每个分离的小文件建立新的 HTTP 请求需要相当长的时间。 将图形合并到一个文件中,大大减少了请求的数量,从而提高了在浏览器中加载时游戏的响应性。

将图形放入一个文件的另一个好处是避免文件格式头的开销。 53 张图像精灵表的大小小于每个文件中带有文件头的 53 张不同图像的总和。

以下扑克牌图形是在 Adobe Illustrator 中绘制和对齐的。 您可以从gamedesign.cc/html5games/css3-matching-game/images/deck.png下载它。

注意

以下文章详细解释了为什么以及如何创建和使用 CSS 精灵表:

css-tricks.com/css-sprites/

下载扑克牌精灵表

设置游戏环境

图形已准备就绪,然后我们需要在游戏区域上设置一个静态页面,并在游戏区域上准备和放置游戏对象。 这样以后添加游戏逻辑和交互会更容易:

执行动作准备卡片匹配游戏的时间

在将复杂的游戏逻辑添加到我们的匹配游戏之前,让我们准备 HTML 游戏结构并准备所有 CSS 样式:

  1. 让我们继续我们的代码。 用以下 HTML 替换index.html文件:
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>CSS3 Matching Game</title>
<link rel="stylesheet" href="css/matchgame.css" />
</head>
<body>
<header>
<h1>CSS3 Matching Game</h1>
</header>
<section id="game">
<div id="cards">
<div class="card">
<div class="face front"></div>
<div class="face back"></div>
</div> <!-- .card -->
</div> <!-- #cards -->
</section> <!-- #game -->
<footer>
<p>This is an example of creating a matching game with CSS3.</p>
</footer>
<script src="img/jquery-1.6.min.js"></script>
<script src="img/html5games.matchgame.js"></script>
</body>
</html>

  1. 为了使游戏更具吸引力,我为游戏桌和页面准备了背景图像。 这些图形资产可以在代码示例包中找到。 背景图像是可选的,它们不会影响匹配游戏的游戏过程和逻辑。

  2. 我们还将把扑克牌精灵表图形放入 images 文件夹中。 从gamedesign.cc/html5games/css3-matching-game/images/deck.png下载deck.png文件,并将其保存到 images 文件夹中。

  3. 在编写任何逻辑之前,让我们为匹配游戏添加样式。 打开matchgame.css并添加以下 body 样式:

body {
text-align: center;
background: #a46740 url(../images/bg.jpg);
}

  1. 继续向game元素添加样式。 它将是游戏的主要区域:
#game {
border-radius: 10px;
border: 1px solid #666;
background: #232 url(../images/table.jpg);
width: 500px;
height: 460px;
margin: 0 auto;
display: box;
box-pack: center;
box-align: center;
}

  1. 我们将所有卡片元素放入名为cards的父 DOM 中。 这样做可以轻松地将所有卡片居中到游戏区域:
#cards {
position: relative;
width: 380px;
height: 400px;
}

  1. 对于每张卡,我们定义了一个perspective属性,以赋予它视觉深度效果:
.card {
-webkit-perspective: 600;
width: 80px;
height: 120px;
position: absolute;
-moz-transition: all .3s;
-webkit-transition: all .3s;
transition: all .3s;
}

  1. 每张卡上有两个面。 面将稍后旋转,我们将定义过渡属性以动画显示样式更改。 我们还希望确保背面是隐藏的:
.face {
border-radius: 10px;
width: 100%;
height: 100%;
position: absolute;
-webkit-transition-property: opacity, transform, box-shadow;
-webkit-transition-duration: .3s;
-webkit-backface-visibility: hidden;
}

  1. 然后我们设置正面和背面样式。 它们与翻转卡片示例几乎相同,只是现在我们为它们提供了背景图像和盒子阴影:
.front {
background: #999 url(../images/deck.png) 0 -480px;
z-index: 10;
card matching memory gamecard matching memory gamegame environment, setting up}
.back {
background: #efefef url(../images/deck.png);
-webkit-transform: rotate3d(0,1,0,-180deg);
z-index: 8;
}
.card:hover .face, .card-flipped .face {
-webkit-box-shadow: 0 0 10px #aaa;
}
.card-flipped .front {
-webkit-transform: rotate3d(0,1,0,180deg);
z-index: 8;
}
.card-flipped .back {
-webkit-transform: rotate3d(0,1,0,0deg);
z-index: 10;
}

  1. 当任何卡被移除时,我们希望将其淡出。 因此,我们声明了一个带有 0 不透明度的 card-removed 类:
.card-removed {
opacity: 0;
}

  1. 为了从卡牌牌组的精灵表中显示不同的扑克牌图形,我们将卡的背景剪切成不同的背景位置:
.cardAQ {background-position: -880px 0;}
.cardAK {background-position: -960px 0;}
.cardBQ {background-position: -880px -120px;}
.cardBK {background-position: -960px -120px;}
.cardCQ {background-position: -880px -240px;}
.cardCK {background-position: -960px -240px;}
.cardDQ {background-position: -880px -360px;}
.cardDK {background-position: -960px -360px;}

  1. 我们已经定义了许多 CSS 样式。 现在是 JavaScript 逻辑的时候了。 打开html5games.matchgame.js文件,并将以下代码放入其中:
$(function(){
// clone 12 copies of the card
for(var i=0;i<11;i++){
$(".card:first-child").clone().appendTo("#cards");
}
// initialize each card's position
$("#cards").children().each(function(index) {
// align the cards to be 4x3 ourselves.
$(this).css({
"left" : ($(this).width() + 20) * (index % 4),
"top" : ($(this).height() + 20) * Math.floor(index / 4)
});
});
});

  1. 现在保存所有文件并在浏览器中预览游戏。 游戏应该有很好的样式,并且中央应该出现 12 张卡。 但是,我们还不能点击卡片,因为我们还没有为卡片设置任何交互逻辑。

执行动作准备卡片匹配游戏的时间

刚刚发生了什么?

我们在 HTML 中创建了游戏结构,并对 HTML 元素应用了样式。我们还使用 jQuery 在网页加载和准备好后在游戏区域创建了 12 张卡片。翻转和移除卡片的样式也已准备好,并可以在稍后使用游戏逻辑应用到卡片上。

由于我们为每张卡片使用绝对定位,我们需要自己将卡片对齐到 4x3 的瓷砖中。在 JavaScript 逻辑中,我们通过循环每张卡片并通过计算循环索引来对齐它:

$("#cards").children().each(function(index) {
// align the cards to be 4x3 ourselves.
$(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,所以它在第三行。

使用 jQuery 克隆 DOM 元素

在我们的 HTML 结构中,我们只有一张卡片,在结果中,我们有 12 张卡片。这是因为我们在 jQuery 中使用了clone函数来克隆卡片元素。克隆目标元素后,我们调用appendTo函数将克隆的卡片元素附加为卡片元素的子元素:

$(".card:first-child").clone().appendTo("#cards");

使用子筛选器在 jQuery 中选择元素的第一个子元素

当我们选择卡片元素并克隆它时,我们使用了以下选择器:

$(".card:first-child")

:first-child是一个子筛选器,选择给定父元素的第一个子元素。

除了:first-child,我们还可以使用:last-child选择最后一个子元素。

注意

您还可以在 jQuery 文档中检查其他与子元素相关的选择器:

api.jquery.com/category/selectors/child-filter-selectors/

垂直对齐 DOM 元素

我们将卡片 DIV 放在游戏元素的中心。CSS3 灵活的盒子布局模块引入了一种实现垂直居中对齐的简单方法。由于这个模块仍在进行中,我们需要应用浏览器供应商前缀。我们将以 Webkit 为例:

display: -webkit-box;
-webkit-box-pack: center;
-webkit-box-align: center;

灵活盒模块定义了元素在其容器中有额外空间时的对齐方式。我们可以通过使用display,一个 CSS2 属性,值为box,一个新的 CSS3 属性值,将元素设置为灵活盒容器的行为。

box-packbox-align是两个属性,用于定义它如何在水平和垂直方向上对齐并使用额外的空间。我们可以通过将这两个属性都设置为center来使元素居中。

垂直对齐只是灵活盒子布局模块的一小部分。在网页设计中进行布局时非常强大。您可以在 W3C 模块的页面(www.w3.org/TR/css3-flexbox/)或 CSS3 Info 网站(www.css3.info/introducing-the-flexible-box-layout-module/)上找到更多信息。

使用 CSS 精灵和背景位置

CSS 精灵表是一个包含许多单独图形的大图像。大的精灵表图像被应用为元素的背景图像。我们可以通过移动固定宽度和高度元素的背景位置来剪裁每个图形。

我们的牌组图像包含总共 53 个图形。为了方便演示背景位置,让我们假设我们有一张包含三张卡片图像的图像,如下面的截图:

使用 CSS 精灵和背景位置

在 CSS 样式中,我们将卡片元素设置为 80 像素宽,120 像素高,背景图像设置为大牌组图像。如果我们想要左上角的图形,我们将背景位置的 X 和 Y 都应用为 0。如果我们想要第二个图形,我们将背景图像移动到左 80 像素。这意味着将 X 位置设置为-80 像素,Y 设置为 0。由于我们有固定的宽度和高度,只有裁剪的 80x120 区域显示背景图像。以下截图中的矩形显示了可视区域:

使用 CSS 精灵和背景位置

为匹配游戏添加游戏逻辑

现在让我们想象手中拿着一副真正的牌组并设置匹配游戏。

我们首先在手中洗牌,然后将每张卡片背面朝上放在桌子上。为了更容易玩游戏,我们将卡片排成 4x3 的数组。现在游戏已经准备好了。

现在我们要开始玩游戏了。我们拿起一张卡片并翻转它使其正面朝上。然后我们拿起另一张并将其朝上。之后,我们有两种可能的操作。如果它们是相同的图案,我们就把这两张卡片拿走。否则,我们将它们再次放回背面,就好像我们没有触摸过它们一样。游戏将继续,直到我们配对所有卡片并将它们全部拿走。

在我们脑海中有了逐步的情景之后,代码流程将会更加清晰。实际上,这个例子中的代码与我们玩真正的牌组的过程完全相同。我们只需要将人类语言替换为 JavaScript 代码。

执行为匹配游戏添加游戏逻辑的操作

在上一个示例中,我们准备了游戏环境,并决定了游戏逻辑与玩真正的牌组相同。现在是时候编写 JavaScript 逻辑了:

  1. 让我们从上一个匹配游戏示例开始。我们已经设计了 CSS,现在是时候在html5games.matchgame.js文件中添加游戏逻辑了。

  2. 游戏是匹配一对扑克牌。现在我们有 12 张卡片,所以我们需要六对扑克牌。以下全局数组声明了六对卡片图案:

var matchingGame = {};
matchingGame.deck = [
'cardAK', 'cardAK',
'cardAQ', 'cardAQ',
'cardAJ', 'cardAJ',
'cardBK', 'cardBK',
'cardBQ', 'cardBQ',
'cardBJ', 'cardBJ',
];

  1. 在上一章中,我们在 jQuery 的ready函数中排列了卡片。现在我们需要在ready函数中准备和初始化更多的代码。将ready函数更改为以下代码。更改后的代码已经突出显示:
$(function(){
matchingGame.deck.sort(shuffle);
for(var i=0;i<11;i++){
$(".card:first-child").clone().appendTo("#cards");
}
$("#cards").children().each(function(index) {
$(this).css({
"left" : ($(this).width() + 20) * (index % 4),
"top" : ($(this).height() + 20) * Math.floor(index / 4)
});
// get a pattern from the shuffled deck
var pattern = matchingGame.deck.pop();
// visually apply the pattern on the card's back side.
$(this).find(".back").addClass(pattern);
// embed the pattern data into the DOM element.
$(this).attr("data-pattern",pattern);
// listen the click event on each card DIV element.
$(this).click(selectCard);
});
});

  1. 与玩真正的牌组类似,我们想要做的第一件事就是洗牌。将以下shuffle函数添加到 JavaScript 文件中:
function shuffle() {
return 0.5 - Math.random();
game logic, adding to matching gamegame logic, adding to matching gamesteps}

  1. 当我们点击卡片时,我们翻转它并安排检查函数。将以下代码附加到 JavaScript 文件中:
function selectCard() {
// we do nothing if there are already two card flipped.
if ($(".card-flipped").size() > 1) {
return;
}
$(this).addClass("card-flipped");
// check the pattern of both flipped card 0.7s later.
if ($(".card-flipped").size() == 2) {
setTimeout(checkPattern,700);
}
}

  1. 当两张卡片被打开时,执行以下函数。它控制我们是移除卡片还是翻转卡片:
function checkPattern() {
if (isMatchPattern()) {
$(".card-flipped").removeClass("card-flipped").addClass ("card-removed");
$(".card-removed").bind("webkitTransitionEnd", removeTookCards);
} else {
$(".card-flipped").removeClass("card-flipped");
}
}

  1. 现在是检查图案的函数的时间。以下函数访问已打开卡片的自定义图案属性,并比较它们是否是相同的图案:
function isMatchPattern() {
var cards = $(".card-flipped");
var pattern = $(cards[0]).data("pattern");
var anotherPattern = $(cards[1]).data("pattern");
return (pattern == anotherPattern);
}

  1. 匹配的卡片淡出后,我们执行以下函数来移除卡片:
function removeTookCards() {
$(".card-removed").remove();
}

  1. 游戏逻辑现在已经准备好了。让我们在浏览器中打开游戏 HTML 并进行游戏。如果有任何错误,请记得检查开发者工具中的控制台窗口。

执行为匹配游戏添加游戏逻辑的操作

刚刚发生了什么?

我们编写了 CSS3 匹配游戏的游戏逻辑。该逻辑为玩牌添加了鼠标点击交互,并控制了图案检查的流程。

在 CSS 过渡结束后执行代码

在播放淡出过渡后,我们移除成对的卡片。我们可以通过使用TransitionEnd事件来安排在过渡结束后执行的函数。以下是我们代码示例中的代码片段,它向成对的卡片添加了card-removed类来开始过渡。然后,它绑定了TransitionEnd事件以在 DOM 中完全移除卡片。此外,请注意webkit供应商前缀,因为它尚未最终确定:

$(".card-flipped").removeClass("card-flipped").addClass("card-removed");
$(".card-removed").bind("webkitTransitionEnd", removeTookCards);

延迟执行翻牌的代码

游戏逻辑流程的设计方式与玩一副真正的牌相同。一个很大的区别是我们使用了几个setTimeout函数来延迟代码的执行。当点击第二张卡时,我们在以下代码示例片段中安排checkPattern函数在 0.7 秒后执行:

if ($(".card-flipped").size() == 2) {
setTimeout(checkPattern,700);
}

我们延迟函数调用的原因是为了给玩家时间来记忆卡片模式。这就是为什么我们在检查卡片模式之前延迟了 0.7 秒。

在 JavaScript 中对数组进行随机化

JavaScript 中没有内置的数组随机化函数。我们必须自己编写。幸运的是,我们可以从内置的数组排序函数中获得帮助。

以下是sort函数的用法:

sort(compare_function);

sort函数接受一个可选参数。

参数 定义 讨论
compare_function 定义数组的排序顺序的函数。compare_function需要两个参数 sort函数通过使用compare函数比较数组中的两个元素。因此,compare函数需要两个参数。当compare函数返回大于 0 的任何值时,它会将第一个参数放在比第二个参数更低的索引处。当返回值小于 0 时,它会将第二个参数放在比第一个参数更低的索引处。

这里的诀窍是我们使用了compare函数,该函数返回-0.5 到 0.5 之间的随机数:

anArray.sort(shuffle);
function shuffle(a, b) {
return 0.5 - Math.random();
}

通过在compare函数中返回一个随机数,sort函数以不一致的方式对相同的数组进行排序。换句话说,我们正在洗牌数组。

注意

来自 Mozilla 开发者网络的以下链接提供了关于使用sort函数的详细解释和示例:

developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/sort

使用 HTML5 自定义数据属性存储内部自定义数据

我们可以通过使用自定义数据属性将自定义数据存储在 DOM 元素内。我们可以使用data-前缀创建自定义属性名称并为其分配一个值。

例如,我们可以在以下代码中将自定义数据嵌入到列表元素中:

<ul id="games">
<li data-chapter="2" data-difficulty="easy">Ping-Pong</li>
<li data-chapter="3" data-difficulty="medium">Matching Game</li>
</ul>

这是 HTML5 规范中提出的一个新功能。根据 W3C 的说法,自定义数据属性旨在存储页面或应用程序私有的自定义数据,对于这些数据没有更合适的属性或元素。

W3C 还指出,这个自定义数据属性是“用于网站自己的脚本使用,而不是用于公开可用的元数据的通用扩展机制。”

我们正在编写我们的匹配游戏并嵌入我们自己的数据到卡片元素中,因此,自定义数据属性符合我们的使用方式。

我们使用自定义属性来存储每张卡片内的卡片模式,因此我们可以通过比较模式值在 JavaScript 中检查两张翻转的卡片是否匹配。此外,该模式也用于将扑克牌样式化为相应的图形:

$(this).find(".back").addClass(pattern);
$(this).attr("data-pattern",pattern);

弹出测验

  1. 根据 W3C 关于自定义数据属性的指南,以下哪种说法是正确的?

a. 我们可以创建一个data-href属性来存储a标签的链接。

b. 我们可能希望在第三方游戏门户网站中访问自定义数据属性。

c. 我们可能希望在每个玩家的 DOM 元素中存储一个data-score属性,以便在我们的网页中对排名进行排序。

d. 我们可以在每个玩家的 DOM 元素中创建一个ranking属性来存储排名数据。

使用 jQuery 访问自定义数据属性

在匹配游戏示例中,我们使用了 jQuery 库中的attr函数来访问我们的自定义数据:

pattern = $(this).attr("data-pattern");

attr函数返回给定属性名称的值。例如,我们可以通过调用以下代码获取所有a标签中的链接:

$("a").attr("href");

对于 HTML5 自定义数据属性,jQuery 还为我们提供了另一个函数来访问 HTML5 自定义数据属性。这就是data函数。

Data函数旨在将自定义数据嵌入到 HTML 元素的 jQuery 对象中。它是在 HTML5 自定义数据属性之前设计的。

以下是data函数的用法:

.data(key)
.data(key,value)

data函数接受两种类型的函数:

函数类型 参数定义 讨论
.data(key) key是命名数据的字符串 当只给出键时,data函数读取与 jQuery 对象关联的数据并返回相应的值。在最近的 jQuery 更新中,此函数被扩展以支持 HTML5 自定义数据属性。
.data(key, value) key是命名数据的字符串value是要与 jQuery 对象关联的数据 当给出键和值参数时,data函数将新的数据条目设置为 jQuery 对象。值可以是任何 JavaScript 类型,包括数组和对象。

为了支持 HTML5 自定义数据属性,jQuery 扩展了data函数,使其能够访问 HTML 代码中定义的自定义数据。

以下代码解释了我们如何使用data函数。

给定以下 HTML 代码:

<div id="target" data-custom-name="HTML5 Games"></div>

我们可以通过调用 jQuery 中的data函数访问data-custom-name属性:

$("#target").data("customName")

它将返回"HTML5 Games"。

快速测验

  1. 给定以下 HTML 代码:
<div id="game" data-score="100"></div>

以下哪两个 jQuery 语句读取自定义分数数据并返回 100?

a. $("#game").attr("data-score");

b. $("#game").attr("score");

c. $("#game").data("data-score");

d. $("#game").data("score");

试试吧

我们已经创建了 CSS3 匹配游戏。这里缺少什么?游戏逻辑没有检查游戏是否结束。尝试在游戏结束时添加你赢了文本。您还可以使用本章讨论的技术来为文本添加动画。

制作其他纸牌游戏

这种 CSS3 纸牌方法适用于创建纸牌游戏。卡片上有两面适合翻转。过渡适合移动卡片。通过移动和翻转,我们可以定义玩法规则并充分利用纸牌游戏。

试试吧

您能否使用纸牌图形和翻转技术创建另一个游戏?比如扑克?

将网络字体嵌入到我们的游戏中

多年来,我们一直在使用有限的字体来设计网页。我们无法使用任何我们想要的字体,因为浏览器从访问者的本地机器加载字体。我们无法控制并确保访问者拥有我们想要的字体。

尽管我们可以将网络字体嵌入到 Internet Explorer 5 中,但格式受限,我们必须等到浏览器供应商支持嵌入最常见的 TrueType 字体格式。

想象一下,我们可以通过嵌入不同样式的网络字体来控制游戏的情绪。我们可以使用我们想要的字体设计游戏,并更好地控制游戏的吸引力。让我们尝试将网络字体嵌入到我们的匹配记忆游戏中。

Time for action Embedding a font from Google Font Directory

Google 字体目录是一个列出可免费使用的网络字体的网络字体服务。我们将嵌入从 Google 字体目录中选择的网络字体:

  1. 转到 Google 字体目录网站:code.google.com/webfonts

  2. 在字体目录中,有一个列出了开源许可的可免费使用的网络字体列表。

  3. 选择其中一个并单击字体名称以继续下一步。在此示例中,我使用了Droid Serif

  4. 单击字体后,字体目录会显示有关该字体的详细信息。我们可以在那里执行几项操作,例如预览字体、从变体中选择,最重要的是获取字体嵌入代码。

  5. 单击获取代码选项卡,您将看到以下屏幕截图。它显示了如何将此字体嵌入到我们的网页中的指南:Time for action Embedding a font from Google Font Directory

  6. 复制谷歌提供的link标签,并将其粘贴到 HTML 代码中。它应该放在任何其他样式定义之前:

<link href='http://fonts.googleapis.com/css?family=Droid+Serif:regular,bold&subset=latin' rel='stylesheet' type='text/css'>

  1. 现在我们可以使用字体来为文本设置样式。将 body 的字体系列属性设置为以下代码:
body {
font-family: 'Droid Serif', arial, serif;
}

  1. 保存所有文件并打开index.html文件。浏览器将从谷歌服务器下载字体并嵌入到网页中。注意字体,它们应该被加载并呈现为我们选择的谷歌字体。

采取行动从谷歌字体目录嵌入字体

刚刚发生了什么?

我们刚刚用一种非常见的网络字体为我们的游戏添加了样式。该字体是通过谷歌字体目录托管和交付的。

注意

除了使用字体目录,我们还可以使用@font face 来嵌入我们的字体文件。以下链接提供了一种可靠的方法来嵌入字体:

www.fontspring.com/blog/the-new-bulletproof-font-face-syntax

提示

在嵌入之前检查字体许可证

通常,字体许可证不包括在网页上的使用。在嵌入字体之前,请务必检查许可证。谷歌字体目录中列出的所有字体都是根据开源许可证授权的,可以在任何网站上使用。

选择不同的字体交付服务

Google 字体目录只是其中一个字体交付服务。Typekit (typekit.com) 和 Fontdeck (fontdeck.com) 是另外两个提供数百种高质量字体的字体服务,通过年度订阅计划提供。

选择不同的字体交付服务

摘要

在本章中,我们学习了使用不同的 CSS3 新属性来创建游戏。

具体来说,我们涵盖了:

  • 通过过渡模块转换和动画游戏对象

  • 使用透视深度错觉来翻转卡片

  • 基于 CSS3 样式和 jQuery 的动画和游戏逻辑创建匹配的记忆游戏

  • 从在线字体交付服务中选择和嵌入网络字体

现在我们已经学会了如何使用 CSS3 功能创建基于 DOM 的 HTML5 游戏,我们将在下一章中探索另一种创建 HTML5 游戏的方法,即使用新的 Canvas 标签和绘图 API。

第四章:使用 Canvas 和绘图 API 构建 Untangle 游戏

HTML5 中一个突出的新功能是 Canvas 元素。我们可以将画布元素视为一个动态区域,可以使用脚本在上面绘制图形和形状。

网站中的图像多年来一直是静态的。有动画 gif,但它无法与访问者进行交互。画布是动态的。我们可以通过 JavaScript 绘图 API 动态绘制和修改画布中的上下文。我们还可以向画布添加交互,从而制作游戏。

在过去的两章中,我们已经讨论了基于 DOM 的游戏开发与 CSS3 和一些 HTML5 功能。在接下来的两章中,我们将专注于使用新的 HTML5 功能来创建游戏。在本章中,我们将介绍一个核心功能,即画布,以及一些基本的绘图技术。

在本章中,我们将涵盖以下主题:

  • 介绍 HTML5 画布元素

  • 在画布中绘制圆

  • 在画布元素中绘制线条

  • 与画布中绘制的对象进行交互的鼠标事件

  • 检测线交点

  • 使用 Canvas 和绘图 API 构建 Untangle 解谜游戏

Untangle 解谜游戏是一个玩家被给予一些连接的圆的游戏。这些线可能会相交,玩家需要拖动圆圈,使得没有线再相交。

以下截图预览了我们将通过本章实现的游戏:

使用 Canvas 和绘图 API 构建 Untangle 游戏

所以让我们从头开始制作我们的画布游戏。

介绍 HTML5 Canvas 元素

W3C 社区表示画布元素和绘图功能是:

一个分辨率相关的位图画布,可用于实时渲染图形、游戏图形或其他视觉图像。

画布元素包含用于绘制的上下文,实际的图形和形状是由 JavaScript 绘图 API 绘制的。

在画布中绘制圆

让我们从基本形状——圆开始在画布上绘制。

在画布上绘制彩色圆圈的时间

  1. 首先,让我们为示例设置新环境。这是一个包含画布元素、一个帮助我们进行 JavaScript 的 jQuery 库、一个包含实际绘图逻辑的 JavaScript 文件和一个样式表的 HTML 文件。在画布上绘制彩色圆圈的时间

  2. 将以下 HTML 代码放入index.html中。这是一个包含画布元素的基本 HTML 文档:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Drawing Circles in Canvas</title>
<link rel="stylesheet" href="css/untangle.css" />
</head>
<body>
<header>
<h1>Drawing in Canvas</h1>
</header>
<canvas id="game" width="768" height="400">
Sorry, your web browser does not support Canvas content.
</canvas>
<script src="img/jquery-1.6.min.js"></script>
<script src="img/html5games.untangle.js"></script>
</body>
</html>

  1. 使用 CSS 在untangle.css中设置画布的背景颜色:
canvas {
background: #333;
}

  1. html5games.untangle.js JavaScript 文件中,我们放置了一个 jQuery ready函数,并在其中绘制了一个彩色圆圈:
$(function(){
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(200, 200, 100, .6)";
ctx.beginPath();
ctx.arc(100, 100, 50 , 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
});

  1. 在 Web 浏览器中打开index.html文件,我们将得到以下截图:

在画布上绘制彩色圆圈的时间

刚刚发生了什么?

我们刚刚在上面创建了一个简单的带有圆圈的画布上下文

画布元素本身没有太多设置。我们设置了画布的宽度和高度,就像我们固定了真实绘图纸的尺寸一样。此外,我们为画布分配了一个 ID 属性,以便在 JavaScript 中更容易地引用它:

<canvas id="game" width="768" height="400">
Sorry, your web browser does not support Canvas content.
</canvas>

当 Web 浏览器不支持画布时放置回退内容

并非所有的 Web 浏览器都支持画布元素。特别是那些古老的版本。Canvas 元素提供了一种简单的方法来提供回退内容,如果不支持画布元素。在画布的开放和关闭标记内的任何内容都是回退内容。如果 Web 浏览器支持该元素,则此内容将被隐藏。不支持画布的浏览器将显示该回退内容。在回退内容中提供有用的信息是一个好的做法。例如,如果画布的目的是动态图片,我们可以考虑在那里放置一个<img>的替代内容。或者我们还可以为访问者提供一些链接,以便轻松升级他们的浏览器。

在这个例子中,我们在画布元素内提供了一个句子。这个句子对于支持画布元素的任何浏览器都是隐藏的。如果他们的浏览器不支持新的 HTML5 画布功能,它将显示给访问者。以下截图显示了旧版本的 Internet Explorer 显示回退内容,而不是绘制画布元素:

在 Web 浏览器不支持画布时放置回退内容

使用画布弧函数绘制圆圈和形状

没有绘制圆的圆函数。画布绘图 API 提供了一个绘制不同弧的函数,包括圆。弧函数接受以下参数

参数 讨论
X 弧的 x 轴中心点。
Y 弧的 y 轴中心点。
半径 半径是中心点和弧周围的距离。绘制圆时,较大的半径意味着较大的圆。
startAngle 起始点是弧度角。它定义了在周边开始绘制弧的位置。
endAngle 结束点是弧度角。弧是从起始角度的位置绘制到这个结束角度。
逆时针 这是一个布尔值,指示从startingAngleendingAngle的弧是顺时针还是逆时针绘制的。这是一个可选参数,默认值为 false。

将度数转换为弧度

弧函数中使用的角度参数是弧度,而不是。如果您熟悉度角,您可能需要在将值放入弧函数之前将度转换为弧度。我们可以使用以下公式转换角度单位:

radians = π/180 x degrees

以下图表包含了一些常见的角度值,分别以度和弧度为单位。图表还指示了角度值的位置,以便我们在绘制画布中的弧时轻松选择起始角度和结束角度参数。

将度数转换为弧度

为了更清楚地绘制具有起始角度和结束角度的不同弧,让我们绘制一些弧。

采取行动 用弧函数绘制不同的弧

让我们通过给出不同的起始和结束角度来对arc函数进行一些实验:

  1. 打开我们刚刚用来绘制圆的html5games.untangle.js文件。

  2. 通过使用以下弧绘制代码替换圆绘制代码:

$(function(){
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgba(200, 200, 100, .6)";
// draw bottom half circle
ctx.beginPath();
ctx.arc(100, 110, 50 , 0, Math.PI);
ctx.closePath();
ctx.fill();
// draw top half circle
ctx.beginPath();
ctx.arc(100, 90, 50 , 0, Math.PI, true);
ctx.closePath();
ctx.fill();
// draw left half circle
ctx.beginPath();
ctx.arc(230, 100, 50 , Math.PI/2, Math.PI*3/2);
ctx.closePath();
ctx.fill();
// draw right half circle
ctx.beginPath();
ctx.arc(250, 100, 50 , Math.PI*3/2, Math.PI/2);
ctx.closePath();
ctx.fill();
// draw a shape that is almost a circle
ctx.beginPath();
ctx.arc(180, 240, 50 , Math.PI*7/6, Math.PI*2/3);
ctx.closePath();
ctx.fill();
// draw a small arc
ctx.beginPath();
ctx.arc(150, 250, 50 , Math.PI*7/6, Math.PI*2/3, true);
ctx.closePath();
ctx.fill();
});

  1. 是时候在 Web 浏览器中测试它了。如下截图所示,画布上应该有六个不同的半圆和弧:

采取行动 用弧函数绘制不同的弧

发生了什么?

我们在弧函数中使用了不同的startAngleendAngle参数来绘制六种不同的弧形状。这些弧形状演示了弧函数的工作原理。

让我们回顾一下度和弧度的关系圆,并看一下顶部的半圆。顶部的半圆从角度 0 开始,到角度π结束,弧是逆时针绘制的。如果我们看一下圆,它看起来像下面的图表:

发生了什么?

如果我们从 210 度开始,到 120 度结束,顺时针方向,我们将得到以下弧:

发生了什么?

小测验

  1. 我们可以使用哪个弧命令来绘制以下弧?小测验

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();

为每种样式开始一个路径

fillstroke函数填充和绘制画布上的路径,但不清除路径列表。以以下代码片段为例。在用红色填充我们的圆之后,我们添加其他圆并用绿色填充。代码的结果是两个圆都被绿色填充,而不仅仅是新圆被绿色填充:

var canvas = document.getElementById('game');
var ctx = 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清空路径列表,所以下次调用fillstroke命令时,它只会应用于beginPath之后的所有路径。

试试看

我们刚刚讨论了一个代码片段,我们打算用红色绘制两个圆,另一个用绿色。结果代码绘制出来的两个圆都是绿色的。我们如何向代码添加beginPath命令,以便正确绘制一个红色圆和一个绿色圆?

关闭路径

closePath函数将从最新路径的最后一个点绘制一条直线到路径的第一个点。这是关闭路径。如果我们只打算填充路径而不打算绘制描边轮廓,closePath函数不会影响结果。以下屏幕截图比较了在半圆上调用closePath和不调用closePath的结果:

关闭路径

快速测验

  1. 如果我们只想填充颜色而不绘制轮廓描边,我们需要在绘制的形状上使用closePath函数吗?

a. 是的,我们需要closePath函数。

b. 不,它不在乎我们是否有closePath函数。

将绘制圆形包装在函数中

绘制圆形是一个常见的函数,我们将经常使用它。最好创建一个绘制圆形的函数,而不是现在输入几行代码。

执行操作将绘制圆形的代码放入函数中

让我们为绘制圆形创建一个函数,并在画布上绘制一些圆圈:

  1. 打开html5games.untangle.js文件。

  2. 用以下代码替换 JavaScript 文件中的原始代码。它基本上将我们刚刚使用的绘制圆形的代码放入一个函数中,并使用 for 循环在画布上随机放置五个圆圈:

var untangleGame = {};
function drawCircle(ctx, x, y, radius) {
ctx.fillStyle = "rgba(200, 200, 100, .9)";
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
$(function(){
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var circleRadius = 10;
var width = canvas.width;
var height = canvas.height;
// random 5 circles
var circlesCount = 5;
for (var i=0;i<circlesCount;i++) {
var x = Math.random()*width;
var y = Math.random()*height;
drawCircle(ctx, x, y, circleRadius);
}
});

  1. 在 Web 浏览器中打开 HTML 文件以查看结果。

执行操作将绘制圆形的代码放入函数中

刚刚发生了什么?

绘制圆形的代码在页面加载和准备就绪后执行。我们使用循环在画布上随机绘制了几个圆圈。

在 JavaScript 中生成随机数

在游戏开发中,我们经常使用random函数。我们可能希望随机召唤一个怪物让玩家战斗,我们可能希望玩家取得进展时随机掉落奖励,我们可能希望随机数成为掷骰子的结果。在这段代码中,我们随机放置圆圈在画布上。

要在 JavaScript 中生成一个随机数,我们使用Math.random()函数。

random函数中没有参数。它总是返回一个介于 0 和 1 之间的浮点数。这个数字大于或等于 0,小于 1。

有两种常见的使用random函数的方式。一种方式是在给定范围内生成随机数。另一种方式是生成真或假值

用法 代码 讨论
获取 A 和 B 之间的随机整数Math.floor(Math.random()*B)+A Math.floor()函数去掉给定数字的小数点。以Math.floor(Math.random()*10)+5为例。Math.random()返回 0 到 0.9999 之间的小数。Math.random()*10是 0 到 9.9999 之间的小数。Math.floor(Math.random()*10)是 0 到 9 之间的整数。最后,Math.floor(Math.random()*10) + 5是 5 到 14 之间的整数。
获取一个随机的布尔值(Math.random() > 0.495)(Math.random() > 0.495)意味着有 50%的假和 50%的真。我们可以进一步调整真/假比例。(Math.random() > 0.7)意味着几乎有 70%的假和 30%的真。

保存圆的位置

当我们开发基于 DOM 的游戏时,比如我们在前几章中构建的游戏,我们经常将游戏对象放入 DIV 元素中,并在代码逻辑中稍后访问它们。在基于画布的游戏开发中情况就不同了。

为了在画布上绘制游戏对象后访问它们,我们需要自己记住它们的状态。比如现在我们想知道有多少个圆被绘制了,它们在哪里,我们需要一个数组来存储它们的位置。

行动时间保存圆的位置

  1. 在文本编辑器中打开html5games.untangle.js文件。

  2. 在 JavaScript 文件的顶部添加以下circle对象定义代码:

function Circle(x,y,radius){
this.x = x;
this.y = y;
this.radius = radius;
}

  1. 现在我们需要一个数组来存储圆的位置。向untangleGame对象添加一个新数组:
var untangleGame = {
circles: []
};

  1. 在画布上绘制每个圆之后,我们将圆的位置保存到circles数组中。在调用drawCircle函数后添加突出显示的行:
$(function(){
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var circleRadius = 10;
var width = canvas.width;
var height = canvas.height;
// random 5 circles
var circlesCount = 5;
for (var i=0;i<circlesCount;i++) {
var x = Math.random()*width;
var y = Math.random()*height;
drawCircle(ctx, x, y, circleRadius); untangleGame.circles.push(new Circle(x,y,circleRadius));
}
});

  1. 现在我们可以在 web 浏览器中测试代码。在画布上绘制随机圆时,这段代码与上一个示例之间没有视觉差异。这是因为我们保存了圆圈,但没有改变任何影响外观的代码。

刚刚发生了什么?

我们保存了每个圆的位置和颜色。这是因为我们无法直接访问画布中绘制的对象。所有线条和形状都是在画布上绘制的,我们无法将线条或形状作为单独的对象访问。绘制的项目都是在画布上绘制的。我们不能像在油画中移动房子一样,也不能直接操作画布元素中的任何绘制项目。

在 JavaScript 中定义一个基本的类定义

JavaScript 是面向对象编程语言。我们可以为我们的使用定义一些对象结构。Circle对象为我们提供了一个数据结构,可以轻松存储一组 x 和 y 位置以及半径。

在定义Circle对象之后,我们可以通过以下代码创建一个新的Circle实例,具有 x、y 和半径值:

var circle1 = new Circle(100, 200, 10);

注意

有关面向对象编程 JavaScript 的更详细用法,请阅读以下链接中的 Mozilla Developer Center:

developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

试一试

我们在画布上随机画了几个圆。它们是相同风格和相同大小的。我们如何随机绘制圆的大小?并用不同的颜色填充圆?尝试修改代码并使用绘图 API 进行操作。

在画布上绘制线条

现在我们这里有几个圆,怎么样用线连接它们?让我们在每个圆之间画一条直线。

行动时间在每个圆之间绘制直线

  1. 打开我们刚刚在圆形绘制示例中使用的index.html

  2. 在 Canvas 中绘制圆的措辞更改为在 Canvas 中绘制线条

  3. 打开html5games.untangle.js JavaScript 文件。

  4. 我们将在现有圆形绘制代码的基础上添加线条绘制代码。用以下代码替换原始代码。修改后的代码已突出显示:

function Circle(x,y,radius){
this.x = x;
this.y = y;
this.radius = radius;
}
function Line(startPoint,endpoint, thickness) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.thickness = thickness;
}
var untangleGame = {
circles: [],
thinLineThickness: 1,
lines: []
};
function drawLine(ctx, x1, y1, x2, y2, thickness) {
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineWidth = thickness;
ctx.strokeStyle = "#cfc";
ctx.stroke();
}
function drawCircle(ctx, x, y, radius) {
ctx.fillStyle = "rgba(200, 200, 100, .9)";
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
$(function(){
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var circleRadius = 10;
var width = canvas.width;
var height = canvas.height;
// random 5 circles
var circlesCount = 5;
for (var i=0;i<circlesCount;i++) {
var x = Math.random()*width;
var y = Math.random()*height;
drawCircle(ctx, x, y, circleRadius);
untangleGame.circles.push(new Circle(x,y,radius));
}
for (var i=0;i< untangleGame.circles.length;i++) {
var startPoint = untangleGame.circles[i];
for(var j=0;j<i;j++) {
var endPoint = untangleGame.circles[j];
drawLine(ctx, startPoint.x, startPoint.y, endPoint.x, endPoint.y, 1);
untangleGame.lines.push(new Line(startPoint, endpoint, untangleGame.thinLineThickness));
}
lines, in canvaslines, in canvasstraight lines, drawing}
});

  1. 在 web 浏览器中测试代码。我们应该看到有线连接到每个随机放置的圆。

时间进行绘制每个圆圈之间的直线

刚刚发生了什么?

与保存圆圈位置的方式类似,我们有一个数组来保存我们绘制的每个线段。我们声明一个线条类定义来存储线段的一些基本信息。也就是说,我们保存线段的起始点和终点以及线条的粗细。

介绍线条绘制 API

有一些绘制 API 供我们绘制和设置线条样式

线条绘制函数 讨论
MoveTo Moveto函数就像我们手中拿着笔在纸上移动而不用笔触到纸。
LineTo 这个函数就像在纸上放下笔并画一条直线到目标点。
lineWidth LineWidth设置我们之后绘制的线条的粗细。
描边 stroke是执行绘制的函数。我们设置了一系列的moveTo, lineTo或样式函数,最后调用stroke函数在画布上执行它。

通常我们使用moveTolineTo对来绘制线条。就像在现实世界中,我们在纸上移动笔到线条的起始点并放下笔来绘制一条线。然后,继续绘制另一条线或在绘制之前移动到其他位置。这正是我们在画布上绘制线条的流程。

注意

我们刚刚演示了绘制一条简单的线。我们可以在画布中为线条设置不同的样式。有关更多线条样式的详细信息,请阅读 W3C 的样式指南(dev.w3.org/html5/2dcontext/#line-styles)和 Mozilla 开发者中心(developer.mozilla.org/En/Canvas_tutorial/Applying_styles_and_colors)。

通过鼠标事件与画布中的绘制对象交互

到目前为止,我们已经展示了我们可以根据逻辑动态在画布中绘制形状。游戏开发中还有一个缺失的部分,那就是输入。

现在想象一下,我们可以在画布上拖动圆圈,连接的线条会跟随圆圈移动。在这一部分,我们将在画布上添加鼠标事件,使我们的圆圈可拖动

拖动画布中的圆圈的时间

  1. 让我们继续之前的代码。打开html5games.untangle.js文件。

  2. 我们需要一个函数来清除画布中的所有绘制。将以下函数添加到 JavaScript 文件的末尾:

function clear(ctx) {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
}

  1. 在 jQuery 的ready函数中删除线条绘制代码。我们将其分成两部分,线条数据和绘制。

  2. 添加以下函数,为每个圆圈分配连接线。这些线将稍后绘制:

function connectCircles()
{
// connect the circles to each other with lines
untangleGame.lines.length = 0;
for (var i=0;i< untangleGame.circles.length;i++) {
var startPoint = untangleGame.circles[i];
for(var j=0;j<i;j++) {
var endPoint = untangleGame.circles[j];
untangleGame.lines.push(new Line(startPoint, endPoint, untangleGame.thinLineThickness));
}
}
}

  1. 将鼠标事件监听器代码添加到 jQuery 的ready函数中。以下是函数现在的样子。高亮显示的代码是鼠标事件处理程序:
$(function(){
// get the reference of canvas element.
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var circleRadius = 10;
var width = canvas.width;
var height = canvas.height;
// random 5 circles
var circlesCount = 5;
for (var i=0;i<circlesCount;i++) {
var x = Math.random()*width;
var y = Math.random()*height;
drawCircle(ctx, x, y, circleRadius);
untangleGame.circles.push(new Circle(x,y,circleRadius));
}
connectCircles();
// Add Mouse Event Listener to canvas
// we find if the mouse down position is on any circle
// and set that circle as target dragging circle.
$("#game").mousedown(function(e) {
var canvasPosition = $(this).offset();
var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;
for(var i=0;i<untangleGame.circles.length;i++)
{
var circleX = untangleGame.circles[i].x;
var circleY = untangleGame.circles[i].y;
var radius = untangleGame.circles[i].radius;
if (Math.pow(mouseX-circleX,2) + Math.pow(mouseY-circleY,2) < Math.pow(radius,2))
if (Math.pow(mouseX-circleX,2) + Math.pow(mouseY-circleY,2) < Math.pow(radius,2))
{
canvascanvascircles, dragginguntangleGame.targetCircle = i;
break;
}
}
});
// we move the target dragging circle when the mouse is moving
$("#game").mousemove(function(e) {
if (untangleGame.targetCircle != undefined)
{
var canvasPosition = $(this).offset();
var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;
var radius = untangleGame.circles[untangleGame.targetCircle]. radius;
untangleGame.circles[untangleGame.targetCircle] = new Circle(mouseX, mouseY,radius);
}
connectCircles();
});
// We clear the dragging circle data when mouse is up
$("#game").mouseup(function(e) {
untangleGame.targetCircle = undefined;
});
// setup an interval to loop the game loop
setInterval(gameloop, 30);
});

  1. 然后我们添加gameloop函数,用于绘制更新后的圆圈和线条:
function gameloop() {
// get the reference of the canvas element and the drawing context.
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
// clear the canvas before re-drawing.
clear(ctx);
// draw all remembered line
for(var i=0;i<untangleGame.lines.length;i++) {
var line = untangleGame.lines[i];
var startPoint = line.startPoint;
var endPoint = line.endPoint;
var thickness = line.thickness;
drawLine(ctx, startPoint.x, startPoint.y, endPoint.x, endPoint.y, thickness);
}
// draw all remembered circles
for(var i=0;i<untangleGame.circles.length;i++) {
var circle = untangleGame.circles[i];
drawCircle(ctx, point.x, point.y, circle.radius);
}
}

  1. 在网络浏览器中打开index.html。应该有五个圆圈,它们之间有连线。尝试拖动圆圈。被拖动的圆圈会跟随鼠标光标移动,连接的线也会跟随移动。

时间进行在画布中拖动圆圈

刚刚发生了什么?

我们在 jQuery 的ready函数中设置了三个鼠标事件监听器。它们是鼠标按下、移动和松开事件。

获取画布元素中的鼠标位置

我们可以通过鼠标事件中的layerXlayerY属性获取相对于元素的鼠标光标位置。以下是我们在代码示例中使用的代码片段。|| 0是为了在layerXlayerY未定义时使结果为 0:

var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;

请注意,我们需要显式设置元素的位置属性,以便获取正确的layerXlayerY属性。

在画布中检测圆圈上的鼠标事件

在讨论了基于 DOM 开发和基于画布开发之间的区别之后,我们不能直接监听画布中任何绘制形状的鼠标事件。这是不可能的。我们不能监视画布中任何绘制形状的事件。我们只能获取画布元素的鼠标事件,并计算画布的相对位置。然后根据鼠标位置改变游戏对象的状态,最后在画布上重新绘制它。

我们如何知道我们点击了一个圆?

我们可以使用点在圆内的公式。这是为了检查圆的中心点与鼠标位置之间的距离。当距离小于圆的半径时,鼠标点击了圆。

我们使用以下公式来计算两点之间的距离:

Distance = (x2-x1)2 + (y2-y1)2

以下图表显示了当中心点与鼠标光标之间的距离小于半径时,光标在圆内的情况:

在画布上检测圆上的鼠标事件

我们使用的以下代码解释了如何在鼠标按下事件处理程序中应用距离检查来知道鼠标光标是否在圆内:

if (Math.pow(mouseX-circleX,2) + Math.pow(mouseY-circleY,2) < Math.pow(untangleGame.circleRadius,2))
{
untangleGame.targetCircle = i;
break;
}

当我们知道鼠标光标按在画布上的圆上时,我们将其标记为在鼠标移动事件上被拖动的目标圆。在鼠标移动事件处理程序中,我们将目标拖动的圆的位置更新为最新的光标位置。当鼠标松开时,我们清除目标圆的引用。

小测验

  1. 我们能直接访问画布中已经绘制的形状吗?

a. 是的

b. 不

  1. 我们可以使用哪种方法来检查一个点是否在圆内?

a. 点的坐标小于圆的中心点的坐标。

b. 点与圆的中心之间的距离小于圆的半径。

c. 点的 x 坐标小于圆的半径。

d. 点与圆的中心之间的距离大于圆的半径。

游戏循环

在第二章《使用基于 DOM 的游戏开发入门》中,我们讨论了游戏循环的方法。在第二章的乒乓球游戏中,游戏循环操作键盘输入并更新基于 DOM 的游戏对象的位置。

在这里,游戏循环用于重新绘制画布以呈现后来的游戏状态。如果我们在改变状态后不重新绘制画布,比如圆的位置,我们将看不到它。

这就像是在电视上刷新图像。电视每秒刷新屏幕 12 次。我们也会每秒重新绘制画布场景。在每次重绘中,我们根据当前圆的位置在画布上绘制游戏状态。

清除画布

当我们拖动圆时,我们重新绘制画布。问题是画布上已经绘制的形状不会自动消失。我们将继续向画布添加新路径,最终搞乱画布上的一切。如果我们在每次重绘时不清除画布,将会发生以下截图中的情况:

清除画布

由于我们已经在 JavaScript 中保存了所有游戏状态,我们可以安全地清除整个画布,并根据最新的游戏状态绘制更新的线条和圆。要清除画布,我们使用画布绘制 API 提供的clearRect函数。clearRect函数通过提供一个矩形裁剪区域来清除矩形区域。它接受以下参数作为裁剪区域:

ctx.clearRect(x,context.clearRect(x, y, width, height)

Argument Definition
x 矩形裁剪区域的左上角点的 x 轴坐标。
y 矩形裁剪区域的左上角点的 y 轴坐标。
width 矩形区域的宽度。
height 矩形区域的高度。

xy设置了要清除的区域的左上位置。widthheight定义了要清除的区域大小。要清除整个画布,我们可以将(0,0)作为左上位置,并将画布的宽度和高度提供给clearRect函数。以下代码清除了整个画布上的所有绘制内容:

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

小测验

  1. 我们可以使用clearRect函数清除画布的一部分吗?

a. 是

b. 否

  1. 以下代码是否清除了画布上的绘制内容?
ctx.clearRect(0, 0, ctx.canvas.width, 0);

a. 是

b. 否

在画布中检测线相交

我们在画布上有可拖动的圆圈和连接的线条。一些线相交,而另一些则不相交。现在想象我们想要区分相交的线。我们需要一些数学公式来检查它们,并加粗这些相交的线。

时间行动 区分相交的线

让我们增加这些相交线的粗细,这样我们就可以在画布中区分它们:

  1. 在文本编辑器中打开html5games.untangle.js文件。

  2. 我们将thinLineThickness设置为默认线条粗细。我们添加以下代码来定义粗线的粗细:

var untangleGame = {
circles: [],
thinLineThickness: 1,
boldLineThickness: 5,
lines: []
};

  1. 为了使代码更具可重用性和可读性,我们希望将线相交逻辑与游戏逻辑隔离开来。我们创建一个函数来检查给定的两条线是否相交。将以下函数添加到 JavaScript 文件的末尾:
function isIntersect(line1, line2)
{
// convert line1 to general form of line: Ax+By = C
var a1 = line1.endPoint.y - line1.point1.y;
var b1 = line1.point1.x - line1.endPoint.x;
var c1 = a1 * line1.point1.x + b1 * line1.point1.y;
// convert line2 to general form of line: Ax+By = C
var a2 = line2.endPoint.y - line2.point1.y;
var b2 = line2.point1.x - line2.endPoint.x;
var c2 = a2 * line2.startPoint.x + b2 * line2.startPoint.y;
// calculate the intersection point
var d = a1*b2 - a2*b1;
// parallel when d is 0
if (d == 0) {
return false;
}else {
line intersectionline intersectiondetermining, in canvasvar x = (b2*c1 - b1*c2) / d;
var y = (a1*c2 - a2*c1) / d;
// check if the interception point is on both line segments
if ((isInBetween(line1.startPoint.x, x, line1.endPoint.x) || isInBetween(line1.startPoint.y, y, line1.endPoint.y)) &&
(isInBetween(line2.startPoint.x, x, line2.endPoint.x) || isInBetween(line2.startPoint.y, y, line2.endPoint.y)))
{
return true;
}
}
return false;
}
// return true if b is between a and c,
// we exclude the result when a==b or b==c
function isInBetween(a, b, c) {
// return false if b is almost equal to a or c.
// this is to eliminate some floating point when
// two value is equal to each other but different with 0.00000...0001
if (Math.abs(a-b) < 0.000001 || Math.abs(b-c) < 0.000001) {
return false;
}
// true when b is in between a and c
return (a < b && b < c) || (c < b && b < a);
}

  1. 接下来,我们有一个函数来检查我们的线是否相交,并用粗体标记该线。将以下新函数添加到代码中:
function updateLineIntersection()
{
// checking lines intersection and bold those lines.
for (var i=0;i<untangleGame.lines.length;i++) {
for(var j=0;j<i;j++) {
var line1 = untangleGame.lines[i];
var line2 = untangleGame.lines[j];
// we check if two lines are intersected,
// and bold the line if they are.
if (isIntersect(line1, line2)) {
line1.thickness = untangleGame.boldLineThickness;
line2.thickness = untangleGame.boldLineThickness;
}
}
}
}

  1. 最后,我们通过在两个地方添加以下函数调用来更新线相交。一个是在连接我们的圆圈之后,另一个是在鼠标移动事件处理程序中:

updateLineIntersection();

  1. 现在是在 Web 浏览器中测试相交的时间了。在画布中查看圆圈和线条,相交的线应该比没有相交的线更粗。尝试拖动圆圈以改变相交关系,线条将变细或变粗。

时间行动 区分相交的线

刚刚发生了什么?

我们刚刚在现有的拖动圆圈示例中添加了线相交检查代码。线相交代码涉及一些数学公式,以获得两条线的交点,并检查该点是否在我们提供的线段内。让我们看看数学部分,看看它是如何工作的。

确定两条线段是否相交

根据我们从几何学中学到的相交方程,对于一般形式中的两条给定线,我们可以得到交点。

一般形式是什么? 在我们的代码中,我们有线段的起点和终点的 x 和 y 坐标。这是一个线段,因为在数学中它只是线的一部分。线的一般形式由Ax + By = C表示。

以下图表解释了一般形式上的线段:

确定两条线段是否相交

我们可以通过以下方程将具有点 1 的线段转换为 x1,y1 和具有点 2 的线段转换为 x2,y2 的一般形式:

A = y2-y1
B = x1-x2
C = A * x1 + B * y2

现在我们有一个线方程AX+BY = C,其中A,B,C是已知的,XY是未知的。

我们正在检查两条相交的线。我们可以将两条线都转换为一般形式,并得到两条线方程:

Line 1: A1X+B1Y = C1
Line 2: A2X+B2Y = C2

通过将两个一般形式方程放在一起,X 和 Y 是两个未知的变量。然后我们可以解这两个方程,得到 X 和 Y 的交点。

如果A1 * B2 - A2 * B1为零,则两条线是平行的,没有交点。否则,我们可以使用以下方程得到交点:

X = (B2 * C1 B1 * C2) / (A1 * B2 A2 * B1)
Y = (A1 * C2 A2 * C1) / (A1 * B2 A2 * B1)

一般形式的交点只能说明两条线不相互平行,并且将在某一点相交。它并不保证交点在两条线段上。

以下图表显示了交点和给定线段的两种可能结果。在左图中,交点不在两条线段之间,在这种情况下,两条线段互不相交。在右侧图中,点在两条线段之间,因此这两条线段相互相交:

确定两条线段是否相交

因此,我们需要另一个名为isInBetween的函数来确定提供的值是否在开始和结束值之间。然后我们使用这个函数来检查方程的交点是否在我们正在检查的两条线段之间。

在获得线条相交的结果后,我们绘制粗线以指示那些相交的线条。

制作解开谜题游戏

现在我们已经创建了一个交互画布,我们可以拖动圆圈和连接圆圈的线条与其他线条相交。我们来玩个游戏吧?有一些预定义的圆圈和线条,我们的目标是拖动圆圈,使没有线条与其他线条相交。这就是所谓的解开谜题游戏

行动时间:在画布中制作解开谜题游戏

让我们在我们的线交点代码中添加游戏逻辑:

  1. 在文本编辑器中打开index.html文件。

  2. 首先,让我们将标题设置为以下内容:

<header>
<h1>Untangle Puzzle Game in Canvas</h1>
</header>

  1. 我们还需要向玩家显示当前级别和进度。在画布元素之后添加以下代码:

谜题0,完成度:0%

  1. 打开html5games.untangle.js JavaScript 文件以添加游戏逻辑。

  2. 添加变量 info,untangleGame。它存储游戏的当前级别:

var untangleGame = {
circles: [],
thinLineThickness: 1,
boldLineThickness: 5,
lines: [],
currentLevel: 0
};

  1. 我们需要一些预定义的级别数据供玩家玩。这是一个定义圆圈放置位置以及它们最初如何连接到彼此的数据集合。将以下级别数据代码添加到untangleGame对象中:
untangleGame.levels =
[
{
"level" : 0,
"circles" : [{"x" : 400, "y" : 156},
{"x" : 381, "y" : 241},
{"x" : 84, "y" : 233},
{"x" : 88, "y" : 73}],
"relationship" : {
"0" : {"connectedPoints" : [1,2]},
"1" : {"connectedPoints" : [0,3]},
"2" : {"connectedPoints" : [0,3]},
"3" : {"connectedPoints" : [1,2]}
}
},
{
"level" : 1,
"circles" : [{"x" : 401, "y" : 73},
{"x" : 400, "y" : 240},
{"x" : 88, "y" : 241},
{"x" : 84, "y" : 72}],
"relationship" : {
"0" : {"connectedPoints" : [1,2,3]},
"1" : {"connectedPoints" : [0,2,3]},
"2" : {"connectedPoints" : [0,1,3]},
"3" : {"connectedPoints" : [0,1,2]}
}
},
{
"level" : 2,
"circles" : [{"x" : 92, "y" : 85},
{"x" : 253, "y" : 13},
{"x" : 393, "y" : 86},
{"x" : 390, "y" : 214},
{"x" : 248, "y" : 275},
{"x" : 95, "y" : 216}],
"relationship" : {
"0" : {"connectedPoints" : [2,3,4]},
"1" : {"connectedPoints" : [3,5]},
"2" : {"connectedPoints" : [0,4,5]},
"3" : {"connectedPoints" : [0,1,5]},
"4" : {"connectedPoints" : [0,2]},
"5" : {"connectedPoints" : [1,2,3]}
}
}
];

  1. 在每个级别开始时,我们需要设置初始级别数据。为了帮助使代码更易读,我们创建一个函数。在 JavaScript 文件的末尾添加以下代码:
function setupCurrentLevel() {
untangleGame.circles = [];
var level = untangleGame.levels[untangleGame.currentLevel];
for (var i=0; i<level.circles.length; i++) {
untangleGame.circles.push(new Point(level.circles[i].x, level. circles[i].y, 10));
}
// setup line data after setup the circles.
connectCircles();
updateLineIntersection();
}

  1. 这是一个有几个级别的游戏。我们需要检查玩家是否解决了当前级别的谜题并跳转到下一个谜题。在文件末尾添加以下函数:
function checkLevelCompleteness() {
if ($("#progress").html() == "100") {
if (untangleGame.currentLevel+1 < untangleGame.levels.length)
untangleGame.currentLevel++;
setupCurrentLevel();
}
}

  1. 我们更新原始的鼠标抬起事件处理程序以检查玩家是否完成了级别:
$("#game").mouseup(function(e) {
untangleGame.targetCircle = undefined;
// on every mouse up, check if the untangle puzzle is solved.
checkLevelCompleteness();
});

  1. 我们将根据级别数据绘制圆圈,而不是随机绘制它们。因此,我们删除 jQueryready函数中的圆圈绘制代码。

  2. 在我们删除 jQueryready函数中的圆圈绘制代码的地方,我们添加以下代码来设置游戏循环使用的圆圈级别数据:

setupCurrentLevel();

  1. 接下来,我们更新connectCircles函数以根据级别数据连接圆圈:
function connectCircles()
{
// setup all lines based on the circles relationship
var level = untangleGame.levels[untangleGame.currentLevel];
untangleGame.lines.length = 0;
for (var i in level.relationship) {
var connectedPoints = level.relationship[i].connectedPoints;
var startPoint = untangleGame.circles[i];
for (var j in connectedPoints) {
var endPoint = untangleGame.circles[connectedPoints[j]];
untangleGame.lines.push(new Line(startPoint, endPoint));
}
}
}

  1. 我们需要另一个函数来更新游戏进度。将以下函数添加到代码中:
function updateLevelProgress()
{
// check the untangle progress of the level
var progress = 0;
for (var i=0;i<untangleGame.lines.length;i++) {
if (untangleGame.lines[i].thickness == untangleGame. thinLineThickness) {
progress++;
}
}
var progressPercentage = Math.floor(progress/untangleGame.lines. length*100);
$("#progress").html(progressPercentage);
// display the current level
$("#level").html(untangleGame.currentLevel);
}

  1. 最后,我们需要在以下鼠标移动事件处理程序中更新级别进度。
$("#game").mousemove(function(e) {
…
connectCircles();
updateLineIntersection();
updateLevelProgress();
…
});

  1. 保存所有文件并在浏览器中测试游戏。我们可以拖动圆圈,线条的粗细将指示它是否与其他线条相交。在鼠标拖动期间,当检测到更多或更少的线交点时,级别完成百分比应该发生变化。如果我们解决了谜题,也就是说没有线条相交,游戏将跳转到下一个级别。当游戏达到最后一个级别时,它将继续显示最后一个级别。这是因为我们还没有添加游戏结束画面。

行动时间:在画布中制作解开谜题游戏

刚刚发生了什么?

我们已经在我们的画布中添加了游戏逻辑,以便我们可以玩我们在整章中创建的圆圈拖动代码。

让我们回顾一下我们添加到untangleGame对象的变量。以下表格列出了这些变量的描述和用法:

变量 描述
circleRadius 所有绘制圆圈的半径设置。
thinLineThickness 绘制细线时的线条粗细。
boldLineThickness 绘制粗线时的线条粗细。
circles 一个数组,用来存储画布中所有绘制的圆圈。
lines 一个数组,用来存储画布中所有绘制的线条。
targetCircle 跟踪我们正在拖动的圆圈。
levels 以 JSON 格式存储每个级别的所有初始数据。
currentLevel 一个数字,用来记录当前级别。

定义级别数据

在每个级别中,我们有解谜游戏中圆圈的初始位置。级别数据被设计为对象数组。每个对象包含每个级别的数据。在每个级别数据中,有三个属性:级别编号、圆圈和连接圆圈的线。下表显示了每个级别数据中的属性:

级别属性 定义 讨论
level 对象的级别编号。 这是每个级别对象中的一个数字,让我们轻松地知道我们在哪个级别。
circles 一个数组,用来存储级别中圆圈的位置。 这定义了当级别设置时圆圈的初始位置。
relationships 一个定义哪些圆圈连接到彼此的关系数组。 每个级别中有一些连接圆圈的线。我们设计线条连接,使每个级别都有解决方案。线条关系定义了哪个圆圈连接到哪个圆圈。例如,以下代码表示圆圈 1 连接到圆圈 2:

在每个级别数据都以我们自定义的结构定义好之后

确定升级

当没有线条相互交叉时,级别完成。我们遍历每条线,并查看有多少条线是细线。细线意味着它们没有与其他线条相交。我们可以使用细线与所有线条的比率来得到级别完成的百分比:

var progress = 0;
for (var i in untangleGame.lines) {
if (untangleGame.lines[i].thickness == untangleGame. thinLineThickness) {
progress++;
}
}
var progressPercentage = Math.floor(progress/untangleGame.lines.length * 100);

当进度达到 100%时,我们可以简单地确定级别已经完成:

if ($("#progress").html() == "100") {
// level complete, level up code
}

显示当前级别和完成进度

在画布游戏下方有一句话描述当前级别的状态和进度。它用于向玩家显示游戏状态,让他们知道他们在游戏中取得了进展:

<p>Puzzle <span id="level">0</span>, Completeness: <span id="progress">0</span>%</p>

我们使用了我们在第二章中讨论的 jQuery HTML 函数,开始 DOM 游戏开发,来更新完成进度。

$("#progress").html(progressPercentage);

Have a go hero

在示例解谜游戏中,我们只定义了三个级别。只有三个级别是不够有趣的。要不要给游戏添加更多级别?如果你想不出级别,可以在互联网上搜索类似的解谜游戏,获取一些级别设计的灵感。

总结

在本章中,我们学到了很多关于绘制形状和与新的 HTML5 画布元素和绘图 API 交互的知识。

具体来说,我们涵盖了:

  • 在画布中绘制不同的路径和形状,包括圆圈、弧线和直线。

  • 添加鼠标事件和与画布中绘制的路径的交互。

  • 在画布中拖动绘制的路径。

  • 通过数学公式来检查线条的交叉。

  • 创建一个解谜游戏,玩家需要拖动圆圈,使连接线不相交。

现在我们已经学习了关于画布和绘图 API 中的基本绘图功能,可以使用它们在画布中创建一个解谜游戏。我们准备学习一些高级的画布绘图技术。在下一章中,我们将使用更多的画布绘图 API 来增强我们的解谜游戏,比如绘制文本、绘制图像和绘制渐变。

第五章:构建 Canvas 游戏大师班

在上一章中,我们探索了一些基本的画布上下文绘图 API,并创建了一个名为 Untangle 的游戏。在本章中,我们将通过使用其他一些上下文绘图 API 来增强游戏。

在本章中,我们将:

  • 用渐变颜色填充我们的游戏对象

  • 在画布中使用自定义网络字体填充文本

  • 在 Canvas 中绘制图像

  • 动画精灵表图像

  • 并构建多个画布层

以下截图是我们将通过本章构建的最终结果的预览。它是一个基于 Canvas 的 Untangle 游戏,带有动画游戏指南和一些细微的细节:

构建 Canvas 游戏大师班

所以让我们开始吧...

用渐变颜色填充形状

在上一章中,我们介绍了填充纯色。Canvas 在填充形状时可以做得更多。我们可以用线性渐变和径向渐变填充形状。

行动时间 给 Untangle 游戏绘制渐变颜色背景

让我们改进一下我们现在的纯黑色背景。如何从上到下绘制一个渐变呢?

  1. 我们将使用上一章中创建的 Untangle 游戏作为起点。在文本编辑器中打开html5games.untangle.js JavaScript 文件。

  2. gameloop函数中清除画布后,添加以下代码以绘制渐变背景:

var bg_gradient = ctx.createLinearGradient(0,0,0,ctx.canvas.height);
bg_gradient.addColorStop(0, "#000000");
bg_gradient.addColorStop(1, "#555555");
ctx.fillStyle = bg_gradient;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);

  1. 保存文件并在浏览器中预览index.html。背景应该是一个线性渐变,顶部是黑色,逐渐变成底部的灰色。

行动时间 给 Untangle 游戏绘制渐变颜色背景

刚刚发生了什么?

我们刚刚用线性渐变颜色填充了一个矩形。要填充线性渐变颜色,我们只需要设置渐变的起点和终点。然后在它们之间添加几个颜色停止。

以下是我们如何使用线性渐变函数的方式:

createLinearGradient(x1, y1, x2, y2);

参数 定义
x1 渐变的起点。
y1
x2 渐变的终点。
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 中有两种渐变类型。我们刚刚使用的是线性渐变。另一种是径向渐变。径向渐变从一个圆到另一个圆填充渐变。

行动时间 用径向渐变颜色填充圆

想象一下,我们现在将我们拖动的圆填充为径向渐变。我们将把实心黄色圆改为白黄渐变:

  1. 打开html5game.untangle.js JavaScript 文件。我们将修改用于在游戏中绘制圆的代码。

  2. 在使用arc函数绘制圆形路径后,填充之前,我们将原始的实色样式设置替换为以下径向渐变颜色:

function drawCircle(ctx, x, y) {
// prepare the radial gradients fill style
var circle_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;
// draw the path
ctx.beginPath();
ctx.arc(x, y, untangleGame.circleRadius, 0, Math.PI*2, true);
ctx.closePath();
// actually fill the circle path
ctx.fill();
}

  1. 保存修改后的文件,并在 Web 浏览器中预览index.html。现在圆形填充了径向渐变颜色。

在下面的屏幕截图中,我将绘图放大到 200%,以更好地演示圆形中的径向渐变:

行动时间 填充圆形与径向渐变颜色

刚刚发生了什么?

我们通过填充径向渐变使拖动圆看起来更真实。

以下是我们创建径向渐变的方法:

createRadialGradient(x1, y1, r1, x2, y2, r2);

参数 定义
x1, y1 画布坐标中起始圆的中心 x 和 y。
r1 起始圆的半径。
x2, y2 画布坐标中结束圆的中心 x 和 y。
r2 结束圆的半径。

下面的屏幕截图显示了径向渐变设置和画布中的最终结果之间的并排比较:

刚刚发生了什么?

径向渐变将颜色从起始圆到结束圆进行混合。在这个渐变圆中,起始圆是中心的小圆,结束圆是最外面的圆。有三个颜色停止点。白色在起始和结束圆处停止;另一种深色在离起始圆 90%的地方停止。

尝试一下英雄填充渐变

我们向渐变中添加颜色停止点来定义颜色的混合方式。如果我们忘记向渐变中添加任何颜色停止点并填充一个矩形会发生什么?如果我们只定义一个颜色停止点会怎样?尝试实验颜色停止点设置。

在径向渐变示例中,小的起始圆在较大的结束圆内。如果起始圆比结束圆大会发生什么?如果起始圆不在结束圆内会怎么样?也就是说,如果两个圆不重叠会发生什么?

在画布中绘制文本

现在想象一下,我们想直接在画布内显示进度级别。画布为我们提供了在画布内绘制文本的方法。

行动时间 在画布元素内显示进度级别文本

  1. 我们将继续使用我们的 Untangle 游戏。在文本编辑器中打开html5games.untangle.js JavaScript 文件。

  2. 首先,让我们将级别进度百分比设为全局变量,这样我们可以在不同的地方使用它:

var untangleGame = {
circles: [],
thinLineThickness: 1,
boldLineThickness: 5,
lines: [],
currentLevel: 0,
progressPercentage: 0
};

  1. gameloop函数中的画布绘制代码之后添加以下代码:
// draw the title text
ctx.font = "26px Arial";
ctx.textAlign = "center";
ctx.fillStyle = "#ffffff";
ctx.fillText("Untangle Game",ctx.canvas.width/2,50);
// draw the level progress text
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText("Puzzle "+untangleGame.currentLevel+", Completeness: " + untangleGame.progressPercentage + "%", 20,ctx.canvas.height-5);

  1. 保存文件并在 Web 浏览器中预览index.html。我们会看到文本现在绘制在画布内。

行动时间 在画布元素内显示进度级别文本

刚刚发生了什么?

我们刚刚在基于画布的游戏中绘制了标题和级别进度文本。我们使用fillText函数在画布中绘制文本。以下表格显示了我们如何使用该函数:

fillText(string, x, y);

参数 定义
string 我们要绘制的文本。
x 文本绘制的 x 坐标。
y 文本绘制的 y 坐标。

这是绘制文本的基本设置。还有几个绘图上下文属性需要设置文本绘制。

上下文属性 定义 讨论
context.font 文本的字体样式。 它与我们在 CSS 中声明字体样式所使用的语法相同。例如,以下代码将字体样式设置为 20 像素的 Arial 粗体:ctx.font = "bold 20px Arial";
context.textAlign 文本对齐。 对齐定义了文本的对齐方式。可以是以下值之一:startendleftrightcenter 例如,如果我们要将文本放在画布的右边缘。使用left对齐意味着我们需要计算文本宽度以知道文本的 x 坐标。在这种情况下使用右对齐,我们只需要将 x 位置直接设置为画布宽度。文本将自动放置在画布的右边缘。
context.textBaseline 文本基线。 以下列出了textBaseline的常见值:topmiddlebottomalphabet 与文本对齐类似,当我们想要将文本放在画布底部时,bottom 基线是有用的。fillText函数的 y 位置是基于文本的底部基线而不是顶部。alphabet基线根据小写字母表对齐 y 位置。以下截图显示了我们使用alphabet基线的文本绘制。

刚刚发生了什么?

注意

请注意,画布中的文本绘制被视为位图图像数据。这意味着访问者无法选择文本;搜索引擎无法索引文本;我们无法搜索它们。因此,我们应该仔细考虑是否要在画布中绘制文本,还是直接将它们放在 DOM 中。

快速测验在画布中绘制文本

  1. 如果我们要在画布的右下角附近绘制文本,哪种对齐和基线设置更好?

a. 左对齐,底部基线。

b. 居中对齐,字母基线。

c. 右对齐,底部基线。

d. 居中对齐,中间基线。

  1. 我们将使用最新的开放网络标准制作一个具有翻页效果的逼真书籍。以下哪种设置更好?

a. 在画布中绘制逼真的书籍,包括所有文本和翻页效果。

b. 将所有文本和内容放在 DOM 中,并在画布中绘制逼真的翻页效果。

在画布中使用嵌入的 Web 字体

在上一章的记忆匹配游戏中,我们使用了自定义字体。自定义字体嵌入也适用于画布。让我们在画布中的 Untangle 游戏中进行一个绘制自定义字体的实验。

执行嵌入 Google Web 字体到画布元素的时间

让我们用手写风格字体绘制画布文本:

  1. 首先,转到 Google 字体目录,选择手写风格字体。我使用了字体Rock Salt,你可以从以下 URL 获取:
http://code.google.com/webfonts/family?family=Rock+Salt&subset=latin#code.

  1. Google 字体目录提供了一个 CSS 链接代码,我们可以将其添加到游戏中以嵌入字体。将以下 CSS 链接添加到index.html的头部:
<link href='http://fonts.googleapis.com/css?family=Rock+Salt' rel='stylesheet' type='text/css'>

  1. 接下来要做的是使用字体。我们打开html5games.untangle.js JavaScript 文件,并将上下文font属性修改为以下内容:
ctx.font = "26px 'Rock Salt'";

  1. 现在是时候在网络浏览器中打开我们的游戏来测试结果了。现在在画布中绘制的文本使用了我们在 Google 字体目录中选择的字体。

执行嵌入 Google Web 字体到画布元素的时间

刚刚发生了什么?

我们刚刚选择了一个网络字体,并将其嵌入到画布中绘制文本时。这表明我们可以像其他 DOM 元素一样为画布中填充的文本设置字体系列。

提示

有时,不同字体系列的文本宽度会有所不同,尽管它们具有相同的字数。在这种情况下,我们可以使用measureText函数来获取我们绘制的文本的宽度。以下链接到 Mozilla 开发者网络解释了我们如何使用该函数:

developer.mozilla.org/en/Drawing_text_using_a_canvas#measureText()

在画布中绘制图像

我们已经在画布内绘制了一些文本。那么绘制图像呢?是的。在画布中绘制图像和图像处理是画布具有的一个重要功能。

执行添加图形到游戏的时间

我们将在游戏中绘制一个黑板背景:

  1. 从代码示例包或以下 URL 下载图形文件。图形文件包括我们在本章中需要的所有图形:
http://gamedesign.cc/html5games/1260_05_example_graphics.zip

  1. 将新下载的图形文件放入名为images的文件夹中。

  2. 我们将加载一幅图像,加载意味着可能需要一段时间直到图像加载完成。理想情况下,我们不应该在所有游戏资源加载完成之前开始游戏。在这种情况下,我们可以准备一个带有加载文字的启动画面,让玩家知道游戏将在稍后开始。在 jQuery 的ready函数中清除画布上下文后,添加以下代码:

// draw a splash screen when loading the game background
// draw gradients background
var bg_gradient = ctx.createLinearGradient(0,0,0,ctx.canvas.height);
bg_gradient.addColorStop(0, "#cccccc");
bg_gradient.addColorStop(1, "#efefef");
ctx.fillStyle = bg_gradient;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// draw the loading text
ctx.font = "34px 'Rock Salt'";
ctx.textAlign = "center";
ctx.fillStyle = "#333333";
ctx.fillText("loading...",ctx.canvas.width/2,canvas.height/2);

  1. 现在是真正加载图像的时候了。我们刚刚下载了一个名为board.png的图形文件。这是一个我们将绘制到画布上的黑板图形背景。在我们刚刚添加的代码之后添加以下代码:
// load the background image
untangleGame.background = new Image();
untangleGame.background.onload = function() {
// setup an interval to loop the game loop
setInterval(gameloop, 30);
}
untangleGame.background.onerror = function() {
console.log("Error loading the image.");
}
untangleGame.background.src = "images/board.png";

  1. gameloop函数中,我们在清除上下文并在绘制任何其他内容之前将图像绘制到画布中。由于图像加载需要时间,我们还需要确保在绘制之前加载它:
// draw the image background
ctx.drawImage(untangleGame.background, 0, 0);

  1. 我们设置了一个levels数组来存储包括初始圆位置在内的级别数据。现在一些圆与背景图像的边框重叠,所以我们可能需要改变圆的位置。使用以下新值更新级别 2 的圆数组:
"circles" : [{"x" : 192, "y" : 155},
{"x" : 353, "y" : 109},
{"x" : 493, "y" : 156},
{"x" : 490, "y" : 236},
{"x" : 348, "y" : 276},
{"x" : 195, "y" : 228}],

  1. 我们还需要调整级别进度文本的位置。修改fill text函数调用为以下代码,使用不同的位置值:
ctx.fillText("Puzzle "+untangleGame.currentLevel+", Completeness: " + untangleGame.progressPercentage + "%", 60, ctx.canvas.height- 80);

  1. 接下来,我们不希望为画布设置背景颜色,因为我们有一个带有透明边框的 PNG 背景。打开untangle.css文件并删除画布中的背景属性。

  2. 现在保存所有文件并在 Web 浏览器中打开index.html。背景应该在那里,手写字体应该与我们的黑板主题相匹配。

执行操作添加图形到游戏

刚刚发生了什么?

我们刚刚在画布元素内绘制了一幅图像。

在画布上绘制图像有两种常见的方法。我们可以引用现有的img标签,也可以在 JavaScript 中动态加载图像。

这是我们在画布中引用现有图像标签的方式。

假设我们在 HTML 中有以下img标签:

<img id="board" src="img/board.png">

我们可以使用以下 JavaScript 代码在画布中绘制图像:

var img = document.getElementById('board');
context.drawImage(img, x, y);

这是另一个加载图像的代码片段,而不将img标签附加到 DOM 中。如果我们在 JavaScript 中加载图像,我们需要确保图像在绘制到画布上之前已加载。因此,我们在图像的onload事件之后绘制图像:

var board = new Image();
board.onload = function() {
context.drawImage(board, x, y);
images, inside canvasimages, inside canvasdrawing}
board.src = "images/board.png";

提示

设置 onload 事件处理程序和分配图像 src 时的顺序很重要

当我们将src属性分配给图像并且如果图像被浏览器缓存,一些浏览器会立即触发onload事件。如果我们在分配src属性后放置onload事件处理程序,我们可能会错过它,因为它是在我们设置事件处理程序之前触发的。

在我们的示例中,我们使用了后一种方法。我们创建了一个 Image 对象并加载了背景。当图像加载完成时,我们启动游戏循环,从而开始游戏。

加载图像时我们还应该处理的另一个事件是onerror事件。当我们访问额外的网络数据时,这是特别有用的。我们有以下代码片段来检查我们示例中的错误:

untangleGame.background.onerror = function() {
console.log("Error loading the image.");
}

试一试

现在加载错误只在控制台中显示消息。玩家通常不会查看控制台。设计一个警报对话框或其他方法来告诉玩家游戏未能加载游戏资源,如何?

使用 drawImage 函数

有三种在画布中绘制图像的行为。我们可以在给定的坐标上绘制图像而不进行任何修改,我们还可以在给定的坐标上绘制具有缩放因子的图像,或者我们甚至可以裁剪图像并仅绘制裁剪区域。

drawImage函数接受几个参数:

drawImage(image, x, y);

参数 定义 讨论
图像 我们要绘制的图像引用。 我们可以通过获取现有的img元素或创建 JavaScriptImage对象来获取图像引用。
x 在画布坐标中放置图像的 x 位置。 x 和 y 坐标是我们放置图像的位置,相对于其左上角。
y 在画布坐标中放置图像的 y 位置。
drawImage(image, x, y, width, height);

参数 定义 讨论
图像 我们要绘制的图像引用。 我们可以通过获取现有的img元素或创建 JavaScriptImage对象来获取图像引用。
x 在画布坐标中放置图像的 x 位置。 x 和 y 坐标是我们放置图像的位置,相对于其左上角。
y 在画布坐标中放置图像的 y 位置。
宽度 最终绘制图像的宽度。 如果宽度和高度与原始图像不同,我们会对图像应用比例。
高度 最终绘制图像的高度。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, width, height);

参数 定义 讨论
图像 我们要绘制的图像引用。 我们可以通过获取现有的img元素或创建 JavaScriptImage对象来获取图像引用。
sx 裁剪区域左上角的 x 坐标。 裁剪 x、y、宽度、高度一起定义了一个矩形裁剪区域。给定的图像将被此矩形裁剪。
sy 裁剪区域左上角的 y 坐标。
sWidth 裁剪区域的宽度。
sHeight 裁剪区域的高度。
参数 定义 讨论
dx 在画布坐标中放置图像的 x 位置。 x 和 y 坐标是我们放置图像的位置,相对于其左上角。
dy 在画布坐标中放置图像的 y 位置。
宽度 最终绘制图像的宽度。 如果宽度和高度与裁剪尺寸不同,我们会对裁剪后的图像应用比例。
高度 最终绘制图像的高度。

试试看英雄 优化背景图像

在示例中,我们在每次调用gameloop函数时将黑板图像作为背景绘制。由于我们的背景是静态的,不会随时间变化,所以一遍又一遍地清除并重新绘制会浪费 CPU 资源。我们如何优化这个性能问题?

装饰基于画布的游戏

我们已经用渐变和图像增强了画布游戏。在继续之前,让我们装饰一下画布游戏的网页。

行动时间为游戏添加 CSS 样式和图像装饰

我们将建立一个居中对齐的布局,带有一个游戏标题:

  1. 我们从 Google 字体目录嵌入了另一种字体来为正常的正文文本设置样式。在index.htmlhead中添加以下 CSS 链接:
<link href='http://fonts.googleapis.com/css?family=Josefin+Sans:600' rel='stylesheet' type='text/css'>

  1. 使用一个分组 DOM 元素来为布局设置样式更容易。我们将所有元素放入一个带有id页面的部分中:
<section id="page">
...
</section>

  1. 让我们对页面布局应用 CSS。用以下代码替换untangle.css文件中的现有内容:
html, body {
background: url(../images/title_bg.png) 50% 0 no-repeat, url(../ images/bg_repeat.png) 50% 0 repeat-y #889ba7;
margin: 0;
font-family: 'Josefin Sans', arial, serif;
color: #111;
}
#game{
position:relative;
}
#page {
width: 821px;
min-height: 800px;
margin: 0 auto;
padding: 0;
text-align: center;
text-shadow: 0 1px 5px rgba(60,60,60,.6);
}
header {
height: 88px;
padding-top: 36px;
margin-bottom: 50px;
font-family: "Rock Salt", Arial, sans-serif;
font-size: 14px;
text-shadow: 0 1px 0 rgba(200,200,200,.5);
color: #121;
}

  1. 现在我们在带上有标题的带子中有了标题文本。在画布中再次显示标题似乎是多余的。让我们删除以下绘制标题的代码行:
ctx.fillText("Untangle Game",ctx.canvas.width/2,50);

  1. 是时候保存所有文件并在 Web 浏览器中预览了。我们应该看到一个居中对齐的标题带和精心设计的布局。以下截图显示了结果:

行动时间为游戏添加 CSS 样式和图像装饰

刚刚发生了什么?

我们刚刚装饰了包含基于画布的游戏的网页。虽然我们的游戏是基于画布绘制的,但这并不限制我们用图形和 CSS 样式装饰整个网页。

注意

画布元素的默认背景

画布元素的默认背景是透明的。如果我们不设置画布的任何背景 CSS 样式,它将是透明的。当我们的绘图不是矩形时,这是有用的。在这个例子中,纹理布局背景显示在画布区域内。

快速测验 设置画布背景

  1. 我们如何将画布背景设置为透明?

a. 将背景颜色设置为#ffffff。

b. 什么也不做。默认情况下是透明的。

在 canvas 中制作精灵表动画

我们在第三章“在 CSS3 中构建记忆匹配游戏”中首次使用了精灵表图像,用于显示一副扑克牌。

行动时间 制作游戏指南动画

在 images 文件夹中有一个名为guide_sprite.png的图形文件。这是一个包含动画每一步的游戏指南图形。

行动时间 制作游戏指南动画

让我们用动画将这个指南画到我们的游戏中:

  1. 在文本编辑器中打开html5games.untangle.js JavaScript 文件。

  2. 在 jQuery 的ready函数中添加以下代码:

// load the guide sprite image
untangleGame.guide = new Image();
untangleGame.guide.onload = function() {
untangleGame.guideReady = true;
// setup timer to switch the display frame of the guide sprite
untangleGame.guideFrame = 0;
setInterval(guideNextFrame, 500);
}
untangleGame.guide.src = "images/guide_sprite.png";

  1. 我们添加以下函数,以便每 500 米将当前帧移动到下一帧:
function guideNextFrame()
{
untangleGame.guideFrame++;
// there are only 6 frames (0-5) in the guide animation.
// we loop back the frame number to frame 0 after frame 5.
if (untangleGame.guideFrame > 5)
{
untangleGame.guideFrame = 0;
}
}

  1. gameloop函数中,我们根据当前帧绘制指南动画。
// draw the guide animation
if (untangleGame.currentLevel == 0 && untangleGame.guideReady)
{
// the dimension of each frame is 80x130.
var nextFrameX = untangleGame.guideFrame * 80;
ctx.drawImage(untangleGame.guide, nextFrameX, 0, 80, 130, 325,
130, 80, 130);
}

  1. 通过打开index.html在 Web 浏览器中观看动画。以下截图演示了游戏指南动画的动画。指南动画将播放并循环,直到玩家升级:

行动时间 制作游戏指南动画

刚刚发生了什么?

在使用drawImage上下文函数时,我们可以只绘制图像的一部分区域。

以下截图逐步演示了动画的过程。矩形是裁剪区域。我们使用一个名为guideFrame的变量来控制显示哪一帧。每帧的宽度为 80。因此,我们通过将宽度和当前帧数相乘来获得裁剪区域的 x 位置:

var nextFrameX = untangleGame.guideFrame * 80;
ctx.drawImage(untangleGame.guide, nextFrameX, 0, 80, 130, 325, 130, 80, 130);

guideFrame变量每 500 米通过以下guideNextFrame函数进行更新:

function guideNextFrame()
{
untangleGame.guideFrame++;
// there are only 6 frames (0-5) in the guide animation.
// we loop back the frame number to frame 0 after frame 5.
if (untangleGame.guideFrame > 5)
{
untangleGame.guideFrame = 0;
}
}

刚刚发生了什么?

在开发游戏时,制作精灵动画是一种常用的技术。在传统视频游戏中使用精灵动画有一些好处。这些原因可能不适用于网页游戏开发,但我们在使用精灵表动画时有其他好处:

  • 所有帧都加载为一个文件,因此一旦精灵文件加载完毕,整个动画就准备就绪。

  • 将所有帧放入一个文件中意味着我们可以减少 Web 浏览器向服务器的 HTTP 请求。如果每一帧都是一个文件,那么浏览器会多次请求文件,而现在它只请求一个文件并使用一个 HTTP 请求。

  • 将不同的图像放入一个文件中还有助于减少重复文件的页眉、页脚和元数据。

  • 将所有帧放入一张图像中意味着我们可以轻松裁剪图像以显示任何帧,而无需复杂的代码来更改图像源。

它通常用于角色动画。以下截图是我在名为邻居的 HTML5 游戏中使用的愤怒猫的精灵动画

刚刚发生了什么?

在这个例子中,我们通过裁剪帧并自行设置定时器来构建精灵表动画。当处理大量动画时,我们可能希望使用一些第三方精灵动画插件或创建自己的画布精灵动画,以更好地重用和管理逻辑代码。

注意

精灵动画是 HTML5 游戏开发中的重要主题,有许多在线资源讨论这个主题。以下链接是其中一些:

CodeUtopia 的精灵动画教程(codeutopia.net/blog/2009/08/21/using-canvas-to-do-bitmap-sprite-animation-in-javascript/)讨论了如何从头开始制作精灵对象并使用它来动画显示精灵。

John Graham 的精灵动画演示(www.johnegraham2.com/web-technology/html-5-canvas-tag-sprite-animation-demo/)提供了另一个精灵对象,用于在画布中动画显示精灵。

另一方面,Spritely(www.spritely.net/)提供了在 DOM 元素上使用 CSS 进行精灵动画。当我们想要在不使用画布的情况下动画显示精灵时,这是很有用的。

创建多层画布游戏

现在所有的东西都绘制到上下文中,它没有其他状态来区分已绘制的项目。我们可以将画布游戏分成不同的图层,并编写逻辑来控制和绘制每个图层。

行动时间将游戏分成四个图层

我们将把 Untangle 游戏分成四个图层:

  1. index.htm中,我们将画布 HTML 更改为以下代码。它包含一个部分内的几个画布:
<section id="layers">
<canvas id="bg" width="768" height="440">
Sorry, your web browser does not support canvas content.
</canvas>
<canvas id="guide" width="768" height="440"></canvas>
<canvas id="game" width="768" height="440"></canvas>
<canvas id="ui" width="768" height="440"></canvas>
</section>

  1. 我们还需要对画布应用一些样式,使它们重叠在一起,以创建多层效果。此外,我们还需要准备一个fadeout类和dim类,使目标变得透明。将以下代码添加到untangle.css文件中:
#layers {
height: 440px;
position: relative;
margin: 0 auto;
width:768px;
height: 440px;
}
#layers canvas{
left: 50%;
margin-left: -384px;
position: absolute;
}
#guide {
opacity: .7;
}
#guide.fadeout {
opacity: 0;
-webkit-transition: opacity .5s linear;
transition: opacity .5s linear;
}
#ui {
-webkit-transition: opacity .3s linear;
transition: opacity .3s linear;
}
#ui.dim {
opacity: .3;
}

  1. html5games.untangle.js JavaScript 文件中,我们修改代码以支持图层功能。首先,我们添加一个数组来存储每个画布的上下文引用:
untangleGame.layers = new Array();

  1. 然后,我们获取上下文引用并将它们存储在数组中:
// prepare layer 0 (bg)
var canvas_bg = document.getElementById("bg");
untangleGame.layers[0] = canvas_bg.getContext("2d");
// prepare layer 1 (guide)
var canvas_guide = document.getElementById("guide");
untangleGame.layers[1] = canvas_guide.getContext("2d");
// prepare layer 2 (game)
var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
untangleGame.layers[2] = ctx;
// prepare layer 3 (ui)
var canvas_ui = document.getElementById("ui");
untangleGame.layers[3] = canvas_ui.getContext("2d");

  1. 由于现在游戏画布重叠在一起,我们在game画布中的鼠标事件监听器不再起作用。我们可以从父layers DIV 中监听事件,该 DIV 具有与画布相同的位置和尺寸:
$("#layers").mousedown(function(e)
$("#layers").mousemove(function(e)
$("#layers").mouseup(function(e)

  1. 我们将绘图部分分成不同的函数,用于不同的图层。在以下的drawLayerBG函数中,它只负责绘制背景:
function drawLayerBG()
{
var ctx = untangleGame.layers[0];
clear(ctx);
// draw the image background
ctx.drawImage(untangleGame.background, 0, 0);
}

  1. 当背景图像加载时,我们绘制背景层。将以下突出显示的代码添加到背景的onload事件中:
untangleGame.background.onload = function() {
drawLayerBG();
// setup an interval to loop the game loop
setInterval(gameloop, 30);
}

  1. 我们将游戏循环分成三个不同的函数,用于指定的图层:
function gameloop() {
drawLayerGuide();
drawLayerGame();
drawLayerUI();
}

  1. 现在我们将指导线动画放入一个专用画布中,这样我们就可以轻松地应用 CSS 样式来淡出指导线:
function drawLayerGuide()
{
var ctx = untangleGame.layers[1];
clear(ctx);
// draw the guide animation
if (untangleGame.guideReady)
{
// the dimension of each frame is 80x130.
var nextFrameX = untangleGame.guideFrame * 80;
ctx.drawImage(untangleGame.guide, nextFrameX, 0, 80, 130, 325, 130, 80, 130);
}
// fade out the guideline after level 0
if (untangleGame.currentLevel == 1)
{
$("#guide").addClass('fadeout');
}
}

  1. 以下的drawLayerGame保留了我们在游戏中使用的所有绘图代码。大部分代码来自原始的gameloop函数:
function drawLayerGame()
{
// get the reference of the canvas element and the drawing context.
var ctx = untangleGame.layers[2];
// draw the game state visually
// clear the canvas before drawing.
clear(ctx);
// draw all remembered line
for(var i=0;i<untangleGame.lines.length;i++) {
var line = untangleGame.lines[i];
var startPoint = line.startPoint;
var endPoint = line.endPoint;
var thickness = line.thickness;
drawLine(ctx, startPoint.x, startPoint.y, endPoint.x, endPoint.y, thickness);
}
// draw all remembered circles
for(var i=0;i<untangleGame.circles.length;i++) {
var circle = untangleGame.circles[i];
drawCircle(ctx, circle.x, circle.y, circle.radius);
}
}

  1. 级别进度文本现在放置在 UI 层中,并由drawLayerUI函数绘制。它使用一个专用层,因此当文本与游戏对象(如圆圈)重叠时,我们可以轻松地降低不透明度:
function drawLayerUI()
multi-layers canvas gamemulti-layers canvas gamefour layers, dividing into{
var ctx = untangleGame.layers[3];
clear(ctx);
// draw the level progress text
ctx.font = "26px 'Rock Salt'";
ctx.fillStyle = "#dddddd";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText("Puzzle "+untangleGame.currentLevel+", Completeness: ", 60,ctx.canvas.height-80);
ctx.fillText(untangleGame.progressPercentage+"%",450, ctx.canvas.height-80);
// get all circles, check if the ui overlap with the game objects
var isOverlappedWithCircle = false;
for(var i in untangleGame.circles) {
var point = untangleGame.circles[i];
if (point.y > 310)
{
isOverlappedWithCircle = true;
}
}
if (isOverlappedWithCircle)
{
$("#ui").addClass('dim');
}
else
{
$("#ui").removeClass('dim');
}
}

  1. 保存所有文件,并在 Web 浏览器中检查我们的大量代码更改。游戏应该显示得好像我们什么都没改变一样。尝试将圆圈拖动到靠近黑板的底部边缘。级别进度文本应该变得不透明。完成第一级时,指导线动画将优雅地淡出。以下截图显示了半透明的级别进度:

行动时间将游戏分成四个图层

刚刚发生了什么?

现在总共有四个画布。每个画布负责一个图层。图层分为背景、游戏指导线、游戏本身和显示级别进度的用户界面。

默认情况下,画布和其他元素一样,是依次排列的。为了重叠所有画布以构建图层效果,我们对它们应用了absolute位置。

以下截图显示了我们游戏中现在设置的四个层。默认情况下,后添加的 DOM 位于之前添加的 DOM 之上。因此,bg画布位于底部,ui位于顶部:

刚刚发生了什么?

将 CSS 技术与画布绘制混合

我们正在创建一个基于画布的游戏,但我们并不局限于只使用画布绘图 API。级别进度信息现在在另一个 ID 为ui的画布中。在这个示例中,我们混合了我们在第三章中讨论的 CSS 技术,在 CSS3 中构建记忆匹配游戏

当我们在画布上拖动圆圈时,它们可能会重叠在级别信息上。在绘制 UI 画布层时,我们会检查是否有任何圆圈的坐标过低并且重叠在文本上。然后我们会淡化 UI 画布的 CSS 不透明度,这样就不会分散玩家对圆圈的注意力。

在玩家升级后,我们还会淡出指南动画。这是通过将整个guide画布淡出到 CSS 过渡缓和为 0 不透明度来实现的。由于guide画布只负责该动画,隐藏该画布不会影响其他元素:

if (untangleGame.currentLevel == 1)
{
$("#guide").addClass('fadeout');
}

提示

只清除改变的区域以提高画布性能

我们可以使用clear函数来清除画布上下文的一部分。这将提高性能,因为它避免了每次重新绘制整个画布上下文。这是通过标记自上次绘制以来状态发生变化的上下文的“脏”区域来实现的。

在我们的示例中的指南画布层,我们可以考虑只清除精灵表图像绘制的区域,而不是整个画布。

在简单的画布示例中,我们可能看不到明显的差异,但是当我们有一个包含许多精灵图像动画和复杂形状绘制的复杂画布游戏时,它有助于提高性能。

试试吧

当玩家进入第 2 级时,我们会淡出指南。当玩家拖动任何圆圈时,我们如何淡出指南动画?我们怎么做?

总结

在本章中,我们学到了很多关于在画布中绘制渐变、文本和图像的知识。

具体来说,我们涵盖了:

  • 用线性或径向渐变填充形状

  • 用字体嵌入和其他文本样式在画布中填充文本

  • 将图像绘制到画布中

  • 通过clipping函数在绘制图像时对精灵表进行动画处理

  • 通过堆叠多个画布元素将游戏分成几个层

  • 在基于画布的游戏中混合 CSS 过渡动画

在这本书中我们没有提到的一件事是画布中的位图操作。画布上下文是一个位图数据,我们可以在每个像素上应用操作。例如,我们可以在画布上绘制图像并对图像应用类似于 Photoshop 的滤镜。我们不会在书中涵盖这个内容,因为图像处理是一个高级话题,而且应用可能与游戏开发无关。

在互联网上有一些很好的画布游戏示例。Canvas Demo (www.canvasdemos.com/type/games/)链接了其他网站上最新的画布游戏。Mozilla 的 Game On 2010 画廊(gaming.mozillalabs.com/games/)列出了他们游戏开发竞赛的一系列游戏条目。其中一些是用画布制作的。

现在我们已经学会了在画布中构建游戏并为游戏对象制作动画,比如游戏角色,我们准备在下一章为我们的游戏添加音频组件和音效。

我们将在第九章中回到基于画布的游戏,

第六章:为你的游戏添加声音效果

我们已经讨论了几种以视觉方式绘制游戏对象的技术。在本章中,我们将专注于使用 HTML5 规范中引入的audio标签。我们可以通过 JavaScript API 添加声音效果、背景音乐,并控制音频。此外,我们将在本章中构建一个音乐游戏。这是一个需要玩家在正确的时间击中正确的琴弦以产生音乐的游戏。

在本章中,我们将学习以下主题:

  • 为播放按钮添加声音效果

  • 构建一个迷你钢琴音乐游戏

  • 链接音乐游戏和播放按钮

  • 为游戏添加键盘驱动

  • 创建一个键盘驱动的音乐游戏

  • 完成具有级别数据记录和游戏结束事件的音乐游戏

以下截图显示了我们将通过本章创建的最终结果:

为你的游戏添加声音效果

所以,让我们开始吧。

为播放按钮添加声音效果

在之前的章节中,我们在 Untangle 游戏示例中有几种鼠标交互。现在想象一下,我们希望在鼠标交互时有声音效果。这要求我们指示游戏使用哪个音频文件。我们将使用audio标签在按钮上创建声音效果。

执行添加声音效果到播放按钮的操作

我们将从代码包中提供的代码示例开始。我们将有类似以下截图所示的文件夹结构:

执行添加声音效果到播放按钮的操作

  1. index.htm文件包含了 HTML 的基本结构。现在让我们在index.htm文件的 body 部分添加以下代码:
<div id="game">
<section id="menu-scene" class="scene">
<a href="#game"><span>Play</span></a>
</section>
</div>
<audio id="buttonover">
<source src="img/button_over.wav" />
<source src="img/button_over.ogg" />
</audio>
<audio id="buttonactive">
<source src="img/button_active.mp3" />
<source src="img/button_active.ogg" />
</audio>

  1. HTML 文件使用样式表完成。该文件可以在名为audiogame.css的代码包中找到。

  2. 接下来,我们将在 JavaScript 文件中为按钮添加声音效果。在html5games.audio.js文件中添加以下 JavaScript:

//a global object variable to store all game scope variable.
var audiogame = {};
// init function when the DOM is ready
$(function(){
// get the references of the audio element.
audiogame.buttonOverSound = document.getElementById("buttonover");
audiogame.buttonOverSound.volume = 0.3;
audiogame.buttonActiveSound = document.getElementById("buttonactive");
audiogame.buttonActiveSound.volume = 0.3;
// listen the button event that links to #game
$("a[href='#game']")
.hover(function(){
audiogame.buttonOverSound.currentTime = 0;
audiogame.buttonOverSound.play();
},function(){
audiogame.buttonOverSound.pause();
sound effect, adding to Play buttonsound effect, adding to Play buttonsteps});
.click(function(){
audiogame.buttonActiveSound.currentTime = 0;
audiogame.buttonActiveSound.play();
return false;
});
});

  1. 在浏览器中打开index.htm文件。在那里,你应该看到一个黄色背景上的PLAY按钮,如下截图所示。尝试将鼠标移动到按钮上并点击它。当你悬停在按钮上时,你应该能听到声音,当你点击它时,你应该能听到另一个声音:

执行添加声音效果到播放按钮的操作

刚刚发生了什么?

我们刚刚创建了一个基本的 HTML5 游戏布局,其中播放按钮放置在页面中间。JavaScript 文件处理按钮的鼠标悬停和点击,并播放相应的声音效果。

定义一个音频元素

使用audio标签的最简单方法是提供一个源文件。以下代码片段显示了如何定义音频元素:

<audio>
<source src="img/button_active.mp3" />
<source src="img/button_active.ogg" />
<!-- Any code for browser that does not support audio tag -->
</audio>

提示

在音频标签中显示回退内容

audio标签是 HTML5 规范中新引入的。我们可以在audio标签内放置回退内容,例如 Flash 电影来播放音频。以下来自 HTML5 Rocks 的链接显示了如何使用具有 Flash 回退的audio标签的快速指南:

www.html5rocks.com/tutorials/audio/quick/

除了设置audio标签的源文件外,我们还可以使用几个属性来进行额外的控制。以下表格显示了我们可以为音频元素设置的属性:

参数 定义 解释
src 定义音频元素的源文件 当我们在audio标签中使用src属性时,它指定了音频文件的一个源文件。例如,我们在以下代码中加载一个声音效果Ogg文件:
参数 定义 解释
autoplay 指定音频一旦加载就自动播放 Autoplay 用作独立属性。这意味着以下两行代码没有区别:
loop 指定音频在播放完成后从头开始再次播放 这也用作独立属性。
preload 指定音频源在页面加载后加载 preload属性可以取以下任一值:preload="auto"``preload="metadata"``preload="none"preload用作独立属性时,它的作用类似于设置为auto,浏览器将预加载音频。当设置为metadata时,浏览器不会预加载音频的内容。但是,它会加载音频的元数据,如持续时间和大小。当设置为none时,浏览器根本不会预加载音频。内容和元数据在播放时加载。
controls 显示音频的播放控件 controls属性是一个独立的属性。它指示浏览器在音频位置显示播放控件。

以下屏幕截图显示 Chrome 显示控件:

定义音频元素

播放声音

我们可以通过调用getElementById函数来获取音频元素的引用。然后,我们通过调用play函数来播放它。以下代码播放buttonactive音频:

<audio id="buttonactive">
<source src="img/button_active.mp3" />
<source src="img/button_active.ogg" />
</audio>
<script>
document.getElementById("buttonactive").play();
</script>

play函数从存储在currentTime属性中的经过的时间播放音频。currentTime的默认值为零。以下代码从 3.5 秒开始播放音频:

<script>
document.getElementById("buttonactive").currentTime = 3.5;
document.getElementById("buttonactive").play();
</script>

暂停声音

与播放按钮类似,我们也可以通过使用pause函数暂停音频元素的播放。以下代码暂停buttonactive音频元素:

<script>
document.getElementById("buttonactive").pause();
</script>

注意

没有stop函数来停止音频元素。相反,我们可以暂停音频并将元素的currentTime属性重置为零。以下代码显示了如何停止音频元素:

<script>

document.getElementById("buttonactive").pause();

document.getElementById("buttonactive").currentTime = 0;

</script>

调整音量

我们还可以设置音频元素的音量。音量必须在 0 和 1 之间。我们可以将音量设置为 0 来静音,将其设置为 1 来达到最大音量。以下代码片段将buttonactive音频的音量设置为 30%:

<script>
document.getElementById("buttonactive").volume = 0.3;
</script>

使用 jQuery hover 事件

jQuery 提供了一个hover函数来定义当我们鼠标悬停和移出 DOM 元素时的行为。以下是我们如何使用hover函数:

.hover(function1, function2);

参数 讨论
function1 当鼠标移入时执行该函数。
function2 这是可选的。当鼠标移出时执行该函数。当未提供此函数时,移出行为与 function1 相同。

在以下代码中,当鼠标移动时,我们播放鼠标悬停音效,并在鼠标移出时暂停音效:

$("a[href='#game']").hover(function(){
audiogame.buttonOverSound.currentTime = 0;
audiogame.buttonOverSound.play();
},function(){
audiogame.buttonOverSound.pause();
});

创建 Ogg 格式音频以支持 Mozilla Firefox

当我们定义音频元素的来源时,我们使用 MP3 格式和Ogg格式文件。Ogg 是一种免费开源的媒体容器格式,受到 Mozilla Firefox 的支持。我们将使用免费音频转换器将我们的 MP3 文件转换为 Ogg 文件。

注意

维基百科在以下网址包含了有关 Ogg 格式的详细解释:

en.wikipedia.org/wiki/Ogg

执行操作 将 MP3 声音转换为 Ogg 格式使用 Audacity

Ogg 是一个开源标准,可以免费使用。有许多支持它的音乐播放器和转换器。我们将使用名为Audacity的免费软件将我们的 MP3 文件转换为 Ogg 格式:

  1. 转到以下网址下载 Audacity:

audacity.sourceforge.net/download/

  1. 按照安装程序的说明安装 Audacity。

  2. 在 Audacity 中打开button_over.mp3。以下截图显示了打开了 MP3 文件的 Audacity,等待我们开始转换:将 MP3 音频转换为 Ogg 格式

  3. 单击文件 | 导出为 Ogg Vorbis以打开导出对话框。

注意

在撰写本书时,Audacity 1.3 beta 版本已发布,导出布局发生了变化。单击文件 | 导出…,并在导出对话框中选择 Ogg 格式。

  1. 在工作目录中保存 Ogg 格式文件。

刚刚发生了什么?

我们刚刚将一个 MP3 格式的音效转换为 Ogg 格式,以使音频在不支持 MP3 格式的浏览器中工作。

支持不同的网络浏览器和不同的音频格式

以下表格显示了撰写本书时最受欢迎的网络浏览器支持的音频格式:

浏览器 Ogg MP3 WAV
Firefox 3.6+ -
Safari 5+ -
Chrome -
Opera 10.5+ -
Internet Explorer 9 -

小测验 使用音频标签

  1. 我们如何停止正在播放的音频元素?

a. 使用stop函数

b. 使用pause函数并将currentTime重置为 0

c. 将currentTime重置为 0

  1. 我们如何在不支持audio标签的浏览器中放置回退内容以显示?

构建一个迷你钢琴音乐游戏

现在想象一下,我们不仅播放音效,还使用audio标签播放整首歌曲。随着歌曲的播放,有一些音乐点向下移动,作为音乐的可视化。

创建音乐游戏的基本背景

首先,我们将在画布上绘制一些路径作为音乐播放的背景。

  1. 我们将继续处理示例并绘制背景。在文本编辑器中打开index.htm文件,并添加以下突出显示的代码,定义了具有两个设置的画布的游戏场景:
<div id="game">
<section id="menu-scene" class="scene">
<a href="#game"><span>Play</span></a>
</section>
<section id="game-scene" class="scene">
<canvas id="game-background-canvas" width="768" height="440">
Sorry, your web browser does not support canvas content.
</canvas>
<canvas id="game-canvas" width="768" height="440"></canvas>
</section>
</div>

  1. 我们在 HTML 文件中添加了一个游戏场景。我们希望将其放在菜单场景的上方,因此我们通过在audiogame.css中添加以下内容来将游戏场景设置为absolute位置:
#game-scene {
background: #efefef url(../images/game_bg.jpg);
}
#game-canvas, #game-background-canvas {
position: absolute;
}

  1. 是时候编写背景绘制代码了。打开html5games.audio.js JavaScript 文件。

  2. 在 jQuery 的ready函数中,我们调用drawBackground函数来绘制背景,如下所示:

drawBackground();

  1. 在 JavaScript 文件的末尾添加以下drawBackground函数。该代码在game-background-canvas画布中绘制了三条黑线和一条灰线:
function drawBackground()
{
// get the reference of the canvas and the context.
var game = document.getElementById("game-background-canvas");
var ctx = game.getContext('2d');
// set the line style of the three vertical lines.
ctx.lineWidth = 10;
ctx.strokeStyle = "#000";
var center = game.width/2;
// draw the three lines
// the left line is placed 100 pixels on the left of center.
ctx.beginPath();
ctx.moveTo(center-100, 50);
ctx.lineTo(center-100, ctx.canvas.height - 50);
ctx.stroke();
// the middle line is placed at the center
ctx.beginPath();
ctx.moveTo(center, 50);
ctx.lineTo(center, ctx.canvas.height - 50);
ctx.stroke();
// the right line is placed 100 pixels on the right of center.
ctx.beginPath();
ctx.moveTo(center+100, 50);
ctx.lineTo(center+100, ctx.canvas.height - 50);
ctx.stroke();
mini piano gamemini piano gamebasic background, creating// draw the horizontal line
ctx.beginPath();
ctx.moveTo(center-150, ctx.canvas.height - 80);
ctx.lineTo(center+150, ctx.canvas.height - 80);
// reset the line style to 1px width and grey before actually drawing the horizontal line.
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(50,50,50,.8)";
ctx.stroke();
}

  1. 当我们在浏览器中打开index.htm文件时,将会看到四条带有背景的线,如下截图所示。现在不用担心播放按钮被隐藏,稍后我们会再次显示它:

创建音乐游戏的基本背景

刚刚发生了什么?

我们创建了一个画布,在这个音乐游戏示例中,我们介绍了 HTML5 游戏中的基本场景管理。

在 HTML5 游戏中创建场景

在 HTML5 中创建场景类似于在上一章中创建图层。它是一个包含多个子元素的 DOM 元素。所有子元素都是绝对定位的。我们的示例中现在有两个场景。以下代码片段显示了整个游戏中可能的场景结构,包括游戏结束场景、信用场景和排行榜场景:

<div id="game">
<section id="menu-scene" class="scene"></section>
<section id="game-scene" class="scene"></section>
<section id="gameover-scene" class="scene"></section>
<section id="credit-scene" class="scene"></section>
<section id="leaderboard-scene" class="scene"></section>
</div>

以下截图显示了场景在网页中放置在同一位置。这与图层结构非常相似。不同之处在于我们将通过显示和隐藏每个场景来控制场景:

在 HTML5 游戏中创建场景

可视化音乐播放

如果您曾经玩过舞动革命、吉他英雄或触觉复仇游戏,那么您可能熟悉音乐点向下或向上移动,玩家在音乐点移动到正确位置时击中音乐点。以下截图展示了触觉复仇游戏:

可视化音乐播放

我们将在画布中以类似的音乐可视化方式播放audio标签中的歌曲。

进行操作在音乐游戏中创建播放可视化

执行以下步骤:

  1. 我们需要一首旋律部分和一个基础部分的歌曲。从下载的文件或media文件夹中的代码捆绑包中复制minuet_in_g.ogg, minuet_in_g.mp3, minuet_in_g_melody.oggminuet_in_g_melody.mp3文件。

  2. 然后,添加带有歌曲作为源文件的audio标签。打开index.htm文件并添加以下代码:

<audio id="melody">
<source src="img/minuet_in_g_melody.mp3" />
<source src="img/minuet_in_g_melody.ogg" />
</audio>
<audio id="base">
<source src="img/minuet_in_g.mp3" />
<source src="img/minuet_in_g.ogg" />
</audio>

  1. 音乐可视化主要是用 JavaScript 完成的。在文本编辑器中打开html5games.audio.js JavaScript 文件。

  2. 添加MusicNote对象类型来表示音乐数据,添加Dot对象类型来表示画布中音乐音符的可视点,如下所示:

function MusicNote(time,line){
this.time = time;
this.line = line;
}
function Dot(distance, line) {
this.distance = distance;
this.line = line;
this.missed = false;
}

  1. 然后,我们需要几个游戏变量来存储MusicNote实例、Dot实例和其他信息。级别数据是一个以分号分隔的时间和出现线的序列。级别数据表示音乐音符应该出现的时间和线:
// an array to store all music notes data.
audiogame.musicNotes = [];
audiogame.leveldata = "1.592,3;1.984,2;2.466,1;2.949,2;4.022,3;";
// the visual dots drawn on the canvas.
audiogame.dots = [];
// for storing the starting time
audiogame.startingTime = 0;
// reference of the dot image
audiogame.dotImage = new Image();

  1. 级别数据以字符串格式存储。我们有以下函数来提取MusicNote对象实例中的字符串并存储在数组中:
function setupLevelData()
{
var notes = audiogame.leveldata.split(";");
for(var i in notes)
{
var note = notes[i].split(",");
var time = parseFloat(note[0]);
var line = parseInt(note[1]);
var musicNote = new MusicNote(time,line);
audiogame.musicNotes.push(musicNote);
}
}

  1. 在 jQueryready函数的开头添加以下代码。它引用melodybase音频标签,并加载点图像以供以后使用:
audiogame.melody = document.getElementById("melody");
audiogame.base = document.getElementById("base");
// load the dot image
audiogame.dotImage.src = "images/dot.png";

  1. 然后,在 jQueryready函数的末尾添加以下代码:
setupLevelData();
setInterval(gameloop, 30);
startGame();

  1. 在 JavaScript 文件中添加以下两个函数。startGame函数设置开始时间,并延迟执行playMusic函数。后者播放旋律和基础音频:
function startGame()
{
// starting game
var date = new Date();
audiogame.startingTime = date.getTime();
setTimeout(playMusic, 3550);
}
function playMusic()
{
// play both the melody and base
audiogame.melody.play();
audiogame.base.play();
}

  1. gameloop函数添加到 JavaScript 中。gameloop函数在游戏顶部创建新的点,并将现有的音符向下移动:
// logic that run every 30ms.
music play back visualizationmusic play back visualizationcreating, stepsfunction gameloop()
{
var game = document.getElementById("game-canvas");
var ctx = game.getContext('2d');
// show new dots
// if the game is started
if (audiogame.startingTime != 0)
{
for(var i in audiogame.musicNotes)
{
// get the elapsed time from beginning of the melody
var date = new Date();
var elapsedTime = (date.getTime() - audiogame.startingTime)/1000;
var note = audiogame.musicNotes[i];
// check if the dot appear time is as same as the elapsed time
var timeDiff = note.time - elapsedTime;
if (timeDiff >= 0 && timeDiff <= .03)
{
// create the dot when the appear time is within one frame of the elapsed time
var dot = new Dot(ctx.canvas.height-150, note.line);
audiogame.dots.push(dot);
}
}
}
// move the dots
for(var i in audiogame.dots)
{
audiogame.dots[i].distance -= 2.5;
}
// only clear the dirty area, that is the middle area
ctx.clearRect(ctx.canvas.width/2-200, 0, 400, ctx.canvas.height);
// draw the music note dots
for(var i in audiogame.dots)
{
// prepare the radial gradients fill style
var circle_gradient = ctx.createRadialGradient (-3,-3,1,0,0,20);
circle_gradient.addColorStop(0, "#fff");
circle_gradient.addColorStop(1, "#cc0");
ctx.fillStyle = circle_gradient;
// prepare the dot position to draw
ctx.save();
var center = game.width/2;
var dot = audiogame.dots[i];
var x = center-100
if (dot.line == 2)
{
x = center;
}
else if (dot.line == 3)
{
x = center+100;
}
// draw the dot at position according to the line and distance
ctx.translate(x, ctx.canvas.height-80- audiogame.dots[i].distance);
ctx.drawImage(audiogame.dotImage, -audiogame.dotImage.width/2, -audiogame.dotImage.height/2);
ctx.restore();
music play back visualizationmusic play back visualizationcreating, steps}
}

  1. 保存所有文件,并在 Web 浏览器中打开index.htm文件。以下截图显示了音乐播放时顶部出现的音乐点并向下移动:

进行操作在音乐游戏中创建播放可视化

刚刚发生了什么?

我们刚刚构建了一个完全功能的音乐游戏,这是基本的播放功能。它播放旋律和基础部分的歌曲,并有一些音乐点向下移动。

选择音乐游戏的合适歌曲

在选择音乐游戏的歌曲时,我们必须小心版权问题。通常需要支付使用费或与歌曲版权所有者达成协议以使用有版权的歌曲。如果您正在制作一个商业音乐游戏,并且收入可以弥补版权使用费,那就没问题。但是,作为一个书本示例,我们将使用无版权的歌曲。这就是为什么我们使用古典曲目《G 大调小步舞曲》,它是免费的公共领域。

存储和提取歌曲级别数据

进行操作部分显示的级别数据只是整个级别数据的一部分。它是一个非常长的字符串,存储音乐音符信息,包括时间和线。它以以下格式存储:

music_current_time, line; music_current_time, line; …

每个音乐点数据包含显示时间和显示的线。这些数据由逗号分隔。每个音乐点数据由分号分隔。以下代码将级别字符串提取为MusicNote对象,通过分号和逗号进行分割:

audiogame.musicNotes = [];
audiogame.leveldata = "1.592,3;1.984,2;2.466,1;2.949,2;4.022,3;";
function setupLevelData()
{
var notes = audiogame.leveldata.split(";");
for(var i in notes)
{
var note = notes[i].split(",");
var time = parseFloat(note[0]);
var line = parseInt(note[1]);
var musicNote = new MusicNote(time,line);
audiogame.musicNotes.push(musicNote);
}
}

级别数据字符串由键盘记录,我们将在本章后面讨论录制。

提示

在这里,级别数据只包含几个音符。在代码包中,有完整歌曲的整个级别数据。

注意

JavaScript parseInt函数有一个可选的第二个参数。它定义要解析的数字的基数。默认情况下,它使用十进制,但当字符串以零开头时,parseInt将解析字符串为八进制。例如,parseInt("010")返回结果 8 而不是 10。如果我们想要十进制数,那么我们可以使用parseInt("010",10)来指定基数。

获取游戏经过的时间

尽管我们可以通过访问currentTime属性来获取音频元素的经过时间,但我们想要从游戏开始时获取时间。

我们可以通过存储开始游戏时的当前计算机时间,并减去当前时间值来获得经过的时间。

我们通过使用Date对象来获取当前计算机时间。以下代码片段显示了我们如何使用startingTime来获取经过的时间:

// starting game
var date = new Date();
audiogame.startingTime = date.getTime();
// some time later
var date = new Date();
var elapsedTime = (date.getTime() - audiogame.startingTime)/1000;

以下截图显示了前面的代码片段在控制台中运行:

获取游戏经过的时间

创建音乐点

gameloop函数中,我们检查所有MusicNote实例,并查看是否是创建该音乐音符的可视点的时间。以下代码显示了我们用来创建可视音乐点的逻辑。基本上,我们获取游戏的经过时间,并将其与每个音乐音符的当前时间进行比较。如果音符的当前时间和经过时间之间的时间差在 30 毫秒内,那么我们就创建可视点实例,并让gameloop函数绘制它:

if (audiogame.startingTime != 0)
{
for(var i in audiogame.musicNotes)
{
// get the elapsed time from beginning of the melody
var date = new Date();
var elapsedTime = (date.getTime() - audiogame.startingTime)/1000;
var note = audiogame.musicNotes[i];
// check if the dot appear time is as same as the elapsed time
var timeDiff = note.time - elapsedTime;
if (timeDiff >= 0 && timeDiff <= .03)
{
// create the dot when the appear time is within one frame of the elapsed time
var dot = new Dot(ctx.canvas.height-150, note.line);
audiogame.dots.push(dot);
}
}
music play back visualizationmusic play back visualizationmusic dots, creating}

移动音乐点

游戏开始和音乐开始之间存在时间差。游戏在音乐开始播放前几秒钟开始。这是因为我们需要在音乐开始之前显示音乐点并将其向下移动。

当点在灰线上时,音乐点应该与歌曲相匹配。音乐点从游戏顶部出现并向下移动到灰线。我们延迟音乐播放以等待点从上到下移动。在这个例子中大约是 3.55 秒,所以我们延迟音乐播放 3.55 秒。

当点被创建时,它被放置在给定的距离处。每次gameloop函数执行时,我们将所有点的距离减少 2.5。距离存储在每个代表它距离灰线有多远的dot对象中:

for(var i in audiogame.dots)
{
audiogame.dots[i].distance -= 2.5;
}

点的 y 位置由灰线减去距离计算如下:

// draw the dot
ctx.save();
var x = ctx.canvas.width/2-100
if (audiogame.dots[i].line == 2)
{
x = ctx.canvas.width/2;
}
else if (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);

以下截图显示了灰线和每个点之间的距离。当距离为零时,它恰好在灰线上:

移动音乐点

将播放按钮链接到音乐游戏场景

现在我们有一个游戏场景正在播放我们的歌曲。但是,它覆盖了我们用播放按钮制作的菜单场景。现在想象一下,我们打开游戏时,播放按钮被显示,然后我们点击按钮,游戏场景滑入并开始播放音乐。

行动时间 动画场景过渡

我们将默认隐藏游戏场景,并在点击播放按钮后显示它:

  1. 首先,我们必须修改样式表。打开audiogame.css文件。

  2. 将以下突出显示的 overflow 属性添加到#game。它有助于将游戏剪切成 768x440px 的蒙版:

#game {
position: relative;
width: 768px;
height: 440px;
overflow: hidden;
}

  1. 接下来,我们添加以下突出显示的代码来样式化游戏场景:
#game-scene {
background: #efefef url(../images/game_bg.jpg);
top: -440px;
}
#game-scene.show-scene {
top: 0;
-webkit-transition: top .3s linear;
-moz-transition: top .3s linear;
transition: top .3s linear;
}

  1. 然后,我们将转到 JavaScript 部分。打开html5games.audio.js JavaScript 文件。

  2. 在 jQuery 的 ready 函数中删除startGame函数的调用。我们将在点击播放按钮时调用它。

  3. 在播放按钮点击处理程序中,我们添加以下突出显示的代码:

$("a[href='#game']").click(function(){
audiogame.buttonActiveSound.currentTime = 0;
audiogame.buttonActiveSound.play();
$("#game-scene").addClass('show-scene');
startGame();
return false;
});

保存所有文件并在浏览器中打开index.htm。当我们点击播放按钮时,应该有一个滑入动画来显示音乐播放场景。以下截图序列显示了滑入动画:

行动时间 动画场景过渡

刚刚发生了什么?

我们刚刚在菜单场景和游戏场景之间创建了一个过渡。

在 CSS3 中创建滑入效果

点击播放按钮时,游戏场景从顶部滑入。这种场景过渡效果是通过 CSS3 过渡来实现的。游戏场景的位置最初是放置在负的顶部数值上。然后我们通过过渡将顶部位置从负值改变为零,这样它就从顶部动画到正确的位置。

使滑动效果生效的另一重要事项是将场景的父 DIV 的溢出设置为隐藏。如果没有隐藏的溢出,即使顶部位置为负值,游戏场景也是可见的。因此,将场景的父 DIV 设置为隐藏的溢出是很重要的。

以下截图展示了游戏场景的滑入过渡。#game DIV 是菜单场景和游戏场景的父级。当我们添加.show-scene类时,游戏场景从顶部移动,将顶部值设置为 0 并进行过渡:

在 CSS3 中创建滑入效果

尝试一下 创建不同的场景过渡效果

当显示游戏时,我们为场景过渡创建了一个滑入效果。通过使用 JavaScript 和 CSS3,我们可以创造许多不同的场景过渡效果。尝试制作自己的过渡效果,比如淡入、从右侧推入,甚至是带有 3D 旋转的翻转效果。

创建一个以键盘驱动的迷你钢琴音乐游戏

现在我们可以点击播放按钮。音乐游戏滑入并播放带有音符下落的歌曲。接下来,我们将为音乐音符添加交互。因此,我们将添加键盘事件来控制三条线击中音乐音符。

行动时间 创建一个迷你钢琴音乐游戏

执行以下步骤:

  1. 我们希望在按键时显示指示。打开index.htm文件,并添加以下突出显示的 HTML:
<section id="game-scene" class="scene">
<canvas id="game-background-canvas" width="768" height="440">
Sorry, your web browser does not support canvas content.
</canvas>
<canvas id="game-canvas" width="768" height="440">
Sorry, your web browser does not support canvas content.
</canvas>
<div id="hit-line-1" class="hit-line hide"></div>
<div id="hit-line-2" class="hit-line hide"></div>
<div id="hit-line-3" class="hit-line hide"></div>
</section>

  1. 然后,我们可能希望通知访问者他们可以通过按J,KL键来玩游戏。将页脚内容修改如下:
<footer>
<p>This is an example of making audio game in HTML5\. Press J, K, L to play.
</p>
</footer>

  1. 现在,我们将转移到样式表。样式表包含在名为audio_game_scene_transition的文件夹中。

  2. 接下来,我们将在 JavaScript 部分添加键盘事件。打开html5games.audio.js JavaScript 文件,并在 jQuery 准备好的函数内添加以下代码:

// keydown
$(document).keydown(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('hide');
$('#hit-line-'+line).addClass('show');
// our target is J(74), K(75), L(76)
var hitLine = e.which-73;
// check if hit a music note dot
for(var i in audiogame.dots)
{
if (hitLine == audiogame.dots[i].line && Math.abs(audiogame.dots[i].distance) < 20)
{
// remove the hit dot from the dots array
audiogame.dots.splice(i, 1);
}
}
});
$(document).keyup(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('show');
$('#hit-line-'+line).addClass('hide');
});

  1. 现在保存所有文件并在浏览器中打开游戏。尝试按下J,KL键。三条击中线指示应该在按下键时出现并消失。如果音乐点在击中正确键时经过灰线,则会消失。

行动时间 创建一个迷你钢琴音乐游戏

刚刚发生了什么?

我们刚刚为我们的音乐游戏添加了键盘交互。击打键时会有发光动画。当在正确时刻按下正确的键时,音乐点会消失。

按键时击中三条音乐线

我们使用J,KL键来击打游戏中的三条音乐线。J键控制左线,K键控制中线,L键控制右线。

还有一个指示,显示我们刚刚击中了音乐线。这是通过在灰线和音乐线的交叉点放置以下图像来实现的:

按键时击中三条音乐线

然后,我们可以使用以下 jQuery 代码来控制击中指示图形的显示和隐藏:

$(document).keydown(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('hide');
$('#hit-line-'+line).addClass('show');
});
$(document).keyup(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('show');
$('#hit-line-'+line).addClass('hide');
});

J,KL键控制音乐线 1 到 3。由于 J,K 和 L 的键码分别为 74、75 和 76,我们可以通过将键码减去 73 来知道它是哪条线。

确定按键时的音乐点击中

如果音符几乎在灰色水平线上,距离接近零。这有助于我们确定音符是否击中了灰线。通过检查按键按下事件和音符距离,我们可以确定是否成功击中了音符。以下代码片段显示了当距离在 20 像素内时,我们认为音符被击中:

$(document).keydown(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('hide');
$('#hit-line-'+line).addClass('show');
// our target is J(74), K(75), L(76)
var hitLine = e.which-73;
// check if hit a music note dot
for(var i in audiogame.dots)
{
if (hitLine == audiogame.dots[i].line && Math.abs(audiogame.dots[i].distance) < 20)
{
// remove the hit dot from the dots array
audiogame.dots.splice(i, 1);
}
}
});

坚定决心,我们在击中时移除音乐点。错过的点仍然会穿过灰线并向底部移动。这创造了一个基本的游戏玩法,玩家必须在歌曲播放时在正确的时刻正确击中所有音乐点。

使用给定索引从数组中删除一个元素

我们在音乐点被击中时(因此不再绘制)从数组中删除音乐点数据。要从数组中删除一个元素,我们使用splice函数。以下代码行从给定索引处的数组中删除一个元素:

array.splice(index, 1);

splice函数有点棘手。这是因为它允许我们在数组中添加或删除元素。然后,它会将删除的元素作为另一个数组返回。听起来很复杂。因此,我们将进行一些实验。

使用 splice 函数删除音乐点的时间

我们将在 Web 浏览器中打开 JavaScript 控制台,对splice函数进行一些测试:

  1. 打开 JavaScript 控制台。

  2. 逐行输入以下命令到控制台。也就是说,在每个命令行上按Enter。这些命令创建一个数组,并使用splice函数对其进行操作。

  3. 我们应该得到类似以下截图中显示的结果:

使用 splice 函数删除音乐点的时间

刚刚发生了什么?

我们刚刚创建了一个数组,并尝试使用splice函数添加和删除元素。请注意,splice 数组会返回另一个包含已删除元素的数组。

以下是我们如何使用splice函数:

array.splice(index, length, element1, element2, …, elementN);

以下表格显示了我们如何使用这些参数:

参数 定义 讨论
index 指定要添加或删除的元素在数组中的索引 索引从 0 开始。0 表示第一个元素,1 表示第二个元素,依此类推。我们还可以使用负索引,比如-1 表示最后一个元素,-2 表示倒数第二个元素,依此类推。
length 指定要删除的元素数量 放入 0 意味着我们不删除任何元素。
element1, element2elementN 要添加到数组中的新元素;这是可选的 这是可选的。在这里放入一系列元素意味着我们在给定的索引处添加元素。

注意

以下是 Mozilla 开发者网络链接,讨论了splice函数的不同用法:

developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice

尝试一下

在类似的商业音乐游戏中,当玩家击中或错过音乐点时会显示一些字。我们如何将这个功能添加到我们的游戏中?

为迷你钢琴游戏添加额外功能

我们已经为游戏创建了基本的交互。我们可以进一步改进游戏,通过添加旋律音量反馈来使表演更加逼真,并计算表演的成功率。

根据玩家调整音乐音量

现在想象我们正在表演音乐。我们击中音乐点演奏旋律。如果我们错过了其中任何一个,那么我们就无法演奏好,旋律就会消失。

使用 splice 函数删除错过的旋律音符的时间

我们将存储一些游戏统计数据,并用它来调整旋律音量。我们将继续进行 JavaScript 文件:

  1. 首先,在变量声明区域中添加以下变量:
audiogame.totalDotsCount = 0;
audiogame.totalSuccessCount = 0;
// storing the success count of last 5 results.
audiogame.successCount = 5;

  1. setupLevelData函数中,我们使用以下突出显示的代码获取了点的总数:
function setupLevelData()
{
var notes = audiogame.leveldata.split(";");
// store the total number of dots
audiogame.totalDotsCount = notes.length;
for(var i in notes)
{
var note = notes[i].split(",");
var time = parseFloat(note[0]);
var line = parseInt(note[1]);
var musicNote = new MusicNote(time,line);
audiogame.musicNotes.push(musicNote);
}
}

  1. 我们不仅想要移除一个点,还想在击中它时跟踪结果。在 jQuery 的 ready 函数中的键盘处理程序中添加以下代码:
// check if hit a music note dot
for(var i in audiogame.dots)
{
if (hitLine == audiogame.dots[i].line && Math.abs(audiogame.dots[i].distance) < 20)
if (hitLine == audiogame.dots[i].line && Math.abs(audiogame.dots[i].distance) < 20)
{
// remove the hit dot from the dots array
audiogame.dots.splice(i, 1);
// increase the success count
audiogame.successCount++;
// keep only 5 success count max.
audiogame.successCount = Math.min (5, audiogame.successCount);
// increase the total success count
audiogame.totalSuccessCount ++;
}
}

  1. gameloop函数中,我们计算所有未击中的点并存储结果。然后,我们可以使用这些统计数据来获得游戏的成功率。在gameloop函数中添加以下代码:
// check missed dots
for(var i in audiogame.dots)
{
if (!audiogame.dots[i].missed && audiogame.dots[i].distance < -10)
{
// mark the dot as missed if it is not mark before
audiogame.dots[i].missed = true;
// reduce the success count
audiogame.successCount--;
// reset the success count to 0 if it is lower than 0.
audiogame.successCount = Math.max (0, audiogame.successCount);
}
// remove missed dots after moved to the bottom
if (audiogame.dots[i].distance < -100)
{
audiogame.dots.splice(i, 1);
}
}
// calculate the percentage of the success in last 5 music dots
var successPercent = audiogame.successCount / 5;
// prevent the successPercent to exceed range(fail safe)
successPercent = Math.max(0, Math.min(1, successPercent));

  1. 最后,我们通过成功率来调整旋律音量。在gameloop函数中刚刚添加的代码之后添加以下代码:
audiogame.melody.volume = successPercent;

  1. 保存所有文件并在浏览器中测试我们的游戏。当玩家继续游戏时,旋律会继续播放。当玩家错过几个音乐点时,旋律消失,只剩下低音播放。

刚刚发生了什么?

我们刚刚使用了玩家表现作为旋律音量的反馈。这给人一种我们真的在演奏音乐的感觉。当我们表现不佳时,旋律音量很低,歌曲听起来也很差。

从游戏中移除点

我们想要移除点,要么是在它掉落到底部边界下方时,要么是在玩家击中它时。游戏循环在游戏画布上显示点列表中的所有点。我们可以通过从点数组中移除其数据来移除点图形。

我们使用以下的splice函数来移除数组中目标索引处的条目:

audiogame.dots.splice(index, 1);

存储最近五次结果中的成功次数

在我们的游戏中,我们需要存储最近五次结果中的成功次数以计算成功率。我们可以通过使用一个代表这个的计数器来实现。当成功击中一个点时,计数器增加 1,但当玩家未能击中一个点时,计数器减少 1。

如果我们将计数器限制在一个范围内,比如在我们的例子中是 0 到 5,那么计数器就代表了最近几次结果中的成功次数。

试试看

在上一章中,我们讨论了如何在 Untangle 游戏中显示游戏进度。我们能否在音乐游戏中应用类似的技术?我们有玩家在游戏过程中的成功百分比。在游戏顶部显示为百分比条形图如何?

记录音符作为级别数据

游戏依赖级别数据进行播放。如果没有级别数据,回放可视化将无法工作。如果回放可视化不起作用,我们也无法进行播放。那么我们如何记录级别数据呢?

现在想象一下,音乐正在播放,游戏中没有任何音乐点出现。我们仔细听音乐,当音乐播放时按下J,K,L键。音乐结束后,我们打印出所有按下的键和时间。这些数据将在音乐的回放可视化中使用。

添加功能记录音乐级别数据的时间

执行以下步骤:

  1. 首先,我们创建一个变量来在录制模式和正常播放模式之间切换。打开html5games.audio.js文件并添加以下代码:
audiogame.isRecordMode = true;

  1. 接下来,在keydown事件处理程序中添加以下突出显示的代码。这段代码将我们按下的所有键存储在一个数组中,并在按下分号键时将它们打印到控制台上:
$(document).keydown(function(e){
var line = e.which-73;
$('#hit-line-'+line).removeClass('hide');
$('#hit-line-'+line).addClass('show');
if (audiogame.isRecordMode)
{
// print the stored music notes data when press ";" (186)
if (e.which == 186)
{
var musicNotesString = "";
for(var i in audiogame.musicNotes)
{
musicNotesString += audiogame.musicNotes[i].time+", "+audiogame.musicNotes[i].line+";";
musicNotesString += audiogame.musicNotes[i].time+", "+audiogame.musicNotes[i].line+";";
}
console.log(musicNotesString);
}
var currentTime = parseInt (audiogame.melody.currentTime * 1000)/1000;
var note = new MusicNote(currentTime, e.which-73);
audiogame.musicNotes.push(note);
}
else
{
// our target is J(74), K(75), L(76)
var hitLine = e.which-73;
// check if hit a music note dot
…
}
});

  1. 最后,我们要确保setupLevelDatagameloop函数在录制模式下不被执行。这些函数仅用于播放模式:
if (!audiogame.isRecordMode) {
setupLevelData();
setInterval(gameloop, 30);
}

  1. 现在在浏览器中打开index.htm。点击play按钮后,游戏开始,音乐播放,但没有音乐音符。尝试按照音乐节奏按下J,KL键。音乐结束后,按分号键在控制台中打印出级别数据。以下截图显示了控制台显示级别数据字符串:

添加功能记录音乐级别数据的时间

刚刚发生了什么?

我们刚刚为游戏添加了录音功能。现在我们可以录制我们的音符。我们可以通过将audiogame.isRecordMode变量设置为 true 和 false 来切换录制模式和播放模式。

在每次按键时,我们获取旋律的经过时间,并创建一个带有时间和行号的MusicNote实例。以下代码显示了我们如何记录按下的键。在保存之前,currentTime被截断为两位小数:

var currentTime = audiogame.melody.currentTime.toFixed(3);
var note = new MusicNote(currentTime, e.which-73);
audiogame.musicNotes.push(note);

我们还捕获了分号键,以便将所有记录的MusicNote数据打印成字符串。字符串遵循time,line;time,line的格式,因此我们可以直接复制打印的字符串,并将其粘贴为级别数据以进行播放。

注意

toFixed函数用给定的小数位数格式化数字。在我们的例子中,我们用它来获得带有3位小数的当前时间。

处理音频事件播放完成

我们现在可以玩游戏了,但是游戏结束时没有指示。现在想象一下,当游戏完成时,我们想知道我们玩得有多好。我们将捕获旋律结束信号,并显示游戏的成功率。

执行指示在控制台中指示游戏结束事件

执行以下步骤:

  1. 打开html5games.audio.js JavaScript 文件。

  2. 在 jQuery ready 函数中添加以下代码:

$(audiogame.melody).bind('ended', onMelodyEnded);

  1. 在文件末尾添加以下事件处理程序函数:
// show game over scene on melody ended.
function onMelodyEnded()
{
console.log('song ended');
console.log('success percent: ',audiogame.totalSuccessCount / audiogame.totalDotsCount * 100 + '%');
}

  1. 现在是时候保存所有文件并在 Web 浏览器中玩游戏了。游戏结束时,我们应该看到成功率打印在控制台中,如下截图所示:

执行指示在控制台中指示游戏结束事件

刚刚发生了什么?

我们刚刚监听了音频元素的ended事件,并用处理程序函数处理了它。

处理音频事件

音频元素中还有许多其他事件。以下表格列出了一些常用的音频事件:

事件 讨论
ended 当音频元素完成播放时发送
play 当音频元素播放或恢复时发送
pause 当音频元素暂停时发送
定期发送progress事件,当音频元素正在下载时
timeupdate currentTime属性改变时发送

这里我们列出了一些常用事件;您可以在 Mozilla 开发者中心的以下网址上查看完整的音频事件列表:

developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Media_events

试试吧

在我们的音乐游戏中,当游戏结束时,我们在控制台中打印出成功率。当游戏结束时,添加一个游戏结束场景,并在游戏结束时显示它会怎样?在显示游戏结束场景时,使用动画过渡也是不错的。

总结

在本章中,我们学到了如何使用 HTML5 音频元素,并制作了一个音乐游戏。

具体来说,我们涵盖了以下主题:

  • 将音频标签添加到 HTML 中。我们可以设置不同的属性来定义音频标签的行为和加载不同格式的源。

  • 使用 JavaScript API 控制音频播放和音量。

  • 使用 jQuery 在鼠标悬停和激活时添加声音效果。

  • 使用键盘输入在画布中创建音乐游戏。

  • 处理音频事件;音频元素在其状态改变时发送多个事件。

我们还讨论了管理场景和动画过渡。

我们已经学习了如何在 HTML5 游戏中添加音乐和音效。现在我们准备在下一章中通过添加排行榜来存储游戏得分,构建一个更完整的游戏。

第七章:使用本地存储存储游戏数据

本地存储是 HTML5 的一个新规范。它允许网站在浏览器中本地存储信息,并在以后访问存储的数据。这是游戏开发中的一个有用功能,因为我们可以将其用作内存插槽,在 Web 浏览器中本地保存任何游戏数据。

我们将在我们在第三章构建 CSS3 记忆匹配游戏中构建的游戏中添加游戏数据存储。除了存储和加载游戏数据,我们还将使用纯 CSS3 样式向玩家通知打破记录的好消息,通过一个漂亮的 3D 丝带。

在本章中,我们将涵盖以下主题:

  • 使用 HTML5 本地存储存储数据

  • 在本地存储中保存对象

  • 使用漂亮的丝带效果通知玩家打破新纪录

  • 保存整个游戏进度

以下截图显示了我们将通过本章创建的最终结果。那么,让我们开始吧:

使用本地存储存储游戏数据

使用 HTML5 本地存储存储数据

还记得我们在第三章中制作的 CSS3 记忆匹配游戏吗?现在想象一下我们已经发布了我们的游戏,玩家们正在努力表现得很好。

我们想告诉玩家他们是否比上次玩得更好或更差。我们将保存最新的分数,并通过比较分数来告知玩家这次是否比上次更好。

当他们表现更好时,他们可能会感到自豪。这可能使他们上瘾,他们可能会继续努力获得更高的分数。

创建游戏结束对话框

在本地存储中实际保存任何内容之前,我们需要一个游戏结束画面。在之前的章节中,我们制作了一些游戏。我们制作了一个乒乓球游戏,记忆匹配游戏,解开谜题游戏和音乐游戏。在这些游戏中,我们没有创建游戏结束画面。现在想象一下我们正在玩我们在第三章中构建的 CSS3 记忆匹配游戏。我们成功匹配并移除了所有卡片。一旦我们完成,游戏结束画面弹出并显示我们用来完成游戏的时间。

执行操作-使用经过的播放时间创建游戏结束对话框

我们将继续使用第三章中制作的记忆匹配游戏的代码。执行以下步骤:

  1. 将 CSS3 匹配游戏文件夹作为我们的工作目录。

  2. 从以下网址下载背景图片(我们将用它作为弹出窗口的背景):

gamedesign.cc/html5games/popup_bg.jpg

  1. 将图像放在images文件夹中。

  2. index.html打开到任何文本编辑器中。

  3. 我们需要一个字体用于游戏结束弹出窗口。将以下字体嵌入 CSS 添加到head部分中:

<link href="http://fonts.googleapis.com/css?family=Orbitron:400,700" rel="stylesheet" type="text/css" >

  1. game部分之前,我们添加一个名为timerdiv,以显示经过的游戏时间。此外,我们添加一个包含弹出窗口对话框的 HTML 标记的新popup部分:
<div id="timer">
Elapsed time: <span id="elapsed-time">00:00</span>
</div>
<section id="game">
<div id="cards">
<div class="card">
<div class="face front"></div>
<div class="face back"></div>
</div> <!-- .card -->
</div> <!-- #cards -->
</section> <!-- #game -->
<section id="popup" class="hide">
<div id="popup-bg">
</div>
<div id="popup-box">
<div id="popup-box-content">
<h1>You Won!</h1>
<p>Your Score:</p>
<p><span class='score'>13</span></p>
</div>
</div>
</section>

  1. 现在我们将转移到样式表。因为它只是用于样式,与我们的逻辑无关,所以我们可以简单地从代码示例包中的matching_game_with_game_over中复制matchgame.css文件。

  2. 现在是编辑游戏逻辑部分的时候了。在编辑器中打开html5games.matchgame.js文件。

  3. 在 jQuery ready 函数中,我们需要一个变量来存储游戏的经过时间。然后,我们创建一个计时器,每秒计算游戏时间,如下所示:

$(function(){
...
// reset the elapsed time to 0.
matchingGame.elapsedTime = 0;
// start the timer
matchingGame.timer = setInterval(countTimer, 1000);
}

  1. 接下来,添加一个countTimer函数,每秒执行一次。它以分钟和秒的格式显示经过的秒数:
function countTimer()
{
matchingGame.elapsedTime++;
// calculate the minutes and seconds from elapsed time
var minute = Math.floor(matchingGame.elapsedTime / 60);
var second = matchingGame.elapsedTime % 60;
// add padding 0 if minute and second is less then 10
if (minute < 10) minute = "0" + minute;
if (second < 10) second = "0" + second;
// display the elapsed time
$("#elapsed-time").html(minute+":"+second);
}

  1. 在我们之前编写的removeTookCards函数中,添加以下突出显示的代码,以在移除所有卡片后执行游戏结束逻辑:
function removeTookCards()
{
$(".card-removed").remove();
// check if all cards are removed and show game over
if ($(".card").length == 0)
{
gameover();
}
}

  1. 最后,我们创建以下gameover函数。它停止计时器,显示游戏结束弹出窗口中的经过时间,并最终显示弹出窗口:
function gameover()
{
// stop the timer
clearInterval(matchingGame.timer);
// set the score in the game over popup
$(".score").html($("#elapsed-time").html());
// show the game over popup
$("#popup").removeClass("hide");
}

  1. 现在,保存所有文件并在浏览器中打开游戏。尝试完成记忆匹配游戏,游戏结束画面将弹出,如下截图所示:

执行创建带有经过时间的游戏结束对话框

刚刚发生了什么?

我们使用 CSS3 过渡动画来显示游戏结束弹出窗口。我们通过玩家完成游戏所用的时间来评估得分。

在浏览器中保存得分

现在,我们将展示玩家上次的游戏表现。游戏结束画面包括上次的经过时间和当前游戏得分。玩家可以看到这次和上次的表现有多大差异。

执行保存游戏得分的操作

  1. 首先,我们需要在popup部分添加一些标记,以显示上次的得分。在index.htmlpopup部分中添加以下 HTML:
<p>
<small>Last Score: <span class='last-score'>20</span>
</small>
</p>

  1. 然后,我们打开html5games.matchgame.js文件,修改gameover函数中的一些游戏逻辑。

  2. gameover函数中添加以下突出显示的代码。它从本地存储加载保存的得分,并将其显示为上次的得分。然后,将当前得分保存在本地存储中:

function gameover()
{
// stop the timer
clearInterval(matchingGame.timer);
// display the elapsed time in the game over popup
$(".score").html($("#elapsed-time"));
// load the saved last score from local storage
var lastElapsedTime = localStorage.getItem ("last-elapsed-time");
// convert the elapsed seconds into minute:second format
// calculate the minutes and seconds from elapsed time
var minute = Math.floor(lastElapsedTime / 60);
var second = lastElapsedTime % 60;
// add padding 0 if minute and second is less then 10
if (minute < 10) minute = "0" + minute;
if (second < 10) second = "0" + second;
// display the last elapsed time in game over popup
$(".last-score").html(minute+":"+second);
// save the score into local storage
localStorage.setItem ("last-elapsed-time", matchingGame.elapsedTime);
// show the game over popup
$("#popup").removeClass("hide");
}

  1. 现在是时候保存所有文件并在浏览器中测试游戏了。当您第一次完成游戏时,上次的得分应该是00:00。然后,尝试第二次完成游戏。游戏结束弹出窗口将显示您上次玩游戏的经过时间。以下截图显示了游戏结束画面与当前和上次得分:

执行保存游戏得分的操作

刚刚发生了什么?

我们刚刚建立了一个基本的得分系统,用于比较玩家的得分和上次的得分。

使用本地存储存储和加载数据

我们可以使用localStorage对象的setItem函数来存储数据。以下表格显示了该函数的用法:

localStorage.setItem(key, value);

参数 定义 描述
key 键是我们用来标识条目的记录名称。 键是一个字符串,每个记录都有一个唯一的键。向现有键写入新值会覆盖旧值。
value 值是要存储的任何数据。 它可以是任何数据,但最终存储的是一个字符串。我们将很快讨论这一点。

在我们的例子中,我们使用以下代码将游戏经过的时间保存为得分,使用键last-elapsed-item

localStorage.setItem("last-elapsed-time", matchingGame.elapsedTime);

setItem相辅相成,我们可以通过以下方式使用getItem函数获取存储的数据:

localStorage.getItem(key);

该函数返回给定键的存储值。当尝试获取一个不存在的键时,它会返回null。这可以用来检查我们是否为特定键存储了任何数据。

本地存储保存了字符串值

本地存储以键值对的形式存储数据。键和值都是字符串。如果我们保存数字、布尔值或任何类型的数据而不是字符串,那么在保存时它会将值转换为字符串。

通常,当我们从本地存储加载保存的值时会出现问题。加载的值是一个字符串,而不管我们保存的类型是什么。在使用之前,我们需要明确将值解析为正确的类型。

例如,如果我们将一个浮点数保存到本地存储中,那么在加载时我们需要使用parseFloat函数。以下代码片段显示了如何使用parseFloat来检索存储的浮点数:

var score = 13.234;
localStorage.setItem("game-score",score);
// result: stored "13.234".
var gameScore = localStorage.getItem("game-score");
// result: get "13.234" into gameScore;
gameScore = parseFloat(gameScore);
// result: 13.234 floating value

在前面的代码片段中,如果我们忘记将gameScore从字符串转换为浮点数,操作可能是不正确的。例如,如果我们在没有使用parseFloat函数的情况下将gameScore增加 1,结果将是13.2341而不是14.234。因此,请确保将值从本地存储转换为其正确的类型。

提示

本地存储的大小限制

对于每个域通过localStorage存储的数据都有大小限制。这个大小限制在不同的浏览器中可能略有不同。通常,大小限制为 5MB。如果超过了限制,那么当向localStorage设置键值时,浏览器会抛出QUOTA_EXCEEDED_ERR异常。

将本地存储对象视为关联数组

除了使用setItemgetItem函数外,我们还可以将localStorage对象视为关联数组,并通过使用方括号访问存储的条目。

例如,我们可以用后一种版本替换以下代码:

使用setItemgetItem:

localStorage.setItem("last-elapsed-time", elapsedTime);
var lastElapsedTime = localStorage.getItem("last-elapsed-time");

访问localStorage的方式如下:

localStorage["last-elapsed-time"] = elapsedTime;
var lastElapsedTime = localStorage["last-elapsed-time"];

在本地存储中保存对象

现在,想象一下我们不仅保存分数,还保存排名创建时的日期和时间。我们可以保存分数和游戏时间的两个单独的键,或者将两个值打包到一个对象中并将其存储在本地存储中。

我们将所有游戏数据打包到一个对象中并进行存储。

行动时间保存得分的时间

执行以下步骤:

  1. 首先,从我们的 CSS3 记忆匹配游戏中打开index.html文件。

  2. 用以下 HTML 替换最后得分的 HTML(它显示游戏结束弹出窗口中的得分和日期时间):

<p>
<small>Last Score: <span class='last-score'>20</span><br>
Saved on: <span class='saved-time'>13/4/2011 3:14pm</span>
</small>
</p>

  1. HTML 标记现在已经准备好。我们将继续进行游戏逻辑。在文本编辑器中打开html5games.matchgame.js文件。

  2. 我们将修改gameover函数。将以下突出显示的代码添加到gameover函数中。当游戏结束时,它获取当前日期时间并将格式化的日期时间与经过的时间一起打包到本地存储中:

function gameover()
{
// stop the timer
clearInterval(matchingGame.timer);
// display the elapsed time in the game over popup
$(".score").html($("#elapsed-time"));
// load the saved last score and save time from local storage
var lastScore = localStorage.getItem("last-score");
// check if there is no any saved record
lastScoreObj = JSON.parse(lastScore);
if (lastScoreObj == null)
{
// create an empty record if there is no any saved record
lastScoreObj = {"savedTime": "no record", "score": 0};
}
var lastElapsedTime = lastScoreObj.score;
// convert the elapsed seconds into minute:second format
// calculate the minutes and seconds from elapsed time
var minute = Math.floor(lastElapsedTime / 60);
var second = lastElapsedTime % 60;
local storagelocal storageobject, saving// add padding 0 if minute and second is less then 10
if (minute < 10) minute = "0" + minute;
if (second < 10) second = "0" + second;
// display the last elapsed time in game over popup
$(".last-score").html(minute+":"+second);
// display the saved time of last score
var savedTime = lastScoreObj.savedTime;
$(".saved-time").html(savedTime);
// get the current datetime
var currentTime = new Date();
var month = currentTime.getMonth() + 1;
var day = currentTime.getDate();
var year = currentTime.getFullYear();
var hours = currentTime.getHours();
var minutes = currentTime.getMinutes();
// add padding 0 to minutes
if (minutes < 10) minutes = "0" + minutes;
var seconds = currentTime.getSeconds();
// add padding 0 to seconds
if (seconds < 10) seconds = "0" + seconds;
var now = day+"/"+month+"/"+year+" "+hours+":"+minutes+":"+seconds;
//construct the object of datetime and game score
var obj = { "savedTime": now, "score": matchingGame.elapsedTime};
// save the score into local storage
localStorage.setItem("last-score", JSON.stringify(obj));
// show the game over popup
$("#popup").removeClass("hide");
}

  1. 我们将保存文件并在 Web 浏览器中打开游戏。

  2. 当我们第一次完成游戏时,我们将得到一个类似以下截图的屏幕,它将显示我们的游戏得分并指出没有先前的记录:行动时间保存得分的时间

  3. 现在尝试重新加载页面并再次玩游戏。当我们第二次完成游戏时,游戏结束对话框将显示我们保存的记录。以下截图显示了它应该是什么样子的:

行动时间保存得分的时间

刚刚发生了什么?

我们刚刚使用了 JavaScript 中的Date对象来获取游戏结束时的当前日期和时间。此外,我们将游戏结束的日期和时间以及游戏经过的时间打包到一个对象中并保存到本地存储中。保存的对象被编码为 JSON 字符串。它还将从存储中加载上次保存的日期和时间以及游戏经过的时间,并将其从字符串解析回 JavaScript 对象。

在 JavaScript 中获取当前日期和时间

JavaScript 中的Date对象用于处理日期和时间。当我们从Date对象创建一个实例时,默认情况下它会存储当前的日期和时间。因此,我们可以通过以下代码片段轻松地获取当前的日期和时间信息:

var currentTime = new Date();
var month = currentTime.getMonth() + 1;
var day = currentTime.getDate();
var year = currentTime.getFullYear();
var hours = currentTime.getHours();
var minutes = currentTime.getMinutes();
var seconds = currentTime.getSeconds();

当我们以人类友好的格式显示日期和时间时,当分钟和秒钟小于 10 时,我们还需要添加零填充。我们可以这样做:

if (minutes < 10) minutes = "0" + minutes;
if (seconds < 10) seconds = "0" + seconds;
var now = day+"/"+month+"/"+year+" "+hours+":"+minutes+":"+seconds;

以下表格列出了Date对象中一些有用的函数以获取日期和时间:

功能 描述
getFullYear 返回四位数的年份
getMonth 返回整数月份,从 0 开始(1 月为 0,12 月为 11)
getDate 返回月份的日期,从 1 开始
getDay 返回星期几,从 0 开始(星期日为 0,星期六为 6)
getHours 返回小时,从 0 到 23
getMinutes 返回分钟
getSeconds 返回秒数
getMilliseconds 返回 3 位数的毫秒
getTime 返回自 1970 年 1 月 1 日 00:00 以来的毫秒数

注意

Mozilla 开发者网络提供了使用Date对象的详细参考,网址如下:

developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date

使用本机 JSON 将对象编码为字符串

我们在第四章,使用 Canvas 和 Drawing API 构建 Untangle 游戏中使用 JSON 表示游戏级别数据。

JSON 是一种友好的对象表示格式,便于机器解析和生成。在这个例子中,我们将最终经过的时间和日期时间打包到一个对象中。然后,我们将对象编码为 JSON。现代 Web 浏览器都具有本机的 JSON 支持。我们可以通过使用stringify函数轻松地将任何 JavaScript 对象编码为 JSON。

JSON.stringify(anyObject);

通常,我们只使用stringify函数的第一个参数。这是我们要编码为字符串的对象。以下代码片段演示了编码为 JavaScript 对象的结果:

var jsObj = {};
jsObj.testArray = [1,2,3,4,5];
jsObj.name = 'CSS3 Matching Game';
jsObj.date = '8 May, 2011';
JSON.stringify(jsObj);
// result: {"testArray":[1,2,3,4,5],"name":"CSS3 Matching Game","date":"8 May, 2011"}

注意

stringify方法可以很好地将具有数据结构的对象解析为字符串。但是,它无法将任何对象转换为字符串。例如,如果我们尝试将 DOM 元素传递给它,它将返回错误。如果我们传递一个日期对象,它将返回表示日期的字符串。或者,它将删除解析对象的所有方法定义。

从 JSON 字符串中加载存储的对象

JSON的完整形式是JavaScript 对象表示法。从名称上我们知道它使用 JavaScript 的语法来表示对象。因此,将 JSON 格式的字符串解析回 JavaScript 对象非常容易。

以下代码片段显示了我们如何在 JSON 对象中使用解析函数:

JSON.parse(jsonFormattedString);

我们可以在 Web Inspector 中打开控制台来测试 JSON JavaScript 函数。以下屏幕截图显示了我们刚刚讨论的代码片段在编码对象和解析它们时的运行结果:

从 JSON 字符串中加载存储的对象

在控制台窗口中检查本地存储

在我们将某些内容保存在本地存储后,我们可能想知道在编写加载部分之前究竟保存了什么。我们可以使用 Web Inspector 中的存储面板来检查我们保存了什么。它列出了同一域下保存的所有键值对。以下屏幕截图显示了我们保存了last-score,值为{"savedTime":"23/2/2011 19:27:02","score":23}

该值是我们用于将对象编码为 JSON 的JSON.stringify函数的结果。您也可以尝试直接将对象保存到本地存储中:

在控制台窗口中检查本地存储

注意

除了localStorage,还有其他未讨论的存储方法。这些方法包括Web SQL Databasewww.w3.org/TR/webdatabase/),它使用 SQLite 来存储数据,以及IndexedDBdeveloper.mozilla.org/en/IndexedDB)。

通过一个漂亮的缎带效果通知玩家打破了新纪录

假设我们想通过通知玩家他们打破了与上次得分相比的新纪录来鼓励他们。我们想在上面显示一个带有New Record文本的缎带。由于新的 CSS3 属性,我们可以完全在 CSS 中创建缎带效果。

创建 CSS3 中的缎带的操作时间

当玩家打破上次得分时,我们将创建一个新的记录缎带并显示它。因此,请执行以下步骤:

  1. 首先,打开index.html,我们将在那里添加缎带 HTML 标记。

  2. popup-box后面和popup-box-content前面添加以下突出显示的 HTML:

<div id="popup-box">
<div class="ribbon hide">
<div class="ribbon-body">
<span>New Record</span>
</div>
<div class="triangle"></div>
</div>
<div id="popup-box-content">
…

  1. 接下来,我们需要关注样式表。整个缎带效果是通过 CSS 完成的。在文本编辑器中打开matchgame.css文件。

  2. popup-box样式中,我们需要为其添加相对位置。我们可以这样做:

#popup-box {
position: relative;
...
}

  1. 然后,我们需要添加以下样式,以在 CSS 文件中创建缎带效果:
.ribbon.hide {
display: none;
}
.ribbon {
float: left;
position: absolute;
left: -7px;
top: 165px;
z-index: 0;
font-size: .5em;
text-transform: uppercase;
text-align: right;
}
.ribbon-body {
height: 14px;
background: #ca3d33;
padding: 6px;
z-index: 100;
-webkit-box-shadow: 2px 2px 0 rgba(150,120,70,.4);
border-radius: 0 5px 5px 0;
color: #fff;
text-shadow: 0px 1px 1px rgba(0,0,0,.3);
}
.triangle{
position: relative;
height: 0px;
width: 0;
left: -5px;
top: -32px;
border-style: solid;
border-width: 6px;
border-color: transparent #882011 transparent transparent;
z-index: -1;
}

  1. 最后,我们需要稍微修改游戏结束逻辑。打开html5games.matchgame.js文件,找到gameover函数。

  2. 将以下代码添加到gameover函数中,用于比较当前得分和上次得分以确定新纪录:

if (lastElapsedTime == 0 || matchingGame.elapsedTime < lastElapsedTime)
{
$(".ribbon").removeClass("hide");
}

  1. 我们将在 Web 浏览器中测试游戏。尝试慢慢完成一局游戏,然后再快速完成一局游戏。当你打破最高分时,游戏结束弹出窗口会显示一个漂亮的NEW RECORD丝带,如下面的屏幕截图所示:

操作时间 在 CSS3 中创建丝带

刚刚发生了什么?

我们刚刚以纯 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 #882011 transparent transparent;
z-index: -1;
}

注意

以下 PVM Garage 网站提供了关于纯 CSS3 丝带使用的详细解释:

www.pvmgarage.com/2010/01/how-to-create-depth-and-nice-3d-ribbons-only-using-css3/

试试看英雄 只保存和比较最快时间

每次游戏结束时,它会将最后得分与当前得分进行比较。然后,它保存当前得分。

如何修改代码以保存最高分并在打破最高分时显示新记录丝带?

保存整个游戏进度

我们通过添加游戏结束画面和存储游戏记录来增强了我们的 CSS3 记忆匹配游戏。现在想象一下,玩家正在进行游戏,然后意外关闭了 Web 浏览器。一旦玩家再次打开游戏,游戏将从头开始,玩家正在玩的游戏将丢失。通过本地存储,我们可以将整个游戏数据编码为 JSON 并存储起来。这样,玩家可以稍后恢复他们的游戏。

保存游戏进度

我们将把游戏数据打包到一个对象中,并在每秒保存到本地存储中。

操作时间 在本地存储中保存所有必要的游戏数据

我们将继续使用我们的 CSS3 记忆匹配游戏:

  1. 打开html5games.matchgame.js JavaScript 文件。

  2. 在声明matchingGame变量后,在 JavaScript 文件的顶部添加以下代码。此代码创建一个名为savingObject的对象,用于保存牌组和移除的卡片数组以及当前经过的时间:

matchingGame.savingObject = {};
matchingGame.savingObject.deck = [];
// an array to store which card is removed by storing their index.
matchingGame.savingObject.removedCards = [];
// store the counting elapsed time.
matchingGame.savingObject.currentElapsedTime = 0;

  1. 在 jQuery 函数中添加以下突出显示的代码。它将牌组的顺序克隆到savingObject中。此外,它为 DOM 数据属性中的每张卡分配一个索引:
$(function(){
// shuffling the deck
matchingGame.deck.sort(shuffle);
// copying the deck into saving object.
matchingGame.savingObject.deck = matchingGame.deck.slice();
// clone 12 copies of the card DOM
for(var i=0;i<11;i++){
$(".card:first-child").clone().appendTo("#cards");
}
...
// embed the pattern data into the DOM element.
$(this).attr("data-pattern",pattern);
// save the index into the DOM element, so we know which is the next card.
$(this).attr("data-card-index",index);
...

  1. 我们有一个countTimer函数,每秒执行一次。我们在countTimer函数中添加了以下突出显示的代码。它将当前经过的时间保存在savingObject中,并将对象保存在本地存储中:
function countTimer()
{
matchingGame.elapsedTime++;
// save the current elapsed time into savingObject.
matchingGame.savingObject.currentElapsedTime = matchingGame.elapsedTime;
...
// save the game progress
saveSavingObject();
}

  1. 当玩家找到一对匹配的卡片时,游戏会移除卡片。我们将原始的$(".card-removed").remove()代码替换为removeTookCards函数中的以下突出显示的代码。它记住了savingObject中移除的卡片:
function removeTookCards()
{
// add each removed card into the array which store which cards are removed
$(".card-removed").each(function(){
matchingGame.savingObject.removedCards.push ($(this).data("card-index"));
$(this).remove();
});
// check if all cards are removed and show game over
if ($(".card").length == 0)
{
gameover();
}
}

  1. 当游戏结束时,我们必须删除本地存储中保存的游戏数据。在gameover函数中添加以下代码:
function gameover()
{
//at last, we clear the saved savingObject
localStorage.removeItem("savingObject");
}

  1. 最后,我们有一个函数将savingObject保存在本地存储中:
function saveSavingObject()
{
// save the encoded saving object into local storage
localStorage["savingObject"] = JSON.stringify(matchingGame.savingObject);
}

  1. 我们已经修改了很多代码,现在是时候在 Web 浏览器中测试游戏了。游戏运行后,尝试清除几张匹配的卡片。然后,在 Web 检查器中打开存储面板。本地存储应该包含类似于下面屏幕截图中所示的条目。它是一个具有键savingObject和值为 JSON 格式的长字符串的记录。JSON 字符串包含洗牌后的牌组、移除的卡片和当前经过的时间:

操作时间 在本地存储中保存所有必要的游戏数据

刚刚发生了什么?

我们刚刚将所有必要的游戏数据输入到一个名为savingObject的对象中。这个savingObject包含了我们以后重建游戏所需的所有信息。它包括卡片的顺序、已删除的卡片和当前经过的时间。

最后,我们在每秒钟将savingObject保存在localStorage中。该对象使用我们在本章前面使用的stringify函数进行 JSON 编码。然后,我们通过解析来自本地存储的 JSON 字符串来重新创建游戏。

从本地存储中删除记录

当游戏结束时,我们需要删除保存的记录。否则,新游戏将无法开始。本地存储提供了一个remoteItem函数来删除特定记录。

以下是我们如何使用该函数来删除具有给定键的记录:

localStorage.removeItem(key);

提示

如果要删除所有存储的记录,可以使用localStorage.clear()函数。

在 JavaScript 中克隆数组

我们在savingObject中克隆了洗过的牌组,这样我们就可以在恢复游戏时使用牌组的顺序来重新创建卡片。但是,我们不能通过将数组分配给另一个变量来复制数组。以下代码无法将数组 A 复制到数组 B:

var a = [1,2,3,4,5];
var b = a;
a.pop();
// result:
// a: [1,2,3,4]
// b: [1,2,3,4]

slice函数提供了一种简单的方法来克隆只包含基本类型元素的数组。只要数组不包含另一个数组或对象作为元素,我们就可以使用slice函数来克隆数组。以下代码成功地将数组 A 克隆到 B:

var a = [1,2,3,4,5];
var b = a.slice();
a.pop();
// result:
// a: [1,2,3,4]
// b: [1,2,3,4,5]

slice函数通常用于通过从现有数组中选择一系列元素来创建一个新数组。当使用slice函数而没有任何参数时,它会克隆整个数组。Mozilla 开发者网络在以下 URL 提供了有关slice函数的详细用法:

developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/slice

恢复游戏进度

我们已保存了游戏进度,但尚未编写恢复游戏的逻辑。所以,让我们继续进行恢复部分。

行动时间 从本地存储中恢复游戏

执行以下步骤:

  1. 打开html5games.matchgame.js JavaScript 文件。

  2. 在 jQuery 的 ready 函数中,我们使用了上一局游戏中牌组的保存顺序,而不是洗牌一个新的牌组。将以下突出显示的代码添加到ready函数中:

$(function(){
// shuffling the deck
matchingGame.deck.sort(shuffle);
// re-create the saved deck
var savedObject = savedSavingObject();
if (savedObject != undefined)
{
matchingGame.deck = savedObject.deck;
}
...

  1. 在 ready 函数中初始化卡片后,我们删除了上一局游戏中删除的卡片。我们还从保存的值中恢复了经过的时间。在 jQuery 的 ready 函数中添加以下突出显示的代码:
// removed cards that were removed in savedObject.
if (savedObject != undefined)
{
matchingGame.savingObject.removedCards = savedObject.removedCards;
// find those cards and remove them.
for(var i in matchingGame.savingObject.removedCards)
{
$(".card[data-card-index="+matchingGame.savingObject. removedCards[i]+"]").remove();
}
}
// reset the elapsed time to 0.
matchingGame.elapsedTime = 0;
// restore the saved elapsed time
if (savedObject != undefined)
{
matchingGame.elapsedTime = savedObject.currentElapsedTime;
matchingGame.savingObject.currentElapsedTime = savedObject. currentElapsedTime;
}

  1. 最后,我们创建了以下函数来从本地存储中检索savingObject
// Returns the saved savingObject from the local storage.
function savedSavingObject()
{
// returns the saved saving object from local storage
var savingObject = localStorage["savingObject"];
if (savingObject != undefined)
{
savingObject = JSON.parse(savingObject);
}
return savingObject;
}

  1. 保存所有文件并在 web 浏览器中打开游戏。尝试通过移除几张匹配的卡片来玩游戏。然后关闭浏览器窗口并再次打开游戏。游戏应该从我们关闭窗口的状态恢复,如下图所示:

行动时间 从本地存储中恢复游戏

刚刚发生了什么?

我们刚刚通过解析整个游戏状态的保存 JSON 字符串完成了游戏加载部分。

然后,我们从加载的savingObject中恢复了经过的时间和牌组的顺序。恢复这两个属性只是简单的变量赋值。棘手的部分是重新创建卡片的移除。在游戏保存部分,我们为每张卡片 DOM 分配了一个索引,使用自定义数据属性 data-card-index。当保存游戏时,我们存储了每张已移除卡片的索引,因此我们可以在加载游戏时知道哪些卡片已被移除。然后,我们可以在游戏设置时移除这些卡片。以下代码在 jQuery 游戏ready函数中移除卡片:

if (savedObject != undefined)
{
matchingGame.savingObject.removedCards = savedObject.removedCards;
// find those cards and remove them.
for(var i in matchingGame.savingObject.removedCards)
{
$(".card[data-card-index="+matchingGame.savingObject. removedCards[i]+"]").remove();
}
}

提示

使用存储事件跟踪存储更改

有时,我们可能想要监听localStorage的变化。我们可以通过监听storage事件来实现。当localStorage中的任何内容发生变化时,该事件将被触发。来自Dive into HTML5的以下链接提供了关于如何使用该事件的详细讨论:

diveintohtml5.org/storage.html#storage-event

小测验 使用本地存储

考虑以下每个陈述是否为真:

  1. 我们可以直接将整数或对象保存在本地存储中。

  2. 我们可以通过将对象编码为字符串来将对象的数据保存到本地存储中。

  3. 我们可以使用localStorage["hello"] = "world"将值"world"与键"hello"保存在本地存储中。

总结

在本章中,我们学到了如何使用本地存储在 Web 浏览器中保存游戏数据。

具体来说,我们涵盖了:

  • 将基本数据保存和检索到键值对本地存储中

  • 将对象编码为 JSON 格式的字符串,然后将字符串解析回 JavaScript 对象

  • 保存整个游戏进度,以便即使中途离开,游戏也可以恢复

我们还使用纯 CSS3 样式创建了一个漂亮的 3D 丝带作为新记录徽章。

现在我们已经学会了如何通过使用本地存储来改进我们以前的游戏,我们准备进入一个名为WebSocket的高级功能,它可以在实时互动中连接玩家。

第八章:使用 WebSockets 构建多人绘画和猜词游戏

在之前的章节中,我们构建了几个本地单人游戏。在本章中,我们将借助 WebSockets 构建一个多人游戏。WebSockets 使我们能够创建基于事件的服务器-客户端架构。所有连接的浏览器之间传递的消息都是即时的。我们将结合 Canvas 绘图、JSON 数据打包和在之前章节中学到的几种技术来构建绘画和猜词游戏。

在本章中,我们将学习以下主题:

  • 尝试现有的多用户绘图板,通过 WebSockets 显示来自不同连接用户的绘画

  • 安装由node.js实现的 WebSockets 服务器

  • 从浏览器连接服务器

  • 使用 WebSocket API 创建一个即时聊天室

  • 在 Canvas 中创建一个多用户绘图板

  • 通过集成聊天室和游戏逻辑进行绘画和猜词游戏的构建

以下屏幕截图显示了我们将在本章中创建的绘画和猜词游戏:

使用 WebSockets 构建多人绘画和猜词游戏

所以,让我们开始吧。

尝试现有的 WebSockets 网络应用程序

在我们开始构建 WebSockets 示例之前,我们将看一下现有的多用户绘图板示例。这个示例让我们知道如何使用 WebSockets 服务器立即在浏览器之间发送数据。

提示

浏览器使用 WebSockets 的能力

在撰写本书时,只有苹果 Safari 和 Google Chrome 支持 WebSockets API。Mozilla Firefox 和 Opera 因协议上的潜在安全问题而放弃了对 WebSockets 的支持。Google Chrome 也计划在安全漏洞修复之前放弃 WebSockets。

Mozilla 的以下链接解释了他们为什么禁用了 WebSockets:

hacks.mozilla.org/2010/12/websockets-disabled-in-firefox-4/

尝试多用户绘图板的时间

执行以下步骤:

  1. 在 Web 浏览器中打开以下链接:

  2. www.chromeexperiments.com/detail/multiuser-sketchpad/

  3. 您将看到一个多用户绘图板的介绍页面。右键单击启动实验选项,选择在新窗口中打开链接

  4. 浏览器会提示一个新窗口,显示绘图板应用程序。然后,我们重复上一步,再次打开绘图板的另一个实例。

  5. 将两个浏览器并排放在桌面上。

  6. 尝试在任一绘图板上画些东西。绘画应该会出现在两个绘图板上。此外,绘图板是与所有连接的人共享的。您还可以看到其他用户的绘画。

  7. 以下屏幕截图显示了两个用户在绘图板上画的一个杯子:

尝试多用户绘图板的时间

刚刚发生了什么?

我们刚刚看到浏览器如何实时连接在一起。我们在绘图板上画了些东西,所有其他连接的用户都可以看到这些图画。此外,我们也可以看到其他人正在画什么。

该示例是使用 HTML5 WebSockets 功能与后端服务器制作的,以向所有连接的浏览器广播绘图数据。

绘画部分是建立在 Canvas 上的,我们已经在第四章,使用 Canvas 和绘图 API 构建 Untangle 游戏中介绍过。WebSocket API 使浏览器能够与服务器建立持久连接。后端是一个名为node.js的基于事件的服务器,我们将在本章中安装和使用。

安装 WebSocket 服务器

HTML5 的 WebSockets 提供了一个客户端 API,用于将浏览器连接到后端服务器。该服务器必须支持 WebSockets 协议,以保持连接持久。

安装 Node.JS WebSocket 服务器

在这一部分,我们将下载并安装一个名为Node.JS的服务器,我们可以在上面安装一个 WebSockets 模块。

安装 Node.JS 的时间

执行以下步骤:

  1. 转到包含Node.JS服务器源代码的以下 URL:

  2. github.com/joyent/node

  3. 单击页面上的下载按钮。它会提示一个对话框询问要下载哪种格式。只需选择 ZIP 格式。

  4. 在工作目录中解压 ZIP 文件。

  5. 在 Linux 或 Mac OSX 中,使用终端并切换到node.js文件所在的目录。

注意

Node.JS在 Linux 和 Mac 上可以直接使用。以下链接提供了一个安装程序,用于在 Windows 上安装Node.JS

node-js.prcn.co.cc/

  1. 运行以下命令:
$ ./configure
$ sudo make install

使用sudo make install命令以 root 权限安装Node.JS,并以 root 访问权限安装所需的第三方库。以下链接讨论了如何在不使用sudo的情况下安装Node.JS

提示

increaseyourgeek.wordpress.com/2010/08/18/install-node-js-without-using-sudo/

  1. sudo make install命令需要输入具有管理员特权的系统用户的密码。输入密码以继续安装。

  2. 安装完成后,可以使用以下命令检查node.js是否已安装:

$ node --version

  1. 上述命令应该打印出node.js的版本号。在我的情况下,它是 0.5 预发布版:
v0.5.0-pre

  1. 接下来,我们将为Node.JS服务器安装 WebSockets 库。在浏览器中转到以下 URL:

  2. github.com/miksago/node-websocket-server

  3. 单击页面上的下载按钮并下载 ZIP 文件。

  4. 在一个目录中解压 ZIP 文件。我们稍后会需要这个包中的lib目录。

刚刚发生了什么?

我们刚刚下载并安装了Node.JS服务器。我们还下载了node.js服务器的 WebSockets 库。通过本章的示例,我们将在此服务器和 WebSockets 库的基础上构建服务器逻辑。

注意

Node.js服务器安装在 Unix 或 Linux 操作系统上运行良好。但是,在 Windows 上安装和运行node.js服务器需要更多步骤。以下链接显示了如何在 Windows 上安装node.js服务器:

github.com/joyent/node/wiki/Building-node.js-on-Cygwin-(Windows)

创建一个用于广播连接计数的 WebSockets 服务器

我们刚刚安装了带有 WebSockets 库的node.js服务器。现在,我们将构建一些内容来测试 WebSockets。现在想象一下,我们需要一个服务器来接受浏览器的连接,然后向所有用户广播连接计数。

执行以下操作创建一个发送连接总数的 WebSocket 服务器

执行以下步骤:

  1. 创建一个名为server的新目录。

  2. node-websocket-server包中的整个lib文件夹复制到server目录中。

  3. server目录下创建一个名为server.js的新文件,并包含以下内容:

var ws = require(__dirname + '/lib/ws/server');
var server = ws.createServer();
server.addListener("connection", function(conn){
// init stuff on connection
console.log("A connection established with id",conn.id);
var message = "Welcome "+conn.id+" joining the party. Total connection:"+server.manager.length;
server.broadcast(message);
});
server.listen(8000);
console.log("WebSocket server is running.");
console.log("Listening to port 8000.");

  1. 打开终端并切换到服务器目录。

  2. 输入以下命令以执行服务器:

node server.js

  1. 如果成功,应该得到以下结果:
$ node server.js
WebSocket server is running.
Listening to port 8000.

刚刚发生了什么?

我们刚刚创建了一个简单的服务器逻辑,初始化了 WebSockets 库,并监听了连接事件。

初始化 WebSockets 服务器

Node.JS中,不同的功能被打包到模块中。当我们需要特定模块中的功能时,我们使用require进行加载。我们加载 WebSockets 模块,然后在服务器逻辑中使用以下代码初始化服务器:

var ws = require(__dirname + '/lib/ws/server');
var server = ws.createServer();

__dirname表示正在执行的服务器 JavaScript 文件的当前目录。我们将lib文件夹放在服务器逻辑文件的同一文件夹下。因此,WebSockets 服务器位于当前目录 | lib | ws | server

最后,我们需要为服务器分配一个端口来监听以下代码:

server.listen(8000);

在上述代码片段中,8000是客户端连接到此服务器的端口号。 我们可以选择不同的端口号,但必须确保所选的端口号不会与其他常见服务器服务重叠。

注意

为了获取有关node.js服务器的全局范围对象和变量的更多信息,请访问以下链接的官方文档:

nodejs.org/docs/v0.4.3/api/globals.html

在服务器端监听连接事件

node.js服务器是基于事件的。 这意味着大多数逻辑是在触发某个事件时执行的。 我们在示例中使用的以下代码监听connection事件并处理它:

server.addListener("connection", function(conn){
console.log("A connection established with id",conn.id);
…
});

connection事件带有一个连接参数。 我们在连接实例中有一个id属性,我们可以用它来区分每个连接的客户端。

以下表列出了两个常用的服务器事件:

WebSockets node.js 的服务器端事件 描述
connection 当客户端建立新连接时触发事件
close 当连接关闭时触发事件

获取服务器端连接的客户端计数

我们可以通过访问服务器管理器来获取 WebSockets node.js服务器中连接的客户端数。 我们可以使用以下代码获取计数:

var totalConnectedClients = server.manager.length;

向所有连接的浏览器广播消息

一旦服务器收到新的connection事件,我们就会向所有客户端广播连接的更新计数。 向客户端广播消息很容易。 我们只需要在server实例中使用string参数调用broadcast函数。

以下代码片段向所有连接的浏览器广播服务器消息:

var message = "a message from server";
server.broadcast(message);

创建一个连接到 WebSocket 服务器并获取总连接数的客户端

我们在上一个示例中构建了服务器,现在我们将构建一个客户端,连接到我们的 WebSocket 服务器并从服务器接收消息。 该消息将包含来自服务器的总连接计数。

行动时间在 WebSocket 应用程序中显示连接计数

执行以下步骤:

  1. 创建一个名为client的新目录。

  2. client文件夹中创建一个名为index.htm的 HTML 文件。

  3. 我们将在我们的 HTML 文件中添加一些标记。 将以下代码放入index.htm文件中:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebSockets demo for HTML5 Games Development: A Beginner's Guide</title>
<meta name="description" content="This is a WebSockets demo for the book HTML5 Games Development: A Beginner's Guide by Makzan">
<meta name="author" content="Makzan">
</head>
<body>
<script src="img/jquery-1.6.min.js"></script>
<script src="img/html5games.websocket.js"></script>
</body>
</html>

  1. 创建一个名为js的目录,并将 jQuery JavaScript 文件放入其中。

  2. 创建一个名为html5games.websockets.js的新文件,如下所示:

var websocketGame = {
}
// init script when the DOM is ready.
$(function(){
// check if existence of WebSockets in browser
if (window["WebSocket"]) {
// create connection
websocketGame.socket = new WebSocket("ws://127.0.0.1:8000");
// on open event
websocketGame.socket.onopen = function(e) {
console.log('WebSocket connection established.');
};
// on message event
websocketGame.socket.onmessage = function(e) {
console.log(e.data);
};
// on close event
websocketGame.socket.onclose = function(e) {
console.log('WebSocket connection closed.');
};
}
});

  1. 我们将测试代码。 首先,我们需要通过node server.js运行带有我们的server.js代码的节点服务器。

  2. 接下来,在 Web 浏览器中的客户端目录中打开index.htm文件两次。

  3. 检查服务器终端。 应该有类似以下的日志消息,指示连接信息和总连接数:

$ node server.js
WebSocket server is running.
Listening to port 8000.
A connection established with id 3863522640
A connection established with id 3863522651

  1. 然后,我们在浏览器中检查控制台面板。 一旦加载页面,我们就可以获得总连接数。 以下屏幕截图显示了客户端端的结果:

行动时间在 WebSocket 应用程序中显示连接计数

刚刚发生了什么?

我们刚刚构建了一个客户端,它与我们在上一节中构建的服务器建立了 WebSockets 连接。 然后,客户端将从服务器接收的任何消息打印到检查器中的控制台面板中。

建立 WebSocket 连接

在支持 WebSockets 的任何浏览器中,我们可以通过使用以下代码创建一个新的 WebSocket 实例来建立连接:

var socket = new WebSocket(url);

url参数是一个带有 WebSockets URL 的字符串。 在我们的示例中,我们正在本地运行我们的服务器。 因此,我们使用的 URL 是ws://127.0.0.1:8000,其中 8000 表示我们正在连接的服务器的端口号。 这是 8000,因为当我们构建服务器端逻辑时,服务器正在监听端口 8000。

WebSockets 客户端事件

与服务器类似,客户端端有几个 WebSockets 事件。以下表格列出了我们将用于处理 WebSockets 的事件:

事件名称 描述
onopen 当与服务器的连接建立时触发
onmessage 当从服务器接收到任何消息时触发
onclose 当服务器关闭连接时触发
onerror 当连接出现任何错误时触发

使用 WebSockets 构建聊天应用程序

我们现在知道有多少浏览器连接。假设我们想要构建一个聊天室,用户可以在各自的浏览器中输入消息,并立即将消息广播给所有连接的用户。

向服务器发送消息

我们将让用户输入消息,然后将消息发送到node.js服务器。然后服务器将消息转发到所有连接的浏览器。一旦浏览器接收到消息,它就会在聊天区域显示出来。在这种情况下,用户一旦加载网页就连接到即时聊天室。

采取行动 通过 WebSockets 向服务器发送消息

执行以下步骤:

  1. 首先,编写服务器逻辑。

  2. 打开server.js并添加以下突出显示的代码:

server.addListener("connection", function(conn){
// init stuff on connection
console.log("A connection established with id",conn.id);
var message = "Welcome "+conn.id+" joining the party. Total connection:"+server.manager.length;
server.broadcast(message);
// listen to the message
conn.addListener("message", function(message){
console.log("Got data '"+message+"' from connection "+conn.id);
});
});

  1. 现在转到client文件夹。

  2. 打开index.htm文件,并在body部分中添加以下标记。它为用户提供了输入并发送消息到服务器的输入:

<input type='text' id="chat-input">
<input type='button' value="Send" id="send">

  1. 然后,将以下代码添加到html5games.websocket.js JavaScript 文件中。当用户单击send按钮或按Enter键时,它将消息发送到服务器:
$("#send").click(sendMessage);
$("#chat-input").keypress(function(event) {
if (event.keyCode == '13') {
sendMessage();
}
});
function sendMessage()
{
var message = $("#chat-input").val();
websocketGame.socket.send(message);
$("#chat-input").val("");
}

  1. 在测试我们的代码之前,检查服务器终端,看看 node 服务器是否仍在运行。按Ctrl+C终止它,然后使用node server.js命令再次运行它。

  2. 在 Web 浏览器中打开index.htm。您应该看到一个带有Send按钮的输入文本字段,如下面的屏幕截图所示:采取行动 通过 WebSockets 向服务器发送消息

  3. 尝试在输入文本字段中输入一些内容,然后单击Send按钮或按Enter。输入文本将被清除。

  4. 现在,切换到服务器终端,我们将看到服务器打印我们刚刚发送的文本。您还可以将浏览器和服务器终端并排放置,以查看消息从客户端发送到服务器的实时性。以下屏幕截图显示了服务器终端上来自两个连接的浏览器的消息:

采取行动 通过 WebSockets 向服务器发送消息

刚刚发生了什么?

我们刚刚通过添加一个输入文本字段来扩展了我们的连接示例,让用户在其中输入一些文本并将其发送出去。文本作为消息发送到 WebSockets 服务器。然后服务器将在终端中打印接收到的消息。

从客户端向服务器发送消息

为了从客户端向服务器发送消息,我们在WebSocket实例中调用以下send方法:

websocketGame.socket.send(message);

在我们的示例中,以下代码片段从输入文本字段中获取消息并将其发送到服务器:

var message = $("#chat-input").val();
websocketGame.socket.send(message);

在服务器端接收消息

在服务器端,我们需要处理刚刚从客户端发送的消息。在 WebSocket node.js库中的连接实例中有一个名为message的事件。我们可以监听连接消息事件以接收来自每个客户端连接的消息。

以下代码片段显示了我们如何使用消息事件监听器在服务器终端上打印消息和唯一连接 ID:

conn.addListener("message", function(message){
console.log("Got data '"+message+"' from connection "+conn.id);
});

注意

在服务器和客户端之间发送和接收消息时,只接受字符串。我们不能直接发送对象。但是,我们可以在传输之前将数据转换为 JSON 格式的字符串。我们将在本章后面展示发送数据对象的示例。

在服务器端广播每条接收到的消息以创建聊天室

在上一个示例中,服务器可以接收来自浏览器的消息。但是,服务器除了在终端中打印接收到的消息之外,什么也不做。因此,我们将向服务器添加一些逻辑,以广播消息。

执行广播消息到所有连接的浏览器的操作

执行以下步骤:

  1. 打开服务器端逻辑的server.js文件。

  2. 将以下突出显示的代码添加到消息事件监听器处理程序中:

conn.addListener("message", function(message){
console.log("Got data '"+message+"' from connection "+conn.id);
var displayMessage = conn.id + " says: "+message;
server.broadcast(displayMessage);
});

  1. 服务器端就是这样。转到client文件夹并打开index.htm文件。

  2. 我们想在聊天历史区域显示聊天消息。将以下代码添加到 HTML 文件中:

<ul id="chat-history"></ul>

  1. 接下来,我们需要客户端 JavaScript 来处理从服务器接收的消息。我们用它将消息打印到控制台面板中,用以下突出显示的代码替换onmessage事件处理程序中的console.log代码:
socket.onmessage = function(e) {
$("#chat-history").append("<li>"+e.data+"</li>");
};

  1. 让我们测试我们的代码。通过Ctrl + C终止任何正在运行的 node 服务器。然后再次运行服务器。

  2. 打开index.htm文件两次,将它们并排放置。在文本字段中输入一些内容,然后按Enter。消息将出现在所有打开的浏览器上。如果打开多个 HTML 文件实例,则消息应该出现在所有浏览器上。下面的截图显示了两个并排显示聊天历史记录的浏览器:

执行广播消息到所有连接的浏览器的操作

刚才发生了什么?

这是我们之前示例的延伸。我们讨论了服务器如何向所有连接的客户端广播连接计数。我们还讨论了客户端如何向服务器发送消息。在这个例子中,我们将这两种技术结合起来,让服务器将接收到的消息广播给所有连接的用户。

比较 WebSocket 和轮询方法

如果您曾经使用服务器端语言和数据库构建过网页聊天室,那么您可能会想知道 WebSocket 实现和传统实现之间有什么区别。

传统的聊天室方法通常使用轮询方法实现。客户端定期向服务器请求更新。服务器会用没有更新或更新的数据来响应客户端。然而,传统方法存在一些问题。客户端直到下一次向服务器请求之前,才能从服务器获取新的更新数据。这意味着数据更新会延迟一段时间,响应不够即时。如果我们想通过缩短轮询持续时间来改善这个问题,那么会利用更多的带宽,因为客户端需要不断向服务器发送请求。

下图显示了客户端和服务器之间的请求。它显示了许多无用的请求被发送,但服务器在没有新数据的情况下响应客户端:

WebSocket 和轮询方法之间的比较

还有一种更好的轮询方法叫做长轮询。客户端向服务器发送请求并等待响应。与传统的轮询方法不同,服务器不会以“没有更新”的方式响应,直到有需要推送给服务器的内容。在这种方法中,服务器可以在有更新时向客户端推送内容。一旦客户端从服务器收到响应,它会创建另一个请求并等待下一个服务器通知。下面的图显示了长轮询方法,客户端请求更新,服务器只在有更新时响应:

WebSocket 和轮询方法之间的比较

在 WebSockets 方法中,请求的数量远少于轮询方法。这是因为客户端和服务器之间的连接是持久的。一旦建立连接,只有在有任何更新时才会从客户端或服务器端发送请求。例如,当客户端想要向服务器更新某些内容时,客户端向服务器发送消息。服务器也只在需要通知客户端数据更新时才向客户端发送消息。在连接期间不会发送其他无用的请求。因此,利用的带宽更少。以下图显示了 WebSockets 方法:

WebSockets 和轮询方法之间的比较

小测验:WebSockets 相对于轮询方法的好处

使用基于事件的 WebSockets 方法实现多用户聊天室的好处是什么?这些好处如何使消息传递如此即时?

使用 Canvas 和 WebSockets 制作共享绘图白板

假设我们想要一个共享的素描本。任何人都可以在素描本上画东西,所有其他人都可以查看,就像我们在本章开头玩的素描本示例一样。我们学习了如何在客户端和服务器之间传递消息。我们将进一步发送绘图数据。

构建本地绘图素描本

在处理数据发送和服务器处理之前,让我们专注于制作一个绘图白板。我们将使用画布来构建一个本地绘图素描本。

行动时间:使用 Canvas 制作本地绘图白板

执行以下步骤:

  1. 在本节中,我们只关注客户端。打开index.htm文件并添加以下canvas标记:
<canvas id='drawing-pad' width='500' height='400'>
</canvas>

  1. 我们将在画布上画一些东西,我们将需要相对于画布的鼠标位置。我们在第四章,使用 Canvas 和 Drawing API 构建 Untangle 游戏中做到了这一点。将以下样式添加到画布:
<style>
canvas{position:relative;}
</style>

  1. 然后,我们打开html5games.websocket.js JavaScript 文件来添加绘图逻辑。

  2. 在 JavaScript 文件的顶部用以下变量替换websocketGame全局对象:

var websocketGame = {
// indicates if it is drawing now.
isDrawing : false,
// the starting point of next line drawing.
startX : 0,
startY : 0,
}
// canvas context
var canvas = document.getElementById('drawing-pad');
var ctx = canvas.getContext('2d');

  1. 在 jQuery 的ready函数中,我们添加以下鼠标事件处理程序代码。该代码处理鼠标按下、移动和松开事件:
// the logic of drawing on canvas
$("#drawing-pad").mousedown(function(e) {
// get the mouse x and y relative to the canvas top-left point.
var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;
startX = mouseX;
startY = mouseY;
isDrawing = true;
});
$("#drawing-pad").mousemove(function(e) {
// draw lines when is drawing
if (websocketGame.isDrawing) {
// get the mouse x and y relative to the canvas top-left point.
var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;
if (!(mouseX == websocketGame.startX && mouseY == websocketGame.startY)) {
drawLine(ctx, websocketGame.startX, websocketGame.startY,mouseX,mouseY,1);
websocketGame.startX = mouseX;
websocketGame.startY = mouseY;
}
}
});
$("#drawing-pad").mouseup(function(e) {
websocketGame.isDrawing = false;
});

  1. 最后,我们有以下函数来在画布上画一条线,给定起点和终点:
function drawLine(ctx, x1, y1, x2, y2, thickness) {
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.lineWidth = thickness;
ctx.strokeStyle = "#444";
ctx.stroke();
}

  1. 保存所有文件并打开index.htm文件。我们应该看到一个空白的空间,我们可以使用鼠标绘制一些东西。绘图尚未发送到服务器,因此其他人无法查看我们的绘图:

行动时间:使用 Canvas 制作本地绘图白板

刚刚发生了什么?

我们刚刚创建了一个本地绘图板。这就像一个白板,玩家可以通过拖动鼠标在画布上绘图。但是,绘图数据尚未发送到服务器;所有绘图只在本地显示。

画线函数与我们在第四章中使用的相同。我们还使用相同的代码来获取鼠标相对于画布元素的位置。但是,鼠标事件的逻辑与第四章不同。

在画布上绘制

当我们在计算机上画东西时,通常意味着我们点击画布并拖动鼠标(或笔)。直到鼠标按钮松开为止才画线。然后,用户再次点击另一个地方并拖动以绘制线条。

在我们的示例中,我们有一个名为isDrawing的布尔标志,用于指示用户是否正在绘图。isDrawing标志默认为 false。当鼠标按钮按下时,我们将标志设置为 true。当鼠标移动时,我们在鼠标按钮按下时的移动点和上一个点之间画一条线。然后,当鼠标按钮松开时,我们再次将isDrawing标志设置为 false。

这就是绘图逻辑的工作方式。

尝试一下:使用颜色绘图

我们能否通过添加颜色支持来修改绘图画板?再加上五个按钮,分别是红色、蓝色、绿色、黑色和白色?玩家可以在绘图时选择颜色。

将绘图广播到所有连接的浏览器

我们将进一步通过将我们的绘图数据发送到服务器,并让服务器将绘图广播到所有连接的浏览器。

通过 WebSockets 发送绘图的时间

执行以下步骤:

  1. 首先,我们需要修改服务器逻辑。打开server.js文件并替换以下代码。它使用 JSON 格式的字符串进行广播,因此我们可以发送和接收数据对象:
// Constants
var LINE_SEGMENT = 0;
var CHAT_MESSAGE = 1;
var ws = require(__dirname + '/lib/ws/server');
var server = ws.createServer();
server.addListener("connection", function(conn){
// init stuff on connection
console.log("A connection established with id",conn.id);
var message = "Welcome "+conn.id+" joining the party. Total connection:"+server.manager.length;
var data = {};
data.dataType = CHAT_MESSAGE;
data.sender = "Server";
data.message = message;
shared drawing whiteboardshared drawing whiteboardconnected browsers drawings, broadcastingserver.broadcast(JSON.stringify(data));
// listen to the message
shared drawing whiteboardshared drawing whiteboardconnected browsers drawings, broadcastingconn.addListener("message", function(message){
console.log("Got data '"+message+"' from connection "+conn.id);
var data = JSON.parse(message);
if (data.dataType == CHAT_MESSAGE) {
// add the sender information into the message data object
data.sender = conn.id;
}
server.broadcast(JSON.stringify(data));
});
});
server.listen(8000);
console.log("WebSocket server is running.");
console.log("Listening to port 8000.");

  1. 在客户端,我们需要逻辑来对服务器做出相同的数据对象定义的响应。在client | js目录中打开html5games.websocket.js JavaScript 文件。

  2. 将以下常量添加到websocketGame全局变量中。相同的常量与相同的值也在服务器端逻辑中定义。

// Contants
LINE_SEGMENT : 0,
CHAT_MESSAGE : 1,

  1. 在客户端处理消息事件时,我们将 JSON 格式的字符串转换回数据对象。如果数据是聊天消息,那么我们将其显示为聊天历史记录,否则我们将其绘制在画布上作为线段。用以下代码替换onmessage事件处理程序:
socket.onmessage = function(e) {
// check if the received data is chat message or line segment
console.log("onmessage event:",e.data);
var data = JSON.parse(e.data);
if (data.dataType == websocketGame.CHAT_MESSAGE) {
$("#chat-history").append("<li>"+data.sender+" said: "+data.message+"</li>");
}
else if (data.dataType == websocketGame.LINE_SEGMENT) {
drawLine(ctx, data.startX, data.startY, data.endX, data.endY, 1);
}
};

  1. 当鼠标移动时,我们不仅在画布上绘制线条,还将线条数据发送到服务器。将以下突出显示的代码添加到鼠标移动事件处理程序中:
$("#drawing-pad").mousemove(function(e) {
// draw lines when is drawing
if (websocketGame.isDrawing) {
// get the mouse x and y relative to the canvas top-left point.
var mouseX = e.layerX || 0;
var mouseY = e.layerY || 0;
if (!(mouseX == websocketGame.startX && mouseY == websocketGame.startY)) {
drawLine(ctx,startX,startY,mouseX,mouseY,1);
// send the line segment to server
var data = {};
data.dataType = websocketGame.LINE_SEGMENT;
data.startX = startX;
data.startY = startY;
data.endX = mouseX;
data.endY = mouseY;
websocketGame.socket.send(JSON.stringify(data));
websocketGame.startX = mouseX;
websocketGame.startY = mouseY;
}
}
});

  1. 最后,我们需要修改发送消息的逻辑。现在,当将消息发送到服务器时,我们将消息打包成一个对象并格式化为 JSON。将sendMessage函数更改为以下代码:
function sendMessage() {
var message = $("#chat-input").val();
// pack the message into an object.
var data = {};
data.dataType = websocketGame.CHAT_MESSAGE;
data.message = message;
websocketGame.socket.send(JSON.stringify(data));
$("#chat-input").val("");
}

  1. 保存所有文件并重新启动服务器。

  2. 在两个浏览器实例中打开index.htm文件。

  3. 首先,通过输入一些消息并发送它们来尝试聊天室功能。然后,在画布上画一些东西。两个浏览器应该显示与以下截图中相同的绘图:

通过 WebSockets 发送绘图的时间

刚刚发生了什么?

我们刚刚构建了一个多用户绘图画板。这类似于我们在本章开头尝试的绘图画板。我们通过发送一个复杂的数据对象作为消息,扩展了构建聊天室时所学到的内容。

定义一个数据对象来在客户端和服务器之间通信

为了正确地在服务器和客户端之间传递多个数据,我们必须定义一个数据对象,服务器和客户端都能理解。

数据对象中有几个属性。以下表格列出了这些属性以及我们为什么需要它们:

属性名称 我们为什么需要这个属性
dataType 这是一个重要的属性,帮助我们了解整个数据。数据要么是聊天消息,要么是绘图线段数据。
sender 如果数据是聊天消息,客户端需要知道谁发送了消息。
message 当数据类型是聊天消息时,我们肯定需要将消息内容本身包含到数据对象中。
startX 当数据类型是绘图线段时,我们包含线的起点的 x/y 坐标。
startY
endX 当数据类型是绘图线段时,我们包含线的终点的 x/y 坐标。
endY

此外,我们在客户端和服务器端都定义了以下常量。这些常量是用于dataType属性的:

// Contants
LINE_SEGMENT : 0,
CHAT_MESSAGE : 1,

有了这些常量,我们可以通过以下可读的代码来比较dataType,而不是使用无意义的整数:

if (data.dataType == websocketGame.CHAT_MESSAGE) {…}

将绘图线数据打包成 JSON 进行广播

在上一章中,当将 JavaScript 对象存储到本地存储中时,我们使用了JSON.stringify函数将其转换为 JSON 格式的字符串。现在,我们需要在服务器和客户端之间以字符串格式发送数据。我们使用了相同的方法将绘画线条数据打包成对象,并将其作为 JSON 字符串发送。

以下代码片段显示了我们如何在客户端打包线段数据并以 JSON 格式的字符串发送到服务器:

// send the line segment to server
var data = {};
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 对象。以下是客户端上的代码,它解析数据并根据数据更新聊天历史或绘制线条:

var data = JSON.parse(e.data);
if (data.dataType == websocketGame.CHAT_MESSAGE) {
$("#chat-history").append("<li>"+data.sender+" said: "+data.message+"</li>");
}
else if (data.dataType == websocketGame.LINE_SEGMENT) {
drawLine(ctx, data.startX, data.startY, data.endX, data.endY, 1);
}

构建多人绘画和猜词游戏

在本章的早些时候,我们构建了一个即时聊天室。此外,我们刚刚构建了一个多用户草图本。那么,如何将这两种技术结合起来构建一个绘画和猜词游戏呢?绘画和猜词游戏是一种游戏,其中一个玩家被给予一个词来绘制。所有其他玩家不知道这个词,并根据绘画猜测这个词。绘画者和正确猜测词语的玩家将获得积分。

采取行动构建绘画和猜词游戏

我们将按照以下方式实现绘画和猜词游戏的游戏流程:

  1. 首先,我们将在客户端添加游戏逻辑。

  2. 在客户端目录中打开index.htm文件。在发送按钮之后添加以下重新启动按钮:

<input type='button' value="Restart" id="restart">

  1. 打开html5games.websocket.js JavaScript 文件。

  2. 我们需要一些额外的常量来确定游戏进行过程中的不同状态。将以下突出显示的代码添加到文件顶部:

// Constants
LINE_SEGMENT : 0,
CHAT_MESSAGE : 1,
GAME_LOGIC : 2,
// Constant for game logic state
WAITING_TO_START : 0,
GAME_START : 1,
GAME_OVER : 2,
GAME_RESTART : 3,

  1. 此外,我们需要一个标志来指示此玩家负责绘制。将以下布尔全局变量添加到代码中:
isTurnToDraw : false,

  1. 当客户端从服务器接收到消息时,它会解析并检查是否是一条线条绘制的聊天消息。现在我们有另一种处理游戏逻辑的消息类型,名为GAME_LOGIC。游戏逻辑消息包含不同的数据,用于不同的游戏状态。将以下代码添加到onmessage事件处理程序中:
else if (data.dataType == websocketGame.GAME_LOGIC) {
if (data.gameState == websocketGame.GAME_OVER) {
websocketGame.isTurnToDraw = false;
$("#chat-history").append("<li>"+data.winner+" wins! The answer is '"+data.answer+"'.</li>");
$("#restart").show();
}
if (data.gameState == websocketGame.GAME_START) {
// clear the canvas.
canvas.width = canvas.width;
// hide the restart button.
$("#restart").hide();
// clear the chat history
$("#chat-history").html("");
if (data.isPlayerTurn) {
isTurnToDraw = true;
$("#chat-history").append("<li>Your turn to draw. Please draw '"+data.answer+"'.</li>");
}
else {
$("#chat-history").append("<li>Game Started. Get Ready. You have one minute to guess.</li>");
}
}
}

  1. 我们已经在客户端添加了游戏逻辑。客户端上有一些包含重新启动逻辑和防止非绘图玩家在画布上绘制的小代码。这些代码可以在代码包中找到。

  2. 是时候转向服务器端了。

  3. 在先前的示例中,服务器端只负责将任何传入的消息广播给所有连接的浏览器。这对于多人游戏来说是不够的。服务器将充当控制游戏流程和确定胜利的游戏主持人。因此,请删除server.js中的现有代码,并使用以下代码。更改部分已经突出显示:

// Constants
var LINE_SEGMENT = 0;
var CHAT_MESSAGE = 1;
var GAME_LOGIC = 2;
// Constant for game logic state
var WAITING_TO_START = 0;
var GAME_START = 1;
var GAME_OVER = 2;
var GAME_RESTART = 3;
var ws = require(__dirname + '/lib/ws/server');
var server = ws.createServer();
// the current turn of player index.
var playerTurn = 0;
var wordsList = ['apple','idea','wisdom','angry'];
var currentAnswer = undefined;
var currentGameState = WAITING_TO_START;
var gameOverTimeout;
server.addListener("connection", function(conn){
// init stuff on connection
console.log("A connection established with id",conn.id);
var message = "Welcome "+conn.id+" joining the party. Total connection:"+server.manager.length;
var data = {};
data.dataType = CHAT_MESSAGE;
data.sender = "Server";
data.message = message;
server.broadcast(JSON.stringify(data));
// send the game state to all players.
var gameLogicData = {};
gameLogicData.dataType = GAME_LOGIC;
gameLogicData.gameState = WAITING_TO_START;
server.broadcast(JSON.stringify(gameLogicData));
// start the game if there are 2 or more connections
if (currentGameState == WAITING_TO_START && server.manager.length >= 2)
{
startGame();
}
// listen to the message
conn.addListener("message", function(message){
console.log("Got data '"+message+"' from connection "+conn.id);
var data = JSON.parse(message);
if (data.dataType == CHAT_MESSAGE)
{
// add the sender information into the message data object.
data.sender = conn.id;
multiplayer draw-and-guess gamemultiplayer draw-and-guess gamebuilding}
server.broadcast(JSON.stringify(data));
// check if the message is guessing right or wrong
if (data.dataType == CHAT_MESSAGE)
{
if (currentGameState == GAME_START && data.message == currentAnswer)
{
var gameLogicData = {};
gameLogicData.dataType = GAME_LOGIC;
gameLogicData.gameState = GAME_OVER;
gameLogicData.winner = conn.id;
gameLogicData.answer = currentAnswer;
server.broadcast(JSON.stringify(gameLogicData));
currentGameState = WAITING_TO_START;
// clear the game over timeout
clearTimeout(gameOverTimeout);
}
}
if (data.dataType == GAME_LOGIC && data.gameState == GAME_RESTART)
{
startGame();
}
});
});
function startGame()
{
// pick a player to draw
playerTurn = (playerTurn+1) % server.manager.length;
// pick an answer
var answerIndex = Math.floor(Math.random() * wordsList.length);
currentAnswer = wordsList[answerIndex];
// game start for all players
multiplayer draw-and-guess gamemultiplayer draw-and-guess gamebuildingvar gameLogicData1 = {};
gameLogicData1.dataType = GAME_LOGIC;
gameLogicData1.gameState = GAME_START;
gameLogicData1.isPlayerTurn = false;
server.broadcast(JSON.stringify(gameLogicData1));
// game start with answer to the player in turn
var index = 0;
server.manager.forEach(function(connection){
if (index == playerTurn)
{
var gameLogicData2 = {};
gameLogicData2.dataType = GAME_LOGIC;
gameLogicData2.gameState = GAME_START;
gameLogicData2.answer = currentAnswer;
gameLogicData2.isPlayerTurn = true;
server.send(connection.id, JSON.stringify(gameLogicData2));
}
index++;
});
// game over the game after 1 minute.
gameOverTimeout = setTimeout(function(){
var gameLogicData = {};
gameLogicData.dataType = GAME_LOGIC;
gameLogicData.gameState = GAME_OVER;
gameLogicData.winner = "No one";
gameLogicData.answer = currentAnswer;
server.broadcast(JSON.stringify(gameLogicData));
currentGameState = WAITING_TO_START;
},60*1000);
currentGameState = GAME_START;
}
server.listen(8000);
console.log("WebSocket server is running.");
console.log("Listening to port 8000.");

  1. 我们将保存所有文件并重新启动服务器。然后,在两个浏览器实例中启动index.htm文件。一个浏览器收到来自服务器的消息,通知玩家绘制某物。另一个浏览器则通知玩家在一分钟内猜测其他人正在绘制什么。

  2. 被告知绘制某物的玩家可以在画布上绘制。绘画将广播给其他连接的玩家。被告知猜测的玩家不能在画布上绘制任何东西。相反,玩家在文本字段中输入他们的猜测并发送到服务器。如果猜测正确,则游戏结束。否则,游戏将持续直到一分钟倒计时结束。

采取行动构建绘画和猜词游戏

刚刚发生了什么?

我们刚刚在 WebSockets 和 Canvas 中创建了一个多人绘画和猜词游戏。游戏和多用户草图本之间的主要区别在于,服务器现在控制游戏流程,而不是让所有用户绘制。

控制多人游戏的游戏流程

控制多人游戏的游戏流程比单人游戏要困难得多。我们可以简单地使用几个变量来控制单人游戏的游戏流程,但是我们必须使用消息传递来通知每个玩家特定的更新游戏流程。

首先,我们需要以下突出显示的常量GAME_LOGIC用于dataType。我们使用这个dataType来发送和接收与游戏逻辑控制相关的消息:

// Constants
var LINE_SEGMENT = 0;
var CHAT_MESSAGE = 1;
var GAME_LOGIC = 2;

游戏流程中有几种状态。在游戏开始之前,连接的玩家正在等待游戏开始。一旦有足够的连接进行多人游戏,服务器向所有玩家发送游戏逻辑消息,通知他们开始游戏。

当游戏结束时,服务器向所有玩家发送游戏结束状态。然后,游戏结束,游戏逻辑暂停,直到有玩家点击重新开始按钮。一旦重新开始按钮被点击,客户端向服务器发送游戏重新开始状态,指示服务器准备新游戏。然后,游戏重新开始。

我们在客户端和服务器中将四个游戏状态声明为以下常量,以便它们理解:

// Constant for game logic state
var WAITING_TO_START = 0;
var GAME_START = 1;
var GAME_OVER = 2;
var GAME_RESTART = 3;

服务器端的以下代码保存了一个指示哪个玩家轮到的索引:

var playerTurn = 0;

发送到玩家(轮到他的回合)的数据与发送到其他玩家的数据不同。其他玩家只收到一个游戏开始信号的数据:

var gameLogicData1 = {};
gameLogicData1.dataType = GAME_LOGIC;
gameLogicData1.gameState = GAME_START;
gameLogicData1.isPlayerTurn = false;

另一方面,玩家(轮到他画画)收到以下包含单词信息的数据:

var gameLogicData2 = {};
gameLogicData2.dataType = GAME_LOGIC;
gameLogicData2.gameState = GAME_START;
gameLogicData2.answer = currentAnswer;
gameLogicData2.isPlayerTurn = true;

在服务器端枚举连接的客户端

我们可以使用server manager类中的forEach方法枚举所有连接的客户端。以下代码显示了用法。它循环遍历每个连接,并调用给定的callback函数,如下所示:

server.manager.forEach(function);

例如,以下代码片段在服务器终端上打印所有连接的 ID:

server.manager.forEach(function(connection){
console.log("This is connection",connection.id);
}
}

在服务器端向特定连接发送消息

在我们之前的示例中,我们使用广播向所有连接的客户端发送消息。除了向每个人发送消息,我们可以使用send方法将消息发送到特定的连接,如下所示:

server.send(connectionID, message);

send方法需要两个参数。connectionID是目标连接的唯一 ID,message是我们要发送的字符串。

在我们从画画和猜图游戏中提取的以下代码中,我们向现在必须画画的玩家的浏览器发送特殊数据。我们使用forEach函数循环遍历连接,并检查连接是否轮到画画。然后,我们打包答案并将这些数据发送给目标连接,如下所示:

server.manager.forEach(function(connection){
if (index == playerTurn)
{
var gameLogicData2 = {};
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 装饰猜画游戏

游戏逻辑基本上已经完成,游戏已经可以玩了。但是,我们忘记了装饰游戏以使其看起来更吸引人。我们将使用 CSS 样式来装饰我们的猜画游戏。

装饰游戏的时间

执行以下步骤:

  1. 装饰只适用于客户端。打开index.htm文件。

  2. 在头部添加以下 CSS 样式链接:

<link href='http://fonts.googleapis.com/css?family=Cabin+Sketch: bold' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" media="all" href="css/drawguess.css">

  1. 将所有标记放在body中的id=gamesection内。此外,我们添加了一个游戏的h1标题,如下所示:
<section id="game">
<h1>Draw & Guess</h1>
...
</section>

  1. 在文本字段输入前添加一个聊天或猜测:,这样玩家就知道在哪里输入他们的猜测词。

  2. 接下来,在client文件夹内创建一个名为css的目录。

  3. 创建一个名为drawguess.css的新文件,并将其保存在css目录中。

  4. 将以下样式放入 CSS 文件中:

body {
background: #ccd6e1;
font-family: 'Cabin Sketch', arial, serif;
}
#game {
width: 500px;
margin: 0 auto;
}
#game h1 {
text-align: center;
margin-bottom: 5px;
text-shadow: 0px 1px 0px #fff;
}
#drawing-pad {
border: 10px solid #fffeff;
background: #f1f3ef;
box-shadow:0px 3px 5px #333;
}
#chat-history {
list-style: none;
padding: 0;
}
#chat-history li {
border-bottom: 1px dashed rgba(20,20,20,.2);
margin: 10px 0;
}

  1. 保存所有文件,并在两个浏览器中再次打开index.htm文件以开始游戏。由于我们只改变了装饰代码,游戏现在应该看起来更好,如下面的截图所示:

Time for action Decorating the game

刚刚发生了什么?

我们刚刚为我们的游戏应用了样式,并嵌入了一个来自Google Font Directory的字体,看起来像是涂鸦文本。画布现在被设计成更像是一个带有粗边框和微妙阴影的画布。

总结

在这一章中,我们学到了很多关于将浏览器连接到 WebSockets 的知识。一个浏览器的消息和事件会几乎实时地广播到另一个浏览器。

具体来说,我们:

  • 学会了 WebSockets 如何通过在现有的多人涂鸦板上绘制来提供实时事件。它显示了其他连接用户的绘画。

  • 安装了一个带有 WebSocket 库的Node.js服务器。通过使用这个服务器,我们可以轻松地构建一个基于事件的服务器来处理来自浏览器的 WebSocket 请求。

  • 讨论了服务器和客户端之间的关系。

  • 构建了一个即时聊天室应用程序。我们学会了如何实现一个服务器脚本来将传入的消息广播到其他连接的浏览器。我们还学会了如何在客户端上显示从服务器接收到的消息。

  • 构建了一个多用户绘图板。我们学会了如何将数据打包成 JSON 格式,以在服务器和浏览器之间传递消息。

  • 通过整合聊天和绘图板来构建一个猜画游戏。我们还学会了如何在多人游戏中创建游戏逻辑。

现在我们已经学会了如何构建一个多人游戏,我们准备在下一章中借助物理引擎来构建物理游戏。

第九章:使用 Box2D 和 Canvas 构建物理汽车游戏

2D 物理引擎是游戏开发中的热门话题。借助物理引擎,我们可以通过定义环境和简单规则轻松创建可玩的游戏。以现有游戏为例,愤怒的小鸟游戏中的玩家将小鸟飞向敌人的城堡以摧毁它。在《切断绳子》中,糖果掉进怪物的嘴里以进入下一关。

在本章中,我们将学习以下主题:

  • 安装 Box2D JavaScript 库

  • 在物理世界中创建一个静态地面实体

  • 在 Canvas 上绘制物理世界

  • 在物理世界中创建一个动态方块

  • 推进世界时间

  • 为游戏添加车轮

  • 创建物理汽车

  • 通过键盘输入向汽车施加力

  • 在 Box2D 世界中检查碰撞

  • 重新启动游戏

  • 为我们的汽车游戏添加关卡支持

  • 用图形替换 Box2D 轮廓绘制

  • 添加最后一点以使游戏有趣

以下屏幕截图显示了本章结束时我们将获得的内容。这是一个汽车游戏,玩家将汽车移向目的地点:

使用 Box2D 和 Canvas 构建物理汽车游戏

所以,让我们开始吧。

安装 Box2D JavaScript 库

现在,假设我们想创建一个汽车游戏。我们对汽车施加力使其向前移动。汽车在坡道上移动,然后飞过空中。之后,汽车落在目的地坡道上,游戏结束。物理世界的每个部分的每次碰撞都会影响这一运动。如果我们必须从头开始制作这个游戏,那么我们至少要计算每个部分的速度和角度。幸运的是,物理库帮助我们处理所有这些物理问题。我们所要做的就是创建物理模型并在画布中呈现它。

行动时间 安装 Box2D 物理库

执行以下步骤:

  1. 我们将获得 Box2D JavaScript 库。原始的 Box2D JavaScript 库基于原型 JavaScript 库。原型库提供了类似于 jQuery 的函数,但 API 略有不同。由于 KJ(kjam.org/post/105)将其移植为适用于 jQuery 的版本,我们可以使用 jQuery 库,而我们的整本书都是基于它的。Box2D 库与起始代码可以在名为box2d_game的代码包中找到。

  2. 现在,我们应该有以下设置:行动时间 安装 Box2D 物理库

提示

我们已经导入了必要的 JavaScript 文件。值得记住的是,如果您以后想使用此基础创建另一个物理游戏,Box2D JS 建议按照完全相同的顺序复制 JavaScript 导入代码,因为文件之间存在依赖关系。

  1. 现在,我们将创建一个空世界来测试我们的 Box2D 库安装。打开html5games.box2dcargame.js JavaScript 文件,并将以下代码放入文件中以创建世界:
// the global object that contains the variable needed for the car game.
var carGame = {
}
var canvas;
var ctx;
var canvasWidth;
var canvasHeight;
$(function() {
carGame.world = createWorld();
console.log("The world is created. ",carGame.world);
// get the reference of the context
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');
canvasWidth = parseInt(canvas.width);
canvasHeight = parseInt(canvas.height);
});
function createWorld() {
// set the size of the world
var worldAABB = new b2AABB();
worldAABB.minVertex.Set(-4000, -4000);
worldAABB.maxVertex.Set(4000, 4000);
// Define the gravity
var gravity = new b2Vec2(0, 300);
// set to ignore sleeping object
var doSleep = false;
// finally create the world with the size, gravity, and sleep object parameter.
var world = new b2World(worldAABB, gravity, doSleep);
return world;
}

  1. 在网络浏览器中打开index.html文件。我们应该看到一个灰色的画布,什么也没有。

我们还没有在画布中呈现物理世界。这就是为什么我们在页面上只看到一个空白画布。但是,我们已经在控制台日志中打印了新创建的世界。以下屏幕截图显示了控制台跟踪带有许多以m_开头的属性的世界对象。这些是世界的物理状态:

行动时间 安装 Box2D 物理库

刚刚发生了什么?

我们刚刚安装了 Box2D JavaScript 库,并创建了一个空世界来测试安装。

使用 b2World 创建新世界

b2World是 Box2D 环境中的核心类。我们所有的物理实体,包括地面和汽车,都是在这个世界中创建的。以下代码显示了如何创建一个世界:

var world = new b2World(worldAABB, gravity, doSleep);

b2World类需要三个参数来初始化,这些参数在下表中列出并附有描述:

参数 类型 讨论
worldAABB b2AABB 代表世界的边界区域
gravity b2Vec2 代表世界的重力
doSleep Bool 定义世界是否忽略休眠的物体

使用 b2AABB 定义边界区域

在物理世界中,我们需要很多边界区域。我们需要的第一个边界是世界边界。世界边界内的所有物体都将被计算,而边界外的物体将被销毁。

我们可以将b2AABB视为具有最低边界点和最高边界点的矩形。以下代码片段显示了如何使用b2AABB类。minVertex是边界的左上角点,而maxVertex是右下角点。以下世界定义了一个 8000x8000 的世界:

var worldAABB = new b2AABB();
worldAABB.minVertex.Set(-4000, -4000);
worldAABB.maxVertex.Set(4000, 4000);

注意

Box2D 数学模型中的单位与我们在计算机世界中通常使用的不同。长度单位是米,而不是像素。此外,旋转单位是弧度。

设置世界的重力

我们必须定义世界的重力。重力由b2Vec2定义。b2Vec2是一个 1x2 矩阵的向量。我们可以将其视为 X 和 Y 轴的向量。因此,以下代码定义了向下 300 个单位的重力:

var gravity = new b2Vec2(0, 300);

设置 Box2D 忽略休眠的物体

休眠的物体是一个不再移动或改变状态的动态物体。

物理库计算世界中所有物体的数学数据和碰撞。当世界中有更多物体需要在每一帧中计算时,性能会变慢。在创建物理世界时,我们需要设置库来忽略休眠的物体或计算所有物体。

在我们的游戏中,只有很少的物体,所以性能还不是问题。此外,如果以后我们创建的物体进入空闲或休眠状态,我们将无法再与它们交互。因此,在本例中,我们将此标志设置为 false。

提示

在撰写本书时,只有 Google Chrome 可以在画布中流畅运行 Box2D JavaScript 库。因此,建议在 Google Chrome 中测试游戏,直到其他网络浏览器可以流畅运行为止。

在物理世界中创建一个静态地面物体

现在世界是空的。如果我们要放置物体,那些物体将会掉下来,最终离开我们的视线。现在假设我们想在世界中创建一个静态地面物体,以便物体可以站在那里。我们可以在 Box2D 中做到这一点。

执行在世界中创建地面的操作

执行以下步骤:

  1. 打开html5games.box2dcargame.js JavaScript 文件。

  2. 将以下函数添加到 JavaScript 文件的末尾。它创建一个固定的物体作为游乐场:

function createGround() {
// box shape definition
var groundSd = new b2BoxDef();
groundSd.extents.Set(250, 25);
groundSd.restitution = 0.4;
// body definition with the given shape we just created.
var groundBd = new b2BodyDef();
groundBd.AddShape(groundSd);
groundBd.position.Set(250, 370);
var body = carGame.world.CreateBody(groundBd);
return body;
}

  1. 在创建世界后调用createGround函数如下:
createGround();

  1. 由于我们仍在定义逻辑,并且尚未以可视化的方式呈现物理世界,所以如果我们打开浏览器,我们将看不到任何东西。但是,如果有错误消息,尝试并检查控制台窗口是一个好习惯。

刚才发生了什么?

我们已经使用形状和物体定义创建了一个地面物体。这是一个我们将经常使用的常见过程,用来在世界中创建不同类型的物体。因此,让我们详细了解一下我们是如何做到的。

创建形状

形状定义了几何数据。在 Box2D 的 JavaScript 端口中,形状还定义了密度、摩擦和恢复等材料属性。形状可以是圆形、矩形或多边形。在前面的示例中使用的以下代码定义了一个框形状定义。在框形状中,我们必须通过设置extents属性来定义框的大小。extents属性接受两个参数:半宽和半高。这是一个半值,因此形状的最终面积是该值的四倍:

// box shape definition
var groundSd = new b2BoxDef();
groundSd.extents.Set(250, 25);
groundSd.restitution = 0.4;

创建一个物体

在定义形状之后,我们可以使用给定的形状定义创建一个物体定义。然后,我们设置物体的初始位置,最后要求世界实例根据我们的物体定义创建一个物体。下面的代码显示了我们如何在世界中创建一个物体,给定形状定义:

var groundBd = new b2BodyDef();
groundBd.AddShape(groundSd);
groundBd.position.Set(250, 370);
var body = carGame.world.CreateBody(groundBd);

没有质量的物体被视为静态物体,或固定物体。这些物体是不可移动的,不会与其他静态物体发生碰撞。因此,这些物体可以用作地面或墙壁,成为关卡环境。另一方面,动态物体将根据重力移动并与其他物体发生碰撞。我们稍后将创建一个动态箱子物体。

在画布中绘制物理世界

我们已经创建了一个地面,但它只存在于数学模型中。我们在画布上看不到任何东西,因为我们还没有在上面画任何东西。为了展示物理世界的样子,我们必须根据物理世界画一些东西。

行动时间将物理世界绘制到画布中

执行以下步骤:

  1. 首先,打开html5games.box2dcargame.js JavaScript 文件。

  2. 在页面加载事件处理程序中添加drawWorld函数调用,如下面的代码所示:

$(function() {
// create the world
carGame.world = createWorld();
// create the ground
createGround();
// get the reference of the context
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');
canvasWidth = parseInt(canvas.width);
canvasHeight = parseInt(canvas.height);
// draw the world
drawWorld(carGame.world, ctx);
});

  1. 接下来,打开 Box2D JavaScript 示例代码中的draw_world.js JavaScript 文件。有两个名为drawWorlddrawShapes的函数。将下面的整个文件复制到我们的 JavaScript 文件的末尾:
// drawing functions
function drawWorld(world, context) {
for (var b = world.m_bodyList; b != null; b = b.m_next) {
for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
drawShape(s, context);
}
}
}
// drawShape function directly copy from draw_world.js in Box2dJS library
function drawShape(shape, context) {
physics worldphysics worlddrawing, in canvascontext.strokeStyle = '#003300';
context.beginPath();
switch (shape.m_type) {
case b2Shape.e_circleShape:
var circle = shape;
var pos = circle.m_position;
var r = circle.m_radius;
var segments = 16.0;
var theta = 0.0;
var dtheta = 2.0 * Math.PI / segments;
// draw circle
context.moveTo(pos.x + r, pos.y);
for (var i = 0; i < segments; i++) {
var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
var v = b2Math.AddVV(pos, d);
context.lineTo(v.x, v.y);
theta += dtheta;
}
context.lineTo(pos.x + r, pos.y);
// draw radius
context.moveTo(pos.x, pos.y);
var ax = circle.m_R.col1;
var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
context.lineTo(pos2.x, pos2.y);
break;
case b2Shape.e_polyShape:
var poly = shape;
var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
context.moveTo(tV.x, tV.y);
for (var i = 0; i < poly.m_vertexCount; i++) {
var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
context.lineTo(v.x, v.y);
}
context.lineTo(tV.x, tV.y);
break;
}
context.stroke();
}

  1. 现在重新在浏览器中打开游戏,我们应该在画布中看到地面物体的轮廓,如下面的屏幕截图所示:

行动时间将物理世界绘制到画布中

刚才发生了什么?

我们刚刚创建了一个函数,用于将世界中的每个形状绘制为带有深绿色轮廓的框。

以下代码显示了我们如何循环遍历世界中的每个形状进行绘制:

function drawWorld(world, context) {
for (var b = world.m_bodyList; b != null; b = b.m_next) {
for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
drawShape(s, context);
}
}
}

注意

drawJoint函数和 Box2D JS 库中的相关代码也是如此。这个关节绘制函数对于我们的示例来说是可选的。添加关节绘制函数可以让我们看到连接两个物体之间的不可见关节。

现在我们将看一下drawShape函数。

在每个形状上,我们想在画布中绘制对象的轮廓。在绘制任何东西之前,我们将线条样式设置为深绿色。然后,我们检查形状是圆形、矩形框还是多边形。如果是圆形,我们就使用极坐标来绘制给定形状的半径的圆。如果是多边形,我们就按照以下方式绘制多边形的每一条边:

function drawShape(shape, context) {
context.strokeStyle = '#003300';
context.beginPath();
switch (shape.m_type) {
case b2Shape.e_circleShape:
// Draw the circle in canvas bases on the physics object shape
break;
case b2Shape.e_polyShape:
// Draw the polygon in canvas bases on the physics object shape
break;
}
context.stroke();
}

在物理世界中创建一个动态框

现在想象我们把一个箱子放入世界中。箱子从空中掉下来,最后撞到地面。箱子会弹起一点,最后停在地面上。这与我们在上一节中创建的不同。在上一节中,我们创建了一个静态地面,它是不可移动的,不会受到重力的影响。现在我们将创建一个动态框。

行动时间将动态框放入世界中

执行以下步骤:

  1. 打开我们的 JavaScript 逻辑文件,并将以下框创建代码添加到页面加载事件处理程序中。将代码放在createGround函数之后:
// create a box
var boxSd = new b2BoxDef();
boxSd.density = 1.0;
boxSd.friction = 1.5;
boxSd.restitution = .4;
boxSd.extents.Set(40, 20);
var boxBd = new b2BodyDef();
boxBd.AddShape(boxSd);
boxBd.position.Set(50,210);
carGame.world.CreateBody(boxBd);

  1. 现在我们将在浏览器中测试物理世界。我们应该看到一个箱子被创建在给定的初始位置。然而,箱子并没有掉下来;这是因为我们还有一些事情要做才能让它掉下来:

行动时间将动态框放入世界中

刚才发生了什么?

我们刚刚在世界中创建了一个动态物体。与不可移动的地面物体相比,这个箱子受到重力的影响,并且在碰撞过程中速度会发生变化。当一个物体包含有质量或密度的形状时,它是一个动态物体。否则,它是静态的。因此,我们为我们的箱子定义了一个密度。Box2D 会使它成为动态的,并根据密度和物体的大小自动计算质量。

使用恢复属性设置弹跳效果

恢复值在 0 和 1 之间。在我们的情况下,箱子掉在地面上。当地面和箱子的恢复值都为 0 时,箱子根本不会弹跳。当箱子或地面中的一个恢复值为 1 时,碰撞是完全弹性的。

提示

当两个物体发生碰撞时,碰撞的恢复值是两个物体的恢复值中的最大值。因此,如果一个恢复值为 0.4 的箱子掉在恢复值为 0.6 的地面上,这次碰撞会使用 0.6 来计算弹跳速度。

推进世界时间

箱子是动态的,但它不会掉下来。我们做错了什么吗?答案是否定的。我们已经正确设置了箱子,但是忘记在物理世界中推进时间。

在 Box2D 物理世界中,所有计算都是按照系统化的迭代进行的。世界根据当前步骤计算所有事物的物理变换。当我们将“步骤”移动到下一个级别时,世界会根据新状态再次进行计算。

进行操作 设置世界步骤循环

我们将通过以下步骤推进世界时间:

  1. 为了推进世界步骤,我们必须定期调用世界实例中的step函数。我们使用setTimeout来不断调用step函数。将以下函数放入我们的 JavaScript 逻辑文件中:
function step() {
world.Step(1.0/60, 1);
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
drawWorld(carGame.world, ctx);
setTimeout(step, 10);
}

  1. 接下来,我们将通过在文档准备好的事件处理程序中调用第一个step函数来启动世界。将以下突出显示的代码添加到加载处理程序函数中:
$(function() {
…
// start advancing the step
step();
});

  1. 我们将在浏览器中再次模拟世界。箱子被创建在初始化位置并正确地落在地面上。以下截图显示了箱子落在地面上的顺序:

进行操作 设置世界步骤循环

刚才发生了什么?

我们已经推进了世界的时间。现在物理库每 10 毫秒模拟一次世界。

step函数类似于我们在第二章,使用基于 DOM 的游戏开发入门中的gameloop函数。它定期执行以计算游戏的新状态。

为游戏添加车轮

现在我们在游戏中有一个箱子。现在想象我们创建两个圆形的车轮。然后,我们将拥有汽车的基本组件,车身和车轮。

进行操作 将两个圆放入世界中

我们将通过以下步骤向世界中添加两个圆:

  1. 打开html5games.box2dcargame.js JavaScript 文件以添加车轮物体。

  2. 在箱子创建代码之后添加以下代码。它调用了我们将编写的createWheel函数来创建一个圆形的物体:

// create two wheels in the world
createWheel(carGame.world, 25, 230);
createWheel(carGame.world, 75, 230);

  1. 现在让我们来处理createWheel函数。我们设计这个函数在给定的世界中以给定的 x 和 y 坐标创建一个圆形的物体。将以下函数放入我们的 JavaScript 逻辑文件中:
function createWheel(world, x, y) {
// wheel circle definition
var ballSd = new b2CircleDef();
ballSd.density = 1.0;
ballSd.radius = 10;
ballSd.restitution = 0.1;
ballSd.friction = 4.3;
// body definition
var ballBd = new b2BodyDef();
ballBd.AddShape(ballSd);
ballBd.position.Set(x,y);
return world.CreateBody(ballBd);
}

  1. 现在我们将在 Web 浏览器中重新加载物理世界。这次,我们应该看到类似以下截图的结果,其中有一个箱子和两个车轮从空中掉下来。这些物体与其他物体碰撞并在撞到墙壁时弹开:

进行操作 将两个圆放入世界中

刚才发生了什么?

在模拟物理世界时,箱子和车轮都会掉下来并相互碰撞以及与地面碰撞。

创建圆形物体类似于创建方形物体。唯一的区别是我们使用CircleDef类而不是方形形状定义。在圆形定义中,我们使用radius属性而不是extents属性来定义圆的大小。

创建一个物理汽车

我们已经准备好了汽车箱体和两个轮子箱体。我们离制作汽车只差一步。现在想象我们有一种胶水可以把车轮粘在车身上。然后,汽车和轮子就不会再分开,我们就会有一辆车。我们可以使用关节来实现这一点。在本节中,我们将使用joint将车轮和车身粘在一起。

执行连接框和两个圆的旋转关节的操作的时间

执行以下步骤:

  1. 我们仍然只在逻辑部分工作。在文本编辑器中打开我们的 JavaScript 逻辑文件。

  2. 在文档顶部添加以下全局变量,以引用汽车车身:

var car;

  1. 创建一个名为createCarAt的函数,它接受坐标作为参数。然后,我们将身体和轮子创建代码移到这个函数中。然后,添加以下突出显示的关节创建代码。最后,返回汽车车身:
function createCarAt(x, y) {
// the car box definition
var boxSd = new b2BoxDef();
boxSd.density = 1.0;
boxSd.friction = 1.5;
boxSd.restitution = .4;
boxSd.extents.Set(40, 20);
// the car body definition
var boxBd = new b2BodyDef();
boxBd.AddShape(boxSd);
boxBd.position.Set(x,y);
var carBody = carGame.world.CreateBody(boxBd);
// creating the wheels
var wheelBody1 = createWheel(carGame.world, x-25, y+20);
var wheelBody2 = createWheel(carGame.world, x+25, y+20);
// create a joint to connect left wheel with the car body
var jointDef = new b2RevoluteJointDef();
jointDef.anchorPoint.Set(x-25, y+20);
jointDef.body1 = carBody;
jointDef.body2 = wheelBody1;
carGame.world.CreateJoint(jointDef);
// create a joint to connect right wheel with the car body
var jointDef = new b2RevoluteJointDef();
jointDef.anchorPoint.Set(x+25, y+20);
jointDef.body1 = carBody;
jointDef.body2 = wheelBody2;
carGame.world.CreateJoint(jointDef);
return carBody;
}

  1. 然后,我们只需要创建一个具有初始位置的汽车。在创建世界之后,将以下代码添加到页面加载事件处理程序中:
// create a car
car = createCarAt(50, 210);

  1. 是时候保存文件并在浏览器中运行物理世界了。此时,车轮和车身不是分开的部分。它们像一辆车一样粘在一起,正确地掉在地面上,如下面的截图所示:

执行连接框和两个圆的旋转关节的操作的时间

刚才发生了什么?

关节对于在两个身体之间(或者在一个身体和世界之间)添加约束很有用。有许多种类型的关节,我们在这个例子中使用的是旋转关节

使用旋转关节在两个身体之间创建一个锚点

旋转关节使用一个公共锚点将两个身体粘在一起。然后,这两个身体被粘在一起,只允许基于公共锚点旋转。下面截图的左侧显示了两个身体是如何连接的。在我们的代码示例中,我们将锚点设置为轮子的中心点。下面截图的右侧显示了我们如何设置关节。轮子因为旋转原点在中心而旋转。这种设置使得汽车和轮子看起来很真实:

使用旋转关节在两个身体之间创建一个锚点

还有其他类型的关节,它们以不同的方式很有用。关节在创建游戏环境中很有用,因为有几种类型的关节,每种关节类型都值得一试,你应该考虑如何使用它们。以下链接是 Box2D 手册,解释了每种类型的关节以及我们如何在不同的环境设置中使用它们:

www.box2d.org/manual.html#_Toc258082974

通过键盘输入对汽车施加力

现在我们已经准备好了汽车。让我们用键盘移动它。

执行对汽车施加力的操作

执行以下步骤:

  1. 在文本编辑器中打开html5games.box2dcargame.js JavaScript 文件。

  2. 在页面加载事件处理程序中,我们在开头添加了以下keydown事件处理程序。它监听X键和Z键以在不同方向施加力:

// Keyboard event
$(document).keydown(function(e) {
switch(e.keyCode) {
case 88: // x key to apply force towards right
var force = new b2Vec2(10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
break;
case 90: // z key to apply force towards left
var force = new b2Vec2(-10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
break;
}
});

  1. 就是这样。保存文件并在浏览器中运行我们的游戏。当你按下XZ键时,汽车就会开始移动。如果你一直按着键,世界就会不断给汽车施加力量,让它飞走:

执行对汽车施加力的操作的时间

刚才发生了什么?

我们刚刚创建了与我们的汽车车身的交互。我们可以通过按下ZX键来左右移动汽车。现在游戏似乎变得有趣起来了。

对身体施加力

我们可以通过调用ApplyForce函数向任何身体施加力。以下代码显示了该函数的用法:

body.ApplyForce(force, point);

这个函数接受两个参数,列在下表中:

参数 类型 讨论
force b2Vec2 要施加到物体上的力向量
point b2Vec2 施加力的点

理解 ApplyForce 和 ApplyImpulse 之间的区别

除了ApplyForce函数,我们还可以使用ApplyImpulse函数移动任何物体。这两个函数都可以移动物体,但它们的移动方式不同。如果我们想改变物体的瞬时速度,那么我们可以在物体上使用ApplyImpulse一次,将速度改变为目标值。另一方面,我们需要不断地对物体施加力以增加速度。

例如,我们想要增加汽车的速度,就像踩油门一样。在这种情况下,我们对汽车施加力。如果我们正在创建一个需要启动球的球类游戏,我们可以使用ApplyImpulse函数向球体添加一个瞬时冲量。

试一试吧

你能想到另一种情况吗,我们需要对物体施加力或冲量吗?

向我们的游戏环境添加坡道

现在我们可以移动汽车。然而,环境还不够有趣。现在想象一下,有一些坡道供汽车跳跃,两个平台之间有一个间隙,玩家必须飞过汽车。使用不同的坡道设置玩起来会更有趣。

时间行动 创建具有坡道的世界

执行以下步骤:

  1. 我们将打开游戏逻辑 JavaScript 文件。

  2. 将当前的地面创建代码移入一个名为createGround的新函数中。然后,更改代码以使用给定的四个参数,如下所示:

function createGround(x, y, width, height, rotation) {
// box shape definition
var groundSd = new b2BoxDef();
groundSd.extents.Set(width, height);
groundSd.restitution = 0.4;
// body definition with the given shape we just created.
var groundBd = new b2BodyDef();
groundBd.AddShape(groundSd);
groundBd.position.Set(x, y);
groundBd.rotation = rotation * Math.PI / 180;
var body = carGame.world.CreateBody(groundBd);
return body;
}

  1. 现在我们有一个创建地面物体的函数。我们将用以下代码替换页面加载处理程序函数中的地面创建代码:
// create the ground
createGround(250, 270, 250, 25, 0);
// create a ramp
createGround(500, 250, 65, 15, -10);
createGround(600, 225, 80, 15, -20);
createGround(1100, 250, 100, 15, 0);

  1. 保存文件并在浏览器中预览游戏。我们应该看到一个坡道和一个目的地平台,如下截图所示。尝试控制汽车,跳过坡道,到达目的地而不掉下来。如果失败,刷新页面重新开始游戏:

时间行动 创建具有坡道的世界

刚才发生了什么?

我们刚刚将地面箱子创建代码封装到一个函数中,这样我们就可以轻松地创建一组地面物体。这些地面物体构成了游戏的级别环境。

此外,这是我们第一次旋转物体。我们使用rotation属性设置物体的旋转,该属性以弧度值为参数。大多数人可能习惯于度单位;我们可以使用以下公式从度获取弧度值:

groundBd.rotation = degree * Math.PI / 180;

通过设置箱子的旋转,我们可以在游戏中设置不同坡度的坡道。

试一试吧 创建具有不同连接器的不同环境

现在我们已经设置了一个坡道,并且可以在环境中玩汽车。如何使用不同类型的连接器来设置游乐场?例如,使用滑轮连接器作为升降机怎么样?另一方面,包括一个带有中心连接器的动态板怎么样?

在 Box2D 世界中检查碰撞

Box2D 物理库会自动计算所有碰撞。现在想象一下,我们设置了一个地面物体作为目的地。玩家成功将汽车移动到目的地时获胜。由于 Box2D 已经计算了所有碰撞,我们所要做的就是获取检测到的碰撞列表,并确定我们的汽车是否撞到了目的地地面。

时间行动 检查汽车和目的地物体之间的碰撞

执行以下步骤:

  1. 同样,我们从游戏逻辑开始。在文本编辑器中打开html5games.box2dcargame.js JavaScript 文件。

  2. 我们在地面创建代码中设置了一个目标地面,并将其分配给carGame全局对象实例内的gamewinWall引用,如下所示:

carGame.gamewinWall = createGround(1200, 215, 15, 25, 0);

  1. 接下来,我们转向step函数。在每一步中,我们从世界中获取完整的接触列表,并检查是否有任何两个相互碰撞的对象是汽车和目标地面:
function step() {
carGame.world.Step(1.0/60, 1);
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
drawWorld(carGame.world, ctx);
setTimeout(step, 10);
//loop all contact list to check if the car hits the winning wall
for (var cn = carGame.world.GetContactList(); cn != null; cn = cn.GetNext()) {
var body1 = cn.GetShape1().GetBody();
var body2 = cn.GetShape2().GetBody();
if ((body1 == carGame.car && body2 == carGame.gamewinWall) ||
(body2 == carGame.car && body1 == carGame.gamewinWall))
{
console.log("Level Passed!");
}
}
}

  1. 现在保存代码并再次在浏览器中打开游戏。这一次,我们必须打开控制台窗口,以跟踪当汽车撞到墙时是否获得Level Passed!输出。尝试完成游戏,我们应该在汽车到达目的地后在控制台中看到输出:

执行检查汽车和目的地物体之间的碰撞

刚刚发生了什么?

我们刚刚通过检查碰撞联系人创建了游戏获胜逻辑。当汽车成功到达目的地地面物体时,玩家获胜。

获取碰撞联系人列表

在每个步骤中,Box2D 计算所有碰撞并将它们放入world实例中的contact list中。我们可以使用carGame.world.GetContactList()函数获取联系人列表。返回的联系人列表是一个链接列表。我们可以通过以下 for 循环遍历整个链接列表:

for (var cn = carGame.world.GetContactList(); cn != null; cn = cn.GetNext()) {
// We have shape 1 and shape 2 of each contact node.
// cn.GetShape1();
// cn.GetShape2();
}

当我们获得碰撞的形状时,我们检查该形状的主体是否是汽车或目的地主体。由于汽车形状可能在形状 1 或形状 2 中,gamewinWall也是如此,我们使用以下代码来检查两种组合:

var body1 = cn.GetShape1().GetBody();
var body2 = cn.GetShape2().GetBody();
if ((body1 == carGame.car && body2 == carGame.gamewinWall) ||
(body2 == carGame.car && body1 == carGame.gamewinWall))
{
console.log("Level Passed!");
}

试试看英雄

我们在第七章,使用本地存储存储游戏数据中创建了一个游戏结束对话框。在这里使用该技术创建一个对话框,显示玩家通过了级别,怎么样?当我们向游戏添加不同的级别设置时,它也将作为级别过渡的工具。

重新开始游戏

您可能已经尝试在上一个示例中多次刷新页面,以使汽车成功跳到目的地。现在想象一下,我们可以按键重新初始化世界。然后,我们可以按照试错的方法直到成功。

按下 R 键重新启动游戏的时间

我们将R键指定为游戏的重新启动键:

  1. 再次,我们只需要更改 JavaScript 文件。在文本编辑器中打开html5games.box2dcargame.js JavaScript 文件。

  2. 我们将创建世界、坡道和汽车代码移入名为restartGame的函数中。它们最初位于页面加载处理程序函数中:

function restartGame() {
// create the world
carGame.world = createWorld();
// create the ground
createGround(250, 270, 250, 25, 0);
// create a ramp
createGround(500, 250, 65, 15, -10);
createGround(600, 225, 80, 15, -20);
createGround(1100, 250, 100, 15, 0);
// create a destination ground
carGame.gamewinWall = createGround(1200, 215, 15, 25, 0);
// create a car
carGame.car = createCarAt(50, 210);
}

  1. 然后,在页面加载事件处理程序中,我们调用restartGame函数来初始化游戏,如下所示:
restartGame();

  1. 最后,我们将以下突出显示的代码添加到keydown处理程序中,以在按下R键时重新启动游戏:
$(document).keydown(function(e) {
switch(e.keyCode) {
case 88: // x key to apply force towards right
var force = new b2Vec2(10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
break;
case 90: // z key to apply force towards left
var force = new b2Vec2(-10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
break;
case 82: // r key to restart the game
restartGame();
break;
}
});

  1. 当玩家通过级别时,怎么样重新开始游戏?将以下突出显示的代码添加到游戏获胜逻辑中:
if ((cn.GetShape1().GetBody() == carGame.car && cn.GetShape2().GetBody() == carGame.gamewinWall) ||
(cn.GetShape2().GetBody() == carGame.car && cn.GetShape1().GetBody() == carGame.gamewinWall))
{
console.log("Level Passed!");
restartGame();
}

  1. 现在是时候在浏览器中测试游戏了。尝试玩游戏并按R键重新启动游戏。

刚刚发生了什么?

我们重构我们的代码来创建一个restartGame函数。每次调用此函数时,世界都会被销毁并重新初始化。我们可以通过创建我们的世界变量的新世界实例来销毁现有世界并创建一个新的空世界,如下所示:

carGame.world = createWorld();

试试看英雄 创建游戏结束墙

现在重新启动游戏的唯一方法是按重新启动键。在世界底部创建一个地面,检查任何下落的汽车怎么样?当汽车掉落并撞到底部地面时,我们知道玩家失败了,然后重新开始游戏。

为我们的汽车游戏添加级别支持

现在想象一下,当完成每个游戏时,我们可以升级到下一个环境设置。对于每个级别,我们将需要几个环境设置。

加载具有级别数据的游戏的时间

我们将重构我们的代码以支持从级别数据结构加载静态地面物体。让我们通过以下步骤来完成它:

  1. 在文本编辑器中打开html5games.box2dcargame.js JavaScript 文件。

  2. 我们将需要每个级别的地面设置。将以下代码放在 JavaScript 文件的顶部。这是一个级别数组。每个级别都是另一个对象数组,其中包含静态地面物体的位置、尺寸和旋转:

carGame.levels = new Array();
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":"box","x":1100,"y":250,"width":100,"height":15, "rotation":0},
{"type":"win","x":1200,"y":215,"width":15,"height":25, "rotation":0}];
carGame.levels[1] = [{"type":"car","x":50,"y":210,"fuel":20},
{"type":"box","x":100, "y":270, "width":190, "height":15, "rotation":20},
{"type":"box","x":380, "y":320, "width":100, "height":15, "rotation":-10},
{"type":"box","x":666,"y":285,"width":80,"height":15, "rotation":-32},
{"type":"box","x":950,"y":295,"width":80,"height":15, "rotation":20},
{"type":"box","x":1100,"y":310,"width":100,"height":15, "rotation":0},
{"type":"win","x":1200,"y":275,"width":15,"height":25, "rotation":0}];
car gamecar gamelevels data, loadingcarGame.levels[2] = [{"type":"car","x":50,"y":210,"fuel":20},
{"type":"box","x":100, "y":270, "width":190, "height":15, "rotation":20},
{"type":"box","x":380, "y":320, "width":100, "height":15, "rotation":-10},
{"type":"box","x":686,"y":285,"width":80,"height":15, "rotation":-32},
{"type":"box","x":250,"y":495,"width":80,"height":15, "rotation":40},
{"type":"box","x":500,"y":540,"width":200,"height":15, "rotation":0},
{"type":"win","x":220,"y":425,"width":15,"height":25, "rotation":23}];

  1. 然后,我们使用carGame对象实例中的以下变量来存储当前级别:
var carGame = {
currentLevel: 0
}

  1. 用以下代码替换restartGame函数。它将函数更改为接受一个level参数。然后,根据关卡数据创建地面或汽车:
function restartGame(level) {
carGame.currentLevel = level;
// create the world
carGame.world = createWorld();
// create a ground in our newly created world
// load the ground info from level data
for(var i=0;i<carGame.levels[level].length;i++) {
var obj = carGame.levels[level][i];
// create car
if (obj.type == "car") {
carGame.car = createCarAt(obj.x,obj.y);
continue;
}
var groundBody = createGround(obj.x, obj.y, obj.width, obj.height, obj.rotation);
if (obj.type == "win") {
carGame.gamewinWall = groundBody;
}
}
}

  1. 在页面加载处理程序函数中,我们通过提供currentLevel来更改restartGame函数的调用:
restartGame(carGame.currentLevel);

  1. 我们还需要在重启键处理程序中提供currentLevel值:
case 82: // r key to restart the game
restartGame(carGame.currentLevel);
break;

  1. 最后,在游戏获胜逻辑中更改以下突出显示的代码。当汽车撞到目的地时,我们升级游戏:
if ((body1 == carGame.car && body2 == carGame.gamewinWall) ||
(body2 == carGame.car && body1 == carGame.gamewinWall))
{
console.log("Level Passed!");
restartGame(carGame.currentLevel+1);
}

  1. 我们现在将在 Web 浏览器中运行游戏。完成关卡后,游戏应该重新开始下一关:

Time for action Loading game with levels data

刚刚发生了什么?

我们刚刚创建了一个数据结构来存储关卡。然后,我们根据给定的关卡号创建了游戏,并使用关卡数据构建了世界。

每个关卡数据都是一个对象数组。每个对象包含世界中每个地面物体的属性。这包括基本属性,如位置、大小和旋转。还有一个名为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(var i=0;i<carGame.levels[level].length;i++) {
var obj = carGame.levels[level][i];
// create car
if (obj.type == "car") {
carGame.car = createCarAt(obj.x,obj.y);
continue;
}
var groundBody = createGround(obj.x, obj.y, obj.width, obj.height, obj.rotation);
if (obj.type == "win") {
carGame.gamewinWall = groundBody;
car gamecar gamelevels data, loading}
}

尝试创建更多关卡

现在我们已经为游戏设置了几个关卡。如何复制关卡数据以创建更有趣的关卡来玩?创建你自己的关卡并玩耍。就像一个孩子搭积木玩一样。

用图形替换 Box2D 轮廓绘图

我们已经创建了一个至少可以玩几个关卡的游戏。然而,它们只是一些轮廓框。我们甚至无法区分游戏中的目的地和其他地面物体。现在想象一下,目的地是一个赛车旗,有一辆汽车图形来代表它。这将使游戏目的更加清晰。

添加旗帜图形和汽车图形到游戏

执行以下步骤:

  1. 首先,我们需要下载这个示例所需的图形。转到以下链接下载图形:

gamedesign.cc/html5games/1260_09_example_graphics.zip

  1. images文件夹中提取 ZIP 文件。

  2. 现在是时候编辑index.htm文件了。在 body 中添加以下 HTML 标记:

<div id="asset">
<img id="flag" src='images/flag.png'>
<img id="bus" src="img/bus.png">
<img id="wheel" src="img/wheel.png">
</div>

  1. 我们想要隐藏包含我们img标签的资产 DIV。打开cargame.css文件,并添加以下 CSS 规则以使资产 DIV 不可见:
#asset {
position: absolute;
top: -99999px;
}

  1. 现在我们将进入逻辑部分。打开html5games.box2dcargame.js JavaScript 文件。

  2. createGround函数中,我们添加一个名为type的新参数以传递类型。然后,如果是获胜的目的地地面,我们添加了突出显示的代码来分配flag图像的引用给地面形状的用户数据:

function createGround(x, y, width, height, rotation, type) {
// box shape definition
var groundSd = new b2BoxDef();
groundSd.extents.Set(width, height);
groundSd.restitution = 0.4;
if (type == "win") {
groundSd.userData = document.getElementById('flag');
}
…
}

  1. 在创建地面时,现在需要传递type属性。用以下代码替换地面创建代码:
var groundBody = createGround(obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.type);

  1. 接下来,我们将bus图像标签的引用分配给汽车形状的用户数据。将以下突出显示的代码添加到汽车框定义创建中:
// the car box definition
var boxSd = new b2BoxDef();
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)给出列表中的第一个原始文档元素

  1. 然后,我们需要处理车轮。我们将wheel图像标签分配给车轮的userData属性。将以下突出显示的代码添加到createWheel函数中:
function createWheel(world, x, y) {
// wheel circle definition
var ballSd = new b2CircleDef();
ballSd.density = 1.0;
ballSd.radius = 10;
ballSd.restitution = 0.1;
ballSd.friction = 4.3;
ballSd.userData = document.getElementById('wheel');
…
}

  1. 最后,我们必须在画布中绘制图像。用以下代码替换drawWorld函数。突出显示的代码是更改的部分:
function drawWorld(world, context) {
for (var b = world.m_bodyList; b != null; b = b.m_next) {
for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
if (s.GetUserData() != undefined) {
// the user data contains the reference to the image
var img = s.GetUserData();
// the x and y of the image.
// We have to substract the half width/height
var x = s.GetPosition().x;
var y = s.GetPosition().y;
var topleftX = - $(img).width()/2;
var topleftY = - $(img).height()/2;
context.save();
context.translate(x,y);
context.rotate(s.GetBody().GetRotation());
context.drawImage(img, topleftX, topleftY);
context.restore();
} else {
drawShape(s, context);
}
}
}
}

  1. 最后,保存所有文件并在 Web 浏览器中运行游戏。我们应该看到一个黄色的公共汽车图形,两个车轮和一个旗帜作为目的地。现在玩游戏,当公共汽车撞到旗帜时游戏应该进入下一关:

Time for action Adding a flag graphic and a car graphic to the game

刚才发生了什么?

我们现在以最少的图形呈现我们的游戏。至少,玩家可以轻松知道他们在控制什么,以及他们应该去哪里。

Box2D 库使用画布来渲染物理世界。因此,我们学到的所有关于画布的技术都可以应用在这里。在第五章,构建一个Canvas Games Masterclass中,我们学习了使用drawImage函数在画布中显示图像。我们使用这种技术在物理世界的画布上绘制旗帜图形。

在形状和物体中使用 userData

我们如何知道哪个物理体需要显示为旗帜图像?每个 Box2D 形状和物体中都有一个名为userData的属性。此属性用于存储与该形状或物体相关的任何自定义数据。例如,我们可以存储图形文件的文件名,或者直接存储图像标签的引用。

我们有一个图像标签列表,引用了游戏中需要的图形资源。然而,我们不想显示这些图像标签,它们只是用于加载和引用。我们通过以下 CSS 样式将这些资源图像标签隐藏在 HTML 边界之外。我们不使用display:none,因为我们无法获取根本没有显示的元素的宽度和高度。我们需要宽度和高度来正确定位物理世界中的图形:

#asset {
position: absolute;
top: -99999px;
}

根据其物理体的状态在每帧绘制图形

从 Box2D 绘制只是用于开发,然后我们用我们的图形替换它。

以下代码检查形状是否分配了用户数据。在我们的示例中,用户数据用于引用该图形资源的image标签。我们获取图像标签并将其传递给画布上下文的drawImage函数进行绘制。

Box2D 中的所有盒形和圆形形状的原点都在中心。然而,在画布中绘制图像需要左上角点。因此,我们有 x/y 坐标和左上角 x/y 点的偏移量,这是图像宽度和高度的负一半:

if (s.GetUserData() != undefined) {
// the user data contains the reference to the image
var img = s.GetUserData();
// the x and y of the image.
// We have to substract the half width/height
var x = s.GetPosition().x;
var y = s.GetPosition().y;
var topleftX = - $(img).width()/2;
var topleftY = - $(img).height()/2;
context.save();
context.translate(x,y);
context.rotate(s.GetBody().GetRotation());
context.drawImage(img, topleftX, topleftY);
context.restore();
}

在画布中旋转和平移图像

我们使用drawImage函数直接绘制图像与坐标。然而,在这里情况不同。我们需要旋转绘制的图像。这是通过在绘制之前旋转上下文,然后在绘制后恢复旋转来完成的。我们可以通过保存上下文状态,平移它,旋转它,然后调用restore函数来实现这一点。以下代码显示了我们如何在给定位置和旋转角度绘制图像。topleftXtopleftY是从图像中心原点到左上角点的偏移距离:

context.save();
context.translate(x,y);
context.rotate(s.GetBody().GetRotation());
context.drawImage(img, topleftX, topleftY);
context.restore();

提示

我们不需要使物理体积与其图形完全相同。例如,如果我们有一个圆形的鸡,我们可以通过一个球体来在物理世界中表示它。使用简单的物理体可以大大提高性能。

尝试一下,将之前学到的技术应用到汽车游戏中

我们已经学会了使用 CSS3 过渡来为记分牌添加动画。将它应用到这个汽车游戏怎么样?此外,怎么样给汽车添加一些引擎声音?尝试应用我们通过这本书学到的知识,为玩家提供完整的游戏体验。

添加最后的修饰,使游戏更有趣

现在想象我们想要发布游戏。游戏逻辑基本上已经完成,但是在黑白环境下看起来相当丑陋。在本节中,我们将为游戏添加一些最后的修饰,使其更具吸引力。我们还将应用一些限制来限制 ApplyForce 的时间。这种限制使游戏更有趣,因为它要求玩家在对汽车施加过多力之前先考虑。

行动时间 装饰游戏并添加燃料限制

执行以下步骤:

  1. 首先,我们需要一些起始画面、游戏获胜画面和每个级别的环境背景的背景图像。这些图形可以从名为box2d_final_game的代码包中找到。以下截图显示了本节中所需的图形:行动时间 装饰游戏并添加燃料限制

  2. 打开index.htm文件,并用以下标记替换画布元素。它创建了两个更多的游戏组件,名为当前级别和剩余燃料,并将游戏组件分组到一个game-container DIV 中:

<section id="game-container">
<canvas id="game" width='1300' height='600' class="startscreen"></canvas>
<div id="fuel" class="progressbar">
<div class="fuel-value" style="width: 100%;"></div>
</div>
<div id="level"></div>
</section>

  1. 接下来,我们将从代码包中复制cargame.css文件。它包含了游戏的几个类样式定义。当我们应用新的样式表时,游戏应该看起来类似于以下截图中显示的游戏:

  2. 现在我们将继续进行 JavaScript 部分。打开html5games.box2dcargame.js文件。

  3. 使用以下额外变量更新carGame对象声明:

var carGame = {
// game state constant
STATE_STARTING_SCREEN : 1,
STATE_PLAYING : 2,
STATE_GAMEOVER_SCREEN : 3,
state : 0,
fuel: 0,
fuelMax: 0,
currentLevel: 0
}

  1. 现在我们有了起始画面。页面加载后不再立即开始游戏。我们显示起始画面,并等待玩家点击游戏画布。在页面ready函数中添加以下逻辑:
// set the game state as "starting screen"
carGame.state = carGame.STATE_STARTING_SCREEN;
// start the game when clicking anywhere in starting screen
$('#game').click(function(){
if (carGame.state == carGame.STATE_STARTING_SCREEN)
{
// change the state to playing.
carGame.state = carGame.STATE_PLAYING;
// start new game
restartGame(carGame.currentLevel);
// start advancing the step
step();
}
});

  1. 我们需要在页面ready函数的末尾删除原始的step()函数调用,因为我们在鼠标点击时调用它。

  2. 接下来,我们需要处理玩家通过所有级别时的游戏获胜画面。在获胜旗帜碰撞检查逻辑中,我们用以下逻辑替换了原始的restartGame函数调用,该逻辑检查我们是显示下一个级别还是结束画面:

if (currentLevel < 4)
{
restartGame(currentLevel+1);
}
else
{
// show game over screen
$('#game').removeClass().addClass('gamebg_won');
// clear the physics world
world = createWorld();
}

  1. 然后,我们将处理游戏播放背景。我们为每个级别设置准备了每个游戏背景。我们将在restartGame函数中切换背景,该函数响应重构世界:
$("#level").html("Level " + (level+1));
// change the background image to fit the level
$('#game').removeClass().addClass('gamebg_level'+level);

  1. 现在游戏图形已经完成,我们不再需要物理对象轮廓绘制。我们可以在drawWorld函数中删除drawShape(s, context)的代码。

  2. 最后,让我们添加一些限制。请记住,在我们的级别数据中,我们包括了一些神秘的燃料数据给汽车。它是一个指示器,指示汽车包含多少燃料。我们将使用这个燃料来限制玩家的输入。每次对汽车施加力时,燃料都会减少。一旦燃料用完,玩家就不能再施加额外的力。这种限制使游戏更有趣:

  3. 使用以下逻辑更新xzkeydown函数:

case 88: // x key to apply force towards right
if (carGame.fuel > 0)
{
var force = new b2Vec2(10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
carGame.fuel--;
$(".fuel-value").width(carGame.fuel/carGame.fuelMax * 100 +'%');
}
break;
case 90: // z key to apply force towards left
if (carGame.fuel > 0)
{
var force = new b2Vec2(-10000000, 0);
carGame.car.ApplyForce (force, carGame.car.GetCenterPosition());
carGame.fuel--;
$(".fuel-value").width(carGame.fuel/carGame.fuelMax * 100 +'%');
}
break;

  1. 此外,在重新开始游戏函数中的汽车创建逻辑中,我们初始化燃料如下:
// create car
if (obj.type == "car")
{
carGame.car = createCarAt(obj.x,obj.y);
carGame.fuel = obj.fuel;
carGame.fuelMax = obj.fuel;
$(".fuel-value").width('100%');
continue;
}

  1. 现在在浏览器中运行游戏。我们应该得到五个图形级别。以下截图显示了最后四个级别的外观:行动时间 装饰游戏并添加燃料限制

  2. 通过所有级别后,我们得到以下获胜画面:

行动时间 装饰游戏并添加燃料限制

刚刚发生了什么?

我们刚刚用更多的图形装饰了我们的游戏。我们还为每个级别环境绘制了背景图像。以下截图说明了视觉地面如何表示逻辑物理框。与汽车和获胜旗帜不同,地面图形与物理地面无关。它只是一个背景图像,其图形位于各自的位置。我们可以使用这种方法,因为这些框永远不会移动:

刚刚发生了什么?

然后,我们可以为每个级别准备几种 CSS 样式,类名中带有级别编号,例如.gamebg_level_1.gamebg_level_2。通过将每个类与每个级别的背景链接起来,我们可以在切换级别时更改背景,如下代码所示:

$('#game').removeClass().addClass('gamebg_level'+level);

添加燃料以在施加力时增加约束

现在我们通过提供有限的燃料来限制玩家的输入。当玩家对汽车施加力时,燃料会减少。我们使用以下keydown逻辑来减少燃料并在燃料耗尽时阻止额外的力量:

case 88: // x key to apply force towards right
if (carGame.fuel > 0)
{
var force = new b2Vec2(10000000, 0);
carGame.car.ApplyForce(force, carGame.car.GetCenterPosition());
carGame.fuel--;
$(".fuel-value").width(carGame.fuel/carGame.fuelMax * 100 +'%');
}

在 CSS3 进度条中呈现剩余燃料

在我们的游戏中,我们将剩余燃料呈现为进度条。进度条实际上是另一个DIV内部的DIV。以下标记显示了进度条的结构。外部DIV定义了最大值,内部DIV显示了实际值:

<div id="fuel" class="progressbar">
<div class="fuel-value" style="width: 100%;"></div>
</div>

以下截图说明了进度条的结构:

在 CSS3 进度条中呈现剩余燃料

有了这个结构,我们可以通过将宽度设置为百分比值来显示特定的进度。我们使用以下代码根据燃料的百分比来更新进度条:

$(".fuel-value").width(carGame.fuel/carGame.fuelMax * 100 +'%');

这是设置进度条并使用宽度样式控制的基本逻辑。此外,我们给进度条的背景添加了漂亮的渐变,如下截图所示:

在 CSS3 进度条中呈现剩余燃料

这是在样式表中完成的,使用以下 CSS3 渐变背景定义:

.progressbar {
background: -webkit-gradient(linear, left top, left bottom, 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, left top, left bottom, color-stop(0%,#A8D751), color-stop(48%,#A8D751), color-stop(51%,#275606), color-stop(54%,#4A8A49), color-stop(100%,#4A8A49));
}

总结

在本章中,我们学到了如何使用 Box2D 物理引擎在画布中创建汽车冒险游戏。

具体来说,我们涵盖了以下主题:

  • 安装 JavaScript 移植的物理引擎

  • 在物理世界中创建静态和动态物体

  • 使用关节来设置汽车的约束和车轮

  • 使用原型库获取键盘输入

  • 通过向汽车添加力与其进行交互

  • 在物理世界中检查碰撞作为级别目的地

  • 将图像绘制为替换我们的物理游戏对象轮廓

我们还讨论了添加燃料条以限制玩家的输入,增加游戏乐趣。

我们现在已经学会了使用 Box2D 物理库来创建基于画布的物理游戏。

我们通过九章讨论了使用 CSS3 和 JavaScript 制作 HTML5 游戏的不同方面。我们学会了在 DOM 中构建传统的乒乓球游戏,在 CSS3 中构建卡片匹配游戏,并在画布中创建了一个解谜游戏。然后,我们探索了向游戏添加声音,并围绕它创建了一个迷你钢琴音乐游戏。接下来,我们讨论了使用本地存储保存和加载游戏状态。此外,我们尝试使用 WebSockets 构建了一个实时多人游戏。最后,在本章中,我们创建了一个带有物理引擎的汽车游戏。

在整本书中,我们构建了不同类型的游戏,并学习了一些制作 HTML5 游戏所需的基本技术。下一步是继续开发自己的游戏。为了帮助开发自己的游戏,有一些资源可以提供帮助。以下列表提供了一些 HTML5 游戏开发的有用链接:

HTML5 游戏引擎

游戏精灵和纹理

音效

附录 A. 小测验答案

第二章:开始使用基于 DOM 的游戏开发

在页面准备好后运行我们的代码

1 d

理解绝对位置的行为

1 c

第三章:在 CSS3 中构建记忆匹配游戏

使用 HTML5 自定义数据属性存储内部自定义数据

1 c 或 d

使用 jQuery 访问自定义数据属性

1 a 和 d

第四章:使用画布和绘图 API 构建解开游戏

使用 startAngle 和 endAngle

1 c

仅使用 fill 命令关闭路径

1 b

在画布中访问形状

1 b
2 d

清除画布中绘制的形状

1 a
2 b

第五章:构建画布游戏大师班

在画布中绘制文本

1 c
2 b

样式化画布背景

1 b

第六章:为游戏添加音效

使用音频标签

1 b
2 请在之间放置回退内容

第七章:使用本地存储存储游戏数据

使用本地存储

1 False
2 True
3 True

第八章:使用 WebSockets 构建多人绘画和猜词游戏

1 参考与 Web Sockets 部分相关的内容在 WebSockets 方法中,请求的数量比轮询方法少得多。这是因为客户端和服务器之间的连接是持久的。一旦建立连接,只有在有任何更新时才会从客户端或服务器端发送请求。例如,当客户端想要向服务器更新某些内容时,它会向服务器发送消息。服务器也只在需要通知客户端进行数据更新时才向客户端发送消息。在连接期间不会发送其他无用的请求。因此使用的带宽要少得多。以下图表显示了 WebSockets 方法。
posted @ 2024-05-24 11:10  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报