HTML5-基础知识指南-全-

HTML5 基础知识指南(全)

原文:The Essential Guide to HTML5

协议:CC BY-NC-SA 4.0

一、基础知识

在本章中,我们将介绍

  • HTML 文档的基本结构

  • htmlheadtitlescriptstylebodyimga元素

  • 级联样式表(CSS)示例

  • 一个 JavaScript 代码示例,使用了Datedocument.write

介绍

超文本标记语言(HTML)是一种在网络上传递内容的语言。HTML 不属于任何人,而是许多国家和许多组织的人们定义语言特性的结果。HTML 文档是可以使用任何文本编辑器生成的文本文档。HTML 文档包含由标签包围的元素——以<符号开始并以>符号结束的文本。标签的一个例子是<img src="home.gif"/>。这个特殊的标签将显示保存在文件home.gif中的图像。这些标签就是标记。正是通过使用标签,网页中包含了超链接、图像和其他媒体。

基本的 HTML 可以包括以称为层叠样式表(CSS)的语言进行格式化的指令,以及以称为 JavaScript 的语言进行交互的程序。浏览器,如 Firefox 和 Chrome,解释 HTML 以及任何 CSS 和 JavaScript,以产生我们访问网站时的体验。HTML 保存网站的内容,标签提供内容的性质和结构的信息,以及对图像和其他媒体的引用。CSS 指定格式。相同的内容可以用不同的方式格式化。JavaScript 是一种编程语言,用于使网站具有动态性和交互性。在除了最小的工作组之外的所有工作组中,不同的人可能负责 HTML、CSS 和 JavaScript,但是对这些不同的工具如何协同工作有一个基本的了解总是一个好主意。如果你已经熟悉了 HTML 的基础知识,以及 CSS 和 JavaScript 是如何结合在一起的,你可以跳到下一章。尽管如此,在我们开始第一个核心示例之前,浏览一下本章的内容,以确保您对所有内容都了如指掌,可能还是值得的。

HTML(及其相关的 CSS 和 JavaScript)的最新版本是 HTML5。它已经产生了相当大的兴奋,因为像画布这样的功能可以显示图片和动画;支持视频和音频;以及用于定义常见文档元素的标签,例如headersectionfooter。你可以用 HTML5 创建一个复杂的、高度互动的网站。在撰写本文时,并非所有的浏览器都接受所有的特性,但是您现在可以开始学习 HTML5、CSS 和 JavaScript。学习 JavaScript 将向您介绍一般的编程概念,如果您尝试学习任何其他编程语言,或者如果您作为团队的一部分与程序员一起工作,这些概念将是有益的。

我将在本书中使用的方法是在具体示例的上下文中解释 HTML5、CSS 和 JavaScript 概念,其中大多数都是熟悉的游戏。在这个过程中,我将使用一些小例子来演示具体的特性。希望这能帮助你们理解自己想做什么,并懂得如何去做。当我解释概念和细节的时候,你会知道我们要去哪里。

本章的任务是建立一个链接到其他网站的网页。这样,通过少量的 CSS 代码和 JavaScript 代码,您将对 HTML 文档的结构有一个基本的了解。对于这个和其他例子,请思考如何让项目对你有意义。该页面可以是您自己的项目、喜爱的网站或特定主题网站的列表。对于每个网站,您将看到文本和超链接。第二个示例包括一些额外的格式,以框的形式围绕文本、图片以及当天的日期和时间。图 1-1 和图 1-2 显示了我创建的不同示例。

img/214814_2_En_1_Fig2_HTML.jpg

图 1-2

带有额外格式的收藏网站

img/214814_2_En_1_Fig1_HTML.jpg

图 1-1

游戏的注释列表

当您重新加载收藏夹网站页面时,日期和时间将根据您的计算机更改为当前日期和时间。

关键要求

对链表应用程序的要求是构建包含文本、链接和图像的网页的最基本要求。对于图 1-1 所示的例子,每个条目都显示为一个段落。相反,在图 1-2 所示的例子中,每个条目周围都有一个方框。第二个例子也包括图像和获取当前日期和时间的方法。后面的应用程序将需要更多的讨论,但是对于这一个,我们将直接讨论如何使用 HTML、CSS 和 JavaScript 来实现它。

HTML5、CSS 和 JavaScript 特性

正如我提到的,HTML 文档是文本,那么我们如何指定链接、图片、格式和编码呢?答案就在标记中,也就是标签。除了定义内容的 HTML,通常还会发现 CSS 样式,它们可以在 HTML 文档内部或外部文档中指定。您还可以包含用于交互性的 JavaScript,这也是在 HTML 文档或外部文档中指定的。我们首先来看看如何构建简单的 HTML 标记,以及如何在同一个文档中添加内联 CSS 和 JavaScript。

基本 HTML 结构和标签

HTML 元素以开始标记开始,后面是元素内容和结束标记。结束标记包括一个符号/,后面跟着元素类型,例如/head。元素可以嵌套在元素中。标准的 HTML 文档如下所示:

<html>
   <head>
      <title>Very simple example
      </title>
   </head>
   <body>
      This will appear as is.
   </body>
</html>

请注意,我在这里缩进了嵌套标签,使它们更加明显,但是 HTML 本身忽略了这种缩进(或者空白,众所周知),您不需要将它添加到您自己的文件中。事实上,对于本书中的大多数例子,我都没有缩进我的代码。

这个文档由html元素组成,由开始标签<html>表示,以结束标签</html>结束。

HTML 文档通常有一个head和一个body元素,就像这个一样。这个head元素包含一个元素title。HTML 标题出现在不同浏览器的不同位置。图 1-3 显示了火狐标签上的标题“非常简单的例子”。

img/214814_2_En_1_Fig3_HTML.jpg

图 1-3

Firefox 浏览器中标签页上的 HTML 标题

在大多数情况下,你会在网页正文中创建一些你认为是标题的东西,但它不会是 HTML 标题!图 1-3 也显示了网页的主体:一小段文本。请注意,html、head、title 和 body 这些词没有出现。标签“告诉”浏览器如何显示 HTML 文档。

我们可以对文本做更多的事情,但让我们继续看看如何让图像出现。这需要一个img元素。与使用开始和结束标签的htmlheadbody元素不同,img元素只使用一个标签。它被称为单例标签。它的元素类型是img(不是 image ),您使用所谓的属性将所有信息与标签本身放在一起。什么信息?最重要的项目是保存图像的文件的名称。标签

<img src="frog.jpg"/>

告诉浏览器查找名为 frog、文件类型为. jpg 的文件。在这种情况下,浏览器会在与 HTML 文件相同的目录或文件夹中查找。你也可以参考其他地方的图像文件,稍后我会展示这一点。src代表来源。它被称为元素的属性。>前的斜线表示这是一个单例标签。不同的元素类型有共同的属性,但是大多数元素类型都有附加的属性。img元素的另一个属性是width属性。

<img src="frog.jpg" width="200"/>

这指定图像应该以 200 像素的宽度显示。高度将是保持图像原始纵横比所需的任何值。如果您想要特定的宽度和高度,即使这可能会扭曲图像,也要指定widthheight属性。

小费

您将会看到一些例子(甚至可能是我的一些例子),在这些例子中,右斜杠丢失了,但却工作得很好。将它包括在内被认为是一种良好的做法。类似地,您会看到文件名没有引号的例子。HTML 在语法(标点)方面比大多数其他编程系统更宽容。最后,您将看到以类型为!DOCTYPE的标记开始的 HTML 文档,并且该 HTML 标记包含其他信息。在这一点上,我们不需要这个,所以我将尽可能保持事情的简单(引用爱因斯坦的话,没有更简单)。

制作超链接类似于制作图像。超链接的元素类型是a,重要属性是href

<a href="http://faculty.purchase.edu/jeanine.meyer">Jeanine Meyer's Academic Activities </a>

正如您所看到的,这个元素有一个开始和结束标记。元素的内容,无论在两个标签之间的是什么——在本例中,是 Jeanine Meyer 的学术活动——都显示为蓝色并带有下划线。起始标签以a开始。记住这一点的一种方法是将它视为 HTML 中最重要的元素,因此它使用字母表的第一个字母。你也可以想到一个锚,这是a实际上代表的意思,但对我来说没有什么意义。href属性(想想超文本链接)指定了当超链接被点击时,浏览器所去的网站。请注意,这是一个完整的 web 地址(简称为统一资源定位器或 URL)。

网址可以是绝对的,也可以是相对的。绝对地址以http://开始。相对地址是相对于 HTML 文件的位置。使用相对地址可以更容易地将你的项目移动到不同的网站,并且你可以通过使用../来指示上一级的文件夹。在我的例子中,frog.gif文件、frogface.gif文件和其他图像文件与我的 HTML 文件位于同一个文件夹中。他们在那里是因为我把他们放在那里!对于大型项目,许多人将所有图像放在一个名为 images 的子文件夹中,并将地址写为img/postcard.gif。文件管理是创建网页的一个重要部分。

我们可以将一个超链接元素和一个img元素结合起来,在屏幕上生成一张用户可以点击的图片。请记住,元素可以嵌套在其他元素中。不是将文本放在起始的<a>标签之后,而是放一个<img>标签:

<a href="http://faculty.purchase.edu/jeanine.meyer">
<img src="jhome.gif" width="100" />
</a>

现在让我们把这些例子放在一起:

<html>

<head>
<title>Second example </title>
</head>
<body>
This will appear as is.
<img src="frog.gif"/>
<img src="frog.gif" width="200"/>
<a href=http://faculty.purchase.edu/jeanine.meyer>Jeanine Meyer's Academic
 Activities </a>
<a href=http://faculty.purchase.edu/jeanine.meyer><img src="jhome.gif"/></a>
</body>
</html>

我创建了 HTML 文件,保存为second.html,然后在 Chrome 浏览器中打开。图 1-4 显示了显示的内容。

img/214814_2_En_1_Fig4_HTML.jpg

图 1-4

图像和超链接示例

这产生了文本;图像的原始宽度和高度;宽度固定为 200 像素、高度成比例的图像;一个超链接,将带你到我的网页(我保证);另一个链接使用的图片也将带您到我的网页。然而,这并不是我想要的。我希望这些元素在页面上间隔排列。

这演示了一些你需要记住的事情:HTML 忽略换行符和其他空白。如果你想要换行,你必须指定它。一种方法是使用br singleton 标签。稍后我会展示其他方法。看看下面修改过的代码。注意<br/>标签不需要单独在一行上。

<html>
<head>
<title>Second example </title>
</head>
<body>
This will appear as is. <br/>
<img src="frog.gif"/>
<br/>
<img src="frog.gif" width="200"/>
<br/>
<a href=http://faculty.purchase.edu/jeanine.meyer>Jeanine Meyer's Academic
 Activities </a>
<br/>
<a href=http://faculty.purchase.edu/jeanine.meyer><img src="jhome.gif"/></a>
</body>
</html>

图 1-5 显示了这段代码产生的结果。

img/214814_2_En_1_Fig5_HTML.jpg

图 1-5

带有换行符的文本、图像和链接

HTML 元素类型有很多:h1h6标题元素产生不同大小的文本;列表和表格有各种元素,表单有其他元素。正如我们马上会看到的,CSS 也用于格式化。您可以为文本选择不同的字体、背景颜色和颜色,并控制文档的布局。将格式放在 CSS 中,将交互性放在 JavaScript 中,并保留内容的 HTML,这被认为是很好的做法。HTML5 提供了新的结构元素——比如articlesectionfooterheader——将格式设置放入样式元素中,并利用新元素,称为语义标签,便于与其他人合作。然而,即使您只是自己工作,将内容、格式和行为分开也能让您轻松地更改格式和交互。格式,包括文档布局,是一个很大的话题。在这本书里,我坚持基本原则。

使用级联样式表

CSS 是一种专门用于格式化的特殊语言。样式本质上是一种规则,它指定了特定元素的格式。这意味着您可以将样式信息放在不同的地方:一个单独的文件,一个位于head元素中的style元素,或者 HTML 文档中的一个样式,也许是在您想要以特定方式格式化的一个元素中。除非指定了不同的样式,否则样式信息会层叠、向下流动。换句话说,最接近元素的样式就是所使用的样式。例如,您可能使用您公司的官方字体,如在head元素的 style 部分中给出的,来流过大部分文本,但是在 local 元素中包含规范来样式化一段特定的文本。因为该样式最接近元素,所以它是所使用的样式。

基本格式包括要格式化的内容的指示符,后跟一个或多个指令。在本章的例子中,我将指定类型为section的元素的格式,即每个项目周围的边框或方框、边距、填充、对齐以及白色背景。清单 1-1 中完整的 HTML 文档是一个混合体(有人会说是一团乱麻!)的特性。元素bodyp(段落)是 HTML 原始版本的一部分。section元素是 HTML5 中新增的元素类型之一。section 元素确实需要格式化,不像bodyp,它们有默认的格式,即主体和每个p元素将在新的一行开始。CSS 可以修改新旧元素类型的格式。请注意,节中文本的背景色不同于节外文本的背景色。

在清单 1-1 的代码中,我为body元素(只有一个)和section元素指定了样式。如果我有不止一个 section 元素,那么样式将适用于每个元素。正文的样式指定背景颜色和文本颜色。最初,浏览器只接受 16 种颜色的名称,包括黑色、白色、红色、蓝色、绿色、青色和粉红色。然而,现在最新的浏览器按名称接受 140 种颜色。 https://www.w3schools.com/colors/colors_names.asp

您还可以使用 RGB(红绿蓝)十六进制代码来指定颜色,但您需要使用图形程序(如 Adobe Photoshop、Corel Paint Shop Pro 或 Adobe Flash Professional)来计算 RGB 值,或者您可以进行实验。我使用 Paint Shop Pro 来确定青蛙头图片中绿色的 RGB 值,并将其用于边框。

指令就像它们听起来的那样:它们指示是将材料居中还是向左对齐。font-size以像素为单位设置文本的大小。边框很复杂,而且在不同的浏览器中看起来不一致。这里我指定了一个 4 像素的纯绿色边框。sectionwidth规范指出浏览器应该使用 85%的窗口,不管那是什么。p的规格将段落的宽度设置为 250 像素。填充是指文本和部分边框之间的间距。边距是指该部分与其周围环境之间的间距。

<html>
<head>
<title>CSS example </title>
<style>
body {
        background-color:tan;
        color: #EE015;
        text-align:center;
        font-size:22px;
}
section {
        width:85%;
        border:4px #00FF63 solid;
        text-align:left;
        padding:5px;

        margin:10px;
        background-color: white;
}

p {
        width: 250px;
}
</style>
</head>
<body>

The background here is tan and the text is the totally arbitrary RED GREEN BLUE➥
 value #EE1055<br/>

<section>Within the section, the background color is white. There is text with➥
 additional HTML markup, followed by a paragraph with text. Then, outside the➥
 section there will be text, followed by an image, more text and then a➥
 hyperlink. <p>The border color of the section matches the color of the➥
 frog image. </p></section>
<br/>
As you may have noticed, I like origami. The next image represents a frog head.<br/>
<img src="frogface.gif"/> <br/>If you want to learn how to fold it, go to

<a href=http://faculty.purchase.edu/jeanine.meyer/origami>the Meyer Family➥
 Origami Page <img src="crane.png" width="100"/></a>

</body>

</html>

Listing 1-1A Complete HTML Document with Styles

这将产生如图 1-6 所示的屏幕。

img/214814_2_En_1_Fig6_HTML.jpg

图 1-6

示例 CSS 样式

小费

如果你不能马上理解所有的事情,不要担心。修改这些例子,自己编一个。你会在网上找到很多帮助。特别是,在 http://dev.w3.org/html5/spec/Overview.html 可以看到 HTML 5 的官方来源。

你可以用 CSS 做很多事情。您可以使用它来指定元素类型的格式,如下所示;您可以指定元素是类的一部分;您还可以使用id属性来识别单个元素。在第六章中,我们创建了一个测验,我使用 CSS 来定位窗口中的特定元素,然后使用 JavaScript 来移动它们。

JavaScript 编程

JavaScript 是一种编程语言,具有访问 HTML 文档各部分的内置特性,包括 CSS 元素中的样式。它被称为脚本语言,以区别于编译语言,如 C++。编译语言在使用前被一次性翻译,而脚本语言由浏览器逐行解释。本文假设读者之前没有编程经验或 JavaScript 知识,但参考其他书籍可能会有所帮助,如特里·麦克纳威奇(2010 年编辑的朋友)的《JavaScript 入门》,或网上资源,如 http://en.wikipedia.org/wiki/JavaScript 。每个浏览器都有自己的 JavaScript 版本。

HTML 文档在位于head元素中的script元素中保存 JavaScript。为了显示如图 1-2 所示的时间和日期信息,我在 HTML 文档的head元素中添加了以下内容:

<script>
document.write(Date());
</script>

与其他编程语言一样,JavaScript 由各种类型的语句组成。在后面的章节中,我将向您展示赋值语句、复合语句,如ifswitchfor语句,以及创建所谓的程序员定义函数的语句。函数是在一个块中一起工作的一个或多个语句,并且可以在您需要该功能的任何时候被调用。函数可以避免一遍又一遍地写出相同的代码。JavaScript 提供了许多内置函数。某些函数与对象相关联(稍后将详细介绍),被称为方法。代码

document.write("hello");

是一个 JavaScript 语句,使用参数"hello"调用文档对象的write方法。参数是传递给函数或方法的附加信息。语句以分号结束。这段代码将写出字符串 h,e,l,l,o 作为 HTML 文档的一部分。

document.write方法写出括号内的任何内容。由于我希望写出的信息随着日期和时间的变化而变化,所以我需要一种方法来访问当前的日期和时间,所以我使用了内置的 JavaScript Date函数。这个函数产生一个带有日期和时间的对象。稍后,您将看到如何使用Date对象来计算玩家完成一个游戏需要多长时间。现在,我想做的就是显示当前的日期和时间信息,这正是这段代码要做的:

document.write(Date());

使用正式的编程语言:这段代码调用document对象的write方法,这是一段内置的代码。句点(.)表示要调用的write是与 HTML 文件生成的文档相关联的方法。所以,有些东西是作为 HTML 文档的一部分写出来的。写出来的是什么?左括号和右括号之间的内容。那是什么?它是调用内置函数Date的结果。Date函数获取本地计算机维护的信息,并将其交给write方法。Date也要求使用括号,这也是你看到这么多的原因。write方法将日期和时间信息显示为 HTML 文档的一部分,如图 1-2 所示。这些结构的组合方式是典型的编程语言。该语句以分号结束。为什么不是一个时期?句点在 JavaScript 中还有其他用途,例如指示方法和用作数字的小数点。

自然语言,比如英语,和编程语言有很多共同点——不同类型的语句;使用某些符号的标点符号;和语法来正确定位元素。在编程中,我们用术语符号代替标点符号,用句法代替语法。编程语言和自然语言都可以让你用独立的部分构建非常复杂的语句。然而,有一个根本的区别:正如我告诉我的学生,我在课堂上说的很多内容很可能在语法上不正确,但他们仍然会理解我。但是,当你通过编程语言与计算机“交谈”时,你的代码必须在语法规则方面完美无缺,才能得到你想要的东西。好消息是,与人类观众不同,计算机不会表现出不耐烦或任何其他人类情感,所以你可以花时间把事情做好。也有一些坏消息,你可能需要一段时间才能领会。如果您在 HTML、CSS 或 JavaScript 中犯了语法错误(称为句法错误),浏览器仍然会尝试显示某些内容。当你在工作中没有得到你想要的结果时,你要找出问题出在哪里。

使用文本编辑器

您使用文本编辑器构建 HTML 文档,并使用浏览器查看/测试/播放该文档。虽然你可以使用任何文本编辑程序来编写 HTML,但我建议你在 PC 上使用 TextPad,在 MAC 上使用 Sublime。这些是共享软件,这使得它们相对便宜。不要使用文字处理程序,它可能会插入非文本字符。记事本也可以工作,尽管其他工具也有好处,比如我将演示的颜色编码。要使用编辑器,您需要打开它并输入代码。图 1-7 展示了 Sublime 屏幕的样子。

img/214814_2_En_1_Fig7_HTML.jpg

图 1-7

从崇高开始

你会希望经常保存你的工作,最重要的是,保存为 type.html 文件。在开始时这样做,然后你将获得颜色编码的好处。在 Sublime 中,点击文件另存为,然后输入带有文件扩展名的名称。html,如图 1-8 。

img/214814_2_En_1_Fig8_HTML.jpg

图 1-8

将文件保存为 HTML 类型

请注意,我给了文件一个名称和文件扩展名,还指定了文件所在的文件夹。保存文件后,出现如图 1-9 所示的窗口,带有颜色编码。

img/214814_2_En_1_Fig9_HTML.jpg

图 1-9

将文件保存为 HTML 后

颜色编码表示标签和引用字符串,只有在文件保存为 HTML 后才能看到。这对于捕捉许多错误很有价值。Sublime 和其他编辑器确实提供了改变配色方案的选项。假设您正在使用这里显示的方法,如果您看到黄色的长段(引用字符串的颜色),这可能意味着缺少右引号。顺便说一下,你可以使用单引号或双引号,但不能混淆。此外,如果你从 Word 或 PowerPoint 中复制粘贴,并复制所谓的“智能”引号,那些曲线,这将导致问题。

构建应用程序

您可以在查看以下应用程序的源代码。???。HTML 文档的源代码通常包括 HTML 文档和其他文件。

  • verysimple.html文件本身是完整的,如图 1-3 所示。

  • second.html应用如图 1-4 所示。引用了两个图像文件:frog.gif两次,jhome.gif一次。

  • 带有花哨颜色的third.html引用了两个图像文件:frogface.gifcrane.gif

  • games.html文件本身是完整的,因为它没有引用任何图像文件。如果在a标签的href属性中提到的文件不存在,那么当点击超链接时将会出现错误消息。

  • ch01FavoriteSites.html文件引用了两个图像文件:avivasmugmug.jpegapressshot.jpeg

跟踪文件是构建 HTML 应用程序的关键部分。

现在让我们深入研究 HTML 编码,首先是描述游戏的带注释的链表,然后是最喜欢的网站。该代码使用了上一节中描述的特性。表 1-1 显示了产生如图 1-1 所示显示的完整代码:链接到不同文件的文本段落,它们都位于同一文件夹中。

表 1-1

“我的游戏”注释链接代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Annotated links</title> | 开始的title标签、标题文本和结束的title标签。 |
| </head> |   |
| <body> | 开始body标签。 |
| <h1>My games</h1> | 开始h1标签,文本,然后结束h1标签。这将使“我的游戏”以大字体出现。实际字体将是默认字体。 |
| <p> | 段落标签的开始p。 |
| The <a href="craps.html">Dice game</a> presents the game called craps. | 带有a元素的文本。开始标签a的属性href被设置为值craps.html。大概这是一个和这个 HTML 文件在同一个文件夹中的文件。a元素的内容——无论是在<a></a>之间的内容——都将被显示出来,首先显示为蓝色,单击后显示为淡紫色,并带有下划线。 |
| </p> | 关闭p标签。 |
| <p> | 开始p标签。 |
| The <a href="cannonball.html">Cannonball</a> is a ballistics simulation. A ball appears to move on the screen in an arc. The program determines when the ball hits the ground or the target. The player can adjust the speed and the angle. | 参见前面的案例。这里的a元素指的是cannonball.html文件,显示的文本是Cannonball。 |
| </p> | 关闭p标签。 |
| <p> | 开始p标签。 |
| The <a href="slingshot.html">Slingshot</a> simulates shooting a slingshot. A ball moves on the screen, with the angle and speed depending on how far the player has pulled back on the slingshot using the mouse. | 参见上一篇。这一段包含了到slingshot.html的超链接。 |
| </p> | 关闭p标签。 |
| <p> | 开始p标签。 |
| The <a href="memory.html">Concentration/memory game</a> presents a set of plain rectangles you can think of as the backs of cards. The player clicks on first one and then another and pictures are revealed. If the two pictures represent a match, the two cards are removed. Otherwise, the backs are displayed. The game continues until all matches are made. The time elapsed is calculated and displayed . | 参见上一篇。这一段包含了到memory.html的超链接。 |
| </p> | 关闭p标签。 |
| <p> | 开始p标签。 |
| The <a href="quiz1.html">Quiz game</a> presents the player with 4 boxes holding names of countries and 4 boxes holding names of capital cities. These are selected randomly from a larger list. The player clicks to indicate matches and the boxes are moved to put the guessed boxes  together. The program displays whether or not the player is correct . | 参见上一篇。这一段包含了到quiz1.html的超链接。 |
| </p> | 关闭p标签。 |
| <p> | 开始p标签。 |
| The <a href="maze.html">Maze</a> program is a multi-stage game. The player builds a maze by using the mouse to build walls. The player then can move a token through the maze. The player can also save the maze on the local computer using a name chosen by the player and retrieve it later, even after closing the browser or turning off the computer . | 参见上一篇。这一段包含了到maze.html的超链接。 |
| </p> | 关闭p标签。 |
| </body> | 关闭body标签。 |
| </ html> | 关闭html标签。 |

一旦您创建了几个自己的 HTML 应用程序,您就可以构建一个像这样的文档,作为您自己的注释列表。如果您使用文件夹,href链接将需要反映 HTML 文档的位置。

Favorite Sites 代码具有带注释的列表的功能,并增加了格式:每个项目周围有一个绿色框,三个项目中有两个项目有一张图片。参见表 1-2 。

表 1-2

收藏夹站点代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Annotated links</title> | 完成title元素:开始和结束标签以及它们之间的注释链接。 |
| <style> | 开始style标签。这意味着我们现在要使用 CSS。 |
| article { | 样式的开始。对正在被样式化的内容的引用是所有的article元素。然后样式有了一个大括号- {。开始和结束括号包围了我们正在创建的样式规则,很像 HTML 中的开始和结束标记。 |
| width:60%; | width设置为包含元素的 60%。注意,每个指令都以一个;(分号)结束。 |
| text-align:left; | 文本靠左对齐。 |
| margin:10px; | 边距为 10 像素。 |
| border:2px green double; | 边框是 2 像素的绿色双线。 |
| padding:2px; | 文本和边框之间的间距为 2 像素。 |
| display:block; | 文章是块,意思是前后有换行符。 |
| } | 关闭article的样式。 |
| img {display:block;} | Style img元素到 block style:前后换行。 |
| </style> | 关闭style标签。 |
| <script> | 开始script标签。我们现在正在编写 JavaScript 代码。 |
| document.write(Date()); | 一条代码语句:写出Date()调用产生的内容。 |
| </script> | 关闭script标签。 |
| </head> |   |
| <body> | 开始body标签。 |
| <h3>Favorite Sites</h3> | 由h3/h3标签包围的文本。这使得文本看起来比正常的要大一些。 |
| <article> | 开始article标签。 |
| The <a href="http://faculty.purchase.edu/jeanine.meyer"Jeanine Meyer's Academic Activities</a> site displays information on my current and past courses, along with publications and other activities. | 该文本将以指定的样式为准。它包括一个a元素。 |
| </article> | 关闭article标签。 |
| <article> | 开始article标签。 |
| The <a href="https://avivameyer . smugmug . com/">Aviva Meyer's photographs</a> site is a collection of Aviva's photographs stored on a site called smugmug. The categories are Music, Adventures and Family (which requires a password)``. | 这篇文章与上一篇相似,有一个a元素和一些文本。 |
| <img src="avivasmugmug.jpeg" width="300"/> | 一个img标签。图像的来源是文件avivasmugmug.jpeg。如果文件的扩展名为. jpg,这将不起作用。宽度设置为 300 像素。由于 style 部分中的 style 指令,前后都有换行符。 |
| </article> | 关闭article标签。 |
| <article> | 开始article标签。 |
| <a href=" http://apress.com ">Apress publishers</a> is the site for the publishers of this book. <br/> | 这类似于上一篇文章:一个 a 元素和一些文本。 |
| <img src="apressshot.jpeg" width="300"/> | 一个img元素。来源是apressshot.jpeg。宽度设置为 300 像素。 |
| </article> | 关闭article标签。 |
| </body> | 关闭body标签。 |
| </ html> | 关闭html标签。 |

如何让这个应用程序成为你自己的非常简单:使用你自己喜欢的网站!在大多数浏览器中,如果您想为超链接使用网站徽标,可以下载并保存图像文件,也可以包含其他图片。我的理解是,制作一个带有评论并包含图像(如徽标)的网站列表属于“合理使用”的范畴,但我不是律师。大多数情况下,人们喜欢链接到他们的网站。这不影响法律问题,但是如果您不想将特定的图像文件下载到您的计算机,然后再上传到您的网站,您也可以选择将img标签中的src设置为图像所在网站的网址。

您也可以通过更改格式使该应用程序成为您自己的应用程序。样式可用于指定字体,包括特定的字体、字体系列和大小。这使您可以选择一种喜爱的字体,并指定在用户计算机上没有该字体时使用的字体。您可以指定边距和填充,或者独立地改变上边距、左边距、上边距等。

测试和上传应用程序

除非您使用完整的网址,否则您需要将所有文件(在本例中是单个 HTML 文件和所有图像文件)放在同一个文件夹中。为了使链接工作,您需要拥有所有href属性的正确地址。我的例子展示了如何对同一文件夹中的 HTML 文件或 Web 上其他地方的 HTML 文件执行此操作。

你可以开始测试你的工作,即使它还没有完全完成。例如,您可以放入一个单独的img元素或一个单独的a元素。打开浏览器,如 Firefox、Chrome 或 Safari。在 Firefox 中,点击文件,然后打开文件,浏览到你的 HTML 文件。在 Chrome 中,在 PC 上按 Ctrl(MAC 上按 CMD)和 o,然后浏览到文件,点击确定打开。你应该看到类似我的例子。

点击超链接进入其他网站。使用浏览器的重新加载图标重新加载页面,并观察不同的时间。如果你没有看到你所期望的——就像我的例子一样——你需要检查你的代码。常见的错误有:

  • 开始和结束标记缺失或不匹配。

  • 图像文件或 HTML 文件的名称错误,或者图像文件的文件扩展名错误。您可以使用 JPG、GIF 或 PNG 类型的图像文件,但是标签中指定的文件扩展名必须与图像的实际文件类型相匹配。

  • 缺少引号。编辑器中可用的颜色编码可以帮助您识别这一点。

摘要

在这一章中,你学习了如何用文本、图像和超链接组成 HTML 文档。这包括:

  • 基本标签,包括htmlheadtitlestylescriptbody

  • 用于显示图像的img元素

  • 超链接的a元素

  • 使用按照级联样式表(CSS)规则编写的样式元素进行简单格式化

  • 提供日期和时间信息的一行 JavaScript 代码

这一章仅仅是个开始,尽管使用基本的 HTML,不管有没有级联样式表,都有可能制作出漂亮的信息丰富的网页。在下一章中,您将学习如何在应用程序中包含随机性和交互性,以及如何使用 Canvas 元素,这是 HTML5 的关键特性。

二、骰子游戏

在本章中,我们将介绍

  • 在画布上绘画

  • 随机处理

  • 游戏逻辑

  • 表单输出

介绍

HTML5 中最重要的新特性之一是canvas元素。这个元素为开发人员提供了一种以完全自由的方式绘制线条画、包含图像和定位文本的方法,这是对旧 HTML 的一个重大改进。尽管在早期版本中你可以做一些花哨的格式化,但布局往往是四四方方的,页面缺乏动态性。你如何在画布上画画?你使用一种脚本语言,通常是 JavaScript。我将向您展示如何在画布上绘图,并解释 JavaScript 的重要特性,我们将需要这些特性来构建一个名为 craps 的骰子游戏的实现:如何定义一个函数,如何调用所谓的伪随机行为,如何实现这个特定游戏的逻辑,以及如何向玩家显示信息。不过,在我们进一步深入之前,您需要了解这个游戏的基础知识。

掷骰子游戏有以下规则:

玩家掷出一对骰子。两个顶面之和才是最重要的,所以 1 和 3 与 2 和 2 是一样的。两个 6 面骰子的和可以是 2 到 12 之间的任何数字。如果玩家第一次掷出 7 或 11,玩家获胜。如果玩家掷出 2、3 或 12,该玩家输了。对于任何其他结果(4,5,6,8,9,10),这个结果被记录为所谓的球员得分,并要求进行后续投掷。在后续的投掷中,一次投掷 7 分失败,一次投掷该球员的点数获胜。对于其他任何事情,游戏继续遵循后续投掷规则。

让我们看看我们的游戏会是什么样子。图 2-1 显示游戏开始时投掷两个一的结果。

img/214814_2_En_2_Fig1_HTML.jpg

图 2-1

第一次投掷,导致玩家失败

这里并不明显,但是我们的骰子游戏应用程序每次都使用canvas标签来绘制骰子面。这意味着没有必要下载单个芯片表面的图像。

掷两个 1 意味着玩家输了,因为规则规定第一次掷 2、3 或 12 是输。下一个例子显示玩家赢了,第一次掷出 7,如图 2-2 所示。

img/214814_2_En_2_Fig2_HTML.jpg

图 2-2

第一次掷出 7 意味着玩家赢了

图 2-3 显示了下一次投掷——8 分。这既不是赢也不是输,而是意味着必须有后续的投掷。

img/214814_2_En_2_Fig3_HTML.jpg

图 2-3

8 分意味着运动员的 8 分被结转的后续投掷

假设玩家最终再次掷出 8,如图 2-4 所示。

img/214814_2_En_2_Fig4_HTML.jpg

图 2-4

又是一次 8 分,玩家赢了

正如前面的序列所示,唯一有价值的是骰子表面值的总和。积分值是用两个 4 定的,但是比赛是用一个 2 和一个 6 赢的。

规则表明,一场游戏不会总是掷出相同数量的骰子。玩家可以在第一次投掷中获胜或失败,也可以有任何次数的后续投掷。游戏制作者的工作是制作一个可以运行的游戏——运行意味着遵守规则,即使这意味着游戏会持续下去。我的学生有时表现得好像他们的游戏只有赢了才有效。在一个正确执行的游戏中,玩家会有赢有输。

关键要求

构建骰子游戏的要求从模拟随机掷骰子开始。起初,这似乎是不可能的,因为编程意味着精确地指定计算机将做什么。幸运的是,与大多数其他编程语言一样,JavaScript 有一个内置的工具,可以产生看似随机的结果。有时语言使用一个很长的位串的中间位(1 和 0)来表示以毫秒为单位的时间。确切的方法对我们并不重要。我们将假设浏览器提供的 JavaScript 在这方面做得很好,这被称为伪随机处理。

现在假设我们可以从 1 到 6 中随机抽取任意一个数字,并对两个骰子面进行两次,我们需要实现游戏规则。这意味着我们需要一种方法来跟踪我们是处于第一次投掷还是后续投掷。它的正式名称是应用程序状态,意思是事情现在的样子,在游戏和其他类型的应用程序中都很重要。然后,我们需要使用基于条件做出决策的结构。像ifswitch这样的条件结构是编程语言的标准组成部分,你很快就会明白为什么像我这样的计算机科学教师——他们从来没有去过赌场或后巷——真的喜欢掷骰子的游戏。

我们需要给玩家一个掷骰子的方法,所以我们将在屏幕上实现一个按钮来点击它。然后我们需要向玩家提供发生了什么的信息。对于这个应用程序,我通过在屏幕上绘制骰子面来产生图形反馈,并且以文本形式显示信息,以指示游戏的阶段、分值和结果。与用户交互的早期术语是输入输出(I/O),那时交互主要涉及文本。术语图形用户界面**【GUI】现在通常用来表示用户与计算机系统交互的各种方式。其中包括使用鼠标点击屏幕上的特定点或者将点击与拖动相结合来模拟移动物体的效果(参见第四章中的弹弓游戏)。在屏幕上绘图需要使用坐标系来指定点。在大多数编程语言中,计算机屏幕的坐标系统都是以类似的方式实现的,我将很快解释这一点。

HTML5、CSS 和 JavaScript 特性

现在让我们来看看 HTML5、CSS 和 JavaScript 的具体特性,它们提供了我们实现骰子游戏所需的东西。

伪随机处理和数学表达式

JavaScript 中的伪随机处理是使用一个名为Math.random的内置方法来执行的。从形式上来说,randomMath 的一个方法。调用Math.random()生成一个从 0 到 1 的数,但不包括 1,结果是一个十进制数,例如 0.253012。这对我们来说似乎不是立即有用的,但实际上这是一个非常简单的过程,将那个数字转换成我们可以使用的数字。我们将这个数乘以 6,得到一个从 0 到 6 的数,但不包括 6。例如,如果我们将. 253012 乘以 6,我们得到 1.518072。这几乎是我们所需要的,但还不完全是。下一步是去掉分数,保留整数。为此,我们使用另一种Math方法Math.floor。此方法在移除任何小数部分后生成一个整数。顾名思义,floor方法向下舍入。在我们的特殊例子中,我们从. 253012 开始,然后到达 1.518072,因此,进行调用Math.floor(1.58072),结果是整数 1。一般来说,当我们将随机数乘以 6 并对其取底时,我们会得到一个从 0 到 5 的数。最后一步是加一个 1,因为我们的目标是得到一个从 1 到 6 的数,一遍又一遍,没有特定的模式。

您可以使用类似的方法获得任何范围内的整数。例如,如果你想要数字 1 到 13,你可以将随机数乘以 13,然后加 1。这对纸牌游戏很有用。你会在本书中看到类似的例子。

我们可以将所有这些步骤组合成一个所谓的表达式。表达式是常量、方法和函数调用的组合,有些东西我们将在后面探讨。我们使用运算符将这些项放在一起,例如+表示加法,*表示乘法。

还记得第一章中的标签如何组合——将一个标签嵌套在另一个标签中——以及我们在 Favorite Sites 应用程序中使用的一行 JavaScript 代码吗:

document.write(Date());

我们可以在这里使用类似的过程。我们可以将random调用作为floor方法的一个参数来传递,而不必分别编写random调用和floor方法。看一下这段代码:

1+Math.floor(Math.random()*6)

这个表达式将产生一个从 1 到 6 的数字。我称它为代码片段,因为它不是一个完整的语句。运算符+*指的是算术运算,与普通数学中使用的相同。操作顺序从内到外依次进行。

  1. 调用Math.random()获得一个从 0 到 1 的十进制数,但不完全是 1。

  2. 将结果乘以 6。

  3. Math.floor去掉分数,留下整数。

  4. 加 1。

在我们的最终代码中,你会看到一个带有这个表达式的语句,但是我们需要先介绍一些其他的东西。

变量和赋值语句

像其他编程语言一样,JavaScript 有一个叫做变量 的结构,它本质上是一个放值的地方,比如一个数字。这是一种将名称与值相关联的方式。您可以在以后通过引用名称来使用该值。一个类比是办公室人员。在美国,我们称之为“总统”。2010 年,当我创作这本书的第一版时,总统是巴拉克·奥巴马。现在(2018 年 7 月),总统是唐纳德·特朗普。“总统”一词的价值发生了变化。在编程中,变量值也可以变化,因此得名。

术语var用于声明为变量。

下一节中描述的变量和函数的名字由程序员决定。有规则,包括内部不能有空格,不能用句号,名字必须以字母字符开头。名字的长度是有限制的,但是我们倾向于使名字简短以避免打字。然而,我建议你不要把它们写得太短,以至于忘了它们是什么。你确实需要保持一致,但你不需要遵守英语拼写规则。例如,如果您想设置一个变量来保存值的总和,并且您认为 sum 的拼写是 som,那就没问题。只要确保你一直使用 som。但是如果你想引用 JavaScript 的一部分,比如functiondocumentrandom,你需要使用 JavaScript 期望的拼写。

您应该避免在 JavaScript 中为变量使用内置结构的名称(比如randomfloor)。尽量使名字独特,但仍然容易理解。编写变量名的一种常见方法是使用所谓的骆驼格。这包括用小写字母开始你的变量名,然后用大写字母表示一个新单词的开始,例如,numberOfTurnsuserFirstThrow。你可以看到为什么它被称为骆驼案——大写字母形成了单词中的“驼峰”。您不必使用这种命名方法,但这是许多程序员遵循的惯例。

保存前面部分解释的伪随机表达式的代码行是一种特殊类型的语句,称为赋值语句。举个例子,

var ch = 1+Math.floor(Math.random()*6);

将名为ch的变量设置为等号右侧表达式的结果值。当在var语句中使用时,它也被称为初始化语句。=符号用于设置变量的初始值,如在这种情况下以及在下面将要描述的赋值语句中。我选择用ch这个名字作为选择的简写。这对我很有意义。一般来说,如果你需要在一个短名字和一个你能记住的长名字之间做出选择,选择一个长的!请注意,该语句以分号结束。你可能会问,为什么不是句号呢?答案是句点用于另外两种情况:作为小数点和用于访问对象的方法和属性,如在document.write中。

赋值语句是编程中最常见的语句类型。下面是一个已经定义的变量的赋值语句的例子:

bookname = "The Essential Guide to HTML5";

等号的使用可能会引起混淆。想象一下,左边的产量等于右边的产量。在本书中,你会遇到许多其他变量以及运算符和赋值语句的其他用法。

警告

定义变量的var语句称为声明语句 JavaScript 不像其他语言,它允许程序员省略声明语句,直接使用变量。我尽量避免这样做,但是你会在很多网上的例子中看到。

对于掷骰子的游戏,我们需要定义游戏状态的变量,即它是第一次投掷还是后续投掷,以及玩家的点数是什么(记住点数是前一次投掷的值)。在我们的实现中,这些值将由所谓的全局变量保存,这些变量是用任何函数定义之外的var语句定义的,以便保留它们的值(当函数停止执行时,函数内部声明的变量值消失)。

你不需要总是使用变量。例如,我们在这里创建的第一个应用程序设置变量来保存骰子的水平和垂直位置。我本可以在代码中放入文字数字,因为我不会更改这些数字,但是因为我在几个不同的地方引用这些值,所以将这些值存储在变量中意味着如果我想更改一个或两个数字,我只需要在一个地方进行更改。

程序员定义的函数

JavaScript 有许多内置的函数和方法,但是它没有您可能需要的所有东西。比如,据我所知,它并没有专门模拟掷骰子的功能。所以 JavaScript 让我们定义和使用自己的函数。这些函数可以使用参数,就像Math.floor方法一样,其中的参数,比如调用Math.floor(rawScore)中的变量rawScore,用于计算不大于当前值rawScore的最大整数。该声明

    score = Math.floor(rawScore);

将用于根据rawScore中的值,用整数设置变量 score,该值可能有小数部分。我在炫耀骆驼肠衣的用途。请记住,这是我的编码,只有我的编码才能建立联系。

参数是可以传递给函数的值。把它们当成额外的信息。

函数定义的格式是术语function,后面是您要赋予函数的名称,后面是包含任何参数名称的括号,再后面是一个开括号、一些代码,然后是一个闭括号。正如我在前面提到的,程序员选择名字。下面是一个函数定义的例子,它返回两个参数的乘积。顾名思义,你可以用它来计算一个矩形的面积。

function areaOfRectangle(wd,ln) {
    return wd * ln;
}

注意关键字return。这告诉 JavaScript 将函数的结果发送给我们。在我们的例子中,这让我们编写类似于rect1 = areaOfRectangle(5,10)的代码,将值 50 (5 × 10)赋给我们的rect1变量。函数定义将被写成script元素中的代码。在现实生活中定义这个函数可能有意义,也可能没有意义,因为在代码中编写乘法非常容易,但它确实是程序员定义函数的一个有用示例。一旦这个定义被执行,很可能就是在 HTML 文件被加载的时候,其他代码就可以通过调用它的名字来使用这个函数,就像在areaOfRectangle(100,200)或者areaOfRectangle(x2-x1,y2-y1)中一样。

第二个表达式假设x1x2y1y2指的是在别处定义的坐标值。

也可以通过设置某些标签属性来调用函数。例如,body标签可以包括对onLoad属性的设置:

<body onLoad="init();">

我的 JavaScript 代码包含一个我称为init的函数的定义。将这个放入body元素意味着当浏览器第一次加载 HTML 文档时或者每当玩家点击重载/刷新按钮时,JavaScript 将调用我的init函数。类似地,使用 HTML5 的一个新特性,我可以包含按钮元素:

<button onClick="throwdice();">Throw dice </button>

这将创建一个保存文本Throw dice的按钮。当玩家点击它时,JavaScript 调用我在script元素中定义的throwdice函数。

稍后描述的form元素可以以类似的方式调用函数。

条件语句:如果切换

掷骰子游戏有一套规则。总结规则的一种方法是,如果是第一次掷骰子,我们检查掷骰子的某些值。如果不是第一次掷骰子,我们检查掷骰子的其他值。JavaScript 为此提供了ifswitch语句。

if语句基于条件 可以是比较或相等检查——例如,名为temp的变量是否大于 85,或者名为course的变量是否保存值"Programming Games"。比较产生两个可能的逻辑值— truefalse。到目前为止,您已经看到了数字值和字符串值。逻辑值是另一种数据类型。它们也被称为布尔值,以数学家乔治·布尔的名字命名。我提到的条件和检查将用代码写成

temp>85

course == "Programming Games"

将第一个表达式读作:变量temp的当前值是否大于 85?

第二个问题是:变量course的当前值是否与字符串"Programming Games"相同?

比较例很好理解;我们使用>来检查一个值是否大于另一个值,使用<来检查相反的情况。表达式的值将是两个逻辑值之一,truefalse

第二种表达方式可能更令人困惑。你可能想知道两个等号,也可能想知道引号。JavaScript(和其他几种编程语言)中检查相等性的比较运算符是两个等号的组合。我们需要两个等号,因为单个等号用在赋值语句中,它不能双重作用。如果我们写了course = "Programming Games",我们会把值"Programming Games"赋给我们的course变量,而不是比较这两个项目。引号定义了一个字符串,以 P 开始,包括空格,以 s 结束。

有了这些,我们现在可以看看如何编写代码,只有当条件为真时才执行某些操作。

if (condition) {

   code

}

如果我们希望我们的代码在条件为真时做一件事,而在条件不为真时做另一件事,格式是:

if (condition) {

   if true code

}

else {

    if not true code

}

请注意,我在这里使用斜体是因为这是所谓的伪代码,不是我们将包含在 HTML 文档中的真实 JavaScript。

下面是一些真实的代码示例。他们使用alert,这是一个内置函数,可以在浏览器中弹出一个小窗口,显示括号中参数所指示的消息。用户必须单击“确定”才能继续。

if (temp>85) {
   alert("It is hot!");
}
if (age >= 21) {
   alert("You are old enough to buy a drink.");
}
else {
   alert("You are too young to be served in a bar.");
}

我们可以只使用if语句来编写 craps 应用程序。然而,JavaScript 提供了另一种更容易理解的结构——switch语句。一般格式是:

switch(x) {

case a:

   codea;

case b:

   codeb;

default: codec;

}

JavaScript 评估switch语句第一行中x的值,并将其与案例中指示的值进行比较。一旦命中,即确定x等于ab,则执行case标签后的代码。如果不匹配,则执行default之后的代码。没有必要有默认的可能性。如果任其自生自灭,计算机将继续运行switch语句,即使它找到了匹配的case语句。如果您希望它在找到匹配时停止,您需要包含一个break语句来中断切换。

你可能已经看到ifswitch将如何做我们在骰子游戏中需要的事情。您将在下一节中了解如何操作。首先,让我们看一个例子,它确定由变量mon表示的一个月中的天数,变量【】包含三个字母的缩写("Jan""Feb"等)。).

switch(mon) {
case "Sep":
case "Apr":
case "Jun":
case "Nov":
        alert("This month has 30 days.");
        break;
case "Feb":
        alert("This month has 28 or 29 days.");
        break;
default:
        alert("This month has 31 days.");
}

如果变量mon的值等于"Sep""Apr""Jun""Nov",则控制流向第一个alert语句,然后由于break而退出switch语句。如果变量mon的值等于"Feb",则执行提到 28 或 29 天的alert语句,然后控制流退出switch。如果mon的值是其他任何值,顺便说一下,包括一个无效的三个字母缩写,则执行提到 31 天的alert

正如 HTML 忽略换行符和其他空白一样,JavaScript 不要求这些语句有特定的布局。如果你愿意,你可以把所有的东西放在一行。然而,让事情变得简单,使用多行和缩进。

在画布上画画

现在我们来看看 HTML5 中最强大的新特性之一,即canvas元素。我将解释应用程序中涉及到canvas的代码片段,然后展示一些简单的例子,最后回到我们在画布上绘制骰子面的目标。回想一下,HTML 文档的大纲是

<html>
        <head>
                <title>... </title>
        <style>...</style>
                <script> .... </script>
        </head>
        <body>
         ... Here is where the initial static content will go...
        </body>
</html>

注意:你不必包含标题、样式或脚本元素,它们可以按任何顺序排列。第一章中的 favorites 示例使用了一个样式元素,但是 dice 示例不会。

为了使用canvas,我们在 HTML 文档的body元素中包含了canvas的标签,在script元素中包含了 JavaScript。我将首先描述一种编写canvas元素的标准方法。

<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>

如果带有此编码的 HTML 文件被不识别 canvas 的浏览器打开,屏幕上会出现消息Your browser doesn't support the HTML5 element canvas.。如果你正在为所有的浏览器准备网页,你可以选择引导访问者到别的地方或者尝试另一种策略。在本书中,我只关注 HTML5。

HTML canvas标签定义这个元素有一个“canvas”的id。这可能是任何东西,但使用画布没有坏处。但是,您可以有多个画布,在这种情况下,您需要为每个 ID 使用不同的值。但是,这不是我们为这个应用程序所做的,所以我们不必担心。设置widthheight的属性来指定这个canvas元素的维度。

现在我们已经看到了body中的画布,让我们看看 JavaScript。在画布上绘图的第一步是在 JavaScript 代码中定义适当的对象。为此,我需要一个变量,所以我用下面的代码设置了一个名为ctx的变量

var ctx;

在任何函数定义之外。这使它成为一个全局变量,可以从任何函数访问或设置。ctx变量是所有绘图都需要的东西。我选择将我的变量命名为ctx,这是上下文的缩写,复制了我在网上看到的许多例子。我可以选择任何名字。

在代码的后面(您将看到后面例子中的所有代码,您可以下载源代码),我编写了设置ctx值的代码。

  ctx = document.getElementById('canvas').getContext('2d');

语句设置ctx在我定义的名为init的函数中,该函数在body标签中被引用

<body onload=”init();>

将语句放在init函数中意味着在下载完主体中的所有内容之后,调用任何其他函数之前,调用该语句。

赋值语句设置ctx首先获取文档中 ID 为'canvas'的元素,然后提取所谓的'2d'上下文。我们都可以预料到,未来可能会带来其他的情境!现在,我们使用2d的一个。

在 JavaScript 代码中,您可以绘制矩形、包括线段和圆弧的路径,并在画布上定位图像文件。你也可以填充矩形和路径。然而,在我们这样做之前,我们需要处理坐标系和弧度。

正如全球定位系统使用纬度和经度来定义您在地图上的位置一样,我们需要一种方法来指定屏幕上的点。这些点被称为像素,我们在前一章中使用它们来指定图像的宽度和边框的厚度。像素是一个非常小的测量单位,如果你做些实验,你就会发现。但是,大家一致同意线性单位是不够的。我们还需要就测量的起点达成一致,就像 GPS 系统使用格林威治子午线和赤道一样。对于作为画布的二维矩形,它被命名为原点注册点。原点是canvas元素的左上角。请注意,在第六章中,当我们通过在 HTML 文档中而不是在canvas元素中创建和定位元素来描述智力竞赛节目时,坐标系是类似的。原点仍然是窗口的左上角。

这不同于你可能从解析几何或制图中回忆起来的。水平数字的值从左向右增加。垂直数字的值随着屏幕上的下移而增加。写坐标的标准方式是先放水平值,再放垂直值。在某些情况下,水平值称为 x 值,垂直值称为 y 值。在其他情况下,水平值是左边(认为它是从左边),垂直值是顶部(认为它是从顶部)。

图 2-5 显示了一个 900 像素宽 600 像素高的浏览器窗口的布局。数字表示角和中间的坐标值。

img/214814_2_En_2_Fig5_HTML.jpg

图 2-5

浏览器窗口的坐标系

现在我们来看几个画图的语句,然后把它们放在一起画出简单的形状(见图 2-6 到 2-10 )。之后,我们将看到如何绘制点和矩形来代表模具面。

下面是绘制矩形的 HTML5 JavaScript 代码:

ctx.strokeRect(100,50,200,300);

这将绘制一个空心矩形,其左上角距离左侧 100 像素,距离顶部 50 像素。该矩形的宽度为 200,高度为 300。该语句将使用线宽和颜色的任何当前设置。

下一段代码演示了如何将线宽设置为 5,并将笔画的颜色(即轮廓)设置为指示的 RGB 值(即红色)。使用变量xywh中的值绘制矩形。

ctx.lineWidth = 5;
ctx.strokeStyle = "rgb(255,0,0)";
ctx.strokeRect(x,y,w,h);

这个片段

ctx.fillStyle = "rgb(0,0,255)";
ctx.fillRect(x,y,w,h);

在指示的位置和尺寸绘制蓝色实心矩形。如果您想要绘制一个带有红色轮廓的蓝色矩形,可以使用两行代码:

ctx.fillRect(x,y,w,h);
ctx.strokeRect(x,y,w,h);

HTML5 让你画出所谓的由弧线和线段组成的路径。使用ctx.moveToctx.lineTo的组合绘制线段。我将在几章中介绍它们:第四章的弹弓游戏,第五章的使用多边形的记忆游戏,以及第九章的刽子手游戏。在第四章的炮弹游戏中,我还会演示如何倾斜一个长方形,第九章的刽子手游戏演示如何画椭圆形。在这一章中,我将把重点放在弧线上。

您可以使用以下方式开始一条路径

ctx.beginPath();

结束它,路径被画出,或者

ctx.closePath();
ctx.stroke();

或者

ctx.closePath();
ctx.fill();

还有一些情况下,您可以省略对closePath的调用。

弧可以是整个圆,也可以是圆的一部分。在骰子应用程序中,我们只画完整的圆来表示每个骰子面上的点数,但是我将解释弧一般是如何工作的,以使代码不那么神秘。绘制圆弧的方法具有以下格式:

ctx.arc(cx, cy, radius, start_angle, end_angle, direction);

其中cxcyradius为圆心横坐标、纵坐标和半径。要解释接下来的两个参数,需要讨论测量角度的方法。你对角度的度数单位很熟悉:我们说 180 度转弯,意思是 U 形转弯,90 度角是由两条垂直线形成的。但是大多数计算机编程语言使用另一种系统,称为弧度。这里有一种直观化弧度的方法——想象一下将一个圆的半径放在这个圆上。你可以挖掘你的记忆,意识到这不会是一个整齐的拟合,因为圆周围有 2* PI 弧度,比 6 多一些。因此,如果我们想画一个完整的圆弧,我们指定起始角度为 0,结束角度为 2 *π。幸运的是,Math类提供了一个常量Math.PI,它是圆周率的值(根据需要,精确到尽可能多的小数位),所以在代码中,我们写2*Math.PI。如果我们想要指定一个半圆的圆弧,我们使用Math.PI,而一个直角(90 度)将是.5*Math.PI

arc 方法还需要一个参数,方向。我们如何画出这些弧线?想象一下钟面上指针的运动。在 HTML 5 中,顺时针是假方向,逆时针是真方向。(别问为什么。这就是 HTML5 中指定的方式。)我使用内置的 JavaScript 值truefalse。当我们需要画非整圆的圆弧时,这将是很重要的。如果你需要画非完整圆的圆弧,这个特殊问题的性质决定了你如何定义角度。

这里有一些例子,带有完整的代码,供你创建(使用 TextPad 或 TextWrangler),然后改变以测试你的理解。第一个画一个弧线,代表微笑。

<html>
<head>
<title>Smile</title>
<script>
function init() {
        var ctx =document.getElementById("canvas").getContext('2d');
        ctx.beginPath();
        ctx.strokeStyle = "rgb(200,0,0)";
        ctx.arc(200, 200,50,0,Math.PI, false);
        ctx.stroke();
}
</script>
</head>
<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>
</body>
</html>

图 2-6 显示了屏幕的一部分,该部分带有由该代码产生的弧线。

img/214814_2_En_2_Fig6_HTML.jpg

图 2-6

表情产生的“微笑”ctx.arc(200,200,50,0,Math.PI, false)

你可以向前看图 2-11 、 2-12 和 2-13 ,其中我捕捉了更多的屏幕以查看绘图的位置。请改变你自己例子中的数字,这样你就能理解坐标系是如何工作的,以及一个像素实际上有多大。

在看到皱眉之前,试着让弧线变宽或变高,或者改变颜色。然后试着上下左右移动整个弧线。提示:你需要改变路线

ctx.arc(200, 200,50,0,Math.PI, false);

改变200,200重置圆心,改变50改变半径。

现在,让我们继续其他的变化。一定要把每一个都拿来做实验。将arc方法的最后一个参数更改为true:

ctx.arc(200,200,50,0,Math.PI,true);

使弧线逆时针旋转。完整的代码是:

<html>
        <head>
                <title>Frown</title>
<script type="text/javascript">
function init() {
        var ctx =document.getElementById("canvas").getContext('2d');
        ctx.beginPath();
        ctx.strokeStyle = "rgb(200,0,0)";
        ctx.arc(200, 200,50,0,Math.PI, true);
        ctx.stroke();
}
</script>
</head>

<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>

</body>
</html>

请注意,我还更改了标题。标题出现在浏览器的选项卡上。你的用户/观众会注意到标题。我发现我在调试中使用标题来跟踪不同的版本。该代码产生如图 2-7 所示的屏幕。

img/214814_2_En_2_Fig7_HTML.jpg

图 2-7

表达式 ctx.arc(200,200,50,0,数学)产生的“皱眉”。PI,真);

输入语句以关闭笔划前的路径:

ctx.closePath();
ctx.stroke();

在皱眉示例中,将“结束”圆弧。完整的代码是

<html>
        <head>
                <title>Frown</title>
<script type="text/javascript">
function init() {
        var ctx =document.getElementById("canvas").getContext('2d');
        ctx.beginPath();
        ctx.strokeStyle = "rgb(200,0,0)";
        ctx.arc(200, 200,50,0,Math.PI, true);
        ctx.closePath();
        ctx.stroke();
}
</script>
</head>

<body>
<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>

</body>
</html>

这将产生如图 2-8 所示的屏幕。

img/214814_2_En_2_Fig8_HTML.jpg

图 2-8

通过在ctx.stroke();前添加ctx.closePath();,眉头皱成了一个半圆

closePath命令并不总是必需的,但是包含它是一个好习惯。您会注意到我等待调用closePath并填充多个点的语句。在这里进行实验,并期待第五章中的弹弓图和第九章中的刽子手图。如果你想填充路径,你可以用ctx.fill()代替ctx.stroke(),这样会产生一个黑色的填充形状,如图 2-9 所示。完整的代码是

<html>
        <head>
                <title>Smile</title>
<script type="text/javascript">
function init() {
        var ctx =document.getElementById("canvas").getContext('2d');
        ctx.beginPath();
        ctx.strokeStyle = "rgb(200,0,0)";
        ctx.arc(200, 200,50,0,Math.PI, false);
        ctx.closePath();
        ctx.fill();
}
</script>
</head>

<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>

</body>
</html>

黑色是默认颜色。

img/214814_2_En_2_Fig9_HTML.jpg

图 2-9

使用ctx.fill()填充半圆

如果你希望一个形状被填充并有一个清晰的轮廓,你可以使用fillstroke命令,并使用fillStylestrokeStyle属性指定不同的颜色。配色方案基于第一章中介绍的相同红/绿/蓝代码。你可以尝试或使用 Photoshop 或在线图片编辑器pixlr.com等工具来获得你想要的颜色。以下是完整的代码:

<html>
        <head>
                <title>Smile</title>
<script type="text/javascript">
function init() {
        var ctx =document.getElementById("canvas").getContext('2d');
        ctx.beginPath();
        ctx.strokeStyle = "rgb(200,0,0)";
        ctx.arc(200, 200,50,0,Math.PI, false);
        ctx.fillStyle = "rgb(200,0,200)";
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle="rgb(255,0,0)";
        ctx.lineWidth=5;
        ctx.stroke();
}
</script>
</head>

<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>

</body>
</html>

这段代码产生一个填充了紫色(红蓝混合)的半圆,用笔画出来,也就是一个纯红的轮廓,如图 2-10 所示。编码指定一个路径,然后将该路径绘制为填充,然后将该路径绘制为笔画。

img/214814_2_En_2_Fig10_HTML.jpg

图 2-10

使用不同颜色的填充和描边

许多不同的命令都会生成一个完整的圆,包括:

ctx.arc(200,200,50,0, 2*Math.PI, true);
ctx.arc(200,200,50, 0, 2*Math.PI, false);
ctx.arc(200,200,50, .5*Math.PI, 2.5*Math.PI, false);

你不妨坚持第一个,它和其他的一样好。注意,我仍然使用closePath命令。从几何角度来看,圆可能是一个封闭的图形,但从 JavaScript 角度来看,这无关紧要。

如果你把canvas元素想象成一块画布,你在上面放一些墨水或颜料,你会意识到你需要擦除画布或它的适当部分来绘制新的东西。为此,HTML5 提供了命令

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

后面的例子展示了如何绘制弹弓(第四章)、记忆/注意力游戏的多边形(第五章)、迷宫的墙壁(第七章)以及《刽子手》中的简笔画(第九章)。现在让我们回到掷骰子游戏需要什么。

使用表单显示文本输出

在画布上写文本是可能的(参见第五章,但是对于 craps 应用程序,我选择使用一个form,一个在 HTML 的旧版本和当前版本中都存在的元素。我不使用玩家输入的表单。我确实用它来输出掷骰子结果的信息。HTML5 规范指出了建立表单的新方法,包括检查或验证输入的类型和范围。下一章的应用程序演示了验证。

我使用下面的 HTML 来生成骰子游戏的表单:

<form name="f">
Stage: <input name="stage" value="First Throw"/>
Point: <input name="pv" value="   "/>
Outcome: <input name="outcome" value="     "/>
</form>

表单以一个name属性开始。文本Stage:Point:Outcome:出现在输入字段旁边。输入标签——注意这些是单独标签——既有名称字段,也有值字段。JavaScript 代码将使用这些名称。您可以将任何 HTML 放入一个表单中,也可以将一个表单放入任何 HTML 中。

因为骰子游戏使用了新的button元素,所以我只是添加了带有用于向玩家显示信息的字段的form元素,而没有包括类型为submit的输入元素。或者,我可以使用带有submit输入字段的标准表单(不需要新的按钮元素),代码如下:

<form name="f" onSubmit="throwdice();">
Stage: <input type="text" name="stage" value="First Throw"/>
Point: <input type="text" name="pv" value="   "/>
Outcome: <input type="text" name="outcome" value="     "/>
<input type="submit" value="THROW DICE"/>
</form>

类型submit的输入元素在屏幕上产生一个按钮。这些都是我们构建 craps 应用程序所需的概念。我们现在可以开始编码了。

构建应用程序并使之成为您自己的应用程序

您可能已经在小例子中尝试过使用本章中描述的 HTML5、CSS 和 JavaScript 结构。提示:请做。唯一的学习方法就是自己树立榜样。作为构建 craps 应用程序的一种方式,我们现在来看三个应用程序:

  • 扔一次骰子,然后重新装弹再扔一次

  • 用一个按钮扔两个骰子

  • 掷骰子的完整游戏

图 2-11 显示了第一个应用程序的可能打开屏幕。我说可能是因为它不会总是 4。我故意捕捉这个截图来显示几乎所有的窗口,这样你就可以看到绘图在屏幕上的位置。

img/214814_2_En_2_Fig11_HTML.jpg

图 2-11

单模应用

图 2-12 显示了掷骰子应用程序的打开屏幕。出现的只是按钮。

img/214814_2_En_2_Fig12_HTML.jpg

图 2-12

双骰子应用程序的开始屏幕

最后,图 2-13 显示玩家点击按钮后的屏幕。

img/214814_2_En_2_Fig13_HTML.jpg

图 2-13

点击按钮掷出一对骰子

逐步构建您的应用程序是一种很好的技术。这些应用程序是使用文本编辑器构建的,如 TextPad 或 TextWrangler。记得将文件保存为类型。html——尽早并经常这样做。您不必在保存前完成。当您完成第一个应用程序并保存和测试它时,您可以使用新的名称再次保存它,然后对这个新副本进行修改以成为第二个应用程序。对第三个应用程序进行同样的操作。

扔一个骰子

第一个应用程序的目的是在画布上显示一个随机的骰子面,以标准方式显示圆形。

对于任何应用程序,通常有许多可行的方法。我意识到,我可以从一些编码中获得双重职责,因为 3 模面的图案可以通过组合 2 和 1 图案来制作。类似地,5 的模式是 4 和 1 的组合。6 的图案是 4 的图案和一些独特图案的组合。我可以把所有的代码都放在init函数中,或者使用一个单独的drawface函数。无论如何,这对我来说是有意义的,我很快就编程并调试了它。表 2-1 列出了所有的功能并指出什么调用什么。表 2-2 显示了完整的代码,解释了每一行的作用。

表 2-2

投掷单个骰子应用程序的完整代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Throwing 1 die</title> | 完整的title元素。 |
| <script> | 开始script标签。 |
| var cwidth = 400; | 保存画布宽度的变量;也用于擦除画布,为重绘做准备。 |
| var cheight = 300; | 保存画布高度的变量;也用于擦除画布,为重绘做准备。 |
| var dicex = 50; | 保持单个模具水平位置的变量。 |
| var dicey = 50; | 保持单个模具垂直位置的变量。 |
| var dicewidth = 100; | 保存模具面宽度的变量。 |
| var diceheight = 100; | 保存模具面高度的变量。 |
| var dotrad = 6; | 保存点的半径的变量。 |
| var ctx; | 保存画布上下文的变量,在所有绘制命令中使用。 |
| function init() { | 开始为init函数定义函数,该函数在文档的onLoad中被调用。 |
| var ch = 1+Math.``floor(Math.random()*6); | 声明并将ch变量的值随机设置为数字 1、2、3、4、5 或 6。 |
| drawface(ch); | 用参数ch调用drawface函数。 |
| } | 结束函数定义。 |
| function drawface(n) { | drawface函数的函数定义的开始,其参数是点数。 |
| ctx = document.getElementBy``Id('canvas').getContext('2d'); | 获取用于在画布上绘制的对象。 |
| ctx.lineWidth = 5; | 将线宽设置为 5。 |
| ctx.clearRect(dicex,dicey,``dicewidth,diceheight); | 清除可能已绘制模具面的空间。这在第一次没有效果。 |
| ctx.strokeRect(dicex,dicey,``dicewidth,diceheight); | 画出模具面的轮廓。 |
| ctx.fillStyle = "#009966"; | 设置圆的颜色。我用一个图形程序来确定这个值。你可以这样做,或者实验。 |
| switch(n) { | 使用点数开始switch。 |
| case 1: | 如果是 1。 |
| draw1(); | 调用draw1函数。 |
| break; | 打开开关。 |
| case 2: | 如果是 2。 |
| draw2(); | 调用draw2函数。 |
| break; | 打开开关。 |
| case 3: | 如果是 3。 |
| draw2(); | 先打draw2然后。 |
| draw1(); | 调用draw1。 |
| break; | 打开开关。 |
| case 4: | 如果是 4。 |
| draw4(); | 调用draw4函数。 |
| break; | 打开开关。 |
| case 5: | 如果是 5。 |
| draw4(); | 调用draw4函数,然后。 |
| draw1(); | 调用draw1函数。 |
| break; | 打开开关。 |
| case 6: | 如果是 6。 |
| draw4(); | 调用draw4函数,然后。 |
| draw2mid(); | 调用draw2mid函数。 |
| break; | 断开开关(并非绝对必要)。 |
| } | 关闭switch语句。 |
| } | 关闭drawface功能。 |
| function draw1() { | 开始定义draw1。 |
| var dotx; | 用于绘制单点的水平位置的变量。 |
| var doty; | 用于绘制单点的垂直位置的变量。 |
| ctx.beginPath(); | 开创一条道路。 |
| dotx = dicex + .5*dicewidth; | 将该点的中心水平设置在模具面的中心 |
| doty = dicey + .5*diceheight; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); | 构造一个圆(用 fill 命令绘制)。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画出路径,也就是填满圆。 |
| } | 关闭draw1。 |
| function draw2() { | 开始draw2功能。 |
| var dotx; | 用于绘制两点的水平位置的变量。 |
| var doty; | 用于绘制两点的垂直位置的变量。 |
| ctx.beginPath(); | 开创一条道路。 |
| dotx = dicex + 3*dotrad; | 将该点的中心设置为距离模具面上角三个半径长度的水平距离 |
| doty = dicey + 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); | 构造第一个点。 |
| dotx = dicex+dicewidth-3*dotrad; | 将该点的中心设置为距离模具面下角三个半径长度的水平距离 |
| doty = dicey+diceheight-3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); | 构造第二个点。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 把两个点都画出来。 |
| } | 关闭draw2。 |
| function draw4() { | 开始draw4功能。 |
| var dotx; | 用于绘制点的水平位置的变量。 |
| var doty; | 用于绘制点的垂直位置的变量。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dicex + 3*dotrad; | 将第一个点水平放置在左上角内 |
| doty = dicey + 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); | 构建圆圈。 |
| dotx = dicex+dicewidth-3*dotrad; | 将第二个点水平放置在右下角 |
| doty = dicey+diceheight-3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构建点。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画两个点。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dicex + 3*dotrad; | 将此点水平放置在左下角 |
| doty = dicey + diceheight-3*dotrad; | ...垂直(注意,这是刚刚使用的相同 y 值)。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构建圆形。 |
| dotx = dicex+dicewidth-3*dotrad; | 将该点水平放置在左上角内侧 |
| doty = dicey+ 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构建圆形。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画两个点。 |
| } | 关闭draw4功能。 |
| function draw2mid() { | 启动draw2mid功能。 |
| var dotx; | 用于绘制两点的水平位置的变量。 |
| var doty; | 用于绘制两点的垂直位置的变量。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dicex + 3*dotrad; | 将这些点水平地放在里面 |
| doty = dicey + .5*diceheight; | 中间垂直。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构建圆形。 |
| dotx = dicex+dicewidth-3*dotrad; | 将这个点放在右边框内。 |
| doty = dicey + .5*diceheight; //no change | 中间位置 y。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构建圆形。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画点。 |
| } | 关闭draw2mid功能。 |
| </script> | 关闭script元素。 |
| </head> | 关闭head元素。 |
| <body onLoad="init();"> | 开始body标签,设置onLoad属性来调用init()函数。 |
| <canvas id="canvas" width="400" height="300">``Your browser doesn't support``the HTML5 element canvas.``</canvas> | 如果浏览器不接受canvas元素,设置画布并提供通知。 |
| </body>``</html> | 关闭body和关闭html元件。 |

表 2-1

单骰子投掷应用中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 由标签<body>中的onLoad动作调用 | drawface |
| drawface | 由init调用 | draw1, draw2, draw4, draw2mid |
| draw1 | 由drawface在三个地方调用 1、3、5 |   |
| draw2 | 由drawface在 2 和 3 两个面调用 |   |
| draw4 | 被drawface在三个地方调用为 4、5、6 |   |
| draw2mid | 在一个地方被drawface呼叫了 6 |   |

您可以并且应该在代码中添加注释。注释是被浏览器忽略的文本片段,但它可以提醒你,也许还有其他稍后会看到这个程序的人,正在发生什么。一种形式的注释以一行上的两个斜线开始。斜线右侧的所有内容都将被忽略。对于较大的注释,可以用斜杠和星号开始注释,用星号和斜杠结束注释。

/*
This is a comment.
*/

这是一个照我说的做,而不是照我做的情况。由于我使用表格在每一行放置注释,并且您可以将整个章节视为注释,所以我没有在代码中包含很多注释。我再说一遍:你应该!

提示:当我开发这段代码(以及任何涉及随机效果的代码)时,我不想用随机编码进行初始测试。所以,就在这条线后面

var ch = 1+Math.floor(Math.random()*6);

我放了线

ch  = 1;

测试了一下,然后我把它改成了

ch = 2;

诸如此类。当我完成这个阶段的测试时,我删除了这一行(或者用//注释掉它)。这属于一般的建议:在开发游戏时,尽量避免玩复杂的游戏。

扔两个骰子

下一个应用程序使用一个按钮让玩家做一些事情,而不仅仅是重新加载网页,它还模拟了一对骰子的投掷。在看代码之前,想想你能从第一个应用程序中继承什么。总的回答是:大部分。“延续”是编写代码和测试代码的一种节约。

第二个应用程序需要对两个模具面的定位做一些事情,为此使用另外两个变量,dxdy。它还需要使用Math.random并调用drawface两次来重复代码,以生成每个模具面。需要改变引发投掷的原因。表 2-3 描述了调用和被调用的函数,本质上与表 2-1 相同,除了现在有一个名为throwdice的函数,它由按钮标签的onClick属性设置的动作调用。表 2-4 包含投掷两个骰子应用程序的完整 HTML 文档。

表 2-4

完整的双骰子应用

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Throwing dice</title> | 完整的title元素。 |
| <script> | 开始script标签。 |
| var cwidth = 400; | 保存画布宽度的变量。 |
| var cheight = 300; | 保存画布高度的变量;也用于擦除画布,为重绘做准备。 |
| var dicex = 50; | 可变地保持单个模具的水平位置;也用于擦除画布,为重绘做准备。 |
| var dicey = 50; | 保持单个模具垂直位置的变量。 |
| var dicewidth = 100; | 保存模具面宽度的变量。 |
| var diceheight = 100; | 保存模具面高度的变量。 |
| var dotrad = 6; | 保存点的半径的变量。 |
| var ctx; | 保存画布上下文的变量,在所有绘制命令中使用。 |
| var dx; | 用于水平定位的变量,并根据两个模面的不同而变化。 |
| var dy; | 用于垂直定位的变量。这对于两个模具面是相同的。 |
| function throwdice() { | 启动throwdice功能。 |
| var ch = 1+Math.floor(Math.random()*6); | 声明变量ch,然后用一个随机值设置它。 |
| dx = dicex; | 为第一个模具面设置dx。 |
| dy = dicey; | 为第一个和第二个模具面设置dy。 |
| drawface(ch); | 以ch为点数调用drawface。 |
| dx = dicex + 150; | 调整第二个模具面的dx。 |
| ch=1 + Math.floor(Math.random()*6); | 用随机值重置ch。 |
| drawface(ch); | 以ch为点数调用drawface。 |
| } | 关闭throwdice功能。 |
| function drawface(n) { | drawface函数的函数定义的开始,其参数是点数。 |
| ctx = document.getElementById``('canvas').getContext('2d'); | 获取用于在画布上绘制的对象。 |
| ctx.lineWidth = 5; | 将线宽设置为 5。 |
| ctx.clearRect(dx,dy,dicewidth,diceheight); | 清除可能已绘制模具面的空间。这第一次没有效果。 |
| ctx.strokeRect(dx,dy,dicewidth,diceheight); | 画出模具面的轮廓。 |
| var dotx; | 保持水平位置的变量。 |
| var doty; | 保持垂直位置的变量。 |
| ctx.fillStyle = "#009966"; | 设置颜色。 |
| switch(n) { | 使用点数开始switch。 |
| case 1: | 如果是 1。 |
| draw1(); | 调用draw1函数。 |
| break; | 打开开关。 |
| case 2: | 如果是 2。 |
| draw2(); | 调用draw2函数。 |
| break; | 打开开关。 |
| case 3: | 如果是 3。 |
| draw2(); | 先打draw2然后。 |
| draw1(); | 调用draw1。 |
| break; | 打开开关。 |
| case 4: | 如果是 4。 |
| draw4(); | 调用draw4函数。 |
| break; | 打开开关。 |
| case 5: | 如果是 5。 |
| draw4(); | 调用draw4函数,然后 |
| draw1(); | 调用draw1函数。 |
| break; | 打开开关。 |
| case 6: | 如果是 6。 |
| draw4(); | 调用draw4函数,然后 |
| draw2mid(); | 调用draw2mid函数。 |
| break; | 断开开关(并非绝对必要) |
| } | 关闭switch语句。 |
| } | 关闭drawface功能。 |
| function draw1() { | 开始定义draw1。 |
| var dotx; | 用于绘制单点的水平位置的变量。 |
| var doty; | 用于绘制单点的垂直位置的变量 |
| ctx.beginPath(); | 开创一条道路。 |
| dotx = dx + .5*dicewidth; | 将该点的中心水平设置在模具面的中心(使用dx) |
| doty = dy + .5*diceheight; | ...(使用dy)垂直。 |
| ctx.arc(dotx,doty,dotrad,``0,Math.PI*2,true); | 构造一个圆(用 fill 命令绘制)。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画出路径,也就是圆。 |
| } | 关闭draw1。 |
| function draw2() { | 开始draw2功能。 |
| var dotx; | 用于绘制两点的水平位置的变量。 |
| var doty; | 用于绘制两点的垂直位置的变量。 |
| ctx.beginPath(); | 开创一条道路。 |
| dotx = dx + 3*dotrad; | 将该点的中心设置为距离模具面上角三个半径长度的水平距离 |
| doty = dy + 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构造第一个点。 |
| dotx = dx+dicewidth-3*dotrad; | 将该点的中心设置为距离模具面下角 3 个半径长度的水平距离 |
| doty = dy+diceheight-3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构造第二个点。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 把两个点都画出来。 |
| } | 关闭draw2。 |
| function draw4() { | 开始draw4功能。 |
| var dotx; | 用于绘制点的水平位置的变量。 |
| var doty; | 用于绘制点的垂直位置的变量。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dx + 3*dotrad; | 将第一个点水平放置在左上角内 |
| doty = dy + 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建圆圈。 |
| dotx = dx+dicewidth-3*dotrad; | 将第二个点水平放置在右下角 |
| doty = dy+diceheight-3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建点。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画两个点。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dx + 3*dotrad; | 将此点水平放置在左下角 |
| doty = dy + diceheight-3*dotrad; | ...垂直(注意,这与刚刚使用的y值相同)。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建圆形。 |
| dotx = dx+dicewidth-3*dotrad; | 将该点水平放置在左上角内侧 |
| doty = dy+ 3*dotrad; | ...垂直地。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建圆形。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画两个点。 |
| } | 关闭draw4功能。 |
| function draw2mid() { | 启动draw2mid功能。 |
| var dotx; | 用于绘制两点的水平位置的变量。 |
| var doty; | 用于绘制两点的垂直位置的变量。 |
| ctx.beginPath(); | 开始路径。 |
| dotx = dx + 3*dotrad; | 将这些点水平地放在里面 |
| doty = dy + .5*diceheight; | 中间垂直。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建圆形。 |
| dotx = dx+dicewidth-3*dotrad; | 将这个点放在右边框内。 |
| doty = dy + .5*diceheight; | 位置y中途(无变化)。 |
| ctx.arc(dotx,doty,dotrad,0,``Math.PI*2,true); | 构建圆形。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.fill(); | 画点。 |
| } | 关闭draw2mid功能。 |
| </script> | 关闭script元素。 |
| </head> | 关闭head元素。 |
| <body> | 开始body标签。 |
| <canvas id="canvas" width="400" height="300"> | 画布tag开始。 |
| Your browser doesn't support the``HTML5 element canvas. | 如果浏览器不接受canvas元素,设置画布并提供通知。 |
| </canvas> | 关闭canvas标签。 |
| <br/> | 换行。 |
| <button onClick="throwdice();">``Throw dice </button> | 按钮元素(注意属性onClick设置调用throwdice)。 |
| </body> | 关闭body标签。 |
| </html> | 关闭html标签。 |

表 2-3

双骰子应用中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| throwdice | 由标签<button>中的onClick动作调用 | drawface |
| drawface | 由throwdice调用 | draw1, draw2, draw4,  draw2mid |
| draw1 | 由drawface在三个地方调用 1、3、5 |   |
| draw2 | 被drawface在两个地方调用为 2 和 3 |   |
| draw4 | 被drawface在三个地方调用为 4、5、6 |   |
| draw2mid | 在一个地方被drawface呼叫了 6 |   |

掷骰子的完整游戏

第三个应用程序是完整的双骰子游戏。同样,许多内容可以从以前的应用程序中继承过来。然而,现在我们需要加入游戏规则。其中,这将意味着使用条件语句ifswitch,以及全局变量,即在任何函数定义之外定义的变量,来跟踪是否是第一回合(firstturn)以及玩家的要点是什么(point)。这两个变量保存了掷骰子游戏的应用程序状态。正是这种相对简单的应用程序状态的存在、全局和局部变量的使用、条件语句和随机处理使得 craps 成为编程教师最喜欢的话题。

功能表与第二个应用给出的功能表相同(见表 2-3 ),所以我不再重复。表 2-5 保存了该应用程序的代码。新动作都在throwdice函数里。我会评论新的台词。

表 2-5

完整的 Craps 应用程序

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Craps game</title> |   |
| <script> |   |
| var cwidth = 400; |   |
| var cheight = 300; |   |
| var dicex = 50; |   |
| var dicey = 50; |   |
| var dicewidth = 100; |   |
| var diceheight = 100; |   |
| var dotrad = 6; |   |
| var ctx; |   |
| var dx; |   |
| var dy; |   |
| var firstturn = true; | 全局变量,初始化为值true。 |
| var point; | 全局变量不需要初始化,因为它将在使用前设置。 |
| function throwdice() { | 开始throwdice功能。 |
| var sum; | 变量来保存两个骰子的值的总和。 |
| var ch = 1+Math.floor(Math.random()*6); | 用第一个随机值设置ch。 |
| sum = ch; | 将此分配给sum。 |
| dx = dicex; | 设置dx。 |
| dy = dicey; | 设置dy。 |
| drawface(ch); | 绘制第一个模具面。 |
| dx = dicex + 150; | 调整水平位置。 |
| ch=1 + Math.floor(Math.random()*6); | 用随机值设置ch。这是给第二个骰子的。 |
| sum += ch; | 将ch添加到已经在sum中的内容中。 |
| drawface(ch); | 画第二个骰子。 |
| if (firstturn) { | 现在开始实施规则。这是第一次转弯吗? |
| switch(sum) { | 如果是,以sum为条件启动一个switch。 |
| case 7: | 为了 7 |
| case 11: | ..或者 11。 |
| document.f.outcome.value="You win!"; | 显示You win! |
| break; | 退出交换机。 |
| case 2: | 对两个人来说, |
| case 3: | ..或者 3 |
| case 12: | ..或者 12 个 |
| document.f.outcome.value="You lose!"; | 显示You lose! |
| break; | 退出交换机。 |
| default: | 还有别的吗 |
| point = sum; | 将总和保存在变量点中。 |
| document.f.pv.value=point; | 显示点值。 |
| firstturn = false; | 将firstturn设置为false。 |
| document.f.stage.value="Need follow-up throw."; | 显示Need follow-up throw。 |
| document.f.outcome.value="   "; | 擦除(清除)结果字段。 |
| } | 结束切换。 |
| } | 结束if-true子句。 |
| else { | 否则(不是第一轮)。 |
| switch(sum) { | 再次使用sum启动开关。 |
| case point: | 如果sum等于point中的值。 |
| document.f.outcome.value="You win!"; | 显示You win!。 |
| document.f.stage.value="Back to first throw."; | 显示Back to first throw。 |
| document.f.pv.value=" "; | 清除点值。 |
| firstturn = true; | 重置firstturn使其再次为真。 |
| break; | 退出交换机。 |
| case 7: | 如果总和等于 7。 |
| document.f.outcome.value="You lose!"; | 显示You lose!。 |
| document.f.stage.value="Back to first throw."; | 显示Back to first throw。 |
| document.f.pv.value=" "; | 清除点值。 |
| firstturn = true; | 重置firstturn,使其再次变为true。 |
| } | 合上开关。 |
| } | 关闭else子句。 |
| } | 关闭throwdice功能。 |
| function drawface(n) { |   |
| ctx = document.getElementById('canvas').getContext('2d'); |   |
| ctx.lineWidth = 5; |   |
| ctx.clearRect(dx,dy,dicewidth,diceheight); |   |
| ctx.strokeRect(dx,dy,dicewidth,diceheight) ; |   |
| ctx.fillStyle = "#009966"; |   |
| switch(n) { |   |
| case 1: |   |
| draw1(); |   |
| break; |   |
| case 2: |   |
| draw2(); |   |
| break; |   |
| case 3 : |   |
| draw2(); |   |
| draw1(); |   |
| break; |   |
| case 4: |   |
| draw4(); |   |
| break; |   |
| case 5: |   |
| draw4(); |   |
| draw1(); |   |
| break; |   |
| case 6: |   |
| draw4(); |   |
| draw2mid(); |   |
| break ; |   |
| } |   |
| } |   |
| function draw1() { |   |
| var dotx; |   |
| var doty; |   |
| ctx.beginPath(); |   |
| dotx = dx + .5*dicewidth; |   |
| doty = dy + .5*diceheight; |   |
| ctx.arc(dotx,doty,dotrad,0, Math.PI*2,true); |   |
| ctx.closePath(); |   |
| ctx.fill(); |   |
| } |   |
| function draw2() { |   |
| var dotx ; |   |
| var doty; |   |
| ctx.beginPath(); |   |
| dotx = dx + 3*dotrad; |   |
| doty = dy + 3*dotrad; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| dotx = dx+dicewidth-3*dotrad; |   |
| doty = dy+diceheight-3*dotrad; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| ctx.closePath(); |   |
| ctx.fill(); |   |
| } |   |
| function draw4() { |   |
| var dotx; |   |
| var doty; |   |
| ctx.beginPath(); |   |
| dotx = dx + 3*dotrad; |   |
| doty = dy + 3*dotrad ; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| dotx = dx+dicewidth-3*dotrad; |   |
| doty = dy+diceheight-3*dotrad; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| ctx.closePath(); |   |
| ctx.fill(); |   |
| ctx.beginPath(); |   |
| dotx = dx + 3*dotrad; |   |
| doty = dy + diceheight-3*dotrad;  //no change |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| dotx = dx+dicewidth-3*dotrad; |   |
| doty = dy+ 3*dotrad; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| ctx.closePath(); |   |
| ctx.fill() ; |   |
| } |   |
| function draw2mid() { |   |
| var dotx; |   |
| var doty ; |   |
| ctx.beginPath(); |   |
| dotx = dx + 3*dotrad; |   |
| doty = dy + .5*diceheight; |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| dotx = dx+dicewidth-3*dotrad; |   |
| doty = dy + .5*diceheight; //no change |   |
| ctx.arc(dotx,doty,dotrad,0,Math.PI*2,true); |   |
| ctx.closePath(); |   |
| ctx.fill(); |   |
| } |   |
| </script> |   |
| </head> |   |
| <body> |   |
| <canvas id="canvas" width="400" height="300"> |   |
| Your browser doesn't support the HTML5 element canvas . |   |
| </canvas> |   |
| <br/> |   |
| <button onClick="throwdice();">Throw dice </button> |   |
| <form name="f"> | 启动一个名为f的表单。 |
| Stage: <input name="stage" value="First Throw"/> | 在文本Stage:之前,设置一个名为stage的输入字段。 |
| Point: <input name="pv" value="   "/> | 在文本Point:之前,设置一个名为pv的输入字段。 |
| Outcome: <input name="outcome" value="     "/> | 在文本Outcome:之前,设置一个名为outcome的输入字段。 |
| </form> | 关闭form。 |
| </body> | 关闭body。 |
| </html> | 关闭html。 |

让应用程序成为你自己的

让这个应用程序成为你自己的并不像 favorite sites 应用程序那样简单,因为骰子的规则就是骰子的规则。然而,你可以做很多事情。使用fillRect并将fillStyle设置为不同的颜色,改变骰子面的大小和颜色。改变整个画布的颜色和大小。将结果的文本更改为更丰富多彩的内容。你也可以使用标准或特制的骰子来实现其他游戏。

你可以看到下一章,学习在画布上绘制图像,而不是用圆弧和矩形来绘制每个模具面。HTML5 提供了一种引入外部图像文件的方法。这种方法的缺点是,您必须跟踪这些单独的文件。

你可以开发保持分数的编码。对于一个赌博游戏,你可以给玩家一个固定的金额,比如说 100,无论货币单位是什么,然后扣除一些金额,比如说 10,然后加上一些金额,比如说 20,如果且仅如果玩家赢了。您可以将这些资金信息作为form元素的一部分添加到正文中:

<form name="f" id="f">
Stage: <input name="stage" value="First Throw"/>
Point: <input name="pv" value="   "/>
Outcome: <input name="outcome" value="     "/>
Bank roll: <input name="bank" value="100"/>
</form>

JavaScript(和其他编程语言)区分数字和代表数字的字符串。即值"100"是一串字符串,“1”、“0”、“0”。值 100 是一个数字。然而,在这两种情况下,变量的值都存储为 1 和 0 的序列。对于数字,这将是以二进制数表示的数字。对于字符串,每个字符将使用标准编码系统表示,如 ASCII 或 UNICODE。在某些情况下,JavaScript 会进行从一种数据类型到另一种数据类型的转换,但是不要依赖于它。我建议的代码使用内置函数StringNumber来完成这些转换。

throwdice函数中,在if(firstturn)语句之前,添加表 2-6 中的代码(或者类似的东西)。

表 2-6

为玩家添加银行

|

密码

|

说明

|
| --- | --- |
| var bank = Number(document.f.bank.value); | 将新变量bank设置为银行输入字段中的值所代表的数字。 |
| if (bank<10) { | 比较bank和 10。 |
| alert("You ran out of money! Add some more and try again."); | 如果bank小于 10,则发出警报。 |
| Return; | 不做任何事情就退出该功能。 |
| } | 关闭if true子句。 |
| bank = bank – 10; | 将bank减少 10。只有当坡度大于 10 时,才达到这条线。 |
| document.f.bank.value = String(bank); | 将该值的字符串表示形式放入 bank 字段。 |

然后,在玩家获胜的每个地方(在 7 和 11 情况后的第一回合的switch语句中,或者在点情况后的后续回合的switch语句中),添加表 2-7 中的代码。

表 2-7

增加银行的价值

|

密码

|

说明

|
| --- | --- |
| bank = Number(document.f.bank.value); | 将bank设置为银行输入字段中的值所代表的数字。再次设置bank允许玩家在游戏中途重新设置银行金额。 |
| bank +=20; | 使用+=运算符将 bank 的值增加 20。 |
| document.f.bank.value = String(bank); | 将银行金额的字符串表示放入银行字段。 |

当玩家输了,或者是后续回合的时候,你不加任何代码。每次新游戏开始前,银行价值都会下降。

测试和上传应用程序

这些应用程序在 HTML 文件中是完整的。不使用其他文件,如图像文件。相反,骰子面是画在画布上的。(供您参考,我用旧 HTML 编写的骰子游戏版本使用了一两个img元素。为了让这些固定的img元素显示不同的图像,我编写了代码,将src属性更改为不同的外部图像文件。当我上传应用程序时,我必须上传所有的图像文件。)

在浏览器中打开 HTML 文件。需要重新加载第一个应用程序以获得新的(单个)芯片。第二个和第三个应用程序(第三个是骰子游戏)使用一个按钮来掷骰子。

我重复我之前写的。为了测试这个程序,你需要检查许多案例。当你作为玩家赢了的时候,你还没有结束。典型问题包括:

  • 开始和结束标记缺失或不匹配。

  • 不匹配的开始和结束括号、{}包围函数、switch语句和if子句。

  • 缺少引号。使用 TextPad 和一些其他编辑器时可用的颜色编码在这里会有所帮助,因为它会高亮显示它识别的关键词。

  • 变量和函数的命名和使用不一致。这些名称可以是您选择的任何名称,但是您需要保持一致。功能draw2mid不会被drawmid2()调用。

除了最后一个,这些都是语法错误,类似于语法和标点符号的错误。语义(即意义)的错误可能更难发现。如果您编写第二个switch语句在 7 上赢,在点值上输,您可能编写了正确的 JavaScript 代码,但这不会是掷骰子的游戏。

这不应该发生在这里,因为你可以复制我的代码,但一个常见的错误是混淆坐标系,认为垂直值在屏幕上向上而不是向下增加。

摘要

在本章中,您学习了如何

  • 声明变量并使用全局变量来表示应用程序状态

  • 编写代码来执行算术运算

  • 定义和使用程序员定义的函数

  • 使用 JavaScript 的几个内置特性,包括Math.randomMath.floor方法

  • 使用ifswitch语句

  • 使用 HTML 元素创建画布

  • 画矩形和圆形

本章介绍了 HTML5 的一个关键特性,画布,以及随机性和交互性的概念。它还展示了许多编程特性,您将在本书剩余部分的示例中使用这些特性。尤其是,分阶段构建应用程序的技术非常有用。下一章的特色是一个球在盒子里弹跳的动画——为第四章的真实游戏做准备——被称为炮弹和弹弓的弹道模拟。

三、弹跳球

在本章中,我们将介绍

  • 创建程序员定义的对象

  • 使用setInterval制作动画

  • 绘制图像

  • 接受和验证表单输入

  • 使用按钮

  • 使用for循环

  • 带渐变的绘图

  • 预加载图像

介绍

动画,无论是在电影中,使用动画书,还是由计算机生成,都包括以足够快的速度显示一系列静止图像,以便我们将所看到的理解为运动,理解为生活。在这一章中,我将向你展示如何通过模拟一个在二维盒子中弹跳的球来制作动画场景,水平和垂直速度可以由玩家来改变。我们程序的第一次迭代以固定的时间间隔计算球的新位置并显示结果,它还确定球和墙何时会发生虚拟碰撞以及球如何从墙上反弹。之后,我们将看到如何用图像替换球,以及如何使用渐变绘制矩形。我们将研究用于验证表单输入的 HTML5 特性。最后,我将向您展示一个交互式示例,它为玩家提供了一种停止和重新开始弹跳的方法。这四个例子是:

  • 在 2D 盒子中弹跳的球(见图 3-1

  • 用一个图像替换球,并对盒子壁使用渐变(见图 3-2

  • 验证输入(参见图 3-3

  • 使图像在背景图像上反弹,并提供一种停止和恢复动作的方法(参见图 3-4

注意

我们要制作的这种动画叫做计算动画,在这种动画中,一个物体的位置被计算机程序重新计算,然后这个物体被重新显示。这与 cel(或逐帧)动画形成对比,后者使用预先绘制的单独静态图片。动画 gif 是 cel 动画的例子,可以在许多图形程序中制作。

你必须想象这些静态图片所代表的动画。在图 3-1 中,注意带有设置水平和垂直速度字段的表格。

img/214814_2_En_3_Fig1_HTML.png

图 3-1

弹跳球

在图 3-2 中,球已经被一个图像取代,墙壁也使用渐变填充。

img/214814_2_En_3_Fig2_HTML.jpg

图 3-2

球现在是来自外部文件的图像

HTML5 让您指定输入应该是什么。在这个例子中,我已经指定输入应该是一个数字,并指出最小和最大值。我使用 CSS 来指定如果用户输入了一个无效的条目,字段的颜色会变成红色。如图 3-3 所示。

img/214814_2_En_3_Fig3_HTML.jpg

图 3-3

显示错误输入的表单

这组应用程序演示了大量的编程,但它并不是真正的游戏,尽管人们喜欢看到头部或其他图像在盒子中跳动。受到最近的一张家庭照片的启发,我决定制作一个程序,它有一张跳动的照片,并带有停止和恢复动画的附加功能。我还包括显示背景图片的功能。图 3-4 为一张截图。游戏的目标是让移动的物体,一张棉花糖的照片,停下来靠近孩子,安妮卡,脸上涂着代表熊猫的颜料。见图 3-4 。这为我提供了一个例子来展示所谓的事件驱动编程的优势。

img/214814_2_En_3_Fig4_HTML.jpg

图 3-4

弹跳棉花糖游戏截图

关键要求

在开始编写任何代码之前,定义需求对于这个应用程序,实际上对于所有的编程都是非常重要的。该应用程序需要我在前面章节中演示过的东西:在画布元素上绘制形状和使用表单画布元素。在这个例子中,我们实际上将使用表单字段进行输入。在第二章描述的骰子游戏中,它们被严格用于输出。

在第一章中,HTML 文档使用了外部图像文件。在第二章中,我们完全用编码绘制了骰子的正面。在这一章中,我将演示两者:一个用代码绘制的弹跳圆圈和一个来自图像文件的弹跳图像。

为了实现这一点,我们需要一些代码能够在固定的时间间隔内做一些事情——现在,做什么并不重要。间隔需要足够短,以使结果看起来像运动。

在这种情况下,要做的事情是重新定位球,或者说球的位置。此外,代码需要确定球是否会碰到墙。现在,没有一个球,也没有任何墙壁。都是虚拟的,所以都是编码。我们将编写代码来计算球的虚拟位置和每面墙的虚拟位置。如果出现虚拟击球,代码会调整水平或垂直位移值,使球从墙上弹回。为了更准确,冒着变得迂腐的风险,代码设置了某些值,以便在下一次迭代中,球对象朝着不同的方向前进。

为了计算重新定位,我们使用初始值或者在表单的输入字段中输入的任何新值。然而,目标是产生一个健壮的系统,它不会对玩家的错误输入采取行动。错误的输入可能不是数字或超出指定范围的数字。我们可以不对错误的输入采取行动。然而,我们想给玩家反馈输入错误,所以我们会让输入框改变颜色,如图 3-3 所示。

为了给用户提供一种方式,现在称之为“播放器”,一种与应用程序交互的方式,我添加了代码来呈现播放器所看到的停止按钮和继续按钮。响应点击停止按钮的功能停止时间间隔事件。响应点击恢复按钮的函数启动时间间隔事件。

HTML5、CSS、JavaScript 特性

让我们看看实现弹跳球应用程序所需的 HTML5、CSS 和 JavaScript 的具体特性。我们将建立在前几章的基础上,特别是 HTML 文档的一般结构,使用一个canvas元素,程序员定义的和内置的函数,以及一个form元素。

画一个球或一幅或多幅图像

如第二章所述,在画布上绘制任何东西,比如一个代表球的圆,需要在HTML文档的body部分包含canvas元素。接下来,我们需要定义一个变量ctx,并添加设置这个变量的值的代码,这样我们就可以使用 JavaScript 了。下面是实现这一点的语句:

ctx = document.getElementById('canvas').getContext('2d');

正如我们在第二章中看到的,一个圆是通过绘制一个圆弧作为路径的一部分来创建的。下面几行代码启动路径,设置填充颜色,指定弧线,然后使用fill方法绘制一个封闭的填充路径。注意,arc方法使用变量来指定圆心坐标和半径。参数0Math.PI*2代表角度,在本例中为0Math.PI*2,形成一个完整的圆。true参数表示逆时针方向,尽管在这种特殊情况下,false会产生相同的效果。

ctx.beginPath();
ctx.fillStyle ="rgb(200,0,50)";
ctx.arc(ballx, bally, ballrad,0,Math.PI*2,true);
ctx.fill();

对于弹跳球的第一个版本,该框被绘制为矩形轮廓。轮廓的宽度,称为笔画,使用

ctx.lineWidth = ballrad;

你可以试试线宽。请记住,如果您将宽度设置得很小,并将球设置为快速移动,球可以一步弹过墙。

绘制矩形的语句是

ctx.strokeRect(boxx,boxy,boxwidth,boxheight);

我把球的代码放在矩形的代码之前,这样矩形就会在上面。我觉得这样跳起来更好看。

该程序的第二个版本显示了球的图像。这需要代码通过调用Image()使用new操作符建立一个img对象,将它赋给一个变量,并给src属性一个值。在应用程序中,我们在一条语句中完成了所有这些工作,但是让我们来看看各个部分。

你会在第二章中读到var语句。这样的语句定义,或者说声明,一个变量。这里我们的var可以用 img 这个名字;与 HTML img元素没有冲突。new操作符名副其实:它创建了一个新对象,在这里是内置类型Image。Image 函数称为构造函数:它构造一个 Image 类型的对象。Image函数没有任何参数,所以只有左括号和右括号。

图像对象有属性,就像 HTML 元素如img一样。使用的特定图像由src属性的值指示。这里,pearl.jpg是与 HTML 文档位于同一文件夹中的图像文件的名称。下面两条语句设置了img变量,并将其src(源)设置为图像文件的地址,即 URL。

var img = new Image();
img.src="pearl.jpg";

对于您的应用程序,请使用您选择的图像文件的名称。它可以是 JPG、PNG 或 GIF 类型,请确保将它放在与 HTML 文档相同的文件夹中,或者包含适当的路径。注意匹配名称和扩展名的大小写。

要在画布上绘制这个图像,我们需要一行代码来指定图像对象、图像左上角的位置以及图像显示中使用的宽度和长度。与矩形的情况一样,这段代码是对一个上下文对象的方法的调用,所以我使用 init 函数中定义的变量ctx。我需要调整用于圆心的ballxbally值来表示上角。我用两倍的球半径来表示宽度和长度。声明是

ctx.drawImage(img,ballx-ballrad,bally-ballrad,2*ballrad,2*ballrad);

现在让我们休息一下。亲爱的读者,轮到你做些工作了。考虑下面的 HTML 文档:

<html>
<head>
<title>The Origami Frog</title>
<script>
var img = new Image();
img.src = "frogface.gif";
var ctx;

function init() {
        ctx =document.getElementById("canvas").getContext('2d');
        ctx.drawImage(img,10,20,100,100);

}
</script>
</head>
<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>
</body>

</html>

找到你自己的图像文件,用它的名字代替frogface.gif。把标题改成合适的。用这条线做实验

ctx.drawImage(img,10,20,100,100);

也就是说,更改 10,20 以重新定位图像,更改 100,100 以更改宽度和高度。进行更改,看看程序是否如您所愿做出响应。请记住,当您指定宽度和高度时,您可能会改变图片的形状,即纵横比

这里要注意的重要一点是,由于代码是在画布上绘制或绘画,为了产生移动球的效果,我们还需要代码来擦除所有内容,然后在新的位置用球重新绘制所有内容。抹去一切的声明是:

ctx.clearRect(boxx,boxy,boxwidth,boxheight);

也许可以只擦除(清除)画布的一部分,但我选择擦除然后重新绘制所有内容。在每种情况下,你需要决定什么是有意义的。

想象一下在画布上画两个图像。你需要用两个不同的变量来代替img。对于这个任务,给变量起一个独特的名字。如果是效仿苏斯博士,可以用thing1thing2;不然就选对自己有意义的吧!

为了绘制背景图像,然后是移动的棉花糖,我的代码简单地首先绘制背景图像,总是在相同的位置,然后在计算的位置绘制棉花糖。完整的代码如下。你将在后面的章节中读到关于moveandcheck的内容。

function moveball(){
 ctx.clearRect(boxx,boxy,boxwidth,boxheight);
 moveandcheck();
 ctx.drawImage(bkg,0,0,4000,3000,0,0,400,300);
 ctx.drawImage(ball,0,0,388,435,ballx-ballrad,bally-ballrad,388/10,435/10);
  ctx.strokeRect(0,0,400,300);
}

你可能会问为什么要重画背景。答案是,一旦在画布上绘制了一些东西,就有相当于颜料的点——术语是像素,图片元素——被设置为特定的颜色。每次迭代都会有一些变化(请等待下一节关于计时间隔的内容),虽然画布的大部分保持不变,但生成新图片的最佳方式是清空画布,绘制背景,然后绘制球。

梯度,附带解释数组

让我们看看如何使用渐变,一种彩虹般的颜色组合,为弹跳程序。您可以使用渐变来设置fillStyle属性。我不想把球放在一个填充的矩形上面,所以我需要弄清楚如何分别画出四面墙。

渐变是 HTML5 中的一种对象类型。有线性梯度和径向梯度。在这个应用中,我们使用线性梯度。代码使用我们之前用变量ctx定义的 canvas 上下文的方法,将变量定义为渐变对象。渐变的代码如下所示:

var grad;
grad=ctx.createLinearGradient(boxx,boxy,boxx+boxwidth,boxy+boxheight);

渐变在矩形上延伸。

渐变包含多组颜色。一个典型的做法是编写代码来设置所谓的颜色停止点,例如使渐变成为彩虹。为此,我在一个名为hue的变量中设置了一个数组的数组。

您可以将数组视为值集合的容器。一个变量只能保存一个值,而一个数组可以保存多个值。在下一章,你将读到一个名为everything的数组,它将保存所有要在屏幕上绘制的对象。在描述刽子手游戏的第九章中,单词列表是一个单词数组。在这本书里,你会读到数组的许多应用。这里有一个具体的例子。下面的var语句将一个变量设置为一个特定的数组:

var family  = ["Daniel","Aviva", "Annika"];

变量family是一个数组。它的数据类型是数组。它包括一份我家的名单。要访问或设置这个数组的第一个元素,可以使用family[0]。指定数组特定成员的值称为索引值或索引。数组索引从零开始。family[0]这个表达会产生Daniel"。表达式family[1]会产生"Aviva"。表达式family[2]会产生"Annika"。如果变量relative的值为 1,那么family[relative]将产生Aviva。要确定数组中元素的数量,可以使用family.length。在本例中,长度为 3。注:长度为 3;指数从 0 到 2。

数组中的各个项可以是任何类型,包括数组。例如,我可以修改族数组以提供更多信息:

var family  = [["Daniel","son"],
  ["Aviva", "daughter"],
  ["Annika","granddaughter"]
 ];

带有换行符和缩进的格式不是必需的,但这是一个好习惯。它不由 JavaScript 解释。我们必须把括号和逗号弄正确!

family[2][1]这个表达产生了“孙女”。请记住:数组索引从 0 开始,因此数组的索引值 2(在这种类型的示例中有时称为外部数组)产生["Annika ","孙女"],对于该数组,索引 1 产生"孙女"。这些内部数组不必长度相同。考虑这个例子:

var family  = [["Daniel","teacher"],
  ["Aviva", "government staff"],
  ["Annika"]
 ];

代码将检查数组的长度,如果它是 2 而不是 1,第二项将是个人的职业。如果内部数组的长度为 1,则可以认为这个人没有职业。

数组的数组对于产品名称和成本非常有用。以下语句指定了商店非常有限的库存:

var inventory = [
                             ["toaster",25.99],
                             ["blender",74.99],
                             ["dish",10.50],
                             ["rug",599.99]
                             ];

这家商店有四种商品,最便宜的是盘子,在索引 2 的位置表示,最贵的是地毯,在索引 3。

现在,让我们看看如何使用这些概念来定义梯度。我们将使用一个数组,它的单个元素也是数组。

每个内部数组保存一种颜色的 RGB 值,即红色、黄色、绿色、青色、蓝色和品红色。

var hue = [
    [255,   0,   0 ],
    [255, 255,   0 ],
    [  0, 255,   0 ],
    [  0, 255, 255 ],
    [  0,   0, 255 ],
    [255,   0, 255 ]
] ;

这些值代表从红色(RGB 255,0,0)到洋红色(RGB 255,0,255)的颜色,其间指定了四种颜色。JavaScript 中的渐变功能填充颜色以产生彩虹图案,如图 3-2 所示。通过沿从 0 到 1 的间隔指定点来定义渐变。您可以指定彩虹以外的渐变。例如,您可以使用图形程序选择一组 RGB 值作为所谓的停止点,JavaScript 将填充值以从一个值混合到下一个值。

数组数值并不是我们所需要的,所以我们必须处理它们来产生 JavaScript 所需要的东西。

数组的操作通常需要对数组的每个成员做一些事情。许多编程语言中都有这样的一个构造,那就是for循环,它使用一个叫做索引变量的变量。for回路的结构是

for (initial value for indexing variable; condition for continuing; change for➥
 indexing variable) {
    code to be done every time. The code usually references the indexing variable

}

这里说:从这个初始值开始;只要这个条件成立,就继续做这个循环;并以这种指定的方式更改索引值。一个典型的变化表达式将使用运算符,如++++操作员将指示变量增加 1。典型的for标题语句是

for (n=0;n<10;n++)

这个for循环使用一个名为n的变量,其中n被初始化为0。如果n的值小于 10,则执行循环内部的语句。每次迭代后,n的值增加 1。在这种情况下,循环代码将被执行 10 次,n 保存值 0、1、2,一直到 9。

这里还有一个例子,一个演示数组的常见例子。让grades变量被设置为保存一组学生的成绩:

var grades = [4.0, 3.7, 3, 2.3, 3];

根据学校的不同,这可能表示 A、A-、B、C+和 B 级。下面的代码片段计算平均绩点,并将其存储在名为gpa的变量中。注意,我们需要初始化名为sum的变量,以 0 值开始。+=运算符将索引值g处的grades数组中的值加到sum中保存的值上。

var sum = 0;
for (g=0;g<grades.length;g++) {
 sum += grades[g];
}
var gpa;
gpa = sum/grades.length;

为了生成构建渐变所需的内容,代码从hue数组中提取值,并使用它们生成表示 RGB 值的字符串。我们使用hue数组和一个名为color的变量来设置色标以定义渐变。使用一个for循环将color设置为所需格式的字符串,即从rgb(开始,包括三个值,将色标设置在 0 和 1 之间的任意点。

for (h=0;h<hue.length;h++) {
 color = 'rgb('+hue[h][0]+','+hue[h][1]+','+hue[h][2]+')';
 grad.addColorStop(h*1/hue.length,color);
}

赋值语句设置color可能对你来说很奇怪:有很多事情正在进行——那些加号在做什么?记住,我们的任务是生成表示特定 RGB 值的字符串。加号不是在这里表示数字的相加,而是字符串的连接。这意味着这些值粘在一起,而不是数学相加,所以当5+5产生 10,'5'+'5'将给出 55。因为第二个例子中的 5 是用引号括起来的,所以它们是字符串而不是数字。方括号表示数组的成员。JavaScript 将数字转换成等价的字符串,然后组合它们。请记住,它查看数组中的数组,因此方括号中的第一个数字(在本例中,由我们的变量h提供)给出了第一个数组,方括号中的第二个数字给出了我们在该数组中的数字。让我们看一个简单的例子。我们的循环第一次运行时,h的值将是 0,这给了我们hue数组中的第一个条目。然后,我们查找该条目的各个部分,以构建我们的最终颜色。

在这之后,我们的代码已经设置了变量grad用于指示填充模式。代码没有将fillStyle设置为一种颜色,而是将其设置为变量grad

ctx.fillStyle = grad;

画矩形和以前一样,但是现在用指定的填充。这是位于原始矩形的左、右、上、下的四面窄墙。我把墙做得和球的半径一样厚。该厚度在垂直壁的情况下是宽度,在水平壁的情况下是高度。

ctx.fillRect(boxx,boxy,ballrad,boxheight);
ctx.fillRect(boxx+boxwidth-ballrad,boxy,ballrad,boxheight);
ctx.fillRect(boxx,boxy,boxwidth,ballrad);
ctx.fillRect(boxx,boxy+boxheight-ballrad,boxwidth,ballrad);

设置定时事件

在 HTML5 中设置定时事件实际上类似于在旧版本的 HTML 中的做法。内置函数有两个:setIntervalsetTimeout。我们将在第五章的记忆游戏中,在这里和setTimeout处查看setInterval。每个函数都有两个参数。请记住,参数是函数或方法调用中包含的额外信息。回到第一章,我们看到document.write将屏幕上显示的内容作为它的唯一参数。

我先描述第二个论点。第二个参数以毫秒为单位指定时间量。一秒有 1000 毫秒。这似乎是一个非常短的工作单元,但它正是我们想要的游戏。对于电脑游戏来说,一秒(1000 毫秒)相当长。

第一个参数指定在第二个参数指定的时间间隔内要做什么。第一个参数可以是函数的名称。对于该应用程序,init函数定义包含以下代码行:

setInterval(moveball,100);

这告诉JavaScript engine每 100 毫秒调用函数moveball(每秒 10 次)。moveball是将在此 HTML 文档中定义的函数的名称;是定时间隔事件事件处理程序。在编写代码定义函数之前,不要担心您是否编写了这行代码。重要的是应用程序运行时存在的内容。

JavaScript 还为事件处理程序提供了一种不同于函数名的方式。你可以写作

setInterval("moveball();",100);

为了同样的效果。换句话说,对于动作是不带参数的函数调用的情况,函数名就可以了。对于更复杂的情况,您可以编写一个字符串来指定代码。假设我有一个名为slide的函数,它本身有一个参数,我希望这个参数是变量d值的 10 倍,我希望每 1 . 5 秒发生一次,我会编写代码

setInterval("slide(10*d);",1500);

我注意到moveball不需要参数的原因是因为位置和位移使用了全局变量。

通常情况下,您希望在屏幕上显示时间的流逝。下面的例子将显示 0,1,…等等。数字每秒都在变化。

<html>
<head>
<title>elapsed</title>
<script>

function init() {
        setInterval(increase,1000);
}
function increase() {
        document.f.secs.value = String(1+Number(document.f.secs.value));
}
</script>
</head>

<body onLoad="init();">
<form name="f">
<input type="text" name="secs" value="0"/>
</form>
</body>
</html>

这是一个花时间编写和运行的好例子,因为它展示了计时事件,也因为它让你知道一秒钟持续了多长时间。代码以名为f的形式从secs输入字段中取出值,将该值转换成一个数字,将1加到该数字上,然后将其转换回一个字符串,作为secs元素的value。尝试用语句替换increase函数中的单个语句

document.f.secs.value = 1+document.f.secs.value;

看看会发生什么。这是数字和字符串区别的一课。请随便举个小例子。如果您想让数字以较小的增量上升,请将 1000 更改为 250,将 1 更改为 0.25。这使得脚本显示四分之一秒的变化。

如果你想让你的代码停止一个特定的事件,你可以设置一个全局变量(在任何函数之外的变量)。我使用一个名为tev的变量,这是我对定时事件的简写。

var tev;

然后,您可以将setInterval调用修改为:

tev = setInterval(moveball,100);

当您想要停止此事件时,您应该包括以下代码:

clearInterval(tev);

顺便说一下,如果我的代码在没有发出clearInterval的情况下再次调用带有setInterval函数的语句,就相当于设置了一个额外的闹钟。效果将是提高速度。当我描述棉花糖游戏时,你会注意到我的代码包含了多个clearInterval语句。

重申一下,setInterval函数设置一个定时事件,该事件会一直发生,直到被清除。如果您知道您希望一个事件只发生一次,那么setTimeout方法正好设置一个事件。您可以使用任何一种方法来产生相同的结果,但是 JavaScript 同时提供了这两种方法来使事情变得更简单。

对于弹跳球应用程序,moveball函数计算球的新位置,进行计算以检查碰撞,当碰撞发生时,重定向球并绘制新的显示。这是一次又一次的重复——对moveball的调用不断发生,因为我们使用了setInterval

计算新位置和碰撞检测

既然我们知道了如何绘制,如何清除和重绘,以及如何在固定的时间间隔做一些事情,那么挑战就是如何计算新的位置以及如何进行碰撞检测。我们将通过声明变量ballxbally来保存球中心的xy坐标;ballvxballvy保持球位要改变的量;以及boxboundxinboxboundxboxboundyinboxboundy来表示比用于碰撞计算的实际盒子稍小的盒子。球位置的变化量被初始化为 4 和 8(完全任意),当球员做出有效的改变(见下一节)并点击改变按钮时,球位置的变化量被改变。这些量被称为位移或增量,以及不太正式的速度或速率。

在这种情况下,方向的改变非常简单。如果球“击中”一面垂直的墙,水平位移必须改变符号;也就是说,如果球向右移动了四个单位,我们撞到了一堵墙,我们要开始给它的位置加-4,这将使它向左移动。垂直位移保持不变。通过将下一个水平值与边界进行比较来确定命中。类似地,如果通过将垂直位置与适当的边界进行比较来确定球“击中”了水平墙,则垂直位移改变符号,而水平位移保持不变。变化是为了下一次迭代。碰撞检查进行了四次,即针对四面墙中的每一面。计算包括将建议的新 x 或 y 值(如果适用)与特定墙的边界条件进行比较。如果球的中心经过四面墙中的一面,正好在边界上,那么暂定的新位置将被调整。这样做的效果是使球稍稍移动到每面墙的后面,或者看起来被每面墙挤压。边界值设置在方框内,上角为boxxboxy,宽度为boxwidth,高度为boxheight。我可以用更复杂的计算来比较圆上的任何一点和墙上的任何一点。然而,这里涉及到一个更基本的原则。没有墙,也没有球。这是基于计算的模拟。计算每隔一段时间进行一次。如果球移动得足够快,并且壁足够薄,比这里指定的ballrad更薄,球可以逃出盒子。这就是为什么我用下一步棋和一个稍微小一点的盒子来计算。

var boxboundx = boxwidth+boxx-ballrad;
var boxboundy = boxheight+boxy-ballrad;
var inboxboundx = boxx+ballrad;
var inboxboundy = boxy+ballrad;

下面是moveandcheck函数的代码,该函数检查碰撞并重新定位球:

function moveandcheck() {
   var nballx = ballx + ballvx;
   var nbally = bally +ballvy;
   if (nballx > boxboundx) {
      ballvx =-ballvx;
      nballx = boxboundx;
   }
   if (nballx < inboxboundx) {
      nballx = inboxboundx
      ballvx = -ballvx;
   }
   if (nbally > boxboundy) {
      nbally = boxboundy;
      ballvy =-ballvy;
   }

   if (nbally < inboxboundy) {
      nbally = inboxboundy;
      ballvy = -ballvy;
   }
   ballx = nballx;
   bally = nbally;
}

你可能会说这里实际发生的事情不多,你是对的。变量ballxbally被修改,以便稍后在画布上绘制时使用。

从这段代码中并不明显,但是请记住,垂直值(y 值)随着屏幕的向下移动而增加,水平值(x 值)随着从左向右移动而增加。

确认

表单,从用户/玩家/客户端获取输入的方式,是原始 HTML 的一部分。表单元素以一个<form>标签开始,它提供了一种指定提交表单时的动作的方法,并包含输入元素。HTML5 为验证表单输入提供了新的工具。表单的创建者可以指定输入域的类型是number而不是text,HTML5 会立即检查用户/玩家输入的数字。类似地,我们可以指定maxmin值。该表单的代码是

<form name="f" id="f" onSubmit="return change();">
  Horizontal velocity <input name="hv" id="hv" value="4" type="number" min="-10" max="10" />
<br>
  Vertical velocity <input name="vv" id="vv" value="8" type="number" min="-10" max="10"/>
<input type="submit" value="CHANGE"/>
</form>

输入仍然是文本,即一个字符串,但值是可以解释为指定范围内的数字的文本。

其他类型的输入包括"email""URL",让 HTML5 检查这些输入非常方便。当然,您可以使用isNumber和更复杂的编码来检查任何字符串,看看它是否是一个数字,包括正则表达式(可以匹配的字符模式),以检查有效的电子邮件地址和 URL。检查电子邮件地址的一个常用策略是让用户输入两次,这样你就可以比较两次,确保用户没有犯任何错误。

我们希望利用 HTML5 将为我们做的工作,但我们也希望让用户/玩家知道是否有问题。通过为有效和无效输入指定样式,您可以使用 HTML5 和 CSS 来做到这一点。

input:valid {background:green;}
input:invalid {background:red;}

HTML5 验证在最新版本的浏览器中是有效的,至少在电脑上是有效的,但是你需要决定你想为老版本的浏览器和设备做什么。如果您使用兼容的浏览器,比如 Chrome,您可以测试下一节给出的例子。请注意,即使在指定了数字的地方输入了无效的值,例如“abc ”,球也会一直反弹,因为程序会继续使用当前的设置。

小费

在任何应用程序中,验证输入并向用户提供适当的反馈都是非常重要的。HTML5 提供的新特性之一是 input 元素中的 pattern 属性,其中一种称为正则表达式的特殊语言可以用来指定有效的输入。将“HTML5 正则表达式”放入搜索字段,以查找最新信息。

停止和恢复由按钮触发的动画

当我决定添加停止和恢复时,我认为一个重要的教训是这可能只是一个添加,对程序的其余部分没有改变。这里发生的事情有一个术语叫做事件驱动编程。我们,建筑者,或多或少清楚地思考不同的事件。我还决定使用按钮元素,这是 HTML5 中引入的一个特性。button 元素提供了一种方式来指定事件,在本例中是onClick,以及将处理该事件的函数。标签<button>和标签</button>之间的文本显示在菱形按钮中。旧的方法是使用表单,对于我的例子来说,这意味着多个表单。

下面的代码生成了这两个按钮。下一节描述了return语句的重要性。&nbsp;是所谓的实体,产生一个空格,但不强制换行。

<button onClick="return stopcc();">STOP </button>  &nbsp; &nbsp;
<button onClick="return resume();">RESUME </button>

我现在欠你stopcc函数和resume函数的定义。

stopcc功能的任务是停止背景上棉花糖图像的移动。您知道如何做:调用clearInterval。我的代码确实需要做更多的事情。因为我想要恢复弹跳,所以我编写代码来保存ballvxballvy值。这可能是不必要的,但某些情况下似乎需要这样做。代码还调用moveBall生成另一张图片。下一节将解释return的使用。代码如下。

function stopcc() {
  clearInterval(tid);
  stoppedx = ballvx;
  stoppedy = ballvy;
  moveBall();
  return false;
}

resume函数确实包含了对setInterval的调用,但是我需要做一些别的事情来保护玩家免受他们自己的伤害。如果玩家没有停止动画就点击了恢复按钮,或者只是想看看会发生什么,那么调用多个setInterval会产生多个计时事件。这反过来会使弹跳越来越快。为了展示这一点,我插入了对clearInterval的调用。如果没有合适的定时事件,什么都不会发生。我的代码使用之前保存的值重置ballvxballvy。这可能没有必要,但这是一种预防措施。

function resume(){
   clearInterval(tid);
   ballvx = stoppedx;
   ballvy = stoppedy;
   tid = setInterval(moveball,100);
   return false;
}

HTML 重新载入页面

在继续之前,我想提及一些可能会导致意外问题的问题。浏览器带有重新加载/刷新按钮。当单击按钮时,文档被重新加载。我们在第二章中的简单掷骰应用中使用了这个。然而,有时您可能想要阻止重新加载,在这种情况下,您可以在没有任何内容可返回的函数中放置一个return (false);来阻止页面重新加载。

当文档有表单时,重新加载并不总是重新初始化表单输入。您可能需要离开该页面,然后使用完整的 URL 重新加载它。

最后,浏览器试图使用先前下载到客户端(用户)计算机的文件,而不是基于日期和时间的检查从服务器请求文件。客户端计算机上的文件存储在所谓的缓存中。如果您认为自己做了更改,但浏览器没有显示最新版本,您可能需要采取清除缓存等措施。

预加载图像

计算机是如此之快,而一般来说,我们的感知是足够慢的,以至于我们做任何事情都不会有延迟。然而,网站上的图像必须从服务器下载到我们的本地计算机,大图像显然是大文件。实际上,我应该提出另一个观点。我们的现代相机产生由数千个像素组成的图像,这被称为高分辨率。这会使文件变大。为了确保图像可以使用,一个技巧是创建在body元素中保存图像的img元素。对于这个例子,这包括背景照片和棉花糖照片。在通过body标签中的onLoad属性的动作调用init功能之前,文件将被加载。在棉花糖图像被绘制在其上之前,完全加载的背景图像将可被绘制。挑战在于如何防止这两幅图像被显示出来。答案是在style元素中包含以下指令。

img {visibility: hidden;}

CSS 指令阻止任何img文件被显示。在我的例子中,img元素从不显示。显示的是由代码创建和操作的Image元素。

构建应用程序并使之成为您自己的应用程序

我现在将解释基本弹跳球应用程序的代码;使用球的图像和墙壁的渐变的应用程序;验证输入和弹跳棉花糖的应用程序。表 3-1 显示了所有的函数调用和被调用的内容。该表包括所有四个应用程序的函数。stopccresume功能只出现在第四个应用程序中。

表 3-1

弹跳球应用中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | body标签中onLoad的动作 | moveball |
| moveball | 由initsetInterval的动作直接调用 | moveandcheck |
| moveandcheck | 由moveball调用 |   |
| change | 由form标签中的onSubmit动作调用 |   |
| stopcc | 由button标签中的onClick动作调用 | moveBall |
| resume | 由button标签中的onClick动作调用 |   |

moveandcheck代码可以是moveball函数的一部分。我选择将它分开,因为定义执行特定操作的函数是一个很好的实践。通常,在开发应用程序时,更多更小的功能比更少更大的功能更好。顺便说一下,当你自己编程时,不要忘记在代码中加入注释,如第二章所述。并添加空行以使代码可读性更好。表 3-2 显示了基本弹跳球应用程序的代码,并解释了每一行的作用。

表 3-2

弹跳球应用程序

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html。 |
| <head> | 开始head。 |
| <title>Bouncing Ball``with inputs</title> | 完成title元素。 |
| <style> | 开始style。 |
| form { | 开始表单样式。 |
| width:330px; | 设置width。 |
| margin:20px; | 设置margin。 |
| background-color:brown; | 设置背景color。 |
| padding:20px; | 设置内部padding。 |
| } | 关闭此样式。 |
| </style> | 关闭样式元素。 |
| <script type="text/javascript"> | 开始script元素。(类型不是必需的。我在这里展示它只是为了让你知道你会在网上的许多例子中看到什么。) |
| var boxx = 20; | 盒子上角的 x 位置。 |
| var boxy = 30; | 盒子上角的 y 位置。 |
| var boxwidth = 350; | 方框宽度。 |
| var boxheight = 250; | 框高。 |
| var ballrad = 10; | 球的半径。 |
| var boxboundx =``boxwidth+boxx-ballrad; | 右边界。 |
| var boxboundy =``boxheight+boxy-ballrad; | 底部边界。 |
| var inboxboundx =``boxx+ballrad; | 左边界。 |
| var inboxboundy =``boxy+ballrad; | 顶部边界。 |
| var ballx = 50; | 球的初始 x 位置。 |
| var bally = 60; | 球的初始 y 位置。 |
| var ctx; | 保存画布上下文的变量。 |
| var ballvx = 4; | 初始水平位移。 |
| var ballvy = 8; | 初始垂直位移。 |
| function init() { | 开始init功能。 |
| ctx = document.getElementById``('canvas').getContext('2d'); | 设置ctx变量。 |
| ctx.linewidth = ballrad; | 设置线条宽度 |
| ctx.fillStyle ="rgb(200,0,50)"; | 设置填充样式 |
| moveball(); | 第一次调用moveball功能,移动、检查并显示球。 |
| setInterval(moveball,100); | 设置定时事件。 |
| } | 关闭init功能。 |
| function moveball(){ | 开始moveball功能。 |
| ctx.clearRect(boxx,boxy,``boxwidth,boxheight); | 清除(擦除)框(包括球上的任何油漆)。 |
| moveandcheck(); | 做检查并移动球。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.arc(ballx, bally, ballrad,0,Math.PI*2,true); | 设置在球的当前位置画圆。 |
| ctx.fill(); | 填写路径;也就是画一个实心圆。 |
| ctx.strokeRect(boxx,boxy,``boxwidth,boxheight); | 绘制矩形轮廓。 |
| } | 关闭moveball。 |
| function moveandcheck() { | 开始moveandcheck。 |
| var nballx = ballx + ballvx; | 设置暂定的下一个 x 位置。 |
| var nbally = bally +ballvy; | 设置暂定的下一个 y 位置。 |
| if (nballx > boxboundx) { | 这个 x 值超出右墙了吗? |
| ballvx =-ballvx; | 如果是,改变水平位移。 |
| nballx = boxboundx; | 将下一个 x 精确地设置在这个边界上。 |
| } | 关闭子句。 |
| if (nballx < inboxboundx) { | 这个 x 值小于左边界吗? |
| nballx = inboxboundx; | 如果是这样,将 x 值设置为正好在边界处。 |
| ballvx = -ballvx; | 改变水平位移。 |
| } | 关闭子句。 |
| if (nbally > boxboundy) { | y 值是否超出了底部边界? |
| nbally = boxboundy; | 如果是这样,将 y 值设置为正好在边界处。 |
| ballvy =-ballvy; | 改变垂直位移。 |
| } | 关闭子句。 |
| if (nbally < inboxboundy) { | y 值是否小于上边界? |
| nbally = inboxboundy; | 如果是这样,将 y 值设置为边界。 |
| ballvy = -ballvy; | 改变垂直位移。 |
| } | 关闭子句。 |
| ballx = nballx; | 将 x 位置设置为nballx。 |
| bally = nbally; | 将 y 位置设置为nbally。 |
| } | 关闭moveandcheck功能。 |
| function change() { | 开始change功能。 |
| ballvx = Number(document.f.hv.value); | 将输入转换成数字并分配给ballvx。 |
| ballvy = Number(document.f.vv.value); | 将输入转换成数字并分配给ballvy。 |
| return false; | 返回false以确保没有页面重载。 |
| } | 关闭功能。 |
| </script> | 关闭脚本。 |
| </head> | 关闭头部。 |
| <body onLoad="init();"> | 开始body元素。设置对init功能的调用。 |
| <canvas id="canvas" width=``"400" height="300"> | canvas元素的开始。 |
| Your browser doesn't support the``HTML5 element canvas. | 针对不兼容浏览器的消息。 |
| </canvas> | 关闭canvas元素。 |
| <br/> | 换行。 |
| <form name="f" id="f" onSubmit=``"return change();"> | 表单开始。给出名称和 id(某些浏览器可能需要)。在提交按钮上设置操作。 |
| Horizontal velocity <input name="hv"``id="hv" value="4" type="number"``min="-10" max="10" /> | 标记水平速度的输入字段。 |
| <br> | 换行。 |
| Vertical velocity <input name=``"vv" id="vv" value="8" type="number"``min="-10" max="10"/> | 标记垂直速度的输入字段。 |
| <input type="submit" value="CHANGE"/> | 提交按钮。 |
| </form> | 关闭form。 |
| </body> | 关闭body。 |
| </html> | 关闭html。 |

使用图像作为球和渐变填充墙的应用程序非常相似。表 3-3 显示了所有的代码——但是我只是注释了不同的代码。我没有偷懒;这个想法是让你看到每个应用程序是如何建立在前一个之上的。

表 3-3

第二个应用程序,使用图像作为球和渐变填充的墙

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Bouncing Ball with inputs</title> |   |
| <style> |   |
| form { |   |
| width:330px; |   |
| margin:20px; |   |
| background-color:#b10515; |   |
| padding:20px; |   |
| } |   |
| </style> |   |
| <script type="text/javascript"> |   |
| var boxx = 20; |   |
| var boxy = 30; |   |
| var boxwidth = 350; |   |
| var boxheight = 250; |   |
| var ballrad = 20; | 这不是一个实质性的变化,但图片需要一个更大的半径。 |
| var boxboundx = boxwidth+boxx-ballrad; |   |
| var boxboundy = boxheight+boxy-ballrad; |   |
| var inboxboundx = boxx+ballrad; |   |
| var inboxboundy = boxy+ballrad; |   |
| var ballx = 50; |   |
| var bally = 60; |   |
| var ballvx = 4; |   |
| var ballvy = 8; |   |
| var img = new Image(); | 将img变量定义为一个Image对象。这就是new操作符和对Image函数的调用所做的事情。 |
| img.src="pearl.jpg"; | 将该图像的src设置为"pearl.jpg"文件。 |
| var ctx; |   |
| var grad; | 将grad设置为变量。它将在init函数中被赋值。 |
| var color; | 用于设置梯度grad。 |
| var hue = [ | 用于设置梯度grad。这是一个数组的数组,每个内部数组提供 RGB 值。 |
| [255,   0,   0 ], | 红色。 |
| [255, 255,   0 ], | 黄色。 |
| [  0, 255,   0 ], | 绿色。 |
| [  0, 255, 255 ], | 青色。 |
| [  0,   0, 255 ], | 蓝色。 |
| [255,   0, 255 ] | 紫色(洋红色)。 |
| ]; | 关闭数组。 |
| function init(){ | 用于设置渐变。 |
| var h; |   |
| ctx = document.getElementById('canvas').``getContext('2d'); |   |
| grad = ctx.createLinearGradient(boxx,boxy,``boxx+boxwidth,boxy+boxheight); | 创建并分配一个渐变值。 |
| for (h=0;h<hue.length;h++) { | 开始for循环。 |
| color = 'rgb('+hue[h][0]+','``+hue[h][1]+','+hue[h][2]+')'; | 将color设置为表示 RGB 值的字符串。 |
| grad.addColorStop(h*1/hue.length,color); | 设置色标来定义渐变。 |
| } | 关闭for回路。 |
| ctx.fillStyle = grad; | 将填充设置为grad。 |
| ctx.lineWidth = ballrad; |   |
| moveball(); |   |
| setInterval(moveball,100); |   |
| } |   |
| function moveball(){ |   |
| ctx.clearRect(boxx,boxy,boxwidth,boxheight); |   |
| moveandcheck(); |   |
| ctx.drawImage(img,ballx-ballrad,``bally-ballrad,2*ballrad,2*ballrad); | 画个像。 |
| ctx.fillRect(boxx,boxy,ballrad,boxheight); | 画左边的墙。 |
| ctx.fillRect(boxx+boxwidth-ballrad,boxy,ballrad,boxheight); | 画右边的墙。 |
| ctx.fillRect(boxx,boxy,boxwidth,ballrad); | 画顶墙。 |
| ctx.fillRect(boxx,boxy+boxheight-ballrad,boxwidth,ballrad); | 画底墙。 |
| } |   |
| function moveandcheck() { |   |
| var nballx = ballx + ballvx; |   |
| var nbally = bally +ballvy; |   |
| if (nballx > boxboundx) { |   |
| ballvx =-ballvx; |   |
| nballx = boxboundx; |   |
| } |   |
| if (nballx < inboxboundx) { |   |
| nballx = inboxboundx; |   |
| ballvx = -ballvx; |   |
| } |   |
| if (nbally > boxboundy) { |   |
| nbally = boxboundy; |   |
| ballvy =-ballvy; |   |
| } |   |
| if (nbally < inboxboundy) { |   |
| nbally = inboxboundy; |   |
| ballvy = -ballvy; |   |
| } |   |
| ballx = nballx; |   |
| bally = nbally; |   |
| } |   |
| function change() { |   |
| ballvx = Number(document.f.hv.value); |   |
| ballvy = Number(document.f.vv.value); |   |
| return false; |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();"> |   |
| <canvas id="canvas" width=``"400" height="300"> |   |
| This browser doesn't support``the HTML5 canvas element. |   |
| </canvas> |   |
| <br/> |   |
| <form name="f" id="f" onSubmit=``"return change();"> |   |
| Horizontal velocity <input name=``"hv" id="hv" value="4" type=``"number" min="-10" max="10" /> |   |
| <br> |   |
| Vertical velocity <input name=``"vv" id="vv" value="8" type=``"number" min="-10" max="10"/> |   |
| <input type="submit" value="CHANGE"/> |   |
| </form> |   |
| </body> |   |
| </html> |   |

我选择在第一个应用程序中对风格信息构建进行适度的更改。表 3-4 显示了第三个弹跳球应用,带有表单验证。同样,我只对新代码进行了注释,但为了完整起见,我包含了所有代码。

表 3-4

第三个弹跳球应用程序,带有表单验证

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Bouncing Ball with inputs</title> |   |
| <style> |   |
| form { |   |
| width:330px; |   |
| margin:20px; |   |
| background-color:brown; |   |
| padding:20px; |   |
| } |   |
| input:valid {background:green;} | 为有效输入设置反馈。 |
| input:invalid {background:red;} | 为无效输入设置反馈。 |
| </style> |   |
| <script type="text/javascript"> |   |
| var cwidth = 400; |   |
| var cheight = 300; |   |
| var ballrad = 10; |   |
| var boxx = 20; |   |
| var boxy = 30; |   |
| var boxwidth = 350; |   |
| var boxheight = 250; |   |
| var boxboundx = boxwidth+boxx-ballrad; |   |
| var boxboundy = boxheight+boxy-ballrad; |   |
| var inboxboundx = boxx+ballrad; |   |
| var inboxboundy = boxy+ballrad; |   |
| var ballx = 50 ; |   |
| var bally = 60; |   |
| var ctx; |   |
| var ballvx = 4; |   |
| var ballvy = 8; |   |
| function init(){ |   |
| ctx = document.getElementById('canvas').``getContext('2d'); |   |
| ctx.lineWidth = ballrad; |   |
| moveball(); |   |
| setInterval(moveball,100); |   |
| } |   |
| function moveball(){ |   |
| ctx.clearRect(boxx,boxy,boxwidth,boxheight); |   |
| moveandcheck(); |   |
| ctx.beginPath(); |   |
| ctx.fillStyle ="rgb(200,0,50)"; |   |
| ctx.arc(ballx, bally, ballrad,0,Math.PI*2,true) ; |   |
| ctx.fill(); |   |
| ctx.strokeRect(boxx,boxy,boxwidth,boxheight); |   |
| } |   |
| function moveandcheck() { |   |
| var nballx = ballx + ballvx; |   |
| var nbally = bally +ballvy; |   |
| if (nballx > boxboundx) { |   |
| ballvx =-ballvx; |   |
| nballx = boxboundx; |   |
| } |   |
| if (nballx < inboxboundx) { |   |
| nballx = inboxboundx; |   |
| ballvx = -ballvx; |   |
| } |   |
| if (nbally > boxboundy) { |   |
| nbally = boxboundy; |   |
| ballvy =-ballvy; |   |
| } |   |
| if (nbally < inboxboundy) { |   |
| nbally = inboxboundy; |   |
| ballvy = -ballvy; |   |
| } |   |
| ballx = nballx; |   |
| bally = nbally; |   |
| } |   |
| function change() { |   |
| ballvx = Number(document.f.hv.value); |   |
| ballvy = Number(document.f.vv.value); |   |
| return false; |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();"> |   |
| <canvas id="canvas" width="400" height="300"> |   |
| Your browser doesn't support the HTML5 element canvas. |   |
| </canvas> |   |
| <br/> |   |
| <form name="f" id="f" onSubmit="return change();"> |   |
| Horizontal velocity <input name="hv" id=``"hv" value="4" type="number" min="-10" max="10" /> |   |
| <br> |   |
| Vertical velocity <input name="vv" id=``"vv" value="8" type="number" min="-10" max="10"/> |   |
| <input type="submit" value="CHANGE"/> |   |
| </form> |   |
| </body> |   |
| </html> |   |

第四个应用程序是带有弹跳棉花糖的游戏。我做的第一件事超出了 HTML/JavaScript/CSS 编程的范围。我使用 pixlr 提取了棉花糖原始图片的一部分,并使用另一张照片来填充缺失的空间。

我不打算包括棉花糖游戏的完整代码,而只是指出增加的部分。

| `` |   | | `` |   | | `` |   | | `` | 身体标签。注意,`init`函数在所有东西都被加载时被调用,包括在``标签中提到的图像文件。 | | `...` |   | | `
` |   | | `...` |   | | `     ` | 调用`stopcc`的按钮。注意使用` `定位下一个按钮。 | | `` | 按钮来调用简历。 | | `
` |   | | `` | 一个`img`标签,使`candy.png`文件在任何事情发生之前被完全加载。 | | `` | 一个`img`标签,使`reunion.jpg`文件在任何事情发生之前被完全加载。 | | `` |   | | `` |   |

有许多方法可以让你自己制作这样的应用程序。你可以选择你自己的球的图像,尝试墙壁的颜色,有或者没有渐变。您可以更改每面墙的位置和尺寸。您可以向页面添加文本和 HTML 标记。您可以更改表单的外观。

您可以包含多个球,跟踪每个球的位置。如果你决定使用两个球,你需要两组变量和两行代码,每一行都是你以前用过的。一种系统化的方法是使用编辑器中的搜索功能来查找所有的ball实例,并且对于每一行,用两行来代替ballx,这样就有了ball1xball2x,并且代替了var ballx = 50;

var ball1x = 50;
var ball2x = 250;

这将第二个球放在画布上 200 像素。

你还需要第二套所有墙壁的对比。

如果你想使用两个以上的球,你可以考虑使用数组。后续章节将向您展示如何处理对象集。

你也可以试着写一些代码,让球每次碰到墙的时候都慢下来。这是一个很好的效果,并且模拟了真实的物理结果。在代码中通过改变适当变量的符号来改变方向的每个地方,添加一个因子来减小绝对值。例如,如果我选择减少 10%的值,我会写

  if (nballx > boxboundx) {
        ballvx =-ballvx *.9;
        nballx = boxboundx;
  }

这意味着垂直方向的增量变化将下降到原来的 90%。

您可以使用自己的照片,使游戏更像游戏,从而在棉花糖游戏的基础上发展和/或从中获得灵感。考虑一个测试休息的地方是否足够好。限制停止和恢复动作的次数。研究本文剩余部分中的例子(并继续 HTML5 和 JavaScript 项目)来学习其他动作,比如使用鼠标或触摸。

测试和上传应用程序

第一个和第三个应用程序在 HTML 文档中完成。第二个和第四个应用程序要求图像文件存在于同一文件夹中。您可以在网上的任何地方访问文件,但您需要确保包含正确的地址。例如,如果您将 HTML 文档上传到名为mygames的文件夹中,并将pearl.jpg上传到名为imagesmygames的子文件夹中,指示这一点的行必须是

img.src = "img/pearl.jpg";

您还必须使用准确的文件扩展名,如 JPG,来指示正确的文件类型。一些浏览器是宽容的,但许多不是。您可以尝试提交错误数据,并使用不同的浏览器查看响应。

摘要

在这一章中,你学习了如何创建一个基于用户输入而改变动画的应用程序。我们讨论了许多编程和 HTML5 特性,包括:

  • 使用setInterval为动画设置一个定时事件,并使用 clearInterval 来结束该事件

  • 验证表单输入

  • 使用程序员定义的函数来水平和垂直地重新定位一个圆或图像,以模拟一个弹跳的球

  • 虚拟碰撞测试

  • 绘制矩形、图像和圆形,包括颜色渐变

  • 使用按钮元素

  • 确保下载图像文件

下一章描述了玩家试图击中目标的炮弹和弹弓游戏。这些应用程序使用了与我们制作动画时相同的编程和 HTML5 特性,但是更进了一步。在第八章中,你还可以看到一个石头剪子布的动画示例。

四、炮弹和弹弓

在本章中,我们将介绍

  • 维护要在屏幕上绘制的对象列表

  • 旋转屏幕上绘制的对象

  • 鼠标拖放操作

  • 模拟弹道运动(重力效应)和碰撞的计算

介绍

本章演示了动画的另一个例子,在这种情况下,弹道模拟,也称为抛射体运动。球或类似球的物体保持恒定的水平(x)位移,垂直位移由于重力而改变。产生的运动是一个弧。当球(实际上)碰到地面或目标时就会停止。您将看到的代码使用演示球在盒子中弹跳的相同技术来生成动画。该代码重新定位球,并以固定的间隔重绘场景。我们将看三个例子。

  • 一个非常简单的弹道模拟。球在击中目标或地面之前起飞并沿弧线飞行。飞行的参数是水平和初始垂直速度,由玩家使用表单输入字段设置。当球碰到目标或地面时,它就停止了。

  • 一种改进的炮弹,用一个长方形代表倾斜一定角度的大炮。飞行的参数是出炮速度和炮角度。同样,这些是由玩家使用表单输入字段设置的。该程序计算初始水平和垂直位移值。

  • 一个弹弓。飞行的参数是由玩家拖动,然后释放,一个球的形状拴在一个代表弹弓的木棒上来决定的。速度是由球到弹弓上一个地方的距离决定的。角度是弹弓这部分与水平面的夹角。

图 4-1 显示了简单的(无加农炮)应用。

img/214814_2_En_4_Fig1_HTML.jpg

图 4-1

球落在了地上

图 4-2 显示了第二个应用程序的开始屏幕。目标是一个Image,代表大炮的矩形可以旋转。注意控件引用了一个角度和一个初速度。

img/214814_2_En_4_Fig2_HTML.jpg

图 4-2

以图像为目标的旋转加农炮

图 4-3 为成功命中后的场景。请注意,加农炮被旋转了,目标的原始图像被替换为新图像。

img/214814_2_En_4_Fig3_HTML.jpg

图 4-3

在开炮并击中目标后

弹弓应用程序的打开屏幕如图 4-4 所示。这个应用程序类似于大炮,但飞行的参数是由玩家使用鼠标在球(代表弹弓中的岩石)上拖动来设置的,目标现在是一只鸡。

img/214814_2_En_4_Fig4_HTML.jpg

图 4-4

弹弓应用程序的打开屏幕

对于弹弓,我决定让球一直飞下去,直到它落地。但是,如果鸡被打了,我想换成羽毛,如图 4-5 。请注意,当松开鼠标按钮,球飞起来的时候,弹弓的线还在原来的位置。我发现我需要更多的时间来观察琴弦,以便计划我的下一次拍摄。如果您愿意,您可以更改游戏,使字符串弹回到它们的原始位置,或者创建一个新的游戏按钮。在我的例子中,通过重新加载 HTML 文件来重放游戏。

img/214814_2_En_4_Fig5_HTML.jpg

图 4-5

球击中鸡后落在地上,那里只剩下羽毛

这些应用程序的编程使用了许多在弹跳球应用程序中演示的相同技术。球在飞行中的重新定位只是因为它需要模拟由于重力而改变的垂直位移的效果。slingshot 应用程序为玩家提供了一种新的与应用程序交互的方式,使用鼠标的拖放操作。

带加农炮的炮弹和弹弓使用加农炮和弹弓的绘图功能以及原始目标和命中目标的外部图像文件。如果你想改变目标,你需要找到图像文件,然后用应用程序上传。

关键要求

我们的第一个要求是通过设置一个事件以固定的时间间隔发生来制作动画,然后设置一个函数通过重新定位球和检查碰撞来处理该事件。我们在前一章的弹跳球应用程序中讨论了这一点。这里新增的是模拟重力的计算。由简单物理模型指示的计算基于以恒定量改变垂直位移,然后计算旧的和新的位移的平均值来计算新的位置,计算出新的垂直位移。

  • 水平位移(由变量dx保存)是水平速度(horvelocity),不变。代码:dx = horvelocity;

  • 间隔开始时的垂直速度是verticalvel1

  • 间隔结束时的垂直速度是verticalvel1加上加速量(gravity)。代码:verticalvel2 = verticalvel1 + gravity;

  • 井段(dy)的垂直位移是verticalvel1verticalvel2的平均值。代码:dy = (verticalvel1 + verticalvel2)*.5;

这是模拟重力或任何其他恒定加速度的标准方式。

注意

我把重力的值定为产生一个令人愉快的弧线。你可以使用一个标准值,但是你需要做一些研究来为大炮和弹弓的初始速度分配一个真实的值。您还需要确定像素和距离之间的映射。炮弹和弹弓的系数是不同的。

该程序的第二个版本必须根据初始值或玩家输入的加农炮口速度和加农炮角度来旋转加农炮,并根据这些值计算水平和垂直值。

该程序的第三个版本“弹弓”必须允许玩家按住鼠标按钮并沿着弹弓的弦拖动球,然后放开鼠标按钮来释放球。运动参数是根据球与弹弓顶部的角度和距离计算的。

该程序的第二版和第三版都需要用另一个图像替换目标图像。

HTML5、CSS 和 JavaScript 特性

现在让我们看看 HTML5 和 JavaScript 的具体特性,它们提供了实现弹道模拟应用程序所需的内容。幸运的是,我们可以使用一个canvas元素、程序员定义的和内置的函数、一个form元素和变量,在前面几章介绍的内容基础上构建,特别是 HTML 文档的一般结构。让我们从程序员定义的对象和使用数组开始。

数组和程序员定义的对象

HTML5 让你在画布上画画,但是一旦画了什么东西,就好像放下了颜料或墨水;画出来的东西不会保留它的个性。HTML5 不像 Flash 那样将对象放置在舞台上,可以单独移动和旋转。然而,我们仍然可以产生相同的效果,包括单个对象的旋转。在后面的章节中,我们将在浏览器窗口中移动对象。

因为这些应用程序的显示有些复杂,所以我决定开发一种更系统的方法来在画布上绘制和重绘不同的东西。为此,我创建了一个名为everything的数组来保存要在画布上绘制的对象列表。将数组视为一个集合,或者更准确地说,是一系列项目。在前面的章节中,我们讨论了用来保存数值(如数字或字符串)的变量。数组是另一种类型的值。我的everything数组将作为需要在画布上绘制的待办事项列表。

我使用的术语对象在英语和编程上都有意义。用编程术语来说,一个对象由属性方法组成,即数据和编码或行为。在第一章描述的带注释的链接例子中,我演示了document对象的write方法。我使用了变量ctx,它是一个canvas对象的 2D 类型上下文,方法如fillRect,,属性如fillStyle。这些是内置的;也就是说,它们已经是 HTML5 版本的 JavaScript 中定义的对象。对于弹道应用程序,我定义了自己的对象,特别是BallPictureMyrectangleSling。这些不同对象中的每一个都包括一个draw方法的定义以及指示位置和维度的属性。我这样做是为了能画出每一个事物的清单。适当的draw方法访问属性来决定画什么和在哪里画。我还包括了旋转单个对象的方法。

定义一个对象很简单:我简单地为BallPictureMyrectangle定义了一个名为构造函数的函数,并使用这些函数和操作符new将值赋给变量。然后,我可以使用熟悉的点符号来编写代码,以访问或分配属性,并调用我在构造函数中设置的方法。下面是一个Ball对象的构造函数:

function Ball(sx,sy,rad,stylestring) {
  this.sx = sx;
  this.sy = sy;
  this.rad = rad;
  this.draw = drawball;
  this.moveit = moveball;
  this.fillstyle = stylestring;
}

术语this指的是这个函数与关键字new一起使用时创建的对象。从代码上看,this.drawthis.moveit被赋予了函数的名称,这一事实并不明显,但事实就是如此。这两个函数的定义如下。请注意,它们都使用术语this来获取绘制和移动对象所需的属性。

function drawball() {
        ctx.fillStyle=this.fillstyle;
        ctx.beginPath();

        ctx.arc(this.sx,this.sy,this.rad,0,Math.PI*2,true);
        ctx.fill();
}

注意

JavaScript 已经开始增加对类和对象的支持,尽管它仍然不包括完全继承。一个相关网站是

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

drawball函数在画布上绘制一个填充圆,一个完整的圆弧。圆圈的颜色是创建此Ball对象时设置的颜色。

函数moveball不会立即移动任何东西。抽象地看这个问题,moveball改变了应用程序放置对象的位置。该函数改变对象的sxsy属性的值,当它下一次显示时,这些新值用于绘图。

function moveball(dx,dy) {
        this.sx +=dx;
        this.sy +=dy;
}

下一个声明变量cball的语句通过使用操作符new和函数Ball构建了一个类型为Ball的新对象。该函数的参数基于加农炮的设定值,因为我希望球出现在加农炮的开口处。

var cball = new Ball(cannonx+cannonlength,cannony+cannonht*.5,ballrad,"rgb(250,0,0)");

PictureMyrectangleSling的功能类似,稍后将进行解释。它们各自指定了一个draw方法。对于这个应用程序,我只为cball使用了moveit,但是我为其他对象定义了moveit,以防我以后想要在这个应用程序上构建。变量cannonground将被设置为保存一个new Myrectangle,变量targethtarget将被设置为保存一个new Picture

小费

程序员编的名字是任意的,但是拼写和大小写保持一致是个好主意。HTML5 似乎不考虑大小写,这与 XHTML 版本形成对比。许多语言将大写字母和小写字母视为不同的字母。我一般使用小写,但是我将BallPictureSlingshotMyrectangle的第一个字母大写,因为按照惯例,打算作为对象构造函数的函数应该以大写字母开头。

使用数组方法push将每个变量添加到everything数组中,该方法在数组末尾添加一个新元素。

绘图的旋转和平移

HTML5 让我们翻译和旋转绘图。正如您在第 2 和 3 章中所看到的,图纸是根据坐标系绘制的,图像等对象是根据坐标系定位的。坐标系的一个重要方面是它的原点,即 0,0 位置。HTML5 提供了一种改变坐标系的方法。一个平移操作改变原点。我们大多数人都熟悉的一种情况是在我们的汽车中使用 GPS 系统。根据我们所处的位置给出了方向。你可以认为这是重置原点。旋转操作绕原点旋转!接下来的几段带你看一些例子。请花时间研究这些示例,并进行修改,看看会发生什么。

看一下下面的代码。我敦促你创建这个例子,然后用它来提高你的理解。该代码在画布上绘制了一个大的红色矩形,其左上角位于(50,50)处,其顶部有一个小的蓝色正方形。

<html>
<head>
    <title>Rectangle</title>
    <script type="text/javascript">
        var ctx;
function init(){
   ctx = document.getElementById('canvas').getContext('2d');
        ctx.fillStyle = "rgb(250,0,0)";
        ctx.fillRect(50,50,100,200);
ctx.fillStyle = "rgb(0,0,250)";

        ctx.fillRect(50,50,5,5);
}
</script>

</head>
<body onLoad="init();">
<canvas id="canvas" width="400" height="300">
Your browser doesn't support the HTML5 element canvas.
</canvas>
</body>
</html>

结果如图 4-6 所示。

img/214814_2_En_4_Fig6_HTML.jpg

图 4-6

矩形(无旋转)

在本练习中,目标是旋转大矩形,以左上角的蓝色小方块为轴。我想逆时针旋转。

大多数编程语言都有一个小小的复杂之处,那就是旋转和三角函数的角度输入必须以弧度为单位,而不是度数。弧度在第二章和中有解释,但是这里有一个提醒。该测量基于圆中数学常数π弧度的两倍,而不是整圆的 360 度。幸运的是,我们可以使用 JavaScript 的内置特性,Math.PI。1π弧度相当于 180 度,π除以 2 相当于直角 90 度。为了指定 30 度的旋转,我们使用π除以 6,或者在编码中使用Math.PI/6。为了改变之前给出的init函数来做一个旋转,我放入一个负圆周率除以 6 的旋转(相当于逆时针旋转 30 度),画出红色的矩形,然后旋转回来,撤销旋转,画出蓝色的正方形:

function init(){
   ctx = document.getElementById('canvas').getContext('2d');
   ctx.fillStyle = "rgb(250,0,0)";
   ctx.rotate(-Math.PI/6);
   ctx.fillRect(50,50,100,200);
   ctx.rotate(Math.PI/6);
   ctx.fillStyle = "rgb(0,0,250)";
   ctx.fillRect(50,50,5,5);
}

可惜图 4-7 中的图不是我想要的。

img/214814_2_En_4_Fig7_HTML.jpg

图 4-7

绘制和旋转矩形

问题是旋转点在原点,(0,0),而不是在红色矩形的角上。因此,我需要编写代码来执行一个转换,以重置原点,然后旋转,然后转换回来,以便在正确的位置绘制下一个项目。我可以使用 HTML5 的特性来做到这一点。画布上的所有绘图都是根据一个坐标系完成的,我可以使用saverestore操作来保存当前的坐标系——轴的位置和方向——然后恢复它以制作更多的绘图。这是代码。

function init(){
   ctx = document.getElementById('canvas').getContext('2d');
   ctx.fillStyle = "rgb(250,0,0)";
   ctx.save();
   ctx.translate(50,50);             //move origin
   ctx.rotate(-Math.PI/6);           //do rotation
   ctx.translate(-50,-50);           // move origin back
   ctx.fillRect(50,50,100,200);      //draw rectangle
   ctx.restore();                    //undo all the transformations
   ctx.fillStyle = "rgb(0,0,250)";
   ctx.fillRect(50,50,5,5);          //draw little blue square
}

rotate 方法需要以弧度为单位的角度,顺时针方向为正方向。所以我的代码逆时针旋转了 30 度,产生了我想要的结果,如图 4-8 所示。

img/214814_2_En_4_Fig8_HTML.jpg

图 4-8

保存、翻译、旋转、翻译、恢复

我再次敦促你修改这个例子,以帮助你理解转换和弧度。做一些小的改变,一次一个陈述。

顺便说一下,我们不能期望我们的玩家用弧度来输入角度。他们,还有我们,太习惯于度(90 度是直角,180 度是你掉头时的弧度等等。).程序必须完成工作。从角度到弧度的转换要乘以 pi/180。

注意

大多数编程语言在三角函数和旋转操作中使用弧度表示角度。

在这种背景下,我向everything数组中的信息添加指示,指示是否有旋转,如果有,则指示所需的平移点。这是我的主意。这与 HTML5 或 JavaScript 无关,本来可以用不同的方式完成。底层任务是创建和维护模拟场景中对象的信息。HTML5 的 canvas 特性提供了一种画图和显示图像的方式,但是它并没有保留对象的信息!

第二个和第三个应用程序的everything数组中的项目本身就是数组。第一个(0 th index)值指向对象。第二个(1 st index)是truefalse。值true意味着旋转角度值和平移的 x 和 y 值跟随其后。实际上,这意味着内部数组要么有两个值,最后一个是false,要么有五个值。

注意

此时,你可能会想:她设置了一个通用系统,只是为了旋转大炮。为什么不为大炮装些东西呢?答案是我们可以,但是一般的系统确实可以工作,而且仅仅为加农炮编写的东西可能有同样多的代码。

第一个应用程序使用从表单中提取的水平和垂直位移值。玩家必须考虑这两个不同的值。对于第二个应用程序,播放器再次输入两个值,但它们是不同的。一个是出炮口的速度,一个是炮的角度。剩下的工作由程序来完成。初始不变的水平位移和初始垂直位移是根据玩家的输入计算出来的:出炮速度和一个角度。计算基于标准的三角学。幸运的是,JavaScript 提供了 trig 函数作为内置方法的Math类的一部分。

图 4-9 显示了玩家指定的炮外位移值和角度值的计算。竖线的负号是由于 JavaScript 屏幕坐标的 y 值随着屏幕向下增加而产生的。

img/214814_2_En_4_Fig9_HTML.jpg

图 4-9

计算水平*垂直位移

在这一点上,您可能想跳过阅读炮弹应用程序的实现。然后你可以回来阅读弹弓需要什么。

绘制线段

对于 slingshot 应用程序,我通过定义两个函数Slingdrawsling添加了一个新的对象类型。我的理想化弹弓由四个位置表示,如图 4-10 。请理解,我们可以用许多不同的方式来做这件事。

img/214814_2_En_4_Fig10_HTML.jpg

图 4-10

理想化的弹弓

Sling函数类似于其他构造函数,例如Ball

function Sling(bx,by,s1x,s1y,s2x,s2y,s3x,s3y,stylestring) {
      this.bx = bx;
      this.by = by;
      this.s1x = s1x;
      this.s1y = s1y;
      this.s2x = s2x;
      this.s2y = s2y;
      this.s3x = s3x;
      this.s3y = s3y;
      this.strokeStyle = stylestring;
      this.draw = drawsling;
      this.moveit = movesling;
}

Sling函数将drawsling设置为每当drawSling对象结合使用时调用的函数。虽然在当前的应用程序中没有发生,movesling将被调用,如果你或我在这个应用程序上移动弹弓的位置。

绘制弹弓包括基于四个点绘制四条线段。这个点将会改变,我将在下一节描述。HTML5 让我们绘制线段作为路径的一部分。我们已经用路径来画圆了。您可以将路径绘制为笔画或填充。对于圆圈,我们使用了fill方法,但是对于弹弓,我只想要线条。画一条线可能涉及两个步骤:移动到线的一端,然后画它。HTML5 提供了moveTolineTo方法。在调用strokefill方法之前,不会绘制路径。drawsling功能很好的说明了画线。

function drawsling() {
   ctx.strokeStyle = this.strokeStyle;
   ctx.lineWidth = 4;
   ctx.beginPath();
   ctx.moveTo(this.bx,this.by);
   ctx.lineTo(this.s1x,this.s1y);
   ctx.moveTo(this.bx,this.by);
   ctx.lineTo(this.s2x,this.s2y);
   ctx.moveTo(this.s1x,this.s1y);
   ctx.lineTo(this.s2x,this.s2y);
   ctx.lineTo(this.s3x,this.s3y);
   ctx.stroke();
}

它执行以下操作:

  • 向路径添加一条从bx,bys1x,s1y的线

  • 向路径添加一条从bx,bys2x,s2y的线

  • 向路径添加一条从s1x,s1ys2x,s2y的线

  • 向路径添加一条从s2x,s2ys3x,s3y的线

和往常一样,学习这个的方法是尝试你自己的设计。如果没有调用moveTo,下一个lineTo将从上一个lineTo的目的地抽取。想象你手里拿着一支笔,要么在纸上移动,要么举起来移动,但不画任何东西。您也可以连接弧。第五章演示绘制多边形。

鼠标事件用于拉动弹弓

slingshot 应用程序用鼠标拖放操作代替了表单输入。这很吸引人,因为它更接近于弹弓的物理行为。

当玩家按下鼠标按钮时,这是程序管理的一系列事件中的第一个。下面是需要完成的工作的伪代码。

当玩家按下鼠标键时,检查鼠标是否在球的上面。如果不是,什么都不做。如果是,设置一个名为 inmotion 的变量。

如果鼠标正在移动,勾选 inmotion 。如果设置好了,移动弹弓的球和弦。一直这样做,直到松开鼠标按钮。

当玩家释放鼠标按钮时,将 inmotion 重置为 false 。计算球的角度和初速度,并由此计算水平速度和初始垂直速度。让球动起来。

您可以使用 HTML5 和 JavaScript 来设置按下标准(左)鼠标按钮、移动鼠标和释放鼠标按钮的事件处理。代码使用了直接基于canvas元素的方法,而不是所谓的上下文。下面是代码,它在init函数中:

canvas1 = document.getElementById('canvas');
canvas1.addEventListener('mousedown',findball,false);
canvas1.addEventListener('mousemove',moveit,false);
canvas1.addEventListener('mouseup',finish,false);

因为这个事件是在整个画布上发生的,所以findball函数必须确定鼠标是否在球上。第一个任务是获得鼠标的 x 和 y 坐标。当我最初写这篇文章时,不同的浏览器以不同的方式实现鼠标事件。以下是推荐用于 Firefox、Chrome 和 Safari 的编码和工作方式。当其他浏览器(如 Internet Explorer)支持 HTML5 时,将需要检查并可能修改这些代码。注意,当不支持canvas时,canvas元素内部的编码确实会返回一条消息。

if ( ev.layerX ||  ev.layerX==0) {
   mx= ev.layerX;
   my = ev.layerY;
}
else if (ev.offsetX || ev.offsetX==0 ) {
   mx = ev.offsetX;
   my = ev.offsetY;
}

这是因为如果ev.layerX不存在,它的值将被解释为false。如果ev.layerX确实存在但值为 0,其值也将被解释为false,但ev.layerX==0将为true

把这段代码想成是在说:有好的ev.layerX值吗?如果有,那就用吧。要不,我们试试ev.offsetX。如果这两个都不起作用,mxmy将不会被设置,我应该添加另一个else子句来告诉玩家代码在他的浏览器中不起作用。

现在,下一步是确定(mx,my)点是否在球上。我在重复自己的话,但重要的是要明白,球现在相当于画布上的墨水或颜料,如果不确定(mx,my)点是否在球的顶部,我们就无法继续下去。我们如何做到这一点?我们可以计算出(mx,my)离球的中心有多远,看看它是否小于球的半径。平面距离有一个标准公式。我的代码是这个想法的一个微小的变化。它通过计算距离的平方并将其与球半径的平方进行比较来做出决定。我这样做是为了避免计算平方根。

如果鼠标点击在球上,即在球中心的半径距离内,该函数将全局变量inmotion设置为truefindball函数以调用drawall()结束。

每当鼠标移动时,就会调用moveit函数,在这里我们检查inmotion是否是true。如果不是,什么都不会发生。如果是,使用与前面相同的代码来获取鼠标坐标和球的中心,并将弹弓的bx,by值设置为鼠标坐标。这有拖动球和拉伸弹弓弦的效果。

当释放鼠标按钮时,我们调用finish函数,如果inmotion不是true,该函数不做任何事情。这什么时候会发生?如果玩家在球上的而不是周围移动鼠标并按下和释放按钮。

如果inmotiontrue,该函数立即将其设置为false,并进行计算以确定球的飞行,生成玩家使用表单在早期的炮弹应用程序中输入的信息。信息是与水平线的角度和球到弹弓直线部分的距离。这是(bx,by)到(s1x, s1y)的夹角,以及(bx,by)到(s1x, s1y)的距离,更准确的说是距离的平方。

我用Math.atan2来做这些计算:根据x的变化和y的变化来计算角度。这是arctangent功能的变体。

我使用distsq函数来确定从(bx,by)到(s1x, s1y))的距离的平方。我想让速度依赖于这个值。将绳子拉得更远意味着飞行速度更快。我做了一些实验,决定用正方形除以 700 产生一个漂亮的弧线。

最后一步是首先调用drawall(),然后调用setInterval来设置计时事件。同样,finish在炮弹应用中做着与fire类似的工作。在第一个应用程序中,我们的玩家输入了水平和初始垂直值。在第二个应用程序中,玩家输入一个角度(以度为单位)和一个速度,剩下的由程序完成。在《弹弓》中,我们去掉了表格和数字,为玩家提供了一种在弹弓上拉回,或者虚拟拉回的方式。在响应鼠标事件和计算方面,该程序有更多的工作要做。

请注意,我没有对球员愚蠢地将球瞄准远离鸡的方向,或者直接瞄准向上,或者将球拉到地面以下做任何规定。在后一种情况下,球向上移动并停在地上。试验并决定您将包括哪些检查和消息。

使用数组拼接更改显示的项目列表

最后一个需要解释的任务是用另一个图片替换目标图片。因为我想要两种不同的效果,所以我使用了不同的方法。对于第二个应用程序,我希望球和原来的target一起消失,并显示我在变量htarget中设置的内容。我所做的是跟踪原来的targeteverything数组中的位置,然后移除它并替换htarget。类似地,我将球从everything数组中移除。对于弹弓操作,我没有移除目标,而是将它的img属性改为feathers。注意,在代码中,chickenfeathersImage对象。每个都有一个指向文件的src属性。

        var chicken = new Image();
        chicken.src = "chicken.jpg";
        var feathers = new Image();
        feathers.src = "feathers.gif";

对于这两个操作,我使用数组方法splice。它有两种形式:您可以删除任意数量的元素,也可以删除然后插入元素。拼接的一般形式是

arrayname.splice(拼接发生的索引,要删除的项目数,要添加的新项目)

如果要添加一个以上的项目,则有更多的参数。在我的代码中,我添加了一个条目,它本身就是一个数组。我在everything数组中对对象的表示为每个对象使用一个数组。数组的第二个参数指示是否有旋转。

下面两行代码做了我需要做的事情:移除目标,在不旋转的情况下粘上htarget,然后移除球。

everything.splice(targetindex,1,[htarget,false]);
everything.splice(ballindex,1);

顺便说一下,如果我只想删除数组中的最后一项,我可以使用方法pop。然而,在这种情况下,目标可能在everything数组中间的某个地方,所以我需要编写代码来跟踪它的索引值。

点之间的距离

在 slingshot 程序中有两个地方我使用了点与点之间的距离,或者更准确地说,距离的平方。我需要找出鼠标光标是否在球的顶部,我想根据弹弓的拉伸,即(bx,by)到(s1x,s1y)的距离,确定初始速度,相当于炮弹的速度。两点 x1,y1 和 x2,y2 之间的距离公式是(x1-x2)和(y1-y2)的平方和的平方根。我决定通过计算平方和来避免计算平方根。这为鼠标光标在球上提供了相同的测试。对于另一个任务,我决定用距离的平方来表示初速度。我试验了一些数字,正如我前面提到的,700 似乎是可行的。

构建应用程序并使之成为您自己的应用程序

现在让我们来看看炮弹基本发射的代码,没有大炮,基于水平和初始垂直速度;从大炮中发射炮弹,基于大炮的角度和初始速度;以及弹弓,基于从鼠标位置确定的角度和初始速度。和前几章一样,我将介绍这些函数,以及它们对每个应用程序的调用或被调用。在这种情况下,这三个应用程序的表虽然不完全相同,但很相似。这种调用比前面的例子更加多样化,因为有些情况下调用函数是因为它们被命名为程序员定义的对象的方法或声明(var)语句的一部分。这是面向对象、事件驱动编程的一个特点。我还将在表格中给出每个应用程序的完整代码,并解释每一行的作用。表 4-1 显示了基本炮弹应用的功能。

表 4-1

在最简单的炮弹应用中的功能

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | body标签中onLoad的动作 | drawall |
| drawall | 由initfirechange直接调用 | 调用everything数组中所有对象的draw方法。这些是drawballdrawrects的功能 |
| fire | 由表单中的onSubmit属性的动作调用 | drawall |
| change | 由在fire中调用的setInterval函数的动作调用 | drawall,调用cballmoveit方法,也就是moveball |
| Ball | 由代码在var语句中直接调用 |   |
| Myrectangle | 由代码在var语句中直接调用 |   |
| drawball | 通过调用一个Ball对象的draw方法来调用 |   |
| drawrects | 通过调用target对象的draw方法来调用 |   |
| moveball | 通过调用一个Ball对象的moveit方法来调用 |   |

表 4-2 显示了最简单应用的完整代码,球以弧线运动,没有实际的大炮。

表 4-2

第一个炮弹应用

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Cannonball</title> | 完成title元素。 |
| <style> | 开始style标签。 |
| form { | 表单的样式。 |
| width:330px; | Width。 |
| margin:20px; | 外部margin。 |
| background-color:brown; | 设置窗体的背景色。 |
| padding:20px; | 内部padding。 |
| } | 关闭此样式。 |
| </style> | 关闭style元素。 |
| <script> | 开始script标签。 |
| var cwidth = 600; | 设置用于清除的画布宽度值。 |
| var cheight = 400; | 设置画布height的值,用于清除。 |
| var ctx; | 保存画布上下文的变量。 |
| var everything = []; | 数组来保存所有要绘制的对象。初始化为空数组。 |
| var tid; | 保存计时事件标识符的变量。 |
| var horvelocity; | 变量来保持水平速度(又名位移)。 |
| var verticalvel1; | 用于保存间隔开始时的垂直位移的变量。 |
| var verticalvel2; | 重力改变后,间隔结束时保持垂直位移的变量。 |
| var gravity = 2; | 垂直位移的变化量。武断。形成一个漂亮的弧线。 |
| var iballx = 20; | 球的初始水平坐标。 |
| var ibally = 300; | 球的初始垂直坐标。 |
| function Ball(sx,sy,rad,stylestring) { | 定义一个Ball的功能开始。对象。使用参数设置属性。 |
| this.sx = sx; | 设置this对象的sx属性。 |
| this.sy = sy; | … sy |
| this.rad = rad; | … rad |
| this.draw = drawball; | … draw。由于drawball是一个函数的名字,这使得draw成为一个可以被调用的方法。 |
| this.moveit = moveball; | … moveit设置为功能moveball。 |
| this.fillstyle = stylestring; | … fillstyle |
| } | 关闭Ball功能。 |
| function drawball() { | drawball功能的标题。 |
| ctx.fillStyle=this.fillstyle; | 使用该对象的属性设置fillStyle。 |
| ctx.beginPath(); | 开创一条道路。 |
| ctx.arc(this.sx,this.sy ,this.rad,0,Math.PI*2,true); | 设置为画一个圆。 |
| ctx.fill(); | 将路径绘制为填充路径。 |
| } | 关闭该功能。 |
| function moveball(dx,dy) { | moveball功能的标题。 |
| this.sx +=dx; | 将sx属性增加dx。 |
| this.sy +=dy; | 将sy属性增加dy。 |
| } | 关闭功能。 |
| var cball = new Ball(iballx,ibally, 10,"rgb(250,0,0)"); | 在指定的位置、半径和颜色创建一个新的Ball对象。将其赋给变量cball。请注意,此时没有绘制任何内容。这些信息只是为以后使用而设置的。 |
| function Myrectangle(sx,sy,swidth, sheight,stylestring) { | 构造一个Myrectangle对象的函数头。 |
| this.sx = sx; | 设置this对象的sx属性。 |
| this.sy = sy; | … sy |
| this.swidth = swidth; | … swidth |
| this.sheight = sheight; | … sheight |
| this.fillstyle = stylestring; | … stylestring |
| this.draw = drawrects; | … draw。这就建立了一个可以调用的方法。 |
| this.moveit = moveball; | ….moveit。这就建立了一个可以调用的方法。这个程序中不使用它。 |
| } | 关闭Myrectangle功能。 |
| function drawrects() { | drawrects功能的标题。 |
| ctx.fillStyle = this.fillstyle; | 设置fillStyle。 |
| ctx.fillRect(this.sx,this.sy, this.swidth,this.sheight); | 使用对象属性绘制矩形。 |
| } | 关闭该功能。 |
| var target = new Myrectangle(300,100, 80,200,"rgb(0,5,90)"); | 构建一个Myrectangle对象并分配给目标。 |
| var ground = new Myrectangle(0,300, 600,30,"rgb(10,250,0)"); | 建立一个Myrectangle物体并分配给地面。 |
| everything.push(target); | 将目标添加到everything。 |
| everything.push(ground); | 添加ground。 |
| everything.push(cball); | 添加cball(它将在最后绘制,因此在所有其他内容之上)。 |
| function init(){ | init功能的标题。 |
| ctx = document.getElementById ('canvas').getContext('2d'); | 设置ctx以便在画布上绘图。 |
| drawall(); | 画出一切。 |
| } | 关闭init。 |
| function fire() { | 头部为fire功能。 |
| cball.sx = iballx; | 将cball重新定位在x中。 |
| cball.sy = ibally; | 将cball重新定位在y中。 |
| horvelocity =  Number(document. f.hv.value); | 从表格中设置水平速度。打个电话。 |
| verticalvel1 = Number(document. f.vv.value); | 从表格中设置初始垂直速度。 |
| drawall(); | 画出一切。 |
| tid = setInterval (change,100); | 开始计时事件。 |
| return false; | 返回false阻止刷新 HTML 页面。 |
| } | 关闭该功能。 |
| function drawall() { | drawall的功能头。 |
| ctx.clearRect (0,0,cwidth,cheight); | 擦除画布。 |
| var i; | 为for循环声明var i。 |
| for (i=0;i<everything.length;i++) { | 对于everything数组中的每一项… |
| everything[i].draw();} | …调用对象的draw方法。关闭for回路。 |
| } | 关闭该功能。 |
| function change() { | change功能的标题。 |
| var dx = horvelocity; | 将dx设置为horvelocity。 |
| verticalvel2 = verticalvel1 + gravity; | 计算新的垂直速度(添加重力)。 |
| var dy = (verticalvel1 + verticalvel2)*.5; | 计算时间间隔的平均速度。 |
| verticalvel1 = verticalvel2; | 现在把旧的变成新的。 |
| cball.moveit(dx,dy); | 移动cball计算量。 |
| var bx = cball.sx; | 设置bx以简化if语句。 |
| var by = cball.sy; | ...和by |
| if ((bx>=target.sx)&&(bx<= (target.sx+target.swidth))&& | 球在水平方向上是否在目标内… |
| (by>=target.sy)&&(by<= (target.sy+target.sheight))) { | 还有纵向? |
| clearInterval(tid); | 如果是这样,停止运动。 |
| } | 关闭if true子句。 |
| if (by>=ground.sy) { | 球越过地面了吗? |
| clearInterval(tid); | 如果是这样,停止运动。 |
| } | 关闭if true子句。 |
| drawall(); | 画出一切。 |
| } | 关闭change功能。 |
| </script> | 关闭script元素。 |
| </head> | 关闭head元素。 |
| <body onLoad="init();"> | 打开body,将通话设置为init。 |
| <canvas id="canvas" width= "600" height="400"> | 定义canvas元素。 |
| Your browser doesn't support the HTML5 element canvas. | 向不兼容浏览器的用户发出警告。 |
| </canvas> | 关闭canvas元件。 |
| <br/> | 换行。 |
| <form name="f" id="f" onSubmit="return fire();"> | 带有名称和 ID 的起始表单标记。这将建立对fire的调用。 |
| Set velocities and fire cannonball. <br/> | 标签和换行。 |
| Horizontal displacement <input name=``"hv" id="hv" value="10" type= | 输入字段的标签和说明。 |
| <br> | 换行。 |
| Initial vertical displacement <input``name="vv" id="vv" value="-25" | 输入字段的标签和说明。 |
| <input type="submit" value="FIRE"/> | 提交input元素。 |
| </form> | 关闭form元素。 |
| </body> | 关闭body元素。 |
| </html> | 关闭html元素。 |

您当然可以对这个应用程序进行改进,但是首先确保您理解了它,然后继续下一个,这可能更有意义。

炮弹:有大炮,角度和速度

我们的下一个应用程序添加了一个矩形来表示大炮,一个原始目标的图片而不是第一个应用程序中使用的简单矩形,以及第二个命中目标的图片。大炮按照表单中输入的内容旋转。我让everything数组成为数组的数组,因为我需要一种方法来添加旋转和平移信息。我还决定让炮弹击中目标时的结果更加戏剧化。这意味着用于检查碰撞的change函数中的代码是相同的,但是if-true子句中的代码删除了旧的目标,放入命中的目标,并删除了球。现在,说了这么多,大部分编码都是一样的。显示功能的表 4-3 增加了两行用于PicturedrawAnImage

表 4-3

第二个炮弹应用程序中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | body标签中onLoad的动作 | drawall |
| drawall | 由initfirechange直接调用 | 调用everything数组中所有对象的draw方法。这些是功能drawballdrawrects |
| fire | 由表单中的onSubmit属性的动作调用 | drawall |
| change | 由在fire中调用的setInterval函数的动作调用 | drawall,调用cballmoveit方法,也就是moveball |
| Ball | 由代码在var语句中直接调用 |   |
| Myrectangle | 由代码在var语句中直接调用 |   |
| drawball | 通过调用一个Ball对象的draw方法来调用 |   |
| drawrects | 通过调用target对象的draw方法来调用 |   |
| moveball | 通过调用一个Ball对象的moveit方法来调用 |   |
| Picture | 由代码在var语句中直接调用 |   |
| drawAnImage | 通过调用Picture对象的draw方法来调用 |   |

表 4-4 显示了第二个应用程序的完整代码,但是只有修改过的行有注释。

表 4-4

第二个炮弹应用

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Cannonball</title> |   |
| <style> |   |
| form { |   |
| width:330px; |   |
| margin:20px; |   |
| background-color:brown; |   |
| padding:20px; |   |
| } |   |
| </style> |   |
| <script type="text/javascript"> |   |
| var cwidth = 600; |   |
| var cheight = 400; |   |
| var ctx; |   |
| var everything = []; |   |
| var tid; |   |
| var horvelocity; |   |
| var verticalvel1; |   |
| var verticalvel2; |   |
| var gravity = 2; |   |
| var cannonx = 10; | 大炮的 x 位置。 |
| var cannony = 280; | 大炮的 y 位置。 |
| var cannonlength = 200; | 加农炮长度(即宽度)。 |
| var cannonht = 20; | 大炮高度。 |
| var ballrad = 10; |   |
| var targetx = 500; | 目标的 x 位置。 |
| var targety = 50; | 目标的 y 位置。 |
| var targetw = 85; | 目标宽度。 |
| var targeth = 280; | 目标高度 |
| var htargetx = 450; | 击中目标的 x 位置。 |
| var htargety = 220; | 击中目标的 y 位置。 |
| var htargetw = 355; | 击中目标宽度。 |
| var htargeth = 96; | 击中目标高度。 |
| function Ball(sx,sy,rad,stylestring) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.rad = rad; |   |
| this.draw = drawball; |   |
| this.moveit = moveball; |   |
| this.fillstyle = stylestring; |   |
| } |   |
| function drawball() { |   |
| ctx.fillStyle=this.fillstyle; |   |
| ctx.beginPath(); |   |
| //ctx.fillStyle= rgb(0,0,0); |   |
| ctx.arc(this.sx,this.sy,this.rad, 0,Math.PI*2,true); |   |
| ctx.fill(); |   |
| } |   |
| function moveball(dx,dy) { |   |
| this.sx +=dx; |   |
| this.sy +=dy; |   |
| } |   |
| var cball = new Ball(cannonx+cannonlength, cannony+cannonht*.5,ballrad,"rgb(250,0,0)"); |   |
| function Myrectangle(sx,sy,swidth,sheight, stylestring) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.swidth = swidth; |   |
| this.sheight = sheight; |   |
| this.fillstyle = stylestring; |   |
| this.draw = drawrects; |   |
| this.moveit = moveball; |   |
| } |   |
| function drawrects() { |   |
| ctx.fillStyle = this.fillstyle; |   |
| ctx.fillRect(this.sx,this.sy, this.swidth,this.sheight); |   |
| } |   |
| function Picture (sx,sy,swidth, sheight,filen) { | 用于设置Picture对象的函数的标题。 |
| var imga = new Image(); | 创建一个Image对象。 |
| imga.src=filen; | 设置文件名。 |
| this.sx = sx; | 设置sx属性。 |
| This.sy = sy; | … sy |
| this.img = imga; | 将img属性设置为imga。 |
| .     this.swidth = swidth; | … swidth |
| this.sheight = sheight; | … sheight |
| this.draw = drawAnImage; | … draw。这将是该类型对象的draw方法。 |
| this.moveit = moveball; | …这将是moveit方法。没用过。 |
| } | 关闭Picture功能。 |
| function drawAnImage() { | drawAnImage功能的标题。 |
| ctx.drawImage(this.img,this.sx, this.sy,this.swidth,this.sheight); | 使用此对象的属性绘制图像。 |
| } | 关闭该功能。 |
| var target = new Picture(targetx,targety, targetw,targeth,"hill.jpg"); | 构造新的Picture对象并赋给target变量。 |
| var htarget = new Picture(htargetx, htargety, htargetw, htargeth, "plateau.jpg"); | 构造新的Picture对象并赋给htarget变量。 |
| var ground = new Myrectangle(0,300, 600,30,"rgb(10,250,0)"); | 构造新的Myrectangle对象并分配给ground。 |
| var cannon = new Myrectangle(cannonx, cannony,cannonlength,cannonht,"rgb(40,40,0)"); | 构造新的Myrectangle对象并分配给cannon。 |
| var targetindex = everything.length; | 保存将成为target索引的内容。 |
| everything.push([target,false]); | 将target加到everything上。 |
| everything.push([ground,false]); | 将ground加到everything上。 |
| var ballindex = everything.length; | 保存将成为cball索引的内容。 |
| everything.push([cball,false]); | 将cball加到everything上。 |
| var cannonindex = everything.length; | 为卡农保存索引。 |
| everything.push([cannon,true,0, cannonx,cannony+cannonht*.5]); | 给everything;加炮预留旋转空间。 |
| function init(){ |   |
| ctx = document.getElementById ('canvas').getContext('2d'); |   |
| drawall(); |   |
| } |   |
| function fire() { |   |
| var angle = Number(document.f .ang.value); | 从表格中提取角度,转换成数字。 |
| var outofcannon = Number (document.f.vo.value); | 从表格中提取加农炮的速度,转换成数字。 |
| var angleradians = angle*Math .PI/180; | 转换为弧度。 |
| horvelocity =  outofcannon*Math .cos(angleradians); | 计算水平速度。 |
| verticalvel1 = - outofcannon*Math .sin(angleradians); | 计算初始垂直速度。 |
| everything[cannonindex][2]= - angleradians; | 设置旋转大炮的信息。 |
| cball.sx = cannonx + cannonlength*Math.cos(angleradians); | 将cballx设定在将要旋转的炮口。 |
| cball.sy = cannony+cannonht*.5 - cannonlength*Math.sin(angleradians); | 将cbally设定在将要旋转的炮口。 |
| drawall(); |   |
| tid = setInterval(change,100); |   |
| return false; |   |
| } |   |
| function drawall() { |   |
| ctx.clearRect(0,0,cwidth,cheight); |   |
| var i; |   |
| for (i=0;i<everything.length;i++) { |   |
| var ob = everything[i]; | 提取对象的数组。 |
| if (ob[1]) { | 需要平移旋转? |
| ctx.save(); | 保存原始轴。 |
| ctx.translate(ob[3],ob[4]); | 做指示翻译。 |
| ctx.rotate(ob[2]); | 做指示旋转。 |
| ctx.translate(-ob[3],-ob[4]); | 翻译回来。 |
| ob[0].draw(); | 绘制对象。 |
| ctx.restore(); } | 恢复坐标轴。 |
| else { | 否则(不旋转)。 |
| ob[0].draw();} | 画画。 |
| } | 关闭for回路。 |
| } | 关闭该功能。 |
| function change() { |   |
| var dx = horvelocity; |   |
| verticalvel2 =verticalvel1 + gravity; |   |
| var dy=(verticalvel1 + verticalvel2)*.5; |   |
| verticalvel1 = verticalvel2; |   |
| cball.moveit(dx,dy); |   |
| var bx = cball.sx; |   |
| var by = cball.sy; |   |
| if ((bx>=target.sx)&&(bx<=(target .sx+target.swidth))&& |   |
| (by>=target.sy)&&(by<=(target .sy+target.sheight))) { |   |
| clearInterval(tid); |   |
| everything.splice (targetindex,1,[htarget,false]); | 移除target并插入htarget。 |
| everything.splice (ballindex,1); | 移开球。 |
| drawall(); |   |
| } |   |
| if (by>=ground.sy) { |   |
| clearInterval(tid); |   |
| } |   |
| drawall(); |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();"> |   |
| <canvas id="canvas" width="600" height="400"> |   |
| Your browser doesn't support the``HTML5 element canvas |   |
| </canvas> |   |
| <br/> |   |
| <form name="f" id="f" onSubmit= "return fire();"> |   |
| Set velocity, angle and fire cannonball. <br/> |   |
| Velocity out of cannon <input name=``"vo" id="vo" value="10" type= | 表明这是从炮口出来的速度的标签。 |
| <br> |   |
| Angle <input name="ang" id="ang"``value="0" type="number" min= | 表明这是加农炮角度的标签。 |
| <input type="submit" value="FIRE"/> |   |
| </form> |   |
| </body> |   |
| </html> |   |

这个应用程序提供了许多可能性,让你把它变成你自己的。你可以改变大炮,球,地面和目标。如果你不想使用图像,你可以为目标和命中目标使用绘图。你可以在画布上画其他东西。你只需要确保炮弹(或者你设置的任何东西)在顶部或者你想让它在的任何地方。例如,你可以让地面盖住球。您可以对任何Image对象使用动画 GIF,包括htarget。你也可以为大炮和球使用图像。一种可能是使用动画 GIF 文件来表示旋转的炮弹。请记住,代码中引用的所有图像文件必须与上传的 HTML 文件位于同一个文件夹中。如果它们在 Web 上的不同位置,请确保引用是正确的。

HTML5 对音频和视频的支持因浏览器而异。你可以期待作为完成第六章测验的奖励的视频展示,以及作为第八章石头剪刀布游戏一部分的音频展示。如果你想解决这个问题,最好有炮弹击中目标时的声音和显示目标爆炸的视频剪辑。

离开游戏的外观,你可以发明一个评分系统,也许可以记录尝试次数和点击次数。

弹弓:用鼠标设置飞行参数

弹弓应用程序是建立在炮弹应用程序之上的。有差异,但大部分是相同的。回顾和理解更复杂的应用程序是如何在更简单的基础上构建的,将有助于您创建自己的作品。

创建弹弓应用程序包括设计弹弓,实现鼠标事件来移动球和弹弓的部件,然后发射球。形式是不存在的,因为玩家的移动只是鼠标动作。此外,当目标被击中时,我使用了一种稍微不同的方法。我检查球是否与目标内 40 像素的区域相交。也就是我要求球打到鸡的中间!当有点击时,我将target.src值改为另一个Image元素,从一张鸡的图片变成一张羽毛的图片。而且我不停止动画,所以球只有落地才停止。正如我前面指出的,我没有让弹弓吊索回到它们原来的位置,因为我想看看这个位置来计划我的下一次尝试。

表 4-5 显示了 slingshot 应用程序中调用和被调用的函数。该表与炮弹应用程序中的表非常相似。

表 4-5

弹弓应用程序中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | body标签中onLoad的动作 | drawall |
| drawall | 由init,直接调用change | 调用everything数组中所有对象的draw方法。这些是功能drawballdrawrectsdrawslingdrawAnImage |
| findball | 由mousedown事件的init中的addEventListener动作调用 | drawall and distsq |
| distsq | 由findball调用 |   |
| moveit | 由mousemove事件的init中的addEventListener动作调用 | drawall |
| finish | 由mouseup事件的init中的addEventListener动作调用 | drawalldistsq |
| change | 由在finish中调用的setInterval函数的动作调用 | drawall,调用cballmoveit方法,也就是moveball |
| Ball | 由代码在var语句中直接调用 |   |
| Myrectangle | 由代码在var语句中直接调用 |   |
| drawball | 通过调用一个Ball对象的draw方法来调用 |   |
| drawrects | 通过调用target对象的draw方法来调用 |   |
| moveball | 通过调用一个Ball对象的moveit方法来调用 |   |
| Picture | 由代码在var语句中直接调用 |   |
| drawAnImage | 通过调用Picture对象的draw方法来调用 |   |
| Sling | 由代码在var语句中直接调用 |   |
| drawsling | 通过调用myslingdraw方法来调用 |   |

表 4-6 显示了 slingshot 应用程序的代码,并对新的或更改的代码行进行了注释。请注意,表单没有出现在body元素中。在查看代码之前,尝试确定哪些部分与炮弹应用程序中的相同,哪些部分不同。

表 4-6

弹弓应用程序

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Slingshot pulling back</title> |   |
| <script type="text/javascript"> |   |
| var cwidth = 1200; |   |
| var cheight = 600; |   |
| var ctx; |   |
| var canvas1; |   |
| var everything = []; |   |
| var tid; |   |
| var startrockx = 100; | 起动位置 |
| var startrocky = 240; | 起动位置 |
| var ballx = startrockx; | 设置ballx。 |
| var bally = startrocky; | 设置bally。 |
| var ballrad = 10; |   |
| var ballradsq = ballrad*ballrad; | 保存该值。 |
| var inmotion = false; |   |
| var horvelocity; |   |
| var verticalvel1; |   |
| var verticalvel2; |   |
| var gravity = 2; |   |
| var chicken = new Image(); | 原始目标的名称。 |
| chicken.src = "chicken.jpg"; | 设置图像文件。 |
| var feathers = new Image(); | 击中目标的名称。 |
| feathers.src = "feathers.gif"; | 设置图像文件。 |
| function Sling(bx,by,s1x,s1y,s2x,s2y, s3x,s3y,stylestring) { | 基于四个点和一种颜色定义弹弓的函数。 |
| this.bx = bx; | 设置属性bx。 |
| this.by = by; | … by |
| this.s1x = s1x; | … s1x |
| this.s1y = s1y; | … s1y |
| this.s2x = s2x; | … s2x |
| this.s2y = s2y; | … s2y |
| this.s3x = s3x; | … s3x |
| this.s3y = s3y; | … s3y |
| this.strokeStyle = stylestring; | … strokeStyle |
| this.draw = drawsling; | 设置draw方法。 |
| this.moveit = movesling; | 设置move方法(未使用)。 |
| } | 关闭该功能。 |
| function drawsling() { | drawsling的功能头。 |
| ctx.strokeStyle = this.strokeStyle; | 设置此样式。 |
| ctx.lineWidth = 4; | 设置线条宽度。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(this.bx,this.by); | 移动到bx,by。 |
| ctx.lineTo(this.s1x,this.s1y); | 设置绘制到s1x,s1y。 |
| ctx.moveTo(this.bx,this.by); | 移动到bx,by。 |
| ctx.lineTo(this.s2x,this.s2y); | 设置绘制到s2x,s2y。 |
| ctx.moveTo(this.s1x,this.s1y); | 移动到s1x,s1y。 |
| ctx.lineTo(this.s2x,this.s2y); | 设置绘制到s2x,s2y。 |
| ctx.lineTo(this.s3x,this.s3y); | 绘制到s3x,s3y。 |
| ctx.stroke(); | 现在画出路径。 |
| } | 关闭该功能。 |
| function movesling(dx,dy) { | movesling的标题。 |
| this.bx +=dx; | 将dx加到bx上。 |
| this.by +=dy; | 将dy加到by上。 |
| this.s1x +=dx; | 将dx加到s1x上。 |
| this.s1y +=dy; | 将dy加到s1y上。 |
| this.s2x +=dx; | 将dx加到s2x上。 |
| this.s2y +=dy; | 将dy加到s2y上。 |
| this.s3x +=dx; | 将dx加到s3x上。 |
| this.s3y +=dy; | 将dy加到s3y上。 |
| } | 关闭该功能。 |
| var mysling= new Sling(startrockx,startrocky,``startrockx+80,startrocky-10,startrockx+80,``startrocky+10,startrockx+70, | 构建新的Sling,并将其赋给变量mysling。 |
| function Ball(sx,sy,rad,stylestring) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.rad = rad; |   |
| this.draw = drawball; |   |
| this.moveit = moveball; |   |
| this.fillstyle = stylestring; |   |
| } |   |
| function drawball() { |   |
| ctx.fillStyle=this.fillstyle; |   |
| ctx.beginPath(); |   |
| ctx.arc(this.sx,this.sy,this.rad, 0,Math.PI*2,true); |   |
| ctx.fill(); |   |
| } |   |
| function moveball(dx,dy) { |   |
| this.sx +=dx; |   |
| this.sy +=dy; |   |
| } |   |
| var cball = new Ball(startrockx,startrocky, ballrad,"rgb(250,0,0)"); |   |
| function Myrectangle(sx,sy,swidth, sheight,stylestring) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.swidth = swidth; |   |
| this.sheight = sheight; |   |
| this.fillstyle = stylestring; |   |
| this.draw = drawrects; |   |
| this.moveit = moveball; |   |
| } |   |
| function drawrects() { |   |
| ctx.fillStyle = this.fillstyle; |   |
| ctx.fillRect(this.sx,this.sy, this.swidth,this.sheight); |   |
| } |   |
| function Picture (sx,sy,swidth, sheight,imga) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.img = imga; |   |
| this.swidth = swidth; |   |
| this.sheight = sheight; |   |
| this.draw = drawAnImage; |   |
| this.moveit = moveball; |   |
| } |   |
| function drawAnImage() { |   |
| ctx.drawImage(this.img,this.sx,this. sy,this.swidth,this.sheight); |   |
| } |   |
| var target = new Picture(700,210,209, 179,chicken); | 构建新的Picture对象,并将其分配给target。 |
| var ground = new Myrectangle(0,370, 1200,30,"rgb(10,250,0)"); |   |
| everything.push(target); |   |
| everything.push(ground); | 把ground放在鸡爪上面。 |
| everything.push(mysling); |   |
| everything.push(cball); |   |
| function init(){ |   |
| ctx = document.getElementById ('canvas').getContext('2d'); |   |
| canvas1 = document.getElementById ('canvas'); |   |
| canvas1.addEventListener('mousedown', findball,false); | 为mousedown事件设置事件处理。 |
| canvas1.addEventListener('mousemove', moveit,false); | 为mousemove事件设置事件处理。 |
| canvas1.addEventListener('mouseup', finish,false); | 为mouseup事件设置事件处理。 |
| drawall(); |   |
| } |   |
| function findball(ev) { | mousedown事件的函数头。 |
| var mx; | 变量来保存鼠标 x。 |
| var my; | 用于保存鼠标 y 的变量。 |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | ev.layerX没事。 |
| mx= ev.layerX; | 用于mx。 |
| my = ev.layerY; } | 用layerY代替my。 |
| else if (ev.offsetX &#124;&#124; ev.offsetX == 0) { | 否则尝试偏移。 |
| mx = ev.offsetX; | 设置mx。 |
| my = ev.offsetY; } | 设置my。 |
| if (distsq(mx,my, cball.sx, cball.sy)<ballradsq) { | 鼠标在球上面吗? |
| inmotion = true; | 设置inmotion。 |
| drawall(); | 画出一切。 |
| } | 如果超过球,则关闭。 |
| } | 关闭功能。 |
| function distsq(x1,y1,x2,y2) { | distsq的标题。 |
| return (x1-x2)*(x1-x2)+(y1-y2)* (y1-y2); | 返回距离的平方。 |
| } | 关闭该功能。 |
| function moveit(ev) { | mousemove事件的函数头。 |
| var mx; | 对于鼠 x。 |
| var my; | 对于老鼠 y。 |
| if (inmotion) { | 运动中? |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | layerX管用吗? |
| mx= ev.layerX; | 用于mx。 |
| my = ev.layerY; | ev.layerYmy。 |
| } else if (ev.offsetX &#124;&#124; ev.offsetX == 0) { | offsetX管用吗? |
| mx = ev.offsetX; | 用于mx。 |
| my = ev.offsetY; | 用offsetY代替my。 |
| } | 如果为真,则关闭。 |
| cball.sx = mx; | 定位球x。 |
| cball.sy = my; | …和y |
| mysling.bx = mx; | 位置sling bx。 |
| mysling.by = my; | …和by |
| drawall(); | 画出一切。 |
| } | 运动中关闭。 |
| } | 关闭该功能。 |
| function finish(ev) { | mousedown的功能。 |
| if (inmotion) { | 运动中? |
| inmotion = false; | 重置〔??〕。 |
| var outofcannon = distsq(mysling.bx,mysling.by, mysling.s1x,mysling.s1y)/700; | 基数outofcannonbx,bys1x,s1y的平方成正比。 |
| var angleradians = -Math.atan2``(mysling.s1y-mysling.by, | 计算角度。 |
| horvelocity =  outofcannon*Math.cos (angleradians); |   |
| verticalvel1 = - outofcannon*Math.sin (angleradians); |   |
| drawall(); |   |
| tid = setInterval(change,100); |   |
| } |   |
| } |   |
| function drawall() { |   |
| ctx.clearRect(0,0,cwidth,cheight); |   |
| var i; |   |
| for (i=0;i<everything.length;i++) { |   |
| everything[i].draw(); |   |
| } |   |
| } |   |
| function change() { |   |
| var dx = horvelocity; |   |
| verticalvel2 = verticalvel1 + gravity; |   |
| var dy = (verticalvel1 + verticalvel2)*.5; |   |
| verticalvel1 = verticalvel2; |   |
| cball.moveit(dx,dy); |   |
| var bx = cball.sx; |   |
| var by = cball.sy; |   |
| if ((bx>=target.sx+40)&&(bx<=``(target.sx+target.swidth-40))&&``(by>=target.sy+40)&&(by<= | 检查目标内部(40 像素)。 |
| target.img = feathers; | 改变目标img。 |
| } |   |
| if (by>=ground.sy) { |   |
| clearInterval(tid); |   |
| } |   |
| drawall(); |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();"> |   |
| <canvas id="canvas" width="1200" height="600"> |   |
| Your browser doesn't support the HTML5 element canvas. |   |
| </canvas> |   |
| <br/> |   |
| Hold mouse down and drag ball. Releasing``the mouse button will shoot the slingshot.``Slingshot remains at the last position. | 鼠标使用说明。 |
| </body> |   |
| </html> |   |

测试和上传应用程序

这些应用程序可以在没有外部图像文件的情况下创建,但是将图像用于目标和命中目标是很有趣的,所以当您上传项目时,您必须记住包括这些文件。你可以选择自己的目标。也许你对鸡有好感!

您需要测试程序在三种情况下是否正确执行:当球落在目标的左边时,当球击中目标时,以及当球飞过目标时。请注意,我修改了值,以便鸡需要被击中中间,因此球有可能接触到头部或尾部,而不会导致羽毛出现。

你可以改变加农炮及其目标和命中目标的位置,以及弹弓、小鸡和羽毛的位置,通过改变startrockx,等变量,你还可以修改重力变量。如果你把弹弓放在离目标更近的地方,你可以有更多的方式击中小鸡:向左拉更多的距离直接射击,而不是向下拉更多的距离。好好享受!

正如我提到的,您可以在炮弹或弹弓应用程序中为击中的目标使用动画 GIF。这会产生很好的效果。

如果你使用更多和/或更大的图片或其他媒体,那么最好使用一种技术来确保所有的媒体在使用前都从你的网站上下载。我在第六章描述了这样一个技术,当玩家成功完成一个回合时,播放一个视频片段和一个音频片段。

摘要

在本章中,您学习了如何创建两个弹道应用程序。理解它们之间的相同和不同是很重要的。编程技术和 HTML5 特性包括:

  • 程序员定义的对象

  • setInterval为动画设置计时事件,就像对弹跳球所做的那样

  • 使用push方法构建一个数组,并将该数组用作要显示内容的列表

  • 使用splice方法修改数组

  • 使用 trig 函数和变换来旋转加农炮,并解析加农炮和弹弓应用程序中的水平和垂直速度,以便模拟重力

  • 使用form进行玩家输入

  • 处理鼠标事件(mousedownmousemovemouseup),用addEventListener获取玩家输入

  • 在画布上绘制圆弧、矩形、直线和图像

程序员定义的对象和使用对象数组来显示的技术将在后面的章节中再次出现。下一章集中在一个熟悉的游戏,被称为记忆或注意力。它使用了不同的计时事件以及第一章中介绍的Date功能。

五、记忆游戏(又名集中注意力)

在本章中,我们将介绍

  • 绘制多边形

  • 将文本放置在画布上

  • 表示信息的编程技术

  • 编程暂停

  • 计算运行时间

  • 洗牌一组卡对象的一种方法

介绍

这一章演示了两个版本的纸牌游戏,分别被称为记忆或专注。纸牌面朝下出现,玩家一次翻两张(通过点击它们)试图找到匹配的对子。该程序从棋盘上移除匹配的牌,但(实际上)会将不匹配的牌翻回来。当玩家完成所有匹配后,游戏会显示经过的时间。

我描述的游戏的第一个版本使用多边形作为面卡;第二种用全家福。您会注意到其他差异,这些差异是为了说明几个 HTML5 特性,但我也敦促您考虑一下这些版本的共同点。

图 5-1 显示版本一的开启画面。当玩家完成游戏时,记录比赛的表单也会显示经过的时间。

img/214814_2_En_5_Fig1_HTML.jpg

图 5-1

记忆游戏第一版的开场画面

图 5-2 显示玩家点击两张牌(紫色方块)后的结果。描绘的多边形不匹配,所以暂停后,程序用卡片背面的图像替换它们,使卡片看起来翻转了。

img/214814_2_En_5_Fig2_HTML.jpg

图 5-2

两张卡片正面:不匹配

当两张卡匹配时,应用程序移除它们,并在表格中记录匹配情况(参见图 5-3 )。

img/214814_2_En_5_Fig3_HTML.jpg

图 5-3

应用程序已移除两张匹配的卡

如图 5-4 所示,当玩家结束时,游戏显示结果——在本例中,36 秒内有 6 场比赛。

img/214814_2_En_5_Fig4_HTML.jpg

图 5-4

玩家完成游戏后的第一个版本

在游戏的第二个版本中,卡片正面显示的是人物的照片,而不是多边形。请注意,尽管许多记忆游戏认为图像只有在完全相同时才是相同的,但这个游戏类似于一副扑克牌中红心 2 与方块 2 的匹配。为了说明编程要点,我们将把一个匹配定义为同一个人,即使是在不同的图片中。这需要一种对我们用来确定匹配状态的信息进行编码的方法。游戏的第二版还演示了在画布上书写文本,如图 5-5 所示,它描绘了开始的屏幕。

img/214814_2_En_5_Fig5_HTML.jpg

图 5-5

记忆游戏,第二版,开屏

要查看在我们的新游戏中点击两张卡的一种可能结果,请看图 5-6 。

img/214814_2_En_5_Fig6_HTML.jpg

图 5-6

此屏幕显示不匹配的照片

因为结果显示了两个不同的人——在暂停让玩家观看两张照片之后——应用程序将卡片翻转过来,让玩家再试一次。图 5-7 显示了一个成功的选择——同一个人的两个图像(尽管在不同的图片中)。

img/214814_2_En_5_Fig7_HTML.jpg

图 5-7

这张截图显示了一场比赛(不同的场景,但同一个人)

应用程序从板上删除匹配的图像。当所有的牌都被移除时,完成游戏所用的时间会出现,同时会显示如何再次玩游戏的说明,如图 5-8 所示。

img/214814_2_En_5_Fig8_HTML.jpg

图 5-8

游戏最终画面(照片版);所有图像都已匹配,因此不会出现卡片

你可以使用源代码中的照片来玩这个游戏,但是使用你自己的照片会更有趣。你可以从少量照片开始,比如两三对照片,然后逐步增加到整个家庭、班级或俱乐部的照片。对于游戏的第一个版本,你可以用自己的设计替换多边形。

关键要求

游戏的数字版本需要用不同的多边形或照片来表现卡片的背面(都是一样的)和正面。应用程序还必须能够告诉哪些卡匹配,以及卡在棋盘上的什么位置。此外,玩家需要反馈。在现实世界的游戏中,参与者翻转两张卡片并寻找匹配(这需要一些时间)。如果没有,他们就把牌翻过来。

电脑程序必须显示所选牌的正面,并在显示第二张牌后暂停,以便玩家有时间看到两张正面。这种停顿是计算机实现所需要的东西的一个例子,当人们玩游戏时,这种停顿或多或少是自然发生的。该应用程序还应该显示当前找到的配对数量,以及当游戏完成时,参与者找到所有配对所花费的时间。该程序的多边形和照片版本使用不同的方法来完成这些任务。

下面总结一下两个游戏版本必须做的事情:

  • 把牌抽回来。

  • 在玩家做出初始选择之前洗牌,这样就不会每次都出现相同的选择。

  • 检测玩家点击卡片的时间,并区分第一次和第二次点击。

  • 在检测到点击时,在游戏版本 1 的情况下,通过绘制多边形来显示适当的卡面,或者在版本 2 中显示正确的照片。

  • 移除匹配的配对。

  • 即使那些讨厌的玩家做了意想不到的事情,比如点击同一张卡两次,或者点击之前被卡占据的空白区域,也要适当地操作。

HTML5、CSS、JavaScript 特性

让我们回顾一下具体的 HTML5 和 JavaScript 特性,它们提供了我们实现游戏所需的东西。我们将建立在之前的材料之上:HTML 文档的一般结构;如何在一个canvas元素上画矩形、图像、线段组成的路径;程序员定义的和内置的函数;程序员对象;form元素;和数组。

新的 HTML5 和 JavaScript 特性包括超时事件,使用Date对象计算运行时间,在画布上书写和绘制文本,以及一些有用的编程技术,您会发现这些技术在未来的应用程序中很有价值。

和前面几章一样,这一节概括地描述了 HTML5 的特性和编程技术。您可以在“构建应用程序”一节中看到上下文中的所有代码。如果您愿意,您可以跳到该部分来查看代码,然后返回到这里来解释这些特性是如何工作的。

代表卡片

当我们手里拿着一张实体卡时,我们可以看到它是什么。有卡面和背面,背面都一样。我们可以清楚地确定纸牌在游戏棋盘上的位置,以及它们是正面还是背面出现。要实现一个电脑游戏,我们必须表现所有的信息。编码是创建许多计算机应用程序的基本部分,不仅仅是游戏。

在这一章(以及整本书),我描述了一种完成任务的方法。但是请记住,实现应用程序的一个特性很少只有一种方法。也就是说,构建应用程序的不同策略可能会有一些共同的技术。

我们处理卡片的方法将使用程序员定义的对象。在 JavaScript 中创建程序员定义的对象涉及到编写构造函数;在这种情况下,我们称之为Card。使用程序员定义的对象的优点是 JavaScript 提供了访问通用类型对象的信息和代码所需的点符号。我们在第四章中为炮弹和弹弓游戏做了这个。

我们将赋予Card对象属性来保存卡片的位置(sxsy)和尺寸(swidthsheight),一个指向为卡片绘制背面的函数的指针,以及对于每种情况,指定适当正面的信息(info)。

在多边形的情况下,info的值将指示要绘制的边数。(在后面的部分中,我们将讨论绘制它的代码。)对于照片卡的正面,该值将是对我们创建的Image对象的引用img。该对象将保存一个特定的图像文件和一个编号(info),该编号将匹配的图片联系在一起。为了绘制文件的图像,我们将使用内置的drawImage方法。

不用说,卡片并不是以物理实体的形式存在,而是有两面的。应用程序在画布上玩家希望看到的地方绘制卡片的正面或背面。函数flipback绘制卡片背面。为了给出一张被移除的牌的外观,flipback通过绘制一个矩形来有效地擦除一张牌,该矩形是棋盘的颜色。

两个应用程序都使用名为makedeck的函数来准备卡片组,这个过程包括创建Card对象。对于游戏的多边形版本,我们在Card对象中存储边数(从 3 到 8)。但是,应用程序在设置过程中没有绘制多边形。photos 版本设置了一个名为pairs的数组,列出照片的图像文件名。你可以按照这个例子来创建自己的家庭或团体记忆游戏。

小费

如果您使用在线代码玩游戏,如前所述,您可以下载图像文件。要制作你自己的游戏,你需要上传图片,然后修改代码来引用你的文件。代码指出了您需要更改的内容。

makedeck函数创建Image对象,并使用pairs数组将src属性设置为image对象。当代码创建Card对象时,它放入控制pairs数组的索引值,以便匹配的照片具有相同的值。与多边形版本一样,应用程序在创建卡片组的过程中不在画布上绘制图像。在屏幕上,所有的牌看起来都一样;然而,信息是不同的。这些牌在固定的位置——洗牌在后面。

对于CardPolygon,代码对位置信息、sxsy属性的解释有所不同。在第一种情况下,信息指的是左上角。在第二种情况下,该值标识多边形的中心。不过,你可以从另一个中计算出一个。

使用日期计时

我们需要一种方法来确定玩家花了多长时间来完成所有的匹配。JavaScript 提供了一种测量运行时间的方法。您可以在“构建应用程序”一节的上下文中查看代码。在这里,我解释了如何确定一个正在运行的程序中两个不同事件之间的秒数。

Date()的调用生成一个带有日期和时间信息的对象。这两条线

   starttime = new Date();
   starttime = Number(starttime.getTime());

将自 1970 年开始以来的毫秒数(千分之一秒)存储在变量starttime中。(JavaScript 使用 1970 的原因并不重要。)您可以用Date对象做算术,但是我选择了提取毫秒值。

当我们的两个内存程序中的任何一个确定游戏结束时,它再次调用Date(),如下所示:

var now = new Date();
var nt = Number(now.getTime());
var seconds = Math.floor(.5+(nt-starttime)/1000);

这个代码

  1. 创建一个new Date对象并将其存储在变量now中。

  2. 使用getTime提取时间,将其转换为Number,并将其赋给变量nt。这意味着nt保存了从 1970 年开始直到代码调用Date时的毫秒数。然后程序从当前时间nt中减去保存的开始时间starttime

  3. 除以 1000 得到秒。

  4. 添加.5并调用Math.floor将结果向上或向下舍入到整秒。我们希望小数部分等于或大于 0.5 的数字向上取整,小于 0.5 的数字向下取整。

如果您需要比秒提供的精度更高的精度,请省略或修改最后一步。

每当需要计算程序中两个事件之间经过的时间时,都可以使用这个代码。

提供暂停

当我们用真正的卡片玩记忆游戏时,我们不会有意识地在翻开不匹配的卡片之前暂停。但是如前所述,我们的计算机实现必须提供暂停,以便玩家有时间看到两张不同的卡。你可能还记得第三章和第四章中的动画应用——弹跳球、炮弹和弹弓——使用 JavaScript 函数setInterval在固定的时间间隔设置事件。我们可以在记忆游戏中使用一个相关的函数setTimeout。(要查看上下文中的完整代码,请转到“构建应用程序”一节。)让我们看看如何设置事件,以及暂停时间用完时会发生什么。

setTimeout函数设置了一个事件,我们可以用它来强制暂停。当玩家点击画布时调用的choose函数首先检查firstpick变量,以确定这个人是做了第一个还是第二个选择。在这两种情况下,程序都在画布上与卡片背面相同的位置绘制卡片正面。如果点击是第二个选择,并且两张卡片匹配,代码将变量matched设置为truefalse,这取决于卡片是否匹配。如果应用程序确定游戏还没有结束,代码就会调用

        setTimeout(flipback,1000);

这导致在 1000 毫秒(1 秒)内调用flipback函数。然后函数flipback使用matched变量来决定是否重画卡片背面或者通过在适当的卡片位置用桌子背景色画矩形来擦除卡片。

您可以使用setTimeout设置任何单独的定时事件。您需要指定时间间隔和时间间隔到期时要调用的函数。记住时间单位是毫秒。

绘图文本

HTML5 包括一个在画布上放置文本的机制。与以前的版本相比,这提供了一种更动态、更灵活的方式来呈现文本。您可以通过将文本放置与我们已经演示过的矩形、直线、弧线和图像的绘制相结合来创建一些好的效果。在这一节中,我们概述了在 canvas 元素中放置文本的步骤,并提供了一个简短的示例供您尝试。如果你愿意,可以直接跳到“构建应用程序”一节来查看完整的代码描述,这些代码会产生你在图 5-5 到 5-8 中看到的记忆游戏的照片版本。

为了将文本放到画布上,我们编写设置font的代码,然后使用fillText从指定的 x-y 位置开始绘制一串字符。下面的示例使用一组折衷的字体来创建单词(请参见本节后面的注意事项)。

<html>
<head>
    <title>Fonts</title>
<script type="text/javascript">

var ctx;
function init(){
   ctx = document.getElementById('canvas').getContext('2d');
   ctx.font="15px Lucida Handwriting";
   ctx.fillText("this is Lucida Handwriting", 10, 20);
   ctx.font="italic 30px HarlemNights";
   ctx.fillText("italic HarlemNights",40,80);
   ctx.font="bold 40px HarlemNights";
   ctx.fillText("HarlemNights",100,200);
   ctx.font="30px Accent";
   ctx.fillText("Accent", 200,300);
}
</script>
</head>
<body onLoad="init();">
<canvas id="canvas" width="900" height="400">
Your browser doesn't support the HTML5 element canvas.
</canvas>
</body>

</html>

这个 HTML 文档生成了如图 5-9 所示的屏幕截图。

img/214814_2_En_5_Fig9_HTML.jpg

图 5-9

使用 font 和 fillText 函数在画布上绘制的不同字体的文本

警告

确保你选择的字体将出现在你所有玩家的电脑上。在第十章中,你将学习如何使用一个叫做font-family的 CSS 特性,它提供了一个系统的方法来指定主字体和备份。

请注意,虽然您看到的看起来是文本,但实际上您看到的是画布上的墨迹,即文本的位图图像,而不是可以就地修改的文本字段。这意味着要改变文本,我们需要编写代码来完全删除当前图像。我们通过将fillStyle设置为之前放入变量tablecolor中的值,并在适当的位置和必要的维度使用fillRect来实现。

创建文本图像后,下一步是将fillStyle设置为不同于tablecolor的颜色。我们将使用我们为卡片背面选择的颜色。对于照片记忆游戏的开始屏幕显示,下面是设置用于所有文本的字体的代码:

ctx.font="bold 20pt sans-serif";

使用sans-serif字体是有意义的,因为它是任何计算机上都有的标准字体。

综合我们到目前为止所做的工作,下面是显示游戏中特定点的匹配数的代码:

ctx.fillStyle= tablecolor;
ctx.fillRect(10,340,900,100);
ctx.fillStyle=backcolor;
ctx.fillText
     ("Number of matches so far: "+String(count),10,360);

前两条语句清除当前的计数,后两条语句放入更新的结果。表达式"Number of matches so far: "+String(count)值得更多的解释。它完成两项任务:

  • 它采用变量count,这是一个数字,并把它变成一个字符串。

  • 它将常量字符串"Number of matches so far: "String(count)的结果连接起来。

串联证明了加号在 JavaScript 中有两种含义:如果操作数是数字,则符号表示加法。如果操作数是字符串,则表明这两个字符串应该连接在一起。一个符号有几个意思,一个有趣的说法是操作符重载

如果一个操作数是字符串,另一个是数字,JavaScript 会做什么?答案取决于两个操作数中的哪一个是什么数据类型。您将看到一些代码示例,在这些代码中,程序员没有输入将文本转换为数字的命令,反之亦然,但是由于特定的操作顺序,该语句仍然有效。

不过,我建议不要冒险。相反,试着记住解释加号的规则。如果您注意到您的程序增加了一个数字,比如说从 1 到 11 到 111,而您期望的是 1、2、3,那么您的代码是连接字符串而不是增加数字,您需要将字符串转换为数字。

绘制多边形

创建多边形很好地展示了 HTML5 的绘图功能。为了理解这里用于绘制多边形的代码开发过程,可以将几何图形想象成一个类似轮子的形状,辐条从其中心向每个顶点发散。辐条不会出现在图中,但会帮助你,就像他们帮助我一样,弄清楚如何画一个多边形。图 5-10 用一个三角形说明了这一点。

img/214814_2_En_5_Fig10_HTML.jpg

图 5-10

将三角形表示为辐条几何形状有助于阐明绘制多边形的代码开发;箭头指示绘图路径中的第一点

为了确定辐条之间的角度,我们将数量2*Math.PI (representing a complete circle)除以多边形的边数。我们使用角度值和moveTo方法来绘制路径的点。源代码有一个简单的 HTML 程序画一个三角形,也就是设置一个变量n3。你可以通过改变声明和初始化n的语句来修改它以绘制其他正多边形。

程序将多边形绘制成一条填充路径,该路径始于由angle值的一半指定的点(如图 5-10 中的箭头所示)。为了说明问题,我们使用了moveTo方法以及半径、Math.sinMath.cos。然后,我们使用lineTo的方法,以顺时针方向进行 n-1 个点。对于三角形来说,n-1 就是多了两个点。对于八角形,它将是七个以上。在用lineTo点运行完一个for循环后,我们调用fill方法来产生一个填充的形状。要查看完整的带注释的代码,请转到“构建应用程序”一节。

注意

绘制和重绘多边形需要时间,但这不会给这个应用程序带来问题。如果一个程序有大量复杂的设计,提前准备好图片可能是有意义的。然而,这种方法要求用户下载文件,这可能需要相当长的时间。您需要进行试验,看看哪种方法总体上更好。

洗牌

如前所述,记忆游戏要求程序在每一轮之前洗牌,因为我们不希望牌一次又一次地出现在同一个位置。改变价值观的最佳方式是广泛研究的主题。在第十章中,描述了一种叫做 21 点的纸牌游戏,你会找到一篇文章的参考资料,该文章描述了一种据称是产生洗牌最有效方法的技术。

对于记忆力/专注力,还是按照我小时候玩游戏的方式来实现吧。我和其他人会摊开所有的卡片,然后拿起并交换配对。当我们认为我们已经做了足够多的次数,我们就会开始玩。在本节中,我们将探索这种方法背后的一些概念。(要检查shuffle函数,您可以跳到“构建应用程序”一节。)

要为洗牌的交换方法编写 JavaScript,我们首先需要定义“足够的次数”让我们把这一副牌中的牌数增加三倍,我们已经在数组变量deck中表示过了。但是既然没有卡片,只有代表卡片的数据,我们交换什么呢?答案是唯一定义每张卡的信息。对于多边形记忆游戏,这是属性info。对于图片游戏,是infoimg

为了得到一张随机的牌,我们使用表达式Math.floor(Math.random()*dl),其中dl代表牌组长度,代表该副牌中牌的数量。我们这样做两次,以获得要(虚拟地)交换的卡对。这可能会产生相同的数字,这意味着一张卡与自己交换,但这不是真正的问题。如果发生了,这个过程中的这一步就没有作用了。代码要求进行大量的交换,因此一次交换不做任何事情是可以的。

实现交换是下一个挑战,它需要一些临时存储。我们将使用一个变量holder作为游戏的多边形版本,两个变量holderimgholderinfo作为图片版本。

实现在卡片上点击

下一步是解释我们如何实现玩家的移动,也就是玩家点击一张卡片。在 HTML5 中,我们可以使用与处理mousedown事件相同的方法来处理click事件(在第四章中描述)。我们将使用addEventListener方法:

canvas1 = document.getElementById('canvas');
canvas1.addEventListener('click',choose,false);

这出现在init功能中。choose函数必须包含代码来决定我们选择洗哪张牌。当玩家点击画布时,程序还必须返回鼠标的坐标。获取鼠标坐标的方法与第四章中的方法相同。

不幸的是,不同的浏览器以不同的方式实现对鼠标事件的处理。这个我在第四章讨论过,我在这里重复解释。以下内容适用于 Chrome、Firefox 和 Safari。

if ( ev.layerX || ev.layerX==0) {
   mx= ev.layerX;
   my = ev.layerY;
}
else if (ev.offsetX || ev.offsetX==0 ) {
   mx = ev.offsetX;
   my = ev.offsetY;
}

这是因为如果ev.layerX不存在,它将被赋予一个值false。如果it确实存在但有值0,该值也会被解释为 false,但ev.layerX==0会是true。所以如果有一个好的ev.layerX值,程序就会使用它。否则代码看ev.offsetX。如果两者都不起作用,mxmy就不会得到设置。

因为卡片是矩形的,所以使用鼠标光标坐标(mxmy)、左上角的位置以及每张卡片的宽度和高度,浏览卡片组并进行比较操作相对容易。我们是这样构造if条件的:

if ((mx>card.sx)&&(mx<card.sx+card.swidth)&&(my>card.sy)&&(my<card.sy+card.sheight))

注意

下一章描述了在运行时创建 HTML 标记的方式,展示了如何为位于屏幕上的特定元素设置事件处理,而不是使用整个canvas元素。

我们清除变量firstpick并将其初始化为true,这表明这是玩家两次选择中的第一次。第一次拾取后,程序将数值改为false,第二次拾取后,数值又回到true。像这样在两个值之间来回翻转的变量被称为标志切换

防止某些类型的欺骗

请注意,本节的细节仅适用于这些记忆游戏,但一般经验适用于构建任何交互式应用程序。玩家至少有两种方法可以阻挠游戏。在同一张卡上点击两次为一次;另一种方法是点击一张卡片被移除的区域(也就是说,棋盘被涂掉了)。

为了处理第一种情况,在决定鼠标是否在某张卡片上的if-true子句之后,插入if语句

if ((firstpick) || (i!=firstcard)) break;

如果索引值(i)是好的,这一行代码触发从for语句的退出,这发生在以下任一情况:1)这是第一次选择,或者 2)这不是第一次选择,并且i不对应于选择的第一张牌。

防止第二个问题——点击“幽灵”卡——需要更多的工作。当应用程序从板上移除卡片时,除了在画布的该区域上绘画之外,我们还可以为sx属性赋值(-1)。这将把卡标记为已被移除。这是flipback功能的一部分。choose函数包含检查sx属性并进行检查的代码(仅当sx为> = 0 时)。该功能在下面的for循环中结合了两种作弊测试:

for (i=0;i<deck.length;i++){
   var card = deck[i];
   if (card.sx >=0)
if ((mx>card.sx)&&(mx<card.sx+card.swidth)&&(my>card.sy)&&(my<card.sy+card.sheight)) {
        if ((firstpick)|| (i!=firstcard)) break;
         }
}

在三个if语句中,第二个是第一个的整个子句。第三个是单语句break,它导致控制离开for循环。一般来说,我建议使用括号(例如:{ and })表示if trueelse子句,但是这里我使用了简化的格式来表示单个语句,这种格式也是因为它看起来足够清晰。

现在让我们继续构建我们的两个记忆游戏。

构建应用程序并使之成为您自己的应用程序

本节介绍了游戏两个版本的完整代码。因为应用程序包含多个函数,所以该部分为每个游戏提供了一个表,说明每个函数调用的内容和被调用的方式。

表 5-1 是记忆游戏多边形版本的函数列表。注意,一些函数的调用是基于事件完成的。

表 5-1

记忆游戏的多边形版本中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 响应于body标签中的onLoad而被调用, | makedeck``shuffle |
| choose | 响应init中的addEventListener时调用。 | Polycard``drawpoly(作为多边形的draw方法调用)。 |
| flipback | 响应choose中的setTimeout调用而调用。 |   |
| drawback | 在makedeckflipback中作为抽牌方法调用。 |   |
| Polycard | 在choose中调用。 |   |
| shuffle | 在init中调用。 |   |
| makedeck | 在init中调用。 |   |
| Card | 由makedeck调用。 |   |
| drawpoly | 在choose中称为Polygondraw方法。 |   |

表 5-2 显示了应用程序完整多边形版本的注释代码。在回顾它的时候,想想它与其他章节中描述的应用程序的相似之处。请记住,这只是说明了命名应用程序组件和编程的一种方式。其他方法可能同样有效。

表 5-2

记忆游戏多边形版本的完整代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Memory game using polygons</title> | 完成title元素。 |
| <style> | 开始style标签。 |
| form { | 指定表单的样式。 |
| width:330px; | 设置width。 |
| margin:20px; | 设置外部margin。 |
| background-color:pink; | 设置color。 |
| Padding:20px; | 设置内部padding。 |
| } | 关闭样式。 |
| input { | 设置输入字段的样式。 |
| text-align:right; | 设置右对齐—适用于数字。 |
| } | 关闭样式。 |
| </style> | 关闭style元件。 |
| <script type="text/javascript"> | 启动script元素。type规范不是必需的,但是在这里包含了它,因为您将会看到它。 |
| var ctx; | 保存画布上下文的变量。 |
| var firstpick = true; | 声明并初始化firstpick。 |
| var firstcard; | 声明一个变量来保存定义第一次选择的信息。 |
| var secondcard; | 声明一个变量来保存定义第二次选择的信息。 |
| var frontbgcolor = "rgb(251,215,73)"; | 设置卡片正面的背景颜色值。 |
| var polycolor = "rgb(254,11,0)"; | 设置多边形的颜色值。 |
| var backcolor = "rgb(128,0,128)"; | 设置卡片背面的颜色值。 |
| var tablecolor = "rgb(255,255,255)"; | 设置纸板(表格)的颜色值。 |
| var cardrad = 30; | 设置多边形的半径。 |
| var deck = []; | 声明卡片组,最初是一个空数组。 |
| var firstsx = 30; | 设置第一张卡在 x 轴上的位置。 |
| var firstsy = 50; | 设置第一张卡在 y 轴上的位置。 |
| var margin = 30; | 设置卡片之间的间距。 |
| var cardwidth = 4*cardrad; | 将卡片宽度设置为多边形半径的四倍。 |
| var cardheight = 4*cardrad; | 将卡片高度设置为多边形半径的四倍。 |
| var matched; | 该变量在choose中设置,并在flipback中使用。 |
| var starttime; | 该变量设置在init中,用于计算经过的时间。 |
| function Card(sx,sy,swidth,sheight,info) { | Card功能的标题,设置卡片对象。 |
| this.sx = sx; | 设置水平坐标。 |
| this.sy = sy; | 设置垂直坐标。 |
| this.swidth = swidth; | 设置宽度。 |
| this.sheight = sheight; | 设置高度。 |
| this.info = info; | 设置info(边数)。 |
| this.draw = drawback; | 指定如何绘制。 |
| } | 关闭该功能。 |
| function makedeck() { | 用于设置台面的功能头。 |
| var i; | 用于for循环。 |
| var acard; | 变量来保存一对牌中的第一张。 |
| var bcard; | 变量来保存一对牌中的第二张。 |
| var cx = firstsx; | 变量来保存 x 坐标。从第一个 x 位置开始。 |
| var cy = firstsy; | 将保持 y 坐标。从第一个 y 位置开始。 |
| for(i=3;i<9;i++) { | 循环生成三角形到八边形的卡片。 |
| acard = new Card(cx,cy,cardwidth,cardheight,i); | 创建卡片和位置。 |
| deck.push(acard); | 添加到甲板上。 |
| bcard = new Card(cx,cy+cardheight+margin,cardwidth,cardheight,i); | 创建一张具有相同信息的卡片,但放在屏幕上上一张卡片的下方。 |
| deck.push(bcard); | 添加到甲板上。 |
| cx = cx+cardwidth+ margin; | 考虑到卡片宽度和边距的增量。 |
| acard.draw(); | 在画布上画第一张牌。 |
| bcard.draw(); | 在画布上画第二张卡片。 |
| } | 关闭for回路。 |
| } | 关闭该功能。 |
| function shuffle() { | shuffle功能的标题。 |
| var i; | 变量来保存对卡的引用。 |
| var k; | 变量来保存对卡的引用。 |
| var holder; | 进行交换所需的变量。 |
| var dl = deck.length; | 变量来保存一副牌中的牌数。 |
| var nt; | 互换数量指数。 |
| for (nt=0;nt<3*dl;nt++) { | for循环。 |
| i = Math.floor(Math.random()*dl); | 随机拿一张牌。 |
| k = Math.floor(Math.random()*dl); | 随机拿一张牌。 |
| holder = deck[i].info; | 存储i的信息。 |
| deck[i].info = deck[k].info; | 为k输入i info。 |
| deck[k].info = holder; | 将k中的内容放入k。 |
| } | 关闭for回路。 |
| } | 关闭功能。 |
| function Polycard(sx,sy,rad,n) { | Polycard的功能头。 |
| this.sx = sx; | 设置 x 坐标。 |
| this.sy = sy; | 设置 y 坐标。 |
| this.rad = rad; | 设置多边形半径。 |
| this.draw = drawpoly; | 设置如何绘制。 |
| this.n = n; | 设置边数。 |
| this.angle = (2*Math.PI)/n | 计算并存储角度。 |
| } | 关闭该功能。 |
| function drawpoly() { | 函数头。 |
| ctx.fillStyle= frontbgcolor; | 设置正面背景。 |
| ctx.fillRect(this.sx-2*this.rad,this.sy-2*this.rad,4*this.rad,4*this.rad); | 矩形的角向上,位于多边形中心的左侧。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.fillStyle=polycolor; | 改变多边形的颜色。 |
| var i; | 索引变量。 |
| var rad = this.rad; | 提取半径。 |
| ctx.moveTo(this.sx+rad*Math.cos(-.5*this.angle),this.sy+rad*Math.sin(-.5*this.angle)); | 移到第一点。 |
| for (i=1;i<this.n;i++) { | 连续点的for循环。 |
| ctx.lineTo(this.sx+rad*Math.cos((i-.5)*this.angle),this.sy+rad*Math.sin((i-.5)*this.angle)); | 设置线段的绘制。 |
| } | 关闭for回路。 |
| ctx.fill(); | 填写路径。 |
| } | 关闭功能。 |
| function drawback() { | 函数头。 |
| ctx.fillStyle = backcolor; | 设置卡片背景颜色。 |
| ctx.fillRect(this.sx,this.sy,this.swidth,this.sheight); | 画矩形。 |
| } | 关闭功能。 |
| function choose(ev) { | 功能头为choose(点击一张卡)。 |
| var mx; | 保持鼠标x的变量。 |
| var my; | 保持鼠标y的变量。 |
| var pick1; | 保存对创建的Polygon对象的引用的变量。 |
| var pick2; | 保存对创建的Polygon对象的引用的变量。 |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | 可以用layerXlayerY吗? |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } | 如果为真,则关闭。 |
| else if (ev.offsetX &#124;&#124; ev.offsetX == 0) { | 可以用offsetXoffset吗? |
| mx = ev.offsetX; | 设置mx。 |
| my = ev.offsetY; | 设置my。 |
| } | 关闭else。 |
| var i; | 在for循环中声明索引变量。 |
| for (i=0;i<deck.length;i++){ | 循环通过整个甲板。 |
| var card = deck[i]; | 提取一个卡引用来简化代码。 |
| if (card.sx >=0) | 检查卡是否被标记为已被移除。 |
| if ((mx>card.sx)&&(mx<card.sx+card.swidth)&&(my>card.sy)&&(my<card.sy+card.sheight)) { | 然后检查鼠标是否在这张卡上。 |
| if ((firstpick)&#124;&#124; (i!=firstcard)) break; | 如果是这样,检查玩家没有再次点击第一张牌,如果是这样,离开for循环。 |
| } | 关闭if true子句。 |
| } | 关闭for回路。 |
| if (i<deck.length) { | for循环提前退出了吗? |
| if (firstpick) { | 如果这是第一次选择… |
| firstcard = i; | …设置firstcard以引用卡片组中的卡片 |
| firstpick = false; | 将firstpick设置为false。 |
| pick1 = new Polycard(card.sx+cardwidth*.5,card.sy+cardheight*.5,cardrad,``card.info | 创建以坐标为中心的多边形。 |
| pick1.draw(); | 画多边形。 |
| } | 如果第一次选择,则关闭。 |
| else { | 否则… |
| secondcard = i; | …设置secondcard以引用卡片组中的卡片。 |
| pick2 = new Polycard(card.sx+cardwidth*.5,card.sy+cardheight*.5,cardrad,``card.info | 创建以坐标为中心的多边形。 |
| pick2.draw(); | 画多边形。 |
| if (deck[i].info==deck[firstcard].info) { | 检查是否匹配。 |
| matched = true; | 将matched设置为true。 |
| var nm = 1+Number(document.f.count.value); | 增加匹配的数量。 |
| document.f.count.value = String(nm); | 显示新的计数。 |
| if (nm>= .5*deck.length) { | 检查游戏是否结束。 |
| var now = new Date(); | 获取新的Date信息。 |
| var nt = Number(now.getTime()); | 提取并转换时间。 |
| var seconds = Math.floor(.5+(nt-starttime)/1000); | 计算经过的秒数。 |
| document.f.elapsed.value = String(seconds); | 输出时间。 |
| } | 如果这是游戏的结尾,请关闭。 |
| } | 如果有匹配就关闭。 |
| else { | 否则… |
| matched = false; | 将matched设置为false。 |
| } | 关闭else子句。 |
| firstpick = true; | 重置〔??〕。 |
| setTimeout(flipback,1000); | 设置暂停。 |
| } | 关闭不是第一次选择。 |
| } | 关闭好的选择(点击卡片— for循环提前退出)。 |
| } | 关闭该功能。 |
| function flipback() { | 功能头— flipback暂停后的处理。 |
| if (!matched) { | 如果不匹配… |
| deck[firstcard].draw(); | …把牌抽回来。 |
| deck[secondcard].draw(); | …把牌抽回来。 |
| } | …关闭该条款。 |
| else { | 否则需要撤牌。 |
| ctx.fillStyle = tablecolor; | 设置桌子/纸板的颜色。 |
| ctx.fillRect(deck[secondcard].sx,deck[secondcard].sy,deck[secondcard].swidth,deck[secondcard].sheight); | 抽出卡片。 |
| ctx.fillRect(deck[firstcard].sx,deck[firstcard].sy,deck[firstcard].swidth,deck[firstcard].sheight); | 抽出卡片。 |
| deck[secondcard].sx = -1; | 设定这个,这样卡就不会被检查。 |
| deck[firstcard].sx = -1; | 设定这个,这样卡就不会被检查。 |
| } | 如果没有匹配就关闭。 |
| } | 关闭该功能。 |
| function init(){ | 函数头初始化。 |
| ctx = document.getElementById('canvas').getContext('2d'); | 设置ctx进行所有绘图。 |
| canvas1 = document.getElementById('canvas'); | 设置canvas1进行事件处理。 |
| canvas1.addEventListener('click',choose,false); | 设置事件处理。 |
| makedeck(); | 创建甲板。 |
| document.f.count.value = "0"; | 初始化可见计数。 |
| document.f.elapsed.value = ""; | 清除所有旧值。 |
| starttime = new Date(); | 设置开始时间的第一步。 |
| starttime = Number(starttime.getTime()); | 重用该变量来设置基准的毫秒数。 |
| shuffle(); | 打乱卡片信息值。 |
| } | 关闭该功能。 |
| </script> | 关闭script元件。 |
| </head> | 关闭head元素。 |
| <body onLoad="init();"> | Body标记,设置init。 |
| <canvas id="canvas" width="900" height="400"> | Canvas开始标记。 |
| Your browser doesn't support the HTML5 element canvas. | 警告消息。 |
| </canvas> | 关闭canvas元素。 |
| <br/> | 指令前换行。 |
| Click on two cards to see if you have a match. | 说明。 |
| <form name="f"> | Form开始标记。 |
| Number of matches:  <input type="text" name="count" value="0" size="1"/> | 用于输出的标签和输入元素。 |
| <p> | 分段符。 |
| Time taken to complete puzzle: <input type="text" name="elapsed" value="  " size="4"/> seconds. | 用于输出的标签和输入元素。 |
| </form> | 关闭form。 |
| </body> | 关闭body。 |
| </html> | 关闭html。 |

无论您做出什么样的编程选择,都要在代码中添加注释(每行使用两个斜杠://)并包含空行。您不需要注释每一行,但是当您必须返回代码进行改进时,做好注释工作会对您有好处。

您可以通过更改表单的字体、字体大小、颜色和背景色来更改此游戏。在这一节的后面,我们会建议更多的方法来使应用程序成为你自己的应用程序。

使用图片的记忆游戏版本与多边形版本的结构非常相似。它不需要一个单独的函数来画图。表 5-3 是这个版本游戏的功能列表。

表 5-3

记忆游戏照片版中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 为响应 body 标签中的onLoad而调用 | makedeck``shuffle |
| choose | 响应init中的addEventListener而调用 |   |
| flipback | 响应choose中的setTimeout调用而调用 | draw方法用于Card对象 |
| drawback | 在makedeckflipback中作为卡片的draw方法调用 |   |
| shuffle | 被叫进来init |   |
| makedeck | 被叫进来init |   |
| Card | 由makedeck调用 |   |

记忆游戏的照片版本的代码类似于多边形版本的代码。大部分逻辑都是一样的。但是因为这个例子演示了在画布上书写文本,所以 HTML 文档没有form元素。代码如下表 5-4 所示,不同行上有注释。我也指出你应该在哪里为你的照片放入图像文件的名字。在看这个记忆游戏的第二个版本之前,想想哪些部分可能是相同的,哪些部分可能是不同的。

表 5-4

记忆游戏照片版的完整代码

|

密码

|

说明

|
| --- | --- |
| <html> |   |
| <head> |   |
| <title>Memory game using pictures</title> | 完整的标题元素。 |
| <script type="text/javascript"> |   |
| var ctx; |   |
| var firstpick = true; |   |
| var firstcard = -1; |   |
| var secondcard; |   |
| var backcolor = "rgb(128,0,128)"; |   |
| var tablecolor = "rgb(255,255,255)"; |   |
| var deck = []; |   |
| var firstsx = 30; |   |
| var firstsy = 50; |   |
| var margin = 30; |   |
| var cardwidth = 100; | 如果您希望图片具有不同的宽度,您可能需要对此进行更改... |
| var cardheight = 100; | ...和/或高度。 |
| var matched; |   |
| var starttime; |   |
| var count = 0; | 需要保持内部计数。 |
| var pairs = [ | 这五个人的成对图像文件的数组。 |
| [``"anneGorge.jpg"``,``"anneNow.jpg"``],[``"esther.jpg"``,``"pigtailEsther.jpg"``],[``"pigtailJeanine.jpg"``,``"jeanineGorge.jpg"``],[``"pigtailAviva.jpg"``,``"avivacuba.jpg"``],[``"pigtailAnnika.jpg"``,``"annikaTooth.jpg"``] | 您可以在这里输入图片文件的名称。 |
|   | 您可以使用任意数量的成对图片,但是请注意保存最后一对图片的数组在括号后没有逗号。 |
| ]; |   |
| function Card(sx,sy,swidth,sheight, img, info) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.swidth = swidth; |   |
| this.sheight = sheight; |   |
| this.info = info; | 表示匹配。 |
| this.img = img; | Img参考。 |
| this.draw = drawback; |   |
| } |   |
| function makedeck() { |   |
| var i; |   |
| var acard; |   |
| var bcard; |   |
| var pica; |   |
| var picb; |   |
| var cx = firstsx; |   |
| var cy = firstsy; |   |
| for(i=0;i<pairs.length;i++) { |   |
| pica = new Image(); | 创建Image对象。 |
| pica.src = pairs[i][0]; | 设置为第一个文件。 |
| acard = new Card(cx,cy,cardwidth,cardheight,pica,i); | 创建Card。 |
| deck.push(acard); |   |
| picb = new Image(); | 创建Image对象。 |
| picb.src = pairs[i][1]; | 设置为第二档。 |
| bcard = new Card(cx,cy+cardheight+margin,cardwidth,cardheight,picb,i); | 创建Card。 |
| deck.push(bcard); |   |
| cx = cx+cardwidth+ margin; |   |
| acard.draw(); |   |
| bcard.draw(); |   |
| } |   |
| } |   |
| function shuffle() { |   |
| var i; |   |
| var k; |   |
| var holderinfo; | 交换的临时地点。 |
| var holderimg; | 交换的临时地点。 |
| var dl = deck.length |   |
| var nt; |   |
| for (nt=0;nt<3*dl;nt++) {  //do the swap 3 times deck.length times |   |
| i = Math.floor(Math.random()*dl); |   |
| k = Math.floor(Math.random()*dl); |   |
| holderinfo = deck[i].info; | 保存info。 |
| holderimg = deck[i].img; | 保存img。 |
| deck[i].info = deck[k].info; | 把kinfo放到i里。 |
| deck[i].img = deck[k].img; | 把kimg放到i里。 |
| deck[k].info = holderinfo; | 设置为原来的info。 |
| deck[k].img = holderimg; | 设置为原来的img。 |
| } |   |
| } |   |
| function drawback() { |   |
| ctx.fillStyle = backcolor; |   |
| ctx.fillRect(this.sx,this.sy,this.swidth,this.sheight); |   |
| } |   |
| function choose(ev) { |   |
| var out; |   |
| var mx; |   |
| var my; |   |
| var pick1; |   |
| var pick2; |   |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | 提醒:这是处理三种浏览器之间差异的代码。 |
| mx= ev.layerX; |   |
| my = ev.layerY; |   |
| } else if (ev.offsetX &#124;&#124; ev.offsetX == 0) { |   |
| mx = ev.offsetX; |   |
| my = ev.offsetY; |   |
| } |   |
| var i; |   |
| for (i=0;i<deck.length;i++){ |   |
| var card = deck[i]; |   |
| if (card.sx >=0)  //this is the way to avoid checking for clicking on this space |   |
| if ((mx>card.sx)&&(mx<card.sx+card.swidth)&&(my>card.sy)&&(my<card.sy+card.sheight)) { |   |
| if ((firstpick)&#124;&#124; (i!=firstcard)) { |   |
| break;} |   |
| } |   |
| } |   |
| if (i<deck.length) { |   |
| if (firstpick) { |   |
| firstcard = i; |   |
| firstpick = false; |   |
| ctx.drawImage(card.img,card.sx,card.sy,card.swidth,card.sheight); | 画照片。 |
| } |   |
| else { |   |
| secondcard = i; |   |
| ctx.drawImage(card.img,card.sx,card.sy,card.swidth,card.sheight); | 画照片。 |
| if ( card.info ==deck[firstcard].info) { | 看看有没有匹配的。 |
| matched = true; |   |
| count++; | 增量count。 |
| ctx.fillStyle= tablecolor; |   |
| ctx.fillRect(10,340,900,100); | 擦除文本所在的区域。 |
| ctx.fillStyle=backcolor; | 重置为文本颜色。 |
| ctx.fillText("Number of matches so far: "+String(count),10,360); | 写出count。 |
| if (count>= .5*deck.length) { |   |
| var now = new Date(); |   |
| var nt = Number(now.getTime()); |   |
| var seconds = Math.floor(.5+(nt-starttime)/1000); |   |
| ctx.fillStyle= tablecolor; |   |
| ctx.fillRect(0,0,900,400); | 擦除整个画布。 |
| ctx.fillStyle=backcolor; | 为绘图设置。 |
| out="You finished in "+String(seconds)+" secs."; | 准备课文。 |
| ctx.fillText(out,10,100); | 写正文。 |
| ctx.fillText("Reload the page to try again.",10,300); | 写正文。 |
| } |   |
| } |   |
| else { |   |
| matched = false; |   |
| } |   |
| firstpick = true; |   |
| setTimeout(flipback,1000); |   |
| } |   |
| } |   |
| } |   |
| function flipback() { |   |
| var card; |   |
| if (!matched) { |   |
| deck[firstcard].draw(); |   |
| deck[secondcard].draw(); |   |
| } |   |
| else { |   |
| ctx.fillStyle = tablecolor; |   |
| ctx.fillRect(deck[secondcard].sx,deck[secondcard].sy,deck[secondcard].swidth,deck[secondcard].sheight); |   |
| ctx.fillRect(deck[firstcard].sx,deck[firstcard].sy,deck[firstcard].swidth,deck[firstcard].sheight); |   |
| deck[secondcard].sx = -1; |   |
| deck[firstcard].sx = -1; |   |
| } |   |
| } |   |
| function init(){ |   |
| ctx = document.getElementById('canvas').getContext('2d'); |   |
| canvas1 = document.getElementById('canvas'); |   |
| canvas1.addEventListener('click',choose,false); |   |
| makedeck(); |   |
| shuffle(); |   |
| ctx.font="bold 20pt sans-serif"; | 设置font。 |
| ctx.fillText("Click on two cards to make a match.",10,20); | 将说明显示为画布上的文本。 |
| ctx.fillText("Number of matches so far: 0",10,360); | 显示计数。 |
| starttime = new Date(); |   |
| starttime = Number(starttime.getTime()); |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();"> |   |
| <canvas id="canvas" width="900" height="400"> |   |
| Your browser doesn't support the HTML5 element canvas. |   |
| </canvas> |   |
| </body> |   |
| </html> |   |

虽然这两个程序是真正的游戏,但它们还可以改进。比如玩家不能输。看完这些材料后,试着想出一个方法来强制亏损,也许是通过限制移动的次数或设置时间限制。

这些应用程序在加载时开始计时。有些游戏等到玩家完成第一个动作后才开始计时。如果您想采用这种更友好的方法,您需要设置一个初始化为false的逻辑变量,并在choose函数中创建一个机制来检查这个变量是否被设置为true。因为可能没有,所以您必须包含设置starttime变量的代码。

这是一个单人游戏。你可以想出一个办法让它成为两个人的游戏。你可能需要假设这些人正在适当地轮流,但是程序可以为每个参与者保留单独的分数。

有些人喜欢设置不同难度的游戏。为此,您可以增加卡片的数量、减少暂停时间和/或采取其他措施。

您可以使用自己的图片将此应用程序变成您的。当然,您可以使用朋友和家庭成员的图像,但您也可以创建一个教育游戏,用图片来表示项目或概念,如音符名称和符号、国家和首都、县和名称的地图等。你也可以改变线对的数量。代码指的是各种数组的length,所以你不需要通过代码来改变一副牌中的牌数。不过,您可能需要调整cardwidthcardheight变量的值,以便在屏幕上排列卡片。

当然,另一种可能性是使用一副标准的 52 张牌(或 54 张带玩笑的牌)。关于使用扑克牌的例子,请跳到第十章,它将带你创建一个 21 点游戏。对于任何匹配游戏,您都需要开发一种方法来表示定义哪些卡片匹配的信息。

测试和上传应用程序

当我们,开发人员,检查我们的程序时,我们倾向于每次都做同样的事情。然而,用户、玩家和顾客经常做奇怪的事情。这就是为什么让别人来测试我们的应用程序是一个好主意。所以请朋友来测试你的游戏。您应该总是让没有参与构建应用程序的人来测试它。你可能会发现你没有发现的问题。

记忆游戏多边形版本的 HTML 文档包含了完整的游戏,因为程序可以动态地绘制和重绘多边形。游戏的照片版需要你上传所有的图片。您可以通过使用网页上的图像文件(在您自己的网页之外)来改变这个游戏。注意,pairs数组需要有完整的地址。

摘要

在本例中,您学习了如何使用编程技术和 HTML5 特性实现游戏的两个版本,即 memory 或 concentration。其中包括:

  • 程序员定义的函数和对象的例子

  • 如何使用moveTolineTo以及Math触发方法在画布上绘制多边形

  • 关于如何使用表单向玩家显示信息的指导

  • 在画布上用指定字体绘制文本的方法

  • 关于如何在画布上绘制图像的说明

  • 使用setTimeout强制暂停

  • 使用Date对象计算运行时间

这些应用程序演示了表示信息的方法,以实现一个熟悉游戏的两个版本。下一章将暂时不使用 canvas 来演示 HTML 元素的动态创建和定位。它还将使用 HTML5 的video元素。

六、测验

在本章中,我们将介绍

  • 通过代码创建 HTML 元素

  • 响应鼠标在特定元素上的点击并停止响应鼠标在特定元素上的点击

  • 创建和访问数组

  • 播放音频剪辑和视频剪辑

  • 检查玩家反应并防止不良行为

介绍

这一章演示了如何动态创建 HTML 元素,然后在屏幕上定位。这不仅不同于在canvas元素上绘图,也不同于使用 HTML 标记创建或多或少静态网页的老方法。我们的目标是制作一个小测验,让玩家按时间顺序排列一组美国总统。总统组是从完整的总统列表中随机选择的。正确排序有奖励:播放一个视频片段和一个音频片段。使用 HTML5 直接显示视频和音频(也称为本地)的能力是对旧系统的一大改进,旧系统需要在玩家的计算机上使用<object>元素和第三方插件。在我们的游戏中,视频和音频只是一个次要的角色,但是开发人员和设计人员可以使用 HTML5 和 JavaScript 在应用程序运行的特定时间点制作特定的视频,这一点非常重要。

自动播放是指在没有用户操作的情况下播放视频剪辑。截至 2018 年 4 月,Chrome 浏览器采用了视频自动播放的政策(详见 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes )。该策略旨在防止在许多情况下自动播放。理由是自动播放视频可能会让用户支付数据费用,并可能使网络过载。视频广告可能很烦人。我接受这个推理;然而,我希望奖励在玩家成功完成游戏后立即生效。Chrome 浏览器有一种方法来确定他们所说的用户参与度。我为玩家成功设定的奖励包括一段静音的视频和一段音频。这似乎通过了 Chrome 的用户参与度测试,媒体也确实得到了播放。不过,自动播放政策是你需要知道的,并在未来进行调查。

测验的基本信息由一个数组组成,内部数组保存总统的姓名,第二项用于确保随机过程不会选择两个同名的实例。该程序选择四位总统的名字,并为包含名字和数字的框创建 HTML 标记。程序在窗口中定位盒子。图 6-1 为开启画面。

img/214814_2_En_6_Fig1_HTML.jpg

图 6-1

测验的开始画面

这给了我一个评论这个游戏的机会。我能按顺序背诵总统,所以能很好地玩这个游戏。这种情况有问题,因为我需要确保当玩家给出错误的答案或在其他方面表现不佳时,测试可以正常进行,我稍后会解释。本章的目的是介绍 HTML、CSS 和 JavaScript 的特性和通用技术,您可以用它们来构建自己的测验,选择自己的题目。请记住,你可能不是在为自己打造游戏。

顺便说一下,对于美国总统来说,我需要提供一些方法来解决格罗弗·克利夫兰(Grover Cleveland)的问题,他是唯一一位连任两届非连续总统的人。我选择将格罗弗·克利夫兰和 ?? 列入名单。也许你需要对你的主题采取类似的步骤。

玩家点击连续的选项。我开始了一个新游戏。图 6-2 显示了玩家选择她认为(知道)是这一套中最早的总统后的屏幕。请注意,数字 2 出现在您的订单下,约翰·昆西·亚当斯盒子现在是金色的。

img/214814_2_En_6_Fig2_HTML.jpg

图 6-2

玩家选择她认为是这一组 4 个中最早的总统

无论正确与否,任何被点击的方块都会变成金色。我不会试图犯任何错误。图 6-3 显示了两种选择以及出现在您订单下的数字 2 1。

img/214814_2_En_6_Fig3_HTML.jpg

图 6-3

玩家点击了约翰·昆西·亚当斯,然后点击了马丁·范·布伦

我完成了测验。图 6-4 显示的是车窗,有些挤压。此时显示的是一个视频剪辑和一个音频文件。我有一个自由女神像附近烟火的视频剪辑。这段视频的配乐是纽约,纽约。我决定找一个免费版的《褶边与华丽》(也叫《向酋长致敬》)。稍后,您将会看到我将视频和音频结合起来所需的一些小步骤。

img/214814_2_En_6_Fig4_HTML.jpg

图 6-4

在成功订购总统组之后

让我调用一个新游戏,现在输入一个错误的顺序。图 6-5 显示了做错订单的结果。显示玩家的订单,并出现消息WRONG

img/214814_2_En_6_Fig5_HTML.jpg

图 6-5

玩家不正确的排序

问答游戏的关键要求

测验需要一种存储信息的方式,或者用一个更好的术语来说,一个知识库。我们需要一种随机选择具体问题的方法,这样玩家每次都会看到不同的挑战。因为我们存储的是名字,所以我们可以使用一个简单的技术。

接下来,我们需要向玩家提出问题,并对玩家的行为提供反馈。我们可以决定反馈的多少。我的游戏在点击后会改变盒子的颜色,订单会显示在“您的订单”标题下。我决定等到完成后再检查玩家的订单。我的技术评论员指出,在游戏的早期版本中,我的编码允许玩家点击同一个框两次。我决定不回应额外的点击来处理这个问题。您可以决定这是否是您想要采取的方法。总的问题是你需要期待玩家/客户/用户可以做奇怪的事情。有时你可能想告诉他们这是错误的,而有时你,也就是你的代码,应该简单地忽略这个动作。

我认为正确的排序应该得到奖励:播放一段爱国视频剪辑。正如我将要解释的,这需要获得一个视频剪辑和一个单独的音频剪辑。

HTML5、CSS 和 JavaScript 特性

现在让我们深入研究 HTML5、CSS 和 JavaScript 的具体特性,这些特性提供了我们实现测验所需的内容。我再次建立在之前已经解释过的基础上,做了一些冗余,以防你在阅读中跳过。

在数组中存储和检索信息

你可能记得数组是一系列的值,变量可以被设置成数组。数组的各个组成部分可以是任何数据类型——包括其他数组!回想一下,在第五章的记忆游戏中,我们使用了一个名为pairs的数组变量,其中每个元素本身是一个由两个元素组成的数组,即匹配的照片图像文件。

在测验应用程序中,我们将再次使用数组的数组。对于智力竞赛节目,我们设置了一个名为facts的变量作为数组来保存总统姓名的信息。关键信息是数组中项目的顺序。facts数组的每个元素本身就是一个数组。创建这个应用程序时,我的第一个想法是,应该有一个简单的字符串对象数组,每个字符串包含一个总统的名字,数组按顺序排列。然而,我随后决定需要一个数组的数组,第二个元素包含一个布尔值(真/假),用于防止在一个游戏中两次选择相同的名称。

使用方括号访问或设置数组的各个组件。JavaScript 中的数组从零开始索引,到数组中元素总数减一结束。记住索引是从零开始的一个技巧是想象数组都是排成一行的。第一个元素将在开始处;第二个 1 单位远;第三个 2 单位远;等等。

数组的长度保存在名为length的数组属性中。要访问facts数组中的第一项,可以使用facts[0];对于二次元,facts[1],以此类推。您将在代码中看到这一点。

对数组中的每个元素做一些事情的常见方法是使用forloop。(另请参见第三章中关于在边界框壁上设置渐变的说明。)假设您有一个名为prices的数组,您的任务是编写代码将每个价格提高 15%。此外,每个价格必须至少增加 1,即使 1 大于 15%。您可以使用表 6-1 中的结构来执行这项任务。正如您在解释栏中看到的,for循环对数组的每个组件做同样的事情,在本例中使用索引变量i。这个例子也展示了Math.max方法的使用。

表 6-1

使用 For 循环增加数组中的价格

|

密码

|

说明

|
| --- | --- |
| for(var i=0;i<prices.length;i++) { | 执行括号内的语句,改变i的值,从 0 开始增加 1(这就是i++所做的),直到值不小于数组中元素的数量prices.length。 |
| prices[i] += Math.max``(prices[i]*.15,1); | 记得从里到外解读这个。计算数组prices的第i个元素的.15倍。看哪个更大,这个值还是 1。如果是这个值,那就是Math.max返回的。如果是 1(如果 1 比prices[i]*.15大),就用 1。将该值与prices[i]的当前值相加。这就是+=的作用。 |
| } | 关闭for回路。 |

注意,代码没有明确说明prices数组的大小。相反,它用表达式prices.length来表示。这很好,因为这意味着当你向数组中添加元素时,length的值会自动改变。当然,在我们的例子中,我们知道数字是 45,但是在其他情况下,最好保持灵活性。当一个事实是一条信息时,这个应用程序可以作为一个包含任意数量事实的测验的模型,其中信息的顺序很重要。

JavaScript 只支持一维数组。facts数组是一维的。但是数组中的项本身就是数组:facts[0]元素本身就是数组,以此类推。

注意

如果知识库非常复杂,或者如果我要共享信息或从其他地方访问信息,我可能需要使用数组之外的东西。我还可以将知识库与 HTML 文档分开存储,也许可以使用扩展标记语言(XML)文件。JavaScript 具有读入和访问 XML 的函数。最重要的是,我会把事实放在服务器上,这样任何玩家都无法查看源代码来了解订单的实际情况。我不这么做的理由是 1)我不想进入服务器端编程,2)如果一个玩家这么努力,他或她会学到一些东西。

测验的设计是为每个游戏随机选择一组四个名字,所以我们定义一个变量nq(代表测验中的数字)为 4。这永远不会改变,但是把它变成一个变量意味着如果我们想改变它,这很容易做到。

动态创建的 HTML(见下一节)将显示一列。这里用伪代码表示的逻辑如下

Make a random choice, from 0 to facts.length. If this fact has been used, try again. Mark this choice as used.

Create new HTML to be a block, with the text and a number (1, 2, 3 or 4 and the name of the president.

Make the block visible and position it in the window.

Set up an event and event handling to respond to the player clicking in the box.

那么我们如何编码呢?我将在下一节解释新 HTML 的创建。如前所述,事实数组包含数组,每个内部数组的第二个元素是一个布尔变量。最初,这些值都是假的,这意味着游戏中还没有用到这些元素。当然,如果随机调用返回一个已经被选中的数字,我会使用另一种类型的循环,一个do-while结构,它会一直尝试,直到出现一个没有被使用的事实:

do {c = Math.floor(Math.random()*facts.length);}
while (facts[c][2]==true);

一旦facts[c][2]为假,即当索引c处的元素可用时,do-while就退出。

facts数组是我完整创建的,并放在 HTML 文档中。它不会改变。相比之下,对于测验中的每一个游戏,我的代码都会创建一个名为“老丨虎丨机”的区域。它从一个空数组开始:

var slots =[];

每当玩家移动一步,也就是点击一个方块,就会使用push方法将信息添加到这个数组中。老丨虎丨机数组由checkorder函数访问,该函数将在“检查玩家的答案”一节中描述。

在程序执行期间创建 HTML

HTML 文档通常由最初编写文档时包含的文本和标记组成。但是,您也可以在浏览器解释文件时向文档添加内容,特别是在执行script元素中的 JavaScript 时(称为执行时间运行时间)。这就是我所说的动态创建 HTML。在这个应用程序中,就像本文中的大多数应用程序一样,body标签的onload属性被设置为调用一个名为init的程序。这个函数调用另一个设置游戏的函数。

对于测验应用程序,我创建了一个名为pres的类型。这通过以下方式完成:

d = document.createElement('pres');

然后我需要在新创建的对象中放一些东西。这实际上需要几个语句。

我使用赋值语句。注意:uniqueid变量已经被设置。

d.innerHTML= "<div  class='thing' id='"+uniqueid+"'>placeholder</div>";

div是一个块类型,这意味着它可以包含其他元素以及文本,并且在它的前后显示有换行符。我用

thingelem = document.getElementById(uniqueid);

设置thingelement以引用新创建的对象。我用

thingelem.textContent = String(i+1)+": "+facts[c][0];

以提供可视内容。i+1是为了让玩家看到从 1 而不是 0 开始的索引。

动态创建的 HTML 需要附加到已经可见的东西上,比如body元素,以便显示。这是使用appendChild完成的。

document.body.appendChild(d);

body元素通常是合适的选择,但是您也可以在其他元素上使用appendChild,这会很有用。例如,您可以使用属性childNodes来获取特定元素的所有子节点的集合(NodeList ),为每个子节点做一些事情,包括删除它。

表 6-2 显示了我们将使用的方法。

表 6-2

动态创建 HTML 时通常使用的方法

|

密码

|

说明

|
| --- | --- |
| createElement | 创建 HTML 元素 |
| appendChild | 通过将元素追加到文档中的某个位置,将元素添加到文档中 |
| getElementByID | 获取对元素的引用 |

每个块的格式化是在 CSS 的 style 元素中完成的(见下一步)。代码为每个块创建一个唯一的 ID。这个惟一的 ID 是根据名称在facts数组中的索引构建的。在检查玩家点餐时使用。

一旦我们创建了这些新的 HTML 元素,我们就使用addEventListener来设置事件和事件处理程序。addEventListener方法用于各种事件。记住,我们在第四章中的canvas元素上使用了它。

安排程序响应玩家使用了addEventListener方法。语句thingelem.addEventListener('click',pickelement);定义了事件,即点击块,以及事件处理:调用pickelement函数。

注意

如果我们没有这些元素和能力来执行addEventListener并使用this引用属性(原谅笨拙的英语),而是在画布上绘制东西,我们将需要执行计算和比较来确定鼠标光标在哪里,然后以某种方式查找相应的信息来检查匹配。(回忆一下第四章中弹弓的编码。)相反,JavaScript 引擎正在做大量的工作,而且比我们自己编写代码更有效、更快。

您将在“构建应用程序”一节中看到完整的代码。

在样式元素中使用 CSS

级联样式表(CSS)允许您指定 HTML 文档各部分的格式。第一章展示了一个非常基本的 CSS 例子,即使对于静态 HTML 来说,它也是强大而有用的。本质上,这个想法是使用 CSS 来格式化,也就是应用程序的外观,而保留 HTML 来构造内容。有关 CSS 的更多信息,请参见 David Powers 的《CSS3 入门》( Apress,2012)。

让我们在这里简单地看一下我们将使用什么来生成保存总统姓名的动态创建的块。

HTML 文档中的样式元素包含一个或多个样式。每种风格都指以下一种:

  • 使用元素类型名称的元素类型

  • 使用id值的特定元素

  • 一个class的元素

在第一章中,我们为body元素和section元素使用了一种样式。为了测试,我为一类我命名为thing的元素写了一个指令。

现在让我们为一类元素设置格式。类是一个可以在任何元素开始标记中指定的属性。对于这个应用程序,我想出了一个类thing。是的,我知道这很无聊。它指的是我们的代码将放在屏幕上的东西。风格是

.thing {position:absolute; left: 0px; top: 0px; border: 2px; border-style: double;➥
 background-color: white; margin: 5px; padding: 5px; }

padding设置决定了文本和文本框之间的间距;margin决定了元素周围的间距。我想到了一个填充的细胞来帮助我记住不同之处。事实上,margin设置在这里是不必要的,因为我的代码使用变量rowsize垂直定位块。

thing前的句点表示这是一个类规范。position被设置为absolutetopleft包括可以通过代码改变的值。

absolute设置指的是在文档窗口中指定position的方式——作为特定的坐标。另一种选择是relative,如果文档的一部分在一个包含块中,可以在屏幕上的任何地方,就可以使用这个选项。度量单位是像素,因此从左到上的位置被给定为 0 像素的 0px,边框、边距和填充度量分别是 2 像素、5 像素和 5 像素。

现在让我们看看如何使用样式属性来定位和格式化块。例如,在创建了保存总统姓名的动态元素之后,我们可以使用下面几行代码来获取对刚刚创建的thing的引用,将保存姓名的文本放入元素中,然后将它定位在屏幕上的指定点。

thingelem = document.getElementBy(uniqueid);
thingelem.textContent=
     String(i+1)+": "+facts[c][0];
thingelem.style.top = String(my)+"px";
thingelem.style.left = String(mx)+"px";

这里,mymx是数字。设置style.topstyle.left需要一个字符串,所以我们的代码将数字转换成字符串,并在字符串末尾添加"px"

响应玩家的移动

pickelement函数中,你会看到响应和跟踪玩家动作的代码。pickelement标题有一个称为ev的单一参数。然而,还有一种我们称之为隐含参数的东西。调用该函数是因为对特定元素的操作。代码中的术语this指的是该元素。

在代码中,this指的是当前实例,即玩家点击的元素。我们为每个元素设置了事件监听,因此当执行pickelement时,代码可以引用使用this听到点击的特定元素。当玩家点击一个写有约翰·昆西·亚当斯名字的方块时,代码知道它,通过“知道”我比我想要的更拟人化了程序。换句话说,同样的pickelement函数将被调用于我们在屏幕上放置的所有方块,但是,通过使用this,代码可以引用玩家每次点击的特定方块。pickelement代码从textContent中的元素和第一个字符中提取 ID。ID 中的信息用于填充一个名为slots的数组,该数组将用于检查玩家的订单。来自textContent的字符,1 或 2 或 3 或 4,将用于向玩家显示已经做出的选择。

我们想在玩家点击盒子时改变它的颜色。我们可以这样做,就像改变topleft来重新定位模块一样。然而,JavaScript 的属性名与 CSS 中的略有不同:没有破折号。

this.style.backgroundColor = "gold";

gold是一组已确定的颜色之一,包括redwhiteblue等。那可以用名字来指代。或者,您可以使用 Adobe Photoshop 等程序或 pixlr.com 等在线网站提供的十六进制 RGB 值。

函数执行另一个任务,我认为说这是一个迟到的添加是有用的,虽然有点尴尬。如果玩家,姑且称他为讨厌的玩家,不止一次点击一个方块会怎么样?在我的测试中,我从未尝试过这一点,但我的技术审查员指出了这一点。你需要为玩家和用户做奇怪的事情做准备和计划。解决方法很简单。我使用代码来停止监听点击事件。声明是

this.removeEventListener('click',functionreference);

functionreference变量已被设置为指向pickelement

pickelement函数提取块 ID 的原始数字部分,并将其转换为数字。这被添加(推)到一个名为slots的数组中。当slots数组的长度等于nq时,调用checkorder函数。

小费

您可以在样式部分指定字体。你可以在任何搜索引擎中输入“安全网页字体”,然后得到一个据称可以在所有浏览器和所有电脑上使用的字体列表。但是,另一种方法是指定一个有序的字体列表,这样如果第一个字体不可用,浏览器将尝试查找下一个。更多信息见第八章。

演示音频和视频

HTML5 提供了音频和视频元素,用于呈现音频和视频,或者作为静态 HTML 文档的一部分,或者在 JavaScript 的控制下。

简而言之,音频和视频有不同的文件类型,就像图像一样。文件类型因视频和相关音频的容器、音频本身以及视频和音频的编码方式而异。浏览器需要知道如何处理容器,如何解码视频以在屏幕上连续显示帧(组成视频的静止图像),以及如何解码音频以将声音发送到计算机扬声器。

视频涉及大量数据,因此人们仍在研究压缩信息的最佳方法,例如,利用帧之间的相似性而不损失太多质量。网站现在显示在手机的小屏幕上,也显示在大的高清电视屏幕上,所以利用任何关于显示设备的知识是很重要的。考虑到这一点,虽然我们可以希望浏览器制造商在未来标准化一种格式,但 HTML5 video元素提供了一种通过引用多个文件来解决缺乏标准化问题的方法。因此,开发人员需要制作同一视频的不同版本(包括我们创建这个测验应用程序的人)。

我下载了一个 7 月 4 日的 fireworks 视频剪辑,然后使用一个免费工具(Miro video converter)创建了三个不同的版本,用不同的格式制作了同一个视频短片。然后我使用新的 HTML5 video元素和source元素来编码对所有三个视频文件的引用。元素中的codecs属性提供了关于在src属性中指定的文件的编码信息。然后,我决定不使用烟花视频的音频,而是使用传统上为美国总统播放的歌曲“荷叶边和花饰”。幸运的是,视频标签附带了一个名为muted的属性,可以让视频的音频静音。我不需要视频和音频完全同步,所以这种方法可行。在身体里,我有

<audio id="ruffles" controls="controls"  preload="auto" alt="Hail to the Chief">
  <source src="hail_to_the_chief.mp3" type="audio/mpeg">
  <source src="hail_to_the_chief.ogg" type="audio/ogg">
Your browser does not accept the audio tag.
 </audio>
 <video id="vid"  preload="auto" width="50%" alt="Fireworks video" muted>
<source src="sfire3.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'>
<source src="sfire3.mp4">
<source src="sfire3.theora.ogv" type='video/ogg; codecs="theora, vorbis"'>

包括controls="controls"将熟悉的控件放在屏幕上,允许玩家/用户开始或暂停音频剪辑。我不提供视频控件。

此时,您可能会问:当测验开始时,视频和音频控件在哪里?答案是我用 CSS 让这两个不显示:

audio {visibility: hidden;}
video {visibility: hidden; display: none; position:absolute;}

您可能还会问,为什么我不编写代码来动态创建videoaudio元素,而是将它们放在 HTML 文档中。答案是,我想确保音频和视频文件被完全下载。因为人类游戏确实需要一些时间,这可能在没有特殊工作的情况下发生,但这是一个很好的预防措施。

小费

CSS 有自己的语言,有时在术语中涉及连字符。表达元素在屏幕上如何分层的 CSS 术语是 z-index;JavaScript 术语是zIndex

检查玩家的答案

checkorder函数执行检查玩家是否以正确的顺序点击了方块的任务。这对我来说并不明显,但我确实意识到我的程序不需要对选择的名字进行排序。相反,我的代码检查在slots数组中表示的玩家列表是否是无序的。slots数组将按照玩家的命令保存每个总统的索引位置。该代码循环访问这些项,以查看是否有任何项大于下一项。这个for循环完成了任务:

var ok = true;
    for (var i=0;i<nq-1;i++){
       if (slots[i]>slots[i+1]){
              ok = false;
              break;
       }
    }

ok变量开始为真,如果与正确排序有任何差异,for循环中的代码将把ok的值改为false。当这种情况发生时,break语句使控制离开for循环。如果ok设置为false,则退出for循环。下一步是提供音频/视频奖励以及显示结果CORRECT或显示结果WRONG

if (ok){

       res.innerHTML= "CORRECT";
       song.style.visibility="visible";
       song.currentTime = 4; //prevent seconds of no sound
       song.play();
       v.style.visibility="visible";
       v.currentTime=0;
       v.style.display="block";

       v.play();
    }
    else {
       res.innerHTML = "WRONG";
    }

有了 JavaScript、HTML 和 CSS 的这些背景知识,我们现在可以描述测验应用程序的编码了。

构建应用程序并使之成为您自己的应用程序

测验的知识库在facts变量中表示,这是一个数组的数组。如果您想将测验更改为另一个主题,一个由成对的姓名或其他文本组成的主题,您只需更改facts。当然,您还需要更改在body元素中作为h1元素出现的文本,让玩家知道问题的类别。我定义了一个名为nq的变量,每次测验中的数字(屏幕上出现的配对数)是 4。当然,如果您想向玩家呈现不同数量的对子,您可以更改该值。其他变量用于块的原始位置和保存状态信息,比如是第一次点击还是第二次点击。

我为这个应用程序创建了四个函数:initsetupgamepickelementcheckorder。我本可以将initsetupgame合并,将pickelement和 checkorder 合并,但是将它们分开以方便重放按钮,也是为了一般原则。为不同的任务定义不同的功能是一种很好的做法。表 6-3 描述了这些函数以及它们调用或被调用的内容。

表 6-3

测验应用程序中的功能

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad动作调用 | setupgame |
| setupgame | init |   |
| pickelement | 因setupgame中的addEventListener调用而被调用 | 检查订单 |
| checkorder | pickelement |   |

setupgame函数是为块创建 HTML 的地方。简而言之,一个使用Math.random的表达式被求值以选择facts数组中的一行。如果该行已被使用,代码会再次尝试。当发现一个未使用的行时,它被标记为已使用(第三个元素,索引值为 2)并创建块。

当点击一个块时,调用pickelement函数。它添加到订单上显示的字符串中,并添加到将由checkorder使用的slots数组中。checkorder函数进行检查。它显示WRONGCORRECT,如果顺序正确,使音频控制和视频可见并开始播放。

注意,我的程序中有多余的代码。我这样做是为了减轻重复播放的负担,而不需要重新加载或“重做”。

表 6-4 提供了代码的逐行解释。

表 6-4

总统测验的完整代码

| `` | HTML 标签。 | | `` | 定义字符集,在这种情况下是一种 Unicode 形式。可以省略,我确实在很多例子中省略了,但是在这里包含它是为了让你看到。 | | `` | 头部标签。 | | `Ordering Quiz with Rewards` | 完整的标题元素。 | | `` | 关闭样式元素。 | | `` | 关闭`script`元素。 | | `` | 关闭`head`元素。 | | `` | `Body`标记。注意`onload`的设置。 | | `` | 关闭`audio`元素。 | | `` | 关闭`video`元素。 | | `

Order the Presidents

` | 航向。 | | 这是一个挑战,让总统按照任期的时间顺序排列。按你认为正确的顺序点击方框。 | 说明书。 | | `
` | 换行。 | | 为新游戏重新加载。 | 更多说明。 | | `
` | 换行。 | | 您的订单: | 前往玩家的答案。 | | `
` | 玩家回答的地方。 | | `

` | 垂直间距。 | | 结果:`

` | 将保持结果。 | | `

` | 垂直间距。 | | `` | 关闭`body`。 | | `` | 关闭`html`。 |

让这个应用程序成为您自己的应用程序的第一步是选择测验的内容。这里的值是名称,保存在文本中,但也可以是事件描述、数学表达式或歌曲名称。您还可以创建img标签,并使用保存在数组中的信息来设置img元素的src值。更复杂但仍然可行的方法是加入音频。从简单的开始,类似于美国总统的名单,然后变得更大胆。

您可以通过修改原始 HTML 和/或创建的 HTML 来更改应用程序的外观。您可以修改或添加到 CSS 部分。

你可以很容易地改变问题的数量(但不能超过 9 个),或将四个问题的游戏改为四个问题的游戏,并在一定数量的猜测或点击按钮后自动进行新一轮游戏。你需要决定是否每一轮都要更换主席。

您还可以加入计时功能。有两种通用的方法:记录时间并在玩家成功完成一局/一轮游戏时简单地显示时间(参见第五章中的记忆游戏)或设定时间限制。第一种方法允许某人与自己竞争,但不会施加太大的压力。第二种情况会给玩家带来压力,你可以减少连续回合的时间。可以使用setTimeout命令来实现。

你可以把讨论事实的网站或谷歌地图位置的链接作为正确答案的迷你奖——或者作为线索。

您可能不喜欢视频播放时测验块停留在屏幕上的方式。您可以使用一个使每个元素不可见的循环来删除它们。期待第九章中的 Hangman 应用程序。

测试和上传应用程序

游戏的随机性不会影响测试。如果您愿意,您可以在Math.random编码后替换固定选项,进行大部分测试,然后删除这些代码行并再次测试。对于这个游戏和类似的游戏,重要的是要确保你的测试包括正确的猜测和错误的猜测,以及玩家的不良行为,比如点击已经做出的选择。

总统游戏在 HTML 文件中是完整的,但是音频和视频剪辑是不同的文件。如果你自己做测验,你没有义务同时使用音频片段和视频片段。对于媒体,您需要

  • 创建或获取视频和/或音频

  • 生产不同的版本,假设你想支持不同的浏览器

  • 将所有文件上传到服务器

您可能需要与服务器工作人员合作,以确保正确指定不同的视频类型。这涉及到一个叫做 htaccess 的文件。HTML5 已经存在了一段时间,这种在网页上显示视频的方式应该为服务器工作人员所熟悉。

或者,您可以识别已经在线的视频和/或音频,并在media元素的source元素中使用绝对 URL 作为src属性。

摘要

在这一章中,我们实施了一个简单的测验,要求玩家将从美国总统的完整列表中随机选择的一小部分按顺序排列。将事件按时间顺序排列是一个合理的测验主题,但本章的主要内容是所使用的独特技术。该应用程序使用了以下编程技术和 HTML5 特性:

  • 使用document.createElementdocument.getElementByIddocument.body.appendChild在运行时创建 HTML

  • 使用addEventListener设置鼠标click事件的事件处理

  • 使用removeEventListener移除鼠标click事件的事件处理

  • 使用代码更改 CSS 设置来更改屏幕上对象的颜色

  • 创建一个数组的数组来保存测验内容

  • 使用for循环迭代数组

  • 使用do-while循环随机选择一个未使用的问题集

  • 使用substring提取检查中使用的字符串

  • 使用Number函数将字符串转换成数字

  • 使用videoaudio元素显示以不同浏览器可接受的格式编码的视频和音频

您可以使用动态创建和重新定位的 HTML 以及在前面章节中学习的画布上的绘制。第九章中描述的 Hangman 的实现就是这么做的。你可以像这里一样,把视频和音频作为应用程序的一小部分,或者作为网站的主要部分。在下一章中,我们将回到在画布上画一个迷宫,然后在迷宫中穿行而不穿过墙壁。

七、迷宫

在本章中,我们将介绍

  • 响应鼠标事件

  • 圆与直线碰撞的计算

  • 响应箭头键

  • 表单输入

  • 使用trycatch对本地存储器中的信息进行编码、保存、解码和恢复,以测试编码是否被识别

  • 使用joinsplit对信息进行编码和解码

  • 在按钮中使用javascript:来调用函数

  • 单选按钮

介绍

在这一章中,我们将继续探索编程技术以及 HTML5 和 JavaScript 特性,这一次我们将使用构建和遍历迷宫的程序。玩家将能够绘制一组墙壁来组成一个迷宫。他们将能够保存和加载他们的迷宫,并使用碰撞检测来遍历它们,以确保它们不会穿越任何墙壁。

一般的编程技术包括为需要在画布上绘制的所有内容使用数组,以及为迷宫中的一组墙壁使用单独的数组。在游戏开始之前,墙的数量是未知的,所以需要一个灵活的方法。一旦迷宫建成,我们将看到如何响应箭头键的按压,以及如何检测游戏棋子(五边形令牌)和墙壁之间的碰撞。使用 HTML5,我们可以处理鼠标事件,这样玩家可以按下鼠标按钮,然后拖动和释放按钮来定义迷宫的每堵墙;响应箭头键来移动令牌;并在本地计算机上保存和检索墙的布局。像往常一样,我们将构建应用程序的多个版本。首先,所有内容都包含在一个 HTML 文件中。也就是说,玩家建造一个迷宫,可以在其中穿行,并可以选择将其保存到本地计算机或恢复先前保存的一组墙壁。在第二个版本中,有一个程序创建迷宫,第二个文件使用单选按钮为玩家提供穿越特定迷宫的选择。也许一个人可以在给定的计算机上建造迷宫,然后请一个朋友试着穿越它们。

HTML5 的本地存储设备只接受字符串,所以我们将看看如何使用 JavaScript 将迷宫信息编码成字符串,然后解码回来重建迷宫的墙壁。保存的信息将保留在计算机上,即使在计算机关闭后。

我们将在本章中讨论的各个功能:构建结构、使用箭头键移动游戏棋子、检查碰撞以及在用户计算机上编码、保存和恢复数据,都可以在各种游戏和设计应用程序中重用。

注意

HTML 文件通常被称为脚本,而术语程序通常是为 Java 或 c 等语言保留的。这是因为 JavaScript 是一种解释型语言:在执行时一次翻译一条语句。相比之下,Java 和 C 程序是编译的,也就是说,一次完全翻译完,结果存储起来以备后用。我们中的一些人并不那么严格,他们使用脚本、程序、应用程序或简单的文件来表示带有 JavaScript 的 HTML 文档。

图 7-1 显示了一体机程序和第二个程序的第一个脚本的开始屏幕。

img/214814_2_En_7_Fig1_HTML.jpg

图 7-1

迷宫游戏的开场画面

图 7-2 显示了在画布上放置了一些相当粗糙的墙壁后的屏幕。

img/214814_2_En_7_Fig2_HTML.jpg

图 7-2

迷宫的墙壁

图 7-3 显示了玩家使用箭头键将令牌移入迷宫后的屏幕。

img/214814_2_En_7_Fig3_HTML.jpg

图 7-3

在迷宫中移动令牌

如果玩家想保存一组墙,他或她输入一个名字并点击按钮。为了检索添加到当前画布上的墙壁,玩家键入名称并按下 GET SAVED WALLS 按钮。如果该名称下没有保存任何内容,则不会发生任何事情。

双脚本应用程序让第二个脚本为玩家提供一个选择。图 7-4 为开启画面。

img/214814_2_En_7_Fig4_HTML.jpg

图 7-4

travelmate 脚本的打开屏幕

这个双脚本应用程序假设有人使用第一个脚本创建并保存了三个迷宫,并在第二个脚本中使用了特定的名称。此外,必须使用相同的浏览器来创建迷宫和进行迷宫活动。我这样做是为了演示 HTML5 的本地存储功能,它类似于 cookie——web 应用程序开发人员存储用户信息的一种方式。

注意

Cookies,以及现在的 HTML5 localStorage,是行为营销的基础。它们给我们带来了便利——我们不必记住密码等某些信息——但它们也是一种被跟踪的方式和销售的目标。我在这里没有立场,只是注意到设施。

图 7-5 显示了一个简单的迷宫。

img/214814_2_En_7_Fig5_HTML.jpg

图 7-5

简单的迷宫

图 7-6 显示了一个稍微复杂一点的迷宫。

img/214814_2_En_7_Fig6_HTML.jpg

图 7-6

中等的迷宫

图 7-7 显示了一个更难的迷宫,更难主要是因为玩家需要从第一个入口点向迷宫底部移动才能通过。当然,迷宫是由玩家/创作者来设计的。

img/214814_2_En_7_Fig7_HTML.jpg

图 7-7

更难的迷宫

一个重要的特性是,在双脚本应用程序中,单击 GET maze 按钮会强制删除当前的迷宫,并绘制新选择的迷宫。这不同于在一体化程序或第二个版本的创建部分中发生的情况,在第二个版本中,旧墙被添加到现有的内容中。和其他例子一样,这些只是程序的存根,用来演示 HTML5 的特性和编程技术。让项目成为你自己的有很多改进的机会。

关键要求

迷宫应用程序需要显示一个不断更新的游戏板,因为新的墙被竖起来了,代币被移动了。

建造迷宫的任务需要响应鼠标事件来收集建造一堵墙所需的信息。应用程序显示正在建造的墙。

走迷宫任务需要响应箭头键来移动令牌。游戏不能让代币穿过任何墙。

保存和检索操作要求程序对墙信息进行编码,将其保存在本地计算机上,然后检索它并使用它来创建和显示保存的墙。迷宫是相当复杂的结构:一组一定数量的墙,每堵墙由起始和结束坐标定义,也就是说,成对的数字代表画布上的 x,y 位置。对于要使用的本地存储设备,这些信息必须转换成一个字符串。

两个文档版本使用单选按钮来选择一个迷宫。

HTML5、CSS 和 JavaScript 特性

现在让我们看看 HTML5 和 JavaScript 的具体特性,它们提供了我们实现迷宫应用程序所需的东西。这是建立在前几章的基础上的:HTML 文档的一般结构;使用程序员定义的函数,包括程序员定义的对象;在canvas元素上绘制由线段组成的路径;程序员对象;和数组。前面的章节已经解决了画布上的鼠标事件(第四章中的炮弹和弹弓游戏以及第五章中的记忆游戏)和 HTML 元素上的鼠标事件(第六章中的问答游戏)。我们将涉及的新特性包括一种不同类型的事件:从玩家按下箭头键中获取输入,称为击键捕获;并且使用本地存储将信息保存在本地计算机上,即使在浏览器已经关闭并且计算机已经关机之后。请记住,您可以跳到“构建应用程序”一节来查看所有带注释的代码,然后返回到这一节来阅读对各个特性和技术的解释。

墙壁和令牌的表示

首先,我们将定义一个函数Wall,用于定义一个墙对象,另一个函数Token,用于定义一个令牌对象。我们将以一种比这个应用程序所要求的更通用的方式来定义这些函数,但是我相信这是没问题的:在性能方面,这种通用性不会影响太多,如果有的话,同时让我们可以自由地将代码用于其他应用程序,例如具有不同游戏棋子的游戏。我选择五边形是因为我喜欢它,并使用mypent作为游戏棋子的变量名。

为墙定义的属性由鼠标动作指定的起点和终点组成。我把这些命名为sxsyfxfy。墙也有一个width和一个strokestyle字符串,一个draw方法被指定为drawAline。这比必要的更普遍的原因是因为所有的墙都有相同的宽度和样式字符串,并且都将使用drawAline函数。当需要将墙保存到本地存储时,我只保存sxsyfxfy值。如果您编写其他程序并需要存储值,您可以使用相同的技术来编码更多的信息。

在迷宫中移动的令牌是通过调用Token函数来定义的。这个函数类似于为多边形记忆游戏定义的Polygon函数。Token函数存储令牌的中心、sxsy,以及半径(rad)、边数(n)和fillstyle,它链接到draw方法的drawtoken函数和moveit方法的movetoken函数。此外,名为angle的属性立即被计算为(2*Math.PI)/n。回想一下,在测量角度的弧度系统中,2*Math。圆周率代表一个完整的圆,所以这个数除以边数就是从圆心到每边两端的角度。

和以前的应用程序一样(参见第四章的,在一个对象被创建后,代码将它添加到everything数组中。我还将所有的墙添加到walls数组中。该数组用于将墙壁信息保存到本地存储。

构建和定位墙的鼠标事件

回想一下,在前面的章节中,我们使用 HTML5 和 JavaScript 来定义事件和指定事件处理程序。init函数包含为玩家按下鼠标主按钮、移动鼠标和释放按钮设置事件处理的代码。

canvas1 = document.getElementById('canvas');
canvas1.addEventListener('mousedown',startwall,false);
canvas1.addEventListener('mousemove',stretchwall,false);
canvas1.addEventListener('mouseup',finish,false);

我们还将使用一个名为inmotion的变量来跟踪鼠标按钮是否被按下。startwall函数确定鼠标坐标(参见章节 4 和 5 获取事件后的鼠标坐标),创建一个新的Wall对象,引用存储在全局变量curwall中,将墙添加到everything数组,绘制everything中的所有项目,并将inmotion设置为true。如果inmotion不是true,那么stretchwall函数不做任何事情立即返回。如果inmotion为真,代码获取鼠标坐标并使用它们来设置curwallfxfy值。当玩家按下按钮移动鼠标时,这种情况会反复发生。释放按钮时,调用功能finish。该函数将inmotion设置回false,并将curwall添加到名为walls的数组中。

检测箭头键

检测键盘上的一个键被按下,并确定哪个键被称为捕获击键。这是 HTML5 和 JavaScript 可以处理的另一种类型的事件。我们需要设置对按键事件的响应,这类似于设置对鼠标事件的响应。对任何按键的响应都将是一个我写的函数,名字是getkeyAndMove,我将很快解释。设置事件包括调用addEventListener方法,这次是调用window,内置的 HTML 对象保存 HTML 文件:

window.addEventListener('keydown',getkeyAndMove,false);

该语句在第一个参数中指定了事件keyDown,在第二个参数中指定了事件getkeyAndMove的处理程序。第三个参数与其他对象响应事件的顺序有关,因为默认值为 false,所以可以省略。对于这个应用程序来说,这不是问题。

这意味着当一个键被按下时,将调用getkeyAndMove功能。

小费

事件处理是编程的一个重要部分。基于事件的编程通常比本书中演示的更复杂。例如,您可能需要考虑被包含的对象或包含的对象是否也应该响应事件,或者如果用户打开了多个窗口该怎么办。诸如手机之类的设备可以检测诸如倾斜、摇晃或用手指敲击屏幕之类的事件。合并视频可能涉及在视频完成时调用某些动作。HTML5 JavaScript 在处理事件时并不完全一致(设置超时或时间间隔并不使用addEventListener),但是此时,您已经了解了足够多的信息,可以进行研究来确定您想要的事件,尝试多种可能性来找出事件需要关联的内容(例如,窗口或画布元素或其他对象),然后编写函数作为事件处理程序。还要注意,一些事件处理使用术语回调。指定函数的调用被称为回调。

现在,正如您在这一点上可能预料到的,获取哪个键被按下的信息的编码涉及不同浏览器的不同代码。下面的代码,有两种方法来获得对应于键的数字,可以在所有当前的浏览器中识别 HTML5 中的其他新功能:

if(event == null)
  {
    keyCode = window.event.keyCode;
        window.event.preventDefault();
  }
  else
  {
    keyCode = event.keyCode;
        event.preventDefault();
  }

preventDefault方法做的就像它听起来的那样:阻止任何默认动作,比如与特定浏览器中的特定键相关联的特殊快捷动作。这个应用程序中唯一感兴趣的键是箭头键。下面的switch语句移动变量mypent引用的Token;也就是说,位置信息被更改,以便下次绘制所有内容时,标记将会移动。(这不完全正确。moveit函数包含一个碰撞检查,以确保我们不会先撞到任何墙壁,但这将在后面描述。)

switch(keyCode)
  {
   case 37:  //left arrow
     mypent.moveit(-unit,0);
     break;
   case 38:  //up arrow
     mypent.moveit(0,-unit);
      break;
    case 39: //right arrow
   mypent.moveit(unit,0);
      break;
    case 40:  //down arrow
      mypent.moveit(0,unit);
      break;
    default:
      window.removeEventListener('keydown',getkeyAndMove,false);
}

小费

一定要在代码中添加注释,如不同箭头键的注释所示。本书中的例子没有太多的注释,因为我已经为相关表格中的每一行代码提供了解释,所以这是一个“照我说的做,而不是照我在本文中做的做”的例子。对于团队项目来说,注释是至关重要的,当你回到以前的工作时,注释可以提醒你发生了什么。在 JavaScript 中,您可以使用//来表示该行的其余部分是一个注释,或者用/**/包围多行。JavaScript 解释器会忽略注释。

我怎么知道左箭头的键码是 37?您可以在网上查找关键代码(例如, www.w3.org/2002/09/tests/keys.html )或者您可以编写代码来发出alert语句:

      alert(" You just pressed keycode "+keyCode);

我们的 maze 应用程序的默认动作(当键不是四个箭头键之一时发生)停止击键时的事件处理。这里的假设是,玩家想要键入一个名称,以便将墙壁信息保存到本地存储中,或者从本地存储中检索墙壁信息。在许多应用程序中,要采取的适当行动是一条消息,可能使用alert,让用户知道预期的键是什么。

碰撞检测:令牌和任何墙

要穿越迷宫,玩家不能将代币移过任何墙壁。我们将通过编写一个函数intersect来加强这种限制,如果给定圆心和半径的圆与一条线段相交,该函数将返回true。对于这个任务,我们需要在语言上精确:线段是线的一部分,从sx, syfx, fy。每面墙对应一个有限的线段。这条线本身是无限的。为数组walls中的每面墙调用intersect函数。

小费

我对交集计算的数学解释相当简单,但如果你有一段时间没有做过任何数学计算,这可能会令人望而生畏。如果您不想从头到尾看一遍,请随意跳过它,按原样接受代码。

intersect函数基于参数化线条的思想。具体来说,一行的参数化形式是(编写数学公式,而不是代码)

等式 a:x = sx+t *(FX-sx);

等式 b: y = sy + t*(my-sy);

当参数 t 从 0 变到 1 时,x 和 y 取线段上 x,y 的相应值。目标是确定圆心为 cx,cy,半径为 rad 的圆是否与线段重叠。一种方法是确定直线上距离 cx,cy 最近的点,并查看该点的距离是否小于 rad。在图 7-8 中,你可以看到一条线的一部分的草图,线段用实线表示,线的其余部分用点表示。一端 t 的值为 0,另一端为 1。有两个点 c1x,c1y 和 c2x,c2y。c1x,c1y 点最接近临界线段外的线。点 c2x,c2y 最接近线段中间的某处。t 的值将在 0 和 1 之间。

img/214814_2_En_7_Fig8_HTML.jpg

图 7-8

一条线段和两个点

两点(x,y)和(cx,cy)之间的距离公式为

         distance = Square_Root(((cx-x)*(cx-x)+(cy-y)*(cy-y)))

用公式 a 和 b 代替 x 和 y,我们得到一个距离公式。

Equation c:   distance = Square_Root(((cx-sx+t*(fx-sx))*(cx- sx + t*(fx-sx))+(cy- sy + t*(fy-sy))*(cy- sy + t*(fy-sy))))

出于我们的目的,我们想确定距离最小时 t 的值。在这种情况下,从微积分和关于最小值与最大值的推理中得到的教训首先告诉我们,我们可以用距离的平方来代替距离,从而避免求平方根。此外,当导数(相对于 t)为零时,该值最小。求导并将表达式设为零,得出 cx,cy 最接近直线的 t 值。在代码中,我们定义了两个额外的变量,dx 和 dy,以使表达式更简单。

  • dx = fx-sx

  • dy = my-sy;

  • t = 0.0-(sx-CX)* dx+(xy-cy)* dy)/(dx * dx)+(dy * dy)

这将为 t 生成一个值。0.0 用于强制以浮点数形式进行计算(带小数部分的数字,不限于整数)。

我们用等式 a 和 b 得到对应于 t 值的 x,y 点,这是最接近 cx,cy 的 x,y 点。如果 t 的值小于 0,我们检查 t = 0 的值,如果它大于 1,我们检查 t = 1 的值。这意味着最近的点不是线段上的一个点,因此我们将检查最靠近该点的线段的适当端点。

cx,cy 到最近点的距离近到可以称之为碰撞吗?我们再次使用距离的平方,而不是距离。我们计算从 cx,cy 到计算出的 x,y 的距离的平方。如果它小于半径的平方,则圆与线段相交。如果没有,就没有交集。使用距离平方并没有什么不同:如果平方值有最小值,那么这个值也有最小值。

好消息是,大多数方程都不是编码的一部分。我预先做了确定导数表达式的工作。下面是intersect函数,带有注释:

function intersect(sx,sy,fx,fy,cx,cy,rad) {
   var dx;
   var dy;
   var t;
   var rt;
   dx = fx-sx;
  dy = fy-sy;
  t =0.0-((sx-cx)*dx+(sy-cy)*dy)/((dx*dx)+(dy*dy));  //closest t
  if (t<0.0) { //closest beyond the line segment at the start
    t=0.0; }
  else if (t>1.0) {  //closest beyond the line segment at the end
    t = 1.0;
  }

  dx = (sx+t*(fx-sx))-cx; // use t to define an x coordinate
  dy = (sy +t*(fy-sy))-cy; // use t to define a y coordinate
  rt = (dx*dx) +(dy*dy);  //distance squared
  if (rt<(rad*rad)) {  // closer than radius squared?
      return true; }   // intersect
else {
      return false;}    // does not intersect
}

在我们的应用程序中,玩家按下一个箭头键,并根据该键计算令牌的下一个位置。我们调用intersect函数来查看令牌(近似为一个圆)和一面墙是否有交集。如果intersect返回true,令牌不移动。一有交叉路口,检查就停止了。这是一种常见的碰撞检查技术。

使用本地存储

Web 最初是为将文件从服务器下载到本地(所谓的客户端计算机)以供查看而设计的,但在本地计算机上没有永久存储。随着时间的推移,建立网站的人和组织认为某种形式的本地存储是有利的。因此,有人想出了使用名为 cookies 的小文件来跟踪事物的想法,例如为了用户和网站所有者的方便而存储的用户 id。随着商业网络的发展,cookies、Flash 的共享对象以及现在的 HTML5 本地存储的使用已经有了很大的增长。与这里显示的应用程序的情况不同,用户通常不知道信息正在被谁存储,以及出于什么目的访问信息。

HTML5 的localStorage功能是特定于浏览器的。也就是说,使用 Chrome 保存的迷宫对于使用 FireFox 的人是不可用的。

让我们通过研究一个保存日期和时间信息的小应用程序来更深入地了解如何使用本地存储。第一章中介绍的本地存储和Date功能提供了一种存储日期/时间信息的方法。可以把本地存储想象成一个存储字符串的数据库,每个字符串都有一个特定的名称。名字叫做,字符串本身就是 ,系统叫做键/值对。本地存储只存储字符串这一事实是一个限制,但是下一节将展示如何解决这个问题。

图 7-9 显示了一个简单的日期保存应用程序的屏幕截图。

img/214814_2_En_7_Fig9_HTML.jpg

图 7-9

一个简单的保存日期应用程序

用户有三个选项:存储当前日期和时间的信息,检索上次保存的信息,以及删除日期信息。图 7-10 显示了第一次使用该应用程序(或在日期被删除后)点击检索日期信息时发生的情况。

img/214814_2_En_7_Fig10_HTML.jpg

图 7-10

尚未保存或删除后的数据

我们的应用程序使用 JavaScript 警告框来显示消息。用户需要单击 OK 按钮从屏幕上删除警告框。

图 7-11 显示了用户点击商店日期信息按钮后的消息。

img/214814_2_En_7_Fig11_HTML.jpg

图 7-11

存储日期信息后

如果用户稍后单击 Retrieve Date Info 按钮,他将看到类似于图 7-12 的消息。

img/214814_2_En_7_Fig12_HTML.jpg

图 7-12

检索存储的日期信息

你可以给你的玩家一个方法,使用删除日期信息按钮来删除存储的信息。图 7-13 显示了结果。

img/214814_2_En_7_Fig13_HTML.jpg

图 7-13

删除存储的信息后

HTML5 允许您使用内置对象localStorage的方法保存、获取和删除键/值对。

命令localStorage.setItem("lastdate",olddate)建立一个新的键/值对,或者用等于lastdate的键替换任何先前的键/值对。该声明

  last = localStorage.getItem("lastdate");

将获取的值分配给变量last。在简单示例的代码中,我们只显示结果。您还可以检查是否有空值,并提供更友好的消息。

命令localStorage.removeItem("lastdate")删除以lastdate为键的键/值对。

对于我们简单的日期应用程序,我们将每个按钮对象的onClick属性设置为一些 JavaScript 代码。例如:

<button onClick="javascript:store();">Store date info. </button>

点击按钮时调用store()

您可能想知道是否有人可以读取本地存储中保存的任何信息。答案是对localStorage(和其他类型的 cookies)中每个键/值对的访问被限制在存储信息的网站上。这是一项安全功能。

Chrome 浏览器允许使用存储在本地计算机上的 HTML5 脚本测试本地存储。在编写第一版的时候,Firefox 并没有这样做,而是要求将文件上传到服务器以使用本地存储。虽然localStorage现在似乎被所有的浏览器所识别,但我提到它是为了让你对不同的浏览器有所准备。

因为可能存在其他问题,比如超出用户为本地存储和 cookies 设置的限制,所以包含一些错误检查是一个好的做法。您可以使用 JavaScript 函数typeof来检查localStorage是否被浏览器接受:

if (typeof(localStorage)=="undefined")

图 7-14 显示了在旧版本的 Internet Explorer 中加载日期应用程序并点击存储日期信息按钮的结果。(当你读到这本书的时候,IE 的最新版本可能已经出来了,这不成问题。)

img/214814_2_En_7_Fig14_HTML.jpg

图 7-14

浏览器无法识别localStorage

JavaScript 还提供了一种避免显示错误的通用机制。复合语句trycatch将尝试执行一些代码,如果不成功,它将转到catch子句。

try {
        olddate = new Date();
        localStorage.setItem("lastdate",olddate);
        alert("Stored: "+olddate);
 }
 catch(e) {
         alert("Error with use of local storage: "+e);}
}

如果您删除了if (typeof(localStorage)测试,并在旧 IE 中尝试了代码,您会看到如图 7-15 所示的消息。

img/214814_2_En_7_Fig15_HTML.jpg

图 7-15

浏览器错误,陷入 try/catch

表 7-1 显示了完整的日期应用。记住:你可能需要将它上传到服务器上进行测试。

表 7-1

日期应用的完整代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Local Storage test</title> | 完成title。 |
| <script> | 打开script。 |
| function store() { | 存储函数头。 |
| if (typeof(localStorage) == "undefined") { | 检查localStorage是否被识别。 |
| alert("Browser does not recognize HTML local storage."); | 显示警告消息。 |
| } | 关闭if子句。 |
| else { | 否则。 |
| try { | 设置try子句。 |
| olddate = new Date(); | 定义新的Date。 |
| localStorage.setItem("lastdate",olddate); | 使用键"lastdate"存储在本地存储器中。 |
| alert("Stored: "+olddate); | 显示消息以显示存储的内容。 |
| } | 关闭try子句。 |
| catch(e) { | Start catch子句:如果有问题。 |
| alert("Error with use of local storage: "+e);} | 显示消息。 |
| } | 关闭try子句。 |
| return false; | 返回false以防止任何页面刷新。 |
| } | 关闭功能。 |
| function remove() { | 移除函数头。 |
| if (typeof(localStorage) == "undefined") { | 检查localStorage是否被识别。 |
| alert("Browser does not recognize HTML local storage."); | 显示alert消息。 |
| } | 关闭if子句。 |
| else { | 否则。 |
| localStorage.removeItem('lastdate'); | 使用键'lastdate'删除存储的项目。 |
| alert("Removed date stored."); | 显示指示所做操作的消息。 |
| } | 关闭子句。 |
| return false; | 返回false阻止页面刷新。 |
| } | 关闭功能。 |
| function fetch() { | 获取函数头。 |
| if (typeof(localStorage) == "undefined") { | 检查localStorage是否被识别。 |
| alert("Browser does not recognize HTML local storage."); | 显示alert消息。 |
| } | 关闭if子句。 |
| else { | 否则。 |
| alert("Stored "+localStorage.getItem('lastdate')); | 获取存储在键'lastdate'下的项目并显示。 |
| } | 关闭子句。 |
| return false; | 返回false阻止页面刷新。 |
| } | 关闭功能。 |
| </script> | 关闭script元素。 |
| </head> | 关闭head元素。 |
| <body> | 开始body标签。 |
| <button onClick="javascript:store();">Store date info </button> | 用于存储的按钮。 |
| <button onClick="javascript:fetch();">Retrieve date info </button> | 按钮进行检索,即获取存储的数据。 |
| <button onClick="javascript:remove();">Remove date info </button> | 用于拆卸的按钮。 |
| </body> | 关闭body标签。 |
| </html> | 关闭html标签。 |

Date功能与localStorage结合起来可以让你做很多事情。例如,您可以计算玩家当前和最后一次使用应用程序之间的时间,或者玩家赢了两局。在第五章中,我们使用Date通过getTime方法来计算运行时间。回想一下,getTime存储了从 1970 年 1 月 1 日开始的毫秒数。您可以将该值转换为一个字符串,存储它,然后当您取回它时,进行算术运算以计算运行时间。

localStorage键/值对持续到它们被删除,不像 JavaScript cookies,您可以为其设置一个持续时间。

为本地存储编码数据

为了简单起见,第一个应用程序只包含一个 HTML 文档。您可以使用这个版本来创建迷宫,存储和检索它们,并通过迷宫移动令牌。应用程序的第二个版本涉及两个 HTML 文档。其中一个脚本与第一个应用程序相同,可用于构建、穿越和保存迷宫以及在每个迷宫中穿行。第二个脚本只是为了在一个固定的保存迷宫列表中穿行。一组单选按钮允许玩家从简单、中等和困难选项中进行选择,假设有人已经创建并保存了名为 easymazemoderatemaze、hard masze的迷宫。这些名称可以是您想要的任何名称,数量不限。你只需要在一个程序中创建的东西和在第二个程序中引用的东西保持一致。

现在让我们来解决localStorage只是存储字符串的问题。这里描述的应用程序必须存储足够的关于墙的信息,以便可以将这些墙添加到画布上。在单文档版本中,旧墙实际上被添加到画布上的任何内容中。两个文档的版本删除任何旧的迷宫并加载请求的迷宫。我使用两个表单,每个表单都有一个姓名输入字段和一个提交按钮。玩家选择名称以保存迷宫,并且必须记住它以进行检索。

要存储的数据是一个字符串,即一段文本。我们将通过对每面墙执行以下操作来创建包含一组墙的信息的文本:

  • sx, sy, fx, fy组合成一个名为w的单墙数组。

  • 使用join方法,使用w数组生成一个由+符号分隔的字符串。

  • 将所有这些字符串添加到一个名为allw的数组中。

  • 再次使用join方法,使用allw数组产生一个名为sw的字符串。

字符串变量sw将保存所有墙壁的所有坐标(每面墙四个数字)。下一步是使用localStorage.setItem方法将sw存储在玩家给定的名字下。我们使用上一节中解释的trycatch结构来实现这一点。

try {
   localStorage.setItem(lsname,sw);
}
catch (e) {
    alert("data not saved, error given: "+e);
}

这是一种通用的技术,它将尝试一些事情,抑制任何错误消息,如果有错误,它将调用 catch 块中的代码。

注意

这可能并不总是如你所愿。例如,当直接在计算机上执行 Firefox 上的应用程序时,而不是从服务器下载文件时,localStorage语句不会导致错误,但是不会存储任何内容。当使用 Firefox 从服务器上下载 HTML 文件时,这段代码确实有效,创建脚本既可以作为本地文件使用,也可以使用 Chrome 下载。两个脚本版本必须使用服务器针对每种浏览器进行测试。

检索信息以相应的方式工作。代码提取玩家给出的名字来设置变量lsname,然后使用

        swalls = localStorage.getItem(lsname);

设置变量swalls。如果这不为空,我们使用字符串方法split来做与 join 相反的事情:在给定的符号上分割字符串(我们在每个分号处分割)并将值分配给数组的连续元素。相关的行是

wallstgs = swalls.split(";");

        sw = wallstgs[i].split("+");

接下来,代码使用刚刚检索到的信息以及墙宽和墙样式的固定信息来创建一个新的Wall对象:

curwall = new Wall(sx,sy,fx,fy,wallwidth,wallstyle);

最后,有代码将curwall添加到everything数组和walls数组中。

单选按钮

单选按钮是一组按钮,其中只能选择一个成员。如果玩家做出新的选择,旧的选择将被取消选择。对于这种应用,它们是硬/中/易选择的合适选择。下面是<body>部分的 HTML 标记:

<form name="gf" onSubmit="return getwalls()" >
<br/>
<input type="radio" value="hard" name="level" />Hard <br/>
<input type="radio" value="moderate" name="level" />Moderate <br/>
<input type="radio" value="easy" name="level" />Easy<br/>
<input type="submit" value="GET maze"/><br/>
</form>

请注意,所有三个输入元素都有相同的名称。这就是定义了只能选择其中一个按钮的按钮组。在这种情况下,标记创建了一个名为level的数组。下一节将详细介绍getwalls功能。它类似于一体机脚本中的函数。然而,在这种情况下,localStorage项的名称是由单选按钮决定的。代码是

for (i=0;i<document.gf.level.length;i++) {
 if (document.gf.level[i].checked) {
     lsname= document.gf.level[i].value+"maze";
        break;
   }
}

for循环遍历所有的输入项。测试是基于 ?? 属性的。当它检测到一个true条件时,变量lsname从该项的值属性中构造,并且break;语句导致执行离开for循环。如果您希望单选按钮从选中的一项开始,请使用如下代码:

<input type="radio" value="easy" name="level" checked />

或者

<input type="radio" value="easy" name="level" checked="true" />

构建应用程序并使之成为您自己的应用程序

现在让我们看看迷宫应用程序的代码,首先是一体化脚本,然后是双脚本版本的第二个脚本。

表 7-2 显示了脚本中用于创建、保存、检索和走迷宫的函数。注意,许多函数的调用都是通过事件处理来完成的:调用onLoadonSubmitaddEventListener。这些并不直接或立即调用函数,而是设置在指示的事件发生时进行调用。

表 7-2

迷宫应用程序中的函数

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 由body标签中的onLoad动作调用 | drawall |
| drawall | initstartwallstretchwallgetkeyAndMovegetwalls | draw用于Wall s 和令牌的方法:drawtokendrawAline |
| Token | var声明mypent的语句 |   |
| Wall | startwall, getwalls |   |
| drawtoken | drawalleverything数组中的令牌对象使用draw方法 |   |
| movetoken | getkeyAndMove使用moveit方法进行mypent | intersect |
| drawAline | drawalleverything数组中的Wall对象使用draw方法 |   |
| startwall | 由init中的addEventListener调用动作调用 | drawall, Wall |
| stretchwall | 由init中的addEventListener调用动作调用 | drawall |
| finish | 由init中的addEventListener调用动作调用 |   |
| getkeyAndMove | 由init中的addEventListener调用动作调用 | movetoken使用moveit方法进行mypent |
| savewalls | 由sf formonSubmit动作调用 |   |
| getwalls | 由gf表单的onSubmit动作调用 | drawall, Wall |

表 7-3 显示了迷宫应用程序的完整代码,并附有注释。

表 7-3

一体化迷宫应用的完整代码

*
|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Build maze & travel maze</title> | 完成title元素。 |
| <script type="text/javascript"> | 开始script标签。 |
| var cwidth = 900; | 清理画布。 |
| var cheight = 350; | 清理画布。 |
| var ctx; | 来保存画布上下文。 |
| var everything = []; | 容纳一切。 |
| var curwall; | 进行中的墙。 |
| var wallwidth = 5; | 固定墙宽。 |
| var wallstyle = "rgb(200,0,200)"; | 固定墙面颜色。 |
| var walls = []; | 守住所有的墙。 |
| var inmotion = false; | 通过拖动建造墙时的标志。 |
| var unit = 10; | 令牌的运动单位。 |
| function Token(sx,sy,rad,stylestring,n) { | 构建令牌的函数头。 |
| this.sx = sx; | 设置sx属性。 |
| this.sy = sy; | … sy |
| this.rad = rad; | … rad(半径)。 |
| this.draw = drawtoken; | 设置draw方法。 |
| this.n = n; | … n边数。 |
| this.angle = (2*Math.PI)/n ; | 计算并设置角度。 |
| this.moveit = movetoken; | 设置moveit方法。 |
| this.fillstyle = stylestring; | 设置颜色。 |
| } | 关闭功能。 |
| function drawtoken() { | 函数头 drawtoken。 |
| ctx.fillStyle=this.fillstyle; | 设置颜色。 |
| var i; | 索引。 |
| var rad = this.rad; | 设置rad。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(this.sx+rad*Math.cos➥(-.5*this.angle),this.sy+rad*Math.sin➥(-.5*this.angle)); | 移动到令牌多边形(五边形)的第一个顶点。 |
| for (i=1;i<this.n;i++) { | For循环绘制令牌的n条边:在本例中是五条边。 |
| ctx.lineTo(this.sx+rad*Math.cos➥((i-.5)*this.angle),this.sy+rad*Math.sin➥((i-.5)*this.angle)); | 指定到下一个顶点的线,设置五边形的边的绘制。 |
| } | 关闭for。 |
| ctx.fill(); | 抽取令牌。 |
| } | 关闭功能。 |
| function movetoken(dx,dy) { | 函数头。 |
| this.sx +=dx; | 增加x值。 |
| this.sy +=dy; | 增加y值。 |
| var i; | 索引。 |
| var wall; | 用于每面墙。 |
| for(i=0;i<walls.length;i++) { | 环绕整面墙。 |
| wall = walls[i]; | 提取第一面墙。 |
| if (intersect(wall.sx,wall.sy,➥wall.fx,wall.fy,this.sx,this.sy,➥this.rad)) { | 检查互联系统。如果在标记的新位置和这个特定的墙壁之间有交叉。 |
| this.sx -=dx; | …将x变回——不要移动。 |
| this.sy -=dy; | …将y变回——不要移动。 |
| break; | 离开for循环,因为没有必要再检查是否与一面墙发生碰撞。 |
| } | 关闭if true子句。 |
| } | 关闭for回路。 |
| } | 关闭功能。 |
| function Wall(sx,sy,fx,fy,width,stylestring) { | 功能头制作Wall。 |
| this.sx = sx; | 设置sx属性。 |
| this.sy = sy; | 设置sy。 |
| this.fx = fx; | 设置fx。 |
| this.fy = fy; | 设置fy。 |
| this.width = width; | 设置width。 |
| this.draw = drawAline; | 设置draw方法。 |
| this.strokestyle = stylestring; | 设置strokestyle。 |
| } | 关闭功能。 |
| function drawAline() { | 功能头drawAline。 |
| ctx.lineWidth = this.width; | 设置线条宽度。 |
| ctx.strokeStyle = this.strokestyle; | 设置strokestyle。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(this.sx,this.sy); | 移动到行首。 |
| ctx.lineTo(this.fx,this.fy); | 将生产线设置为结束。 |
| ctx.stroke(); | 划清界限。 |
| } | 关闭功能。 |
| var mypent = new Token(100,100,20,"rgb(0,0,250)",5); | 将mypent设置成五边形作为游戏棋子。 |
| everything.push(mypent); | 添加到everything。 |
| function init(){ | 功能头init。 |
| ctx = document.getElementById➥('canvas').getContext('2d'); | 定义所有绘图的ctx(上下文)。 |
| canvas1 = document.getElementById('canvas'); | 定义canvas1,用于事件。 |
| canvas1.addEventListener('mousedown',➥startwall,false); | 为mousedown设置处理。 |
| canvas1.addEventListener('mousemove',➥stretchwall,false); | 为mousemove设置处理。 |
| canvas1.addEventListener('mouseup',finish,➥false); | 为mouseup设置处理。 |
| window.addEventListener('keydown',➥getkeyAndMove,false); | 为箭头键的使用设置处理。 |
| drawall(); | 画出一切。 |
| } | 关闭功能。 |
| function startwall(ev) { | 功能头startwall。 |
| var mx; | 按住鼠标x。 |
| var my; | 按住鼠标y。 |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | 我们可以用layerX来确定鼠标的位置吗?有必要,因为浏览器不一样。 |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX➥ &#124;&#124; ev.offsetX == 0) { | 我们还能用offsetX吗? |
| mx = ev.offsetX; | 设置mx。 |
| my = ev.offsetY; | 设置my。 |
| } | 关闭子句。 |
| curwall = new Wall(mx,my,mx+1,my+1,wallwidth,wallstyle); | 创建新墙。在这一点上它是小的。 |
| inmotion = true; | 将inmotion设置为true。 |
| everything.push(curwall); | 给每样东西都加上curwall。 |
| drawall(); | 画出一切。 |
| } | 关闭功能。 |
| function stretchwall(ev) { | 功能头stretchwall to,在拖动鼠标的同时,通过拖动鼠标来拉伸一面墙。 |
| if (inmotion) { | 检查inmotion是否。 |
| var mx; | 按住鼠标x。 |
| var my; | 按住鼠标y。 |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | 我们可以用layerX吗? |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX➥ &#124;&#124; ev.offsetX == 0) { | 我们还能用offsetX吗?这对于不同的浏览器是必要的。 |
| mx = ev.offsetX; | 设置mx。 |
| my = ev.offsetY; | 设置my。 |
| } | 关闭子句。 |
| curwall.fx = mx; | 将curwall.fx改为mx。 |
| curwall.fy = my; | 将curwall.fy改为my。 |
| drawall(); | 绘制所有内容(将显示不断增长的墙)。 |
| } | 如果inmotion关闭。 |
| } | 关闭功能。 |
| function finish(ev) { | 功能头finish。 |
| inmotion = false; | 将inmotion设置为false。 |
| walls.push(curwall); | 将curwall加到walls上。 |
| } | 关闭功能。 |
| function drawall() { | 功能头drawall。 |
| ctx.clearRect(0,0,cwidth,cheight); | 擦除整个画布。 |
| var i; | 索引。 |
| for (i=0;i<everything.length;i++) { | 循环遍历所有内容。 |
| everything[i].draw(); | 绘制everything. |
| } | 闭环。 |
| } | 关闭功能。 |
| function getkeyAndMove(event) { | 功能头getkeyAndMove。 |
| var keyCode; | 按住keyCode。 |
| if(event == null) { | 如果事件null。 |
| keyCode = window.event.keyCode; | 使用window.event获取keyCode。 |
| window.event.preventDefault(); | 停止默认操作。 |
| } | 关闭子句。 |
| else { | 否则。 |
| keyCode = event.keyCode; | 从事件中获取keyCode。 |
| event.preventDefault(); | 停止默认操作。 |
| } | 关闭子句。 |
| switch(keyCode) { | 打开keyCode。 |
| case 37: | 如果向左箭头。 |
| mypent.moveit(-unit,0); | 水平向后移动。 |
| break; | 离开开关。 |
| case 38: | 如果向上箭头。 |
| mypent.moveit(0,-unit); | 上移屏幕。 |
| break; | 离开开关。 |
| case 39: | 如果向右箭头。 |
| mypent.moveit(unit,0); | 向左移动。 |
| break; | 离开开关。 |
| case 40: | 如果向下箭头。 |
| mypent.moveit(0,unit); | 向屏幕下方移动。 |
| break; | 离开开关。 |
| default: | 还有别的吗? |
| window.removeEventListener('keydown',➥getkeyAndMove,false); | 停止监听钥匙。假设玩家试图保存到本地存储或从本地存储中检索。 |
| } | 关闭开关。 |
| drawall(); | 画出一切。 |
| } | 关闭功能。 |
| function intersect(sx,sy,fx,fy,cx,cy,rad) { | 函数头相交。 |
| var dx; | 对于中间值。 |
| var dy; | 对于中间值。 |
| var t; | 用于t中的表达式。 |
| var rt; | 保持距离的平方。 |
| dx = fx-sx; | 设置 x 差异。 |
| dy = fy-sy; | 设置 y 差异。 |
| t =0.0-((sx-cx)*dx+(sy-cy)*dy)/➥((dx*dx)+(dy*dy)); | 这条线是从每个点到cx,cy的距离平方公式推导出来的。然后求导,求出 0。 |
| if (t<0.0) { | 如果最近的是在t <0。 |
| t=0.0; } | 在 0 处检查(这将更进一步)。 |
| else if (t>1.0) { | 如果最近的是在t>1。 |
| t = 1.0; | 在 1 处检查(这将更进一步)。 |
| } | 关闭子句。 |
| dx = (sx+t*(fx-sx))-cx; | 计算该值t的差值。 |
| dy = (sy +t*(fy-sy))-cy; | 计算该值t的差值。 |
| rt = (dx*dx) +(dy*dy); | 计算距离平方。 |
| if (rt<(rad*rad)) { | 与rad的平方进行比较。 |
| return true; } | 返回true。 |
| else { | 否则。 |
| return false;} | 返回false。 |
| } | 关闭功能。 |
| function savewalls() { | 功能savewalls标题。 |
| var w = []; | 临时数组。 |
| var allw=[]; | 临时数组。 |
| var sw; | 握住最后一根弦。 |
| var onewall; | 握住中间绳。 |
| var i; | 索引。 |
| var lsname = document.sf.slname.value; | 提取玩家的名字用于本地存储。 |
| for (i=0;i<walls.length;i++) { | 环绕整面墙。 |
| w.push(walls[i].sx); | 将sx添加到w数组中。 |
| w.push(walls[i].sy); | 将sy添加到w数组 |
| w.push(walls[i].fx); | 将fx添加到w数组 |
| w.push(walls[i].fy); | 将fy添加到w数组 |
| onewall = w.join("+"); | 做一串。 |
| allw.push(onewall); | 添加到allw数组。 |
| w = []; | 将w重置为空数组。 |
| } | 闭环。 |
| sw = allw.join(";"); | 现在把allw做成一个字符串。 |
| try { | 试试看。 |
| localStorage.setItem(lsname,sw); | 保存localStorage。 |
| } | 结束尝试。 |
| catch (e) { | 如果一个可捕捉的错误。 |
| alert("data not saved,➥ error given: "+e); | 显示消息。 |
| } | 结束 catch 子句。 |
| return false; | 返回false避免刷新。 |
| } | 关闭功能。 |
| function getwalls() { | 功能头getwalls。 |
| var swalls; | 临时存储。 |
| var sw; | 临时存储。 |
| var i; | 索引。 |
| var sx; | 保持sw值。 |
| var sy; | 保持sy值。 |
| var fx; | 保持fx值。 |
| var fy; | 保持fy值。 |
| var curwall; | 保持正在创建的墙。 |
| var lsname = document.gf.glname.value; | 提取玩家的名字用于存储被检索。 |
| swalls=localStorage.getItem(lsname); | 去拿仓库。 |
| if (swalls!=null) { | 如果有东西被拿走了。 |
| wallstgs = swalls.split(";"); | 分裂成一个阵列。 |
| for (i=0;i<wallstgs.length;i++) { | 遍历这个数组。 |
| sw = wallstgs[i].split("+"); | 拆分单个项目。 |
| sx = Number(sw[0]); | 提取第 0 值并转换为数字。 |
| sy = Number(sw[1]); | …1 st |
| fx = Number(sw[2]); | …2 个 |
| fy = Number(sw[3]); | …3 个 rd |
| curwall = new Wall(sx,sy,fx,fy,wallwidth,wallstyle); | 使用提取值和固定值创建新的Wall。 |
| walls.push(curwall); | 添加到walls数组。 |
| everything.push(curwall); | 添加到everything数组。 |
| } | 闭环。 |
| drawall(); | 画出一切。 |
| } | 如果不为空,则关闭。 |
| else { | 为空。 |
| alert("No data retrieved."); | 没有数据。 |
| } | 关闭子句。 |
| window.addEventListener('keydown',➥getkeyAndMove,false); | 设置keydown动作。 |
| return false; | 返回false防止刷新。 |
| } | 关闭功能。 |
| </script> |   |
| </head> | 结束head元素。 |
| <body onLoad="init();" > | 启动body,建立对init的呼叫。 |
| <canvas id="canvas" width="900" height="350"> | Canvas标记。 |
| Your browser doesn't support the HTML5 element canvas. | 某些浏览器的警告。 |
| </canvas> | 关闭canvas。 |
| <br/> | 换行。 |
| Press mouse button down, drag➥ and release to make a wall. | 说明书。 |
| Use arrow keys to move token. <br/> | 指令和换行符。 |
| Pressing any other key will stop key➥ capture and allow you to save the➥ maze locally. | 说明书。 |
| <form name="sf" onSubmit="return savewalls()" > | 表单标签,设置对savewalls的调用。 |
| To save your maze, enter in a name and➥ click on the SAVE WALLS button. <br/> | 说明书。 |
| Name: <input name="slname" value="maze_name" type="text"> | 标签和输入字段。 |
| <input type="submit" value="SAVE WALLS"/> | Submit按钮。 |
| </form> | 关闭form。 |
| <form name="gf" onSubmit="return➥ getwalls()" > | 表单标签,设置对getwalls的调用。 |
| To add old walls, enter in the name and➥ click on the GET SAVED WALLS button. <br/> | 说明书。 |
| Name: <input name="glname" value="maze_name" type="text"> | 标签和输入字段。 |
| <input type="submit" value="GET➥ SAVED WALLS"/> | Submit按钮。 |
| </form> | 关闭form。 |
| </body> | 关闭body。 |
| </html> | 关闭 HTML。 |

创建第二个迷宫应用程序

localStorage数据可以由不同于创建数据的应用程序访问,只要它在同一服务器上。如前所述,这是一个安全特性,将本地存储的读者限制为同一服务器上的脚本。

第二个脚本基于这个特性。表 7-4 显示调用或被调用的函数;它是前一个的子集。

表 7-4

中的功能旅行迷宫脚本**

|

功能

|

调用方/被调用方

|

打电话

|
| --- | --- | --- |
| init | 由body标签中的onLoad动作调用 | drawall |
| drawall | InitstartwallstretchwallgetkeyAndMovegetwalls | draw用于Wall s 和令牌的方法:drawtokendrawAline |
| Token | var声明mypent的语句 |   |
| Wall | startwall, getwalls |   |
| drawtoken | drawalleverything数组中的令牌对象使用draw方法 |   |
| movetoken | getkeyAndMove使用moveit方法进行mypent | intersect |
| drawAline | drawalleverything数组中的Wall对象使用 draw 方法 |   |
| getkeyAndMove | 由init中的addEventListener调用动作调用 | movetoken使用moveit方法进行mypent |
| getwalls | 由gf表单的onSubmit动作调用 | drawall, Wall |
| intersect | movetoken |   |

这些函数与 all-in-one 脚本中的完全相同,只有一个例外,即getwalls函数,所以我只对新的或更改的代码进行了注释。这个应用程序也有单选按钮来代替表单输入字段。表 7-5 显示了 travelmaze 应用程序的完整代码。

表 7-5

完整代码为 的旅行迷宫脚本

|

密码

|

说明

|
| --- | --- |
| |   |
| |   |
| <title>Travel maze</title> | 旅行迷宫。 |
| <script type="text/javascript"> |   |
| var cwidth = 900; |   |
| var cheight = 700; |   |
| var ctx; |   |
| var everything = []; |   |
| var curwall; |   |
| var wallwidth = 5; |   |
| var wallstyle = "rgb(200,0,200)"; |   |
| var walls = []; |   |
| var inmotion = false; |   |
| var unit = 10 ; |   |
| function Token(sx,sy,rad,stylestring,n) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.rad = rad; |   |
| this.draw = drawtoken; |   |
| this.n = n; |   |
| this.angle = (2*Math.PI)/n |   |
| this.moveit = movetoken; |   |
| this.fillstyle = stylestring; |   |
| } |   |
| function drawtoken() { |   |
| ctx.fillStyle=this.fillstyle; |   |
| ctx.beginPath(); |   |
| var i; |   |
| var rad = this.rad ; |   |
| ctx.beginPath(); |   |
| ctx.moveTo(this.sx+rad*Math.cos➥(-.5*this.angle),this.sy+rad*Math.sin➥(-.5*this.angle)); |   |
| for (i=1;i<this.n;i++) { |   |
| ctx.lineTo(this.sx+rad*Math.cos➥((i-.5)*this.angle),this.sy+rad*➥Math.sin((i-.5)*this.angle)); |   |
| } |   |
| ctx.fill(); |   |
| } |   |
| function movetoken(dx,dy) { |   |
| this.sx +=dx; |   |
| this.sy +=dy; |   |
| var i; |   |
| var wall; |   |
| for(i=0;i<walls.length;i++) { |   |
| wall = walls[i]; |   |
| if (intersect(wall.sx,wall.sy,➥wall.fx,wall.fy,this.sx,this.sy,``this.rad)) { |   |
| this.sx -=dx; |   |
| this.sy -=dy ; |   |
| break; |   |
| } |   |
| } |   |
| } |   |
| function Wall(sx,sy,fx,fy,width,stylestring) { |   |
| this.sx = sx; |   |
| this.sy = sy; |   |
| this.fx = fx; |   |
| this.fy = fy; |   |
| this.width = width; |   |
| this.draw = drawAline; |   |
| this.strokestyle = stylestring; |   |
| } |   |
| function drawAline() { |   |
| ctx.lineWidth = this.width; |   |
| ctx.strokeStyle = this.strokestyle; |   |
| ctx.beginPath(); |   |
| ctx.moveTo(this.sx,this.sy); |   |
| ctx.lineTo(this.fx,this.fy); |   |
| ctx.stroke() ; |   |
| } |   |
| var mypent = new Token(100,100,20,"rgb(0,0,250)",5); |   |
| everything.push(mypent); |   |
| function init(){ |   |
| ctx = document.getElementById('canvas')➥.getContext('2d'); |   |
| window.addEventListener('keydown',➥getkeyAndMove,false); |   |
| drawall(); |   |
| } |   |
| function drawall() { |   |
| ctx.clearRect(0,0,cwidth,cheight); |   |
| var i; |   |
| for (i=0;i<everything.length;i++) { |   |
| everything[i].draw() ; |   |
| } |   |
| } |   |
| function getkeyAndMove(event) { |   |
| var keyCode; |   |
| if(event == null) |   |
| { |   |
| keyCode = window.event.keyCode; |   |
| window.event.preventDefault(); |   |
| } |   |
| else |   |
| { |   |
| keyCode = event.keyCode; |   |
| event.preventDefault(); |   |
| } |   |
| switch(keyCode) |   |
| { |   |
| case 37:  //left arrow |   |
| mypent.moveit(-unit,0); |   |
| break ; |   |
| case 38:  //up arrow |   |
| mypent.moveit(0,-unit); |   |
| break; |   |
| case 39: //right arrow |   |
| mypent.moveit(unit,0); |   |
| break; |   |
| case 40:  //down arrow |   |
| mypent.moveit(0,unit); |   |
| break; |   |
| default: |   |
| window.removeEventListener➥('keydown',getkeyAndMove,false); |   |
| } |   |
| drawall(); |   |
| } |   |
| function intersect(sx,sy,fx,fy,cx,cy,rad) { |   |
| var dx; |   |
| var dy; |   |
| var t ; |   |
| var rt; |   |
| dx = fx-sx; |   |
| dy = fy-sy; |   |
| t =0.0-((sx-cx)*dx+(sy-cy)*dy)/((dx*dx)+(dy*dy)); |   |
| if (t<0.0) { |   |
| t=0.0; } |   |
| else if (t>1.0) { |   |
| t = 1.0; |   |
| } |   |
| dx = (sx+t*(fx-sx))-cx; |   |
| dy = (sy +t*(fy-sy))-cy; |   |
| rt = (dx*dx) +(dy*dy); |   |
| if (rt<(rad*rad)) { |   |
| return true; } |   |
| else { |   |
| return false;} |   |
| } |   |
| function getwalls() { |   |
| var swalls ; |   |
| var sw; |   |
| var i; |   |
| var sx; |   |
| var sy; |   |
| var fx; |   |
| var fy; |   |
| var curwall; |   |
| var lsname; |   |
| for (i=0;i<document.gf.level.length;i++) { | 遍历gf表单中的单选按钮,级别组。 |
| if (document.gf.level[i].checked) { | 这个单选按钮被选中了吗? |
| lsname= document.gf.level[i].value+"maze"; | 如果是这样,使用单选按钮元素的 value 属性构造本地存储名称。 |
| break; | 离开for循环。 |
| } | 关闭if。 |
| } | 关闭for。 |
| swalls=localStorage.getItem(lsname); | 从本地存储中获取此项目。 |
| if (swalls!=null) { | 如果不是 null,就是好数据。 |
| wallstgs = swalls.split(";"); | 提取每面墙的线。 |
| walls = []; | 从墙阵列中移除任何旧墙。 |
| everything = []; | 从everything阵列中移除任何旧墙。 |
| everything.push(mypent); | 将名为mypent的五边形令牌添加到所有东西中。 |
| for (i=0;i<wallstgs.length;i++) { | 开始解码每面墙。其余代码与一体化应用程序相同。 |
| sw = wallstgs[i].split("+"); |   |
| sx = Number(sw[0]); |   |
| sy = Number(sw[1]); |   |
| fx = Number(sw[2]); |   |
| fy = Number(sw[3]); |   |
| curwall = new Wall(sx,sy,fx,fy,wallwidth,wallstyle); |   |
| walls.push(curwall); |   |
| everything.push(curwall); |   |
| } |   |
| drawall(); |   |
| } |   |
| else { |   |
| alert("No data retrieved."); |   |
| } |   |
| window.addEventListener('keydown',➥getkeyAndMove,false); |   |
| return false ; |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();" > |   |
| <canvas id="canvas" width="900" height="700"> |   |
| Your browser doesn't support the HTML5 element canvas. |   |
| </canvas> |   |
| <br/> |   |
| Choose level and click GET MAZE button to➥ get a maze : |   |
| <form name="gf" onSubmit="return getwalls()" > |   |
| <br/> |   |
| <input type="radio" value="hard" ➥ name="level" />Hard <br/> | 设置单选按钮,普通级别,硬值。 |
| <input type="radio" value="moderate" ➥ name="level" />Moderate <br/> | 设置单选按钮,普通级别,值适中。 |
| <input type="radio" value="easy" ➥ name="level" />Easy<br/> | 设置单选按钮,普通级别,值容易。 |
| <input type="submit" value="GET maze"/><br/> |   |
| </form> |   |
| <p> |   |
| Use arrow keys to move token . |   |
| </p> |   |
| </body> |   |
| </html> |   |

有很多方法可以让这个应用程序成为你自己的。

在一些应用程序中,用户通过拖动将对象放置在屏幕上,这种做法通过将端点对齐网格点来限制可能性,甚至可能将迷宫的墙壁严格限制为水平或垂直。

第二个应用程序有两个级别的用户:迷宫的创建者和试图穿越迷宫的玩家。你可能想设计非常复杂的迷宫,为此你需要一个编辑工具。另一个很棒的新增功能是计时功能。回头看看第五章中记忆游戏的计时,了解计算经过时间的方法。

正如我们在第六章中为智力竞赛节目添加了一段视频,当有人完成一个迷宫时,你可以播放一段视频。

保存到本地存储的能力是一个强大的功能。对于这一点,以及任何需要相当长时间的游戏或活动,您可能希望添加保存当前状态的功能。本地存储的另一个常见用途是保存最佳分数。

请理解,我想演示复杂数据的本地存储的使用,这些应用程序确实做到了这一点。但是,您可能想使用本地存储之外的东西来开发迷宫程序。要构建这个应用程序,您需要为每面墙定义起点和终点的顺序,总共四个数字,并相应地定义墙。期待第九章 Hangman 游戏中实现为外部脚本文件的单词列表。

本章和上一章演示了鼠标、按键和计时的事件和事件处理。新设备提供了新的事件,如摇动手机或在屏幕上使用多点触摸。利用您在这里获得的知识和经验,您将能够组装许多不同的交互式应用程序。

测试和上传应用程序

第一个应用程序在一个 HTML 文档buildmazesavelocally.html中完成。第二个应用程序使用两个文件,buildmazes.htmltravelmaze.html。除了标题之外,buildmazesavelocally.htmlbuildmaze.html文件完全相同。这三个文件都有源代码。请注意,在您创建迷宫并使用本地存储保存到您自己的计算机上之前,travelmaze.html不会起作用。

双脚本版本的两个 HTML 文档可以在本地为现代浏览器工作,但是必须都上传到同一个服务器上,以测试服务器上的构建程序保存的迷宫是否可以被服务器上的旅行程序使用。

有些人可能会限制本地存储和 cookies 的使用。这些结构之间存在差异。在生产应用程序中使用这些都需要大量的工作。最终的退路是使用 PHP 之类的语言将信息存储在服务器上。

如果您打开了多个应用程序,您需要意识到“计算机”,即操作系统,需要确定哪个应用程序来处理任何按键操作。使用的术语是“焦点”。你可能需要用鼠标点击装有迷宫程序的窗口。这将设置焦点,单击箭头键将起作用。

摘要

在这一章中,你学习了如何实现一个程序来支持建造一个墙壁迷宫,并把它存储在本地计算机上。您还学习了如何创建一个迷宫旅行游戏。我们使用了以下编程技术和 HTML5 特性:

  • 程序员定义的对象

  • 捕捉击键;也就是说,为按键设置事件处理并解密哪个按键被按下

  • 用于在玩家的电脑上保存迷宫墙壁的布局

  • trycatch检查某个编码是否可接受

  • 用于数组的join方法和用于字符串的split方法

  • 鼠标事件

  • 用于确定标记和迷宫墙壁之间碰撞的数学计算

  • 单选按钮为玩家提供选择。

对于这个应用程序来说,本地存储的使用相当复杂,需要对迷宫信息进行编码和解码。一个更简单的用途可能是存储任何游戏的最高分数或当前分数。你可以回到前面的章节,看看你是否能加入这个特性。记住localStorage是和浏览器绑定的。在下一章中,你将学习如何实现石头剪子布游戏,以及如何在你的应用程序中加入音频。*

八、石头剪刀布

在本章中,我们将介绍

  • 与电脑对战

  • 创建用作按钮的图形

  • 游戏规则的数组

  • font-family属性

  • 继承的样式设置

  • 声音的

介绍

本章结合了编程技术和 HTML5 JavaScript 特性来实现大家熟悉的石头剪子布游戏。在这个游戏的校园版中,每个玩家用手的符号来表示三种可能性中的一种:石头、布或剪刀。术语是玩家抛出三个选项中的一个。游戏规则是这样规定的:

  • 石头压碎剪刀

  • 纸覆盖岩石

  • 剪刀剪纸

所以每个符号打败一个其他的符号:石头打败剪刀;纸打败了石头;剪刀打败了布。如果两个玩家扔同样的东西,那就是平局。

由于这是一个双人游戏,我们的玩家将与电脑对战,我们必须创造电脑的移动。我们将生成随机移动,玩家需要相信程序正在这样做,而不是基于玩家的投掷来移动。演示必须加强这种信任。

我们游戏的第一个版本只是使用了你将在这里看到的视觉效果。第二个版本增加了音频、由三个获胜事件控制的四个不同的剪辑以及平局选项。您可以使用源代码提供的声音文件,也可以使用您自己的声音。请注意,您需要更改代码中的文件名,以匹配您使用的声音文件。

在这种情况下,我们希望为玩家的移动使用特殊的图形。图 8-1 显示了应用程序的开始屏幕,包括三个用作按钮的图形,以及一个标有字符串"Score:"的字段,该字段保存初始值零。

img/214814_2_En_8_Fig1_HTML.jpg

图 8-1

石头剪刀布开场画面

玩家通过点击其中一个符号来移动。让我们看一个玩家点击石头图标的例子。我们假设电脑选择了剪刀。在一个简短的动画序列之后,一个剪刀符号开始变小并在屏幕上变大,出现一条文本消息,如图 8-2 所示。在添加了音频的版本中,音频剪辑将播放与石头压碎剪刀相对应的声音。请注意,现在的分数是 1。

img/214814_2_En_8_Fig2_HTML.jpg

图 8-2

玩家扔石头,电脑扔剪刀

接下来在游戏中,玩家和电脑打成平手,如图 8-3 所示。出现平局时分数没有变化,所以分数仍然是 1。

img/214814_2_En_8_Fig3_HTML.jpg

图 8-3

一条领带

后来,游戏已经打平,但玩家输了,分数降到负 1,这意味着玩家落后,如图 8-4 所示。

img/214814_2_En_8_Fig4_HTML.jpg

图 8-4

在游戏的后期,一个失败的举动

与本书中的所有示例一样,这个应用程序只是一个开始。普通版本和音频版本都为玩家保持一个连续的分数,在该分数中,损失导致减少。另一种方法是保留玩家和电脑各自的分数,只计算双方的胜利。你可以显示一个单独的游戏计数。如果您不想显示负数,这是更可取的。你也可以使用localStorage保存玩家的分数,如第七章中的迷宫游戏所述。

一个更精细的增强功能可能是视频剪辑(回头看第六章)或动画 gif,显示石头粉碎剪刀、纸覆盖石头和剪刀切纸。你也可以把它看作许多不同游戏的模型。在所有情况下,你都需要确定如何捕捉玩家的走法,如何生成电脑的走法;你需要代表并执行游戏规则;并且你需要维护游戏的状态并显示给玩家。石头剪子布游戏除了跑分没有状态信息。换句话说,一场游戏只有一个回合。这与第二章中描述的掷骰子游戏形成对比,在第章中,一个游戏可以包括一次到任意次掷骰子,或者第五章中描述的集中游戏,其中一轮包括两次纸牌选择,一个完整的游戏可以进行任意轮次,最少等于纸牌数量的一半。

注意

有石头剪子布的比赛,也有计算机系统,其中计算机根据玩家的移动历史来移动。甚至还有计算机对抗计算机的比赛。

关键要求

石头剪子布的实现使用了前面章节中演示的许多 HTML5 和 JavaScript 结构,在这里以不同的方式放在一起。编程类似于写作。它是以某种逻辑顺序将想法的表达放在一起,就像将单词组合成句子,将句子组合成段落,等等。在阅读本章的时候,回想一下你所学的关于在画布上绘制矩形、图像和文本,检测玩家点击鼠标的位置,使用setInterval设置定时事件来制作动画,以及使用数组来保存信息。这些是石头剪子布应用程序的构建块。

在设计这个应用程序时,我知道我想让我们的玩家点击按钮,一个按钮对应游戏中的一种投掷方式。一旦玩家投掷了一次,我想让程序自己移动,即随机选择,并在屏幕上显示与该移动相对应的图片。然后程序会应用游戏规则来显示结果。会播放一种声音,对应于一掷胜另一掷的三种可能情况,还有平局时的呻吟。

这个应用程序从屏幕上出现的按钮或图标开始。玩家可以点击这些图片进行移动。还有一个放乐谱的盒子。

应用程序必须随机生成计算机的走法,然后以一种看起来好像计算机和玩家同时出招的方式显示出来。我的想法是让适当的符号在屏幕上开始变小,然后变大,看起来就像电脑向玩家投掷一样。这个动作在玩家点击三个可能的投掷中的一个后立即开始,但它很快就足以给人一种两者同时发生的印象。

游戏规则必须遵守!这既包括什么打败什么,也包括解释它的民间信息——“石头砸剪刀”;“纸包石头”,还有“剪刀剪纸”。显示的分数会增加一分、减少一分或保持不变,这取决于该回合是赢、输还是平。

游戏的音频增强版本必须根据情况播放四个音频剪辑中的一个。

HTML5、CSS 和 JavaScript 特性

现在让我们看看 HTML5、CSS 和 JavaScript 的具体特性,它们提供了我们实现游戏所需的东西。除了基本的 HTML 标签和函数以及变量,这里的解释都是完整的。如果你已经阅读了其他章节,你会注意到这一章的大部分内容重复了前面给出的解释。

我们当然可以使用其他章节中演示的按钮类型,但是我希望这些按钮看起来像它们所代表的投掷。正如您将看到的,我们实现按钮的方式是建立在前面章节中演示的概念之上的。我们再次使用 JavaScript 伪随机处理来定义计算机移动,并使用setInterval来显示计算机移动的动画。

我们的石头剪子布游戏将展示 HTML5 的原生音频设备。我们将整合音频编码和游戏规则的应用。

为玩家提供图形按钮

在屏幕上生成可点击的按钮或图标有两个方面:在画布上绘制图形,检测玩家何时将鼠标移到按钮上并点击了鼠标主按钮。

我们将生成的按钮或图标包括一个矩形的轮廓(笔画),一个实心矩形,然后在矩形的顶部有一个垂直和水平边距的图像。由于所有三个按钮都会发生类似的操作,我们可以使用在第四章的炮弹和弹弓游戏中首次介绍的方法。我们将通过编写一个名为Throw的函数来建立一个程序员定义的对象类。回想一下,对象由组合在一起的数据和编码组成。该函数被描述为一个构造函数,将与操作符new一起使用,创建一个类型为Throw的新对象。术语this在函数中用于设置与每个对象相关的值。

function Throw(sx,sy, smargin,swidth,sheight,rectcolor,picture) {
  this.sx = sx;
  this.sy = sy;
  this.swidth = swidth;
  this.bwidth = swidth + 2*smargin;
  this.bheight = sheight + 2*smargin;
  this.sheight = sheight;
  this.fillstyle = rectcolor;
  this.draw = drawThrow;
  this.img = new Image();
  this.img.src = picture;
  this.smargin = smargin;
}

函数的参数保存所有信息。名称sxsy等的选择通过一个简单的修改避免了内置术语:将s放在前面。按钮的位置在sx, sy。矩形的颜色用rectcolor表示。图像的文件名由picture保存。我们认为的内部和外部宽度以及内部和外部高度是基于输入smarginsheightswidth计算的。bheightbwidth中的b代表大。s代表小型储物。不要太纠结于专有名词——根本没有这种东西。名字由你决定,如果一个名字有用,意味着你记得它,它就有用。

一个Throw对象的img属性是一个Image对象。那个Image对象的src指向在picture参数中传递给函数的文件名。

注意,属性this.draw被设置为drawThrow。这将设置drawThrow函数作为Throw类型的所有对象的draw方法。代码比它需要的更通用:三个图形中的每一个都有相同的边距、宽度和高度。然而,编写通用的代码并没有什么坏处,如果您想在这个应用程序的基础上构建一个更复杂的表示玩家选择的对象的应用程序,大部分代码都是可行的。

小费

如果你有this.draw = drawThrow;之类的代码,还没有写drawThrow函数,那么写程序的时候也不用担心。你会的。有时,在函数或变量被创建之前,避免引用它们是不可能的。关键因素是,所有这些编码都是在您尝试执行程序之前完成的。

下面是drawThrow方法:

function drawThrow() {
  ctx.strokeStyle = "rgb(0,0,0)";
  ctx.strokeRect(this.sx,this.sy,this.bwidth,this.bheight);
  ctx.fillStyle = this.fillstyle;
  ctx.fillRect(this.sx,this.sy,this.bwidth,this.bheight);
  ctx.drawImage(this.img,this.sx+this.smargin,this.sy+this.smargin,➥
      this.swidth,this.sheight);
}

正如承诺的那样,这使用黑色为颜色rgb(0,0,0)绘制了一个矩形的轮廓。回想一下,ctx是用用于绘图的canvas元素的属性设置的变量。黑色实际上是默认颜色,这一行就没有必要了。但是,我们将把它放在这里,以防您在之前已经更改过颜色的应用程序中重用这段代码。接下来,该函数使用为这个特定对象传入的rectcolor绘制一个填充矩形。最后,代码在矩形的顶部绘制一个图像,水平和垂直偏移一定的边距。计算出的bwidthbheight分别比swidthsheight大两倍的smargin值。这实际上使图像在矩形内居中。

三个按钮通过使用var语句创建为Throw对象,其中变量使用new操作符和对Throw构造函数的调用进行初始化。为了完成这项工作,我们需要石头、布、剪刀的照片,这些照片是我通过各种途径获得的。这三个图像文件与 HTML 文件位于同一文件夹中。

var rockb = new Throw(rockbx,rockby,8,50,50,"rgb(250,0,0)","rock.jpg");
var paperb = new Throw(paperbx,paperby,8,50,50,"rgb(0,200,200)","paper.gif");
var scib = new Throw(scissorsbx,scissorsby,8,50,50,"rgb(0,0,200)","scissors.jpg");

与我们之前的应用程序一样,名为everything的数组被声明并初始化为空数组。我们将所有三个变量放入everything数组,这样我们就可以系统地处理它们。

everything.push(rockb);
everything.push(paperb);
everything.push(scib);

例如,为了绘制所有的按钮,我们使用一个名为drawall的函数,该函数遍历everything数组中的元素。

function drawall() {
  ctx.clearRect(0,0,cwidth,cheight);
  var i;
  for (i=0;i<everything.length;i++) {
    everything[i].draw();
  }
}

同样,这比要求的更通用,但是它是有用的,特别是当涉及到面向对象编程时,尽可能地保持事物的通用性。

但是我们如何让这些图形充当可点击的按钮呢?因为这些是在画布上绘制的,所以代码需要为整个画布设置 click 事件处理,然后使用编码来检查哪个按钮(如果有)被单击了。

在第四章描述的弹弓游戏中,你看到了处理整个画布的mousedown事件的函数计算鼠标光标是否在球上的代码。在第六章描述的智力竞赛节目中,我们为每个国家和首都街区设置了事件处理。内置的 JavaScript 机制表明哪个对象收到了click事件。这个应用程序就像弹弓。

我们在init函数中设置了事件处理,在下一节中有完整的解释。任务是让 JavaScript 监听鼠标点击事件,然后在点击发生时执行我们指定的操作。我们想要的是调用函数choose。下面两行完成了这项任务。

canvas1 = document.getElementById('canvas');
canvas1.addEventListener('click',choose,false);

小费

我们的代码需要区分带有id画布的元素和由getContext('2d')返回的该元素的属性。这就是 HTML5 人决定要做的事情。这不是你可以自己推断出来的。

choose功能的任务是确定选择了哪种投掷方式,生成计算机移动并设置该移动的显示,以及应用游戏规则。现在,我们只看一下决定哪个按钮被点击的代码。

在我的实现中,我没有让任何讨厌的玩家在计算机移动出现时点击其中一个选项,也就是说,在屏幕上变得越来越大。我的能干的技术评论员,知道如何表现得像一个行为不端的玩家,提出了解决方案。我们使用一个全局变量,称为inmotion,并将其初始化为false

      var inmotion = false;

如果inmotiontrue,则choose功能不起作用。该变量在flyin功能中被设置为true,并且在确定动画结束时也被设置回false

代码从处理浏览器之间的差异开始。作为调用addEventListener的结果而被调用的函数被调用时带有一个保存事件信息的参数。这个参数,正如我们在choose函数中所称的ev,被检查以查看哪些属性可以被使用。这种复杂性是强加给我们的,因为浏览器使用不同的术语实现事件处理。

function choose(ev) {
if (!inmotion) {
var mx;
var my;
if ( ev.layerX ||  ev.layerX == 0) {
  mx= ev.layerX;
  my = ev.layerY;
} else if (ev.offsetX || ev.offsetX == 0) {
  mx = ev.offsetX;
  my = ev.offsetY;
}

这部分代码的目标是让变量mxmy在鼠标按钮被点击时分别保存鼠标光标的水平和垂直坐标。某些浏览器将光标信息保存在名为layerXlayerYev参数的属性中,而其他浏览器则使用offsetXoffsetY。我们将使用局部变量来确保在所有浏览器中跟踪光标位置。如果ev.layerX对于该浏览器不存在,或者如果它存在并且具有值0,则条件ev.layerX将评估为false。因此,为了检查属性是否存在,我们需要使用复合条件(ev.layerX || ev.layerX == 0)来确保代码在所有情况下都有效。顺便说一句,如果第二个if测试失败,什么都不会发生。这段代码适用于 Chrome、Firefox 和 Safari,但可能最终会适用于所有浏览器。

下一段代码遍历everything的元素(有三个元素,但没有明确提到)来查看光标是否在任何一个矩形上。变量ch保存了对Throw的引用,因此所有的Throw属性,即sxsybwidthbheight,都可以在比较语句中使用。这是保存在everything数组中的所有投掷选择的简写。

var i;
for (i=0;i<everything.length;i++){
  var ch = everything[i];
  if ((mx>ch.sx)&&(mx<ch.sx+ch.bwidth)&&(my>ch.sy)&&(my<ch.sy+ch.bheight)) {
    ...
    break;
  }
}

这...表示稍后将解释的编码。复合条件将点mx,my与代表玩家可能投掷的三个对象中的每一个的外部矩形的左侧、右侧、顶部和底部进行比较。这四个条件中的每一个都必须为真,点才会在矩形内。这是由& &符指示的。虽然很长,但这是检查矩形内部点的标准方法,您将习惯使用它。

这就是图形在画布上的绘制方式,以及它们作为按钮的作用。请注意,如果玩家在任何按钮之外单击,什么都不会发生。有些人可能会建议在这一点上向玩家提供反馈,比如一个警告框说:

Please make your move by clicking on the rock, paper, or scissors!

其他人会告诉你不要在屏幕上乱糟糟的,并假设玩家会知道该怎么做。

生成计算机移动

生成计算机移动类似于生成掷骰子,正如我们在第二章的掷骰子游戏中所做的那样。在石头剪子布游戏中,我们希望从三个可能的投掷中随机选择,而不是六个可能的死亡面。我们用下面一行得到这个数字:

var compch = Math.floor(Math.random()*3);

对内置方法Math.random()的调用产生一个从 0 到 1 的数,但不包括 1。将其乘以3得到一个从 0 到 3 的数,但不包括 3。应用Math.floor会产生一个不大于其参数的整数。它将数字向下舍入,去掉最高整数下限上的任何值。所以右边的表达式产生 0,1,或者 2,这正是我们想要的。这个值被分配给compch,它被声明(设置)为一个变量。

该代码采用计算机移动,通过涉及随机函数的计算选择数字 0、1 或 2 中的一个,并将其用作choices数组的索引:

var choices = ["rock.jpg","paper.gif","scissors.jpg"];

这三个元素指的是按钮中使用的相同的三个图片。

在这一点上,以防你担心,石头、布、剪刀的排序是任意的。我们需要保持一致,但顺序并不重要。如果在每一种情况下,我们都用纸、剪刀、石头来订货,一切都还会正常。玩家永远看不到 0 代表石头,1 代表布,2 代表剪刀的编码。

choose函数中接下来的几行提取其中一个文件名,并将其分配给图像变量compimgsrc属性。

var compchn = choices[compch];
compimg.src = compchn;

本地变量的名称compchn代表计算机选择名称。compimg变量是保存一个Image对象的全局变量。代码将其src属性设置为适当图像文件的名称,该文件将用于显示计算机移动。

为了实现游戏规则,我设置了两个数组:

var beats = [
  ["TIE: you both threw rock.","You win: paper covers rock.",➥
      "You lose: rock crushes scissors."],
  ["You lose: paper covers rock.","TIE: you both threw paper.",➥
       "You win: scissors cuts paper."],
  ["You win: rock crushes scissors.","You lose: scissors cuts paper.",➥
       "TIE: you both threw scissors"]];

以及:

var points = [
  [0,1,-1],
  [-1,0,1],
  [1,-1,0]];

每个都是数组的数组。这两个阵列一起被称为平行结构,意味着元素相互对应。当我解释声音的添加时,我将描述另一个并行结构,第三个数组。beats数组保存所有的消息,而points数组保存要加到玩家分数上的金额。加 1 增加玩家的分数。加一个-1,玩家的分数减少 1,这是玩家输一轮时我们想要的效果。加 0 保持分数不变。现在,你可能认为在平局的情况下什么都不做比加零更容易,但是从编码的角度来看,以统一的方式处理这个问题是更容易的方法,加 0 实际上可能比做一个if测试来查看它是否是平局花费的时间更少。

每个数组中的第一个索引来自计算机棋步compch,第二个索引i表示内部数组中的元素,来自玩家棋步。beatspoints阵列被称为并行结构。beats数组用于文本消息,而points数组用于计分。让我们通过选择一个对应于 2 的计算机棋步(比如剪刀)和一个对应于 0 的玩家棋步(比如石头)来检查信息是否正确。在beats数组中,计算机移动的值告诉我们转到索引值为 2 的数组。(我避免说第二个数组,因为数组从索引 0 开始,而不是从 1 开始。由 2 表示的值是数组的第三个元素。)元素是:

["You win: rock crushes scissors.","You lose: scissors cuts paper.",➥
       "TIE: you both threw scissors"]];

现在使用播放器值,即0,来索引这个数组。结果是"You win: rock crushes scissors.",这正是我们想要的。对points数组做同样的事情,索引为 2 的元素是:

[1,-1,0]

而这个数组中索引为 0 的值是1,也正是我们想要的:玩家的分数会被调整 1。

result = beats[compch][i];
...
newscore +=points[compch][i];

回想一下运算符+=中的语句

a += b;

解释如下:

获取变量 a 的值

将+运算符应用于该值和表达式 b 的值

将结果赋回变量 a

第二步是以一种通用的方式编写的,因为这可以应用于+解释为数字的相加以及字符串的连接。在这种特殊情况下,第二步是:

将 a 和 b 相加

这个结果被赋回给变量a

两个变量resultnewscore是全局变量。这意味着其他函数也可以使用它们,我们就是这样使用它们的:在一个函数中设置,在另一个函数中引用。

分数是用 HTML 文档的body元素中的form元素表示的。

<form name="f">
Score: <input name="score" value="0" size="3"/>
</form>

为了向您展示这些事情是如何完成的,我们将对 score 字段使用样式。我们设置了两种样式,一种用于表单,另一种用于输入字段。

form {
  color: blue;
  font-family: Georgia, "Times New Roman", Times, serif;
  font-size:16px;
}
input {
  text-align:right;
  font:inherit;
  color:inherit;
}

我们将表单中的文本颜色设置为蓝色,并使用font-family属性指定字体。如果客户端电脑上不存在特定字体,这是一种指定该字体和备份的方式。这是一个强大的功能,因为这意味着你可以在字体方面尽可能地具体,并且在工作中,仍然确保每个人都可以阅读材料。

小费

你可以在网上搜索网页安全字体,看看哪些字体可以广泛使用。然后你可以选择你最喜欢的字体作为第一选择,网页安全字体作为第二选择,最后选择衬线字体或无衬线字体。如果愿意,您甚至可以指定三个以上的选项。查看 http://en.wikipedia.org/wiki/Web_typography 获取创意。另一种选择是获取一种字体,并将该文件放在你的服务器上,并使用 CSS @font-face 规则将其与其他文件一起下载(参见 https://www.w3schools.com/css/css3_fonts.asp )。

在这种风格中,我们指定名为Georgia的字体,然后是"Times New Roman",然后是Times,然后是计算机上任何带有衬线的标准字体。衬线是字母上额外的小旗。因为名称涉及多个术语,所以Times New Roman周围的引号是必要的。其他字体名称的引号不会错,但它们不是必需的。我们还将大小指定为 16 像素。输入字段从它的父元素form中继承字体,包括大小和颜色。但是,因为分数是一个数字,所以我们使用text-align属性来表示字段中的右对齐。标签Scoreform元素中。实际分数在input元素中。使用输入样式属性的inherit设置使两者以相同的字体、大小和颜色显示。

将提取输入字段中的值,并使用其名称score进行设置。举个例子,

newscore = Number(document.f.score.value);

这里需要Number来产生字段中文本所代表的数字;这是 0,而不是“0”(字符)。如果我们把值保留为一个字符串,而代码使用一个加号把 1 加到一个字符串上,这就不是加法;而是字符串的连接。(顺便说一下,这被称为操作符重载:加号表示不同的操作,取决于操作数的数据类型。)将“1”连接到“0”上将产生“01”。你可能认为这没什么,但是下一次,我们会得到“011”或“010”或“01-1”。啊。我们不希望这样,所以我们编写代码来确保值被转换成数字。

要将调整后的新分数放回到字段中,代码为

document.f.score.value = String(newscore);

现在,正如我经常告诉我的学生,我不得不告诉你真相。其实,String在这里可能没有必要。JavaScript 有时会自动进行这些转换,也称为转换。然而,有时并不是这样,所以将其显式化是一种很好的做法。

字段的大小是三个字符所需的最大值。Georgia 字体不是等宽字体,所有字符的大小都不一样,所以这是可能需要的最大空间。根据字段中的文本,您可能会注意到不同的剩余空间量。

注意

JavaScript 使用圆括号、花括号和方括号。它们不可互换。圆括号用于function标题以及函数和方法调用中;在ifforswitchwhile报表表头;以及用于指定复杂表达式中的运算顺序。花括号用于界定函数的定义以及ifforswitchwhile语句的子句。方括号用于定义数组并返回数组的特定成员。级联样式表的语言将每种样式用花括号括起来。HTML 标记包括<>,通常称为尖括号或尖括号。

使用动画显示结果

你已经在第三章的弹跳球应用程序和第四章的炮弹和弹弓中看到了动画的例子。概括地说,动画是通过快速连续地显示一系列静止图片来制作的。单独的图片被称为帧。在所谓的计算动画中,对象在屏幕上的新位置是为每一个连续的帧计算的。制作动画的一种方法是使用setInterval命令设置一个interval事件,如下所示:

tid = setInterval(flyin,100);

这导致每 100 毫秒调用一次flyin函数(每秒 10 次)。用于定时器标识符的变量tid被设置,因此代码可以关闭interval事件。flyin功能将创建尺寸不断增加的Throw对象,并保存相应的图像。当对象达到指定的大小时,代码显示结果并调整分数。这就是为什么变量resultnewscore必须是全局变量——它们在choose中设置,在flyin中使用。

flyin函数还使用一个名为size的全局变量,该变量从 15 开始,每次调用flyin时递增 5。当size超过 50 时,计时事件停止,显示结果信息,分数改变。

function flyin() {
  inmotion = true;
  ctx.drawImage(compimg, 70,100,size,size);
  size +=5;
  if (size>50) {
    clearInterval(tid);
    ctx.fillText(result,200,100,250);
    document.f.score.value = String(newscore);
    inmotion = false;
  }
}

注意,flyin函数在每次被调用时都将inmotion设置为true,这意味着当inmotion已经为真时,它将被设置为true。这很好,也是这样做的。做任何检查都没有意义。请注意,它仅被设置为false一次。

顺便说一下,为了抓取这些截图,我不得不修改代码。图 8-5 是第一次调用flyin后的画面。

img/214814_2_En_8_Fig5_HTML.jpg

图 8-5

第一次调用 flyin,用一个小图像表示计算机移动

在对代码进行不同的修改后,图 8-6 显示了在后续步骤中暂停的动画。

img/214814_2_En_8_Fig6_HTML.jpg

图 8-6

动画的进一步发展

图 8-7 显示动画已完成,但就在带有结果的文本消息之前。

img/214814_2_En_8_Fig7_HTML.jpg

图 8-7

就在结果上显示文本之前

现在,这里有一个可以提供信息的坦白。您可能需要跳过前面的部分,或者等到通读完所有代码后再欣赏它。当我第一次创建这个应用程序时,我有在choose函数中显示消息和调整分数的代码。毕竟,这是代码决定值的地方。然而,这产生了很坏的影响。玩家在动画中看到计算机从屏幕上出现之前就看到了结果。看起来比赛被操纵了!当我意识到问题所在时,我修改了choose中的代码,将消息和新的得分值存储在全局变量中,在动画完成后,只显示消息并在form input字段中设置更新的得分。在开始之前,不要假设你能了解你的应用程序的一切。一定要假设你会发现问题并能够解决它们。公司有专门致力于质量保证的团队。

音频和 DOM 处理

音频的情况与视频的情况非常相似(参见第六章)。同样,坏消息是浏览器不能识别相同的格式。同样,好消息是 HTML5 提供了<audio>元素,JavaScript 提供了播放音频的特性,以及引用不同浏览器接受的不同音频格式的方法。此外,还提供了从一种格式转换到另一种格式的工具。我在这些例子中使用的两种格式是 MP3 和 OGG,对于 Chrome、Firefox 和 Safari 来说已经足够了。我使用了免费的音频剪辑源,并在 WAV 和 MP3 中找到了可接受的样本。然后我用之前下载的 Miro 转换器来处理视频,为 WAV 文件制作 MP3 和 OGG,为其他文件制作 OGG。OGG 的米罗名字是theor.ogv,为了简单起见我改了一下。音频转换有许多替代方法。这里的要点是,这种方法要求每个声音文件有两个版本。

警告

音频文件引用的顺序应该并不重要,但是我发现了警告,如果 MP3 列在最前面,Firefox 将无法工作。也就是说,它不会继续尝试处理另一个文件。这个问题现在可能已经消失了,因为浏览器在处理媒体时变得更加健壮。

<audio>元素具有我在石头剪刀布游戏中没有使用的属性。属性在加载时立即开始播放,尽管你需要记住大文件的加载不是瞬间的。src属性指定了来源。然而,好的做法是不要在<audio>标签中使用src属性,而是使用<source>元素作为<audio>元素的子元素来指定多个源。loop属性指定循环,即重复剪辑。属性将控件放在屏幕上。这可能是一件好事,因为剪辑可以非常响亮。但是,为了让音频更有惊喜,也为了不增加视觉展示的混乱,我选择不这样做。

这里有一个简单的例子供你尝试。你需要从这本书的下载页面下载sword.mp3,或者找到你自己的音频文件并在这里按名称引用。如果你在 Chrome 中打开下面的 HTML,你会看到如图 8-8 所示的内容。

img/214814_2_En_8_Fig8_HTML.jpg

图 8-8

带控件的音频标签

Audio example <br/>
<audio src="sword.mp3" autoplay controls>
Your browser doesn't recognize audio
</audio>

请记住:在我们的游戏中,我们将播放石头压碎剪刀、纸盖住石头、剪刀剪开纸以及任何平局的声音。以下是“石头剪子布”中四个音频片段的编码:

<audio preload= "auto">
<source src="hithard.ogg" />
<source src="hithard.mp3" />
</audio>
<audio preload= "auto">
<source src="inhale.ogg" />
<source src="inhale.mp3" />
</audio>
<audio preload= "auto">
<source src="sword.ogg" />
<source src="sword.mp3" />
</audio>
<audio preload= "auto"r>
<source src="crowdohh.ogg" />
<source src="crowdohh.mp3" />
</audio>

这对于描述四组音频文件来说应该是合理的,但是您可能想知道代码如何知道播放哪一组。我们可以在每个<audio>标签中插入id属性。然而,为了展示更多在许多情况下有用的 JavaScript,让我们做些别的事情。你已经看到了方法document.getElementById。还有一个类似的方法:document.getElementsByTagname。该行:

musicelements = document.getElementsByTagName("audio");

提取参数指示的标记名的所有元素,并创建一个数组,在这行代码中,该数组将数组赋给一个名为musicelements的变量。我们在init函数中使用这一行,所以它在应用程序的最开始执行。我们构造了另一个数组,这个数组叫做music,并增加了两个全局变量,总共有三个全局变量用于处理声音。

var music = [
  [3,1,0],
  [1,3,2],
  [0,2,3]];
var musicelements;
var musicch;

你可以查一下musicbeats并联结构,0 代表碎岩剪刀,1 代表覆岩纸,2 代表剪刀切纸,3 代表平手。choose函数将有额外的一行:

musicch = music[compch][i];

musicch变量——这个名字代表音乐的选择——将包含 0、1、2 或 3。当动画完成时,这会在flyin函数中设置一些事情发生。我们不立即播放剪辑,正如我在忏悔中解释的那样。

musicelements[musicch].play();

使用musicch的索引引用了musicelements中的第 0、第 1、第 2 或第 3 个元素,然后调用其play方法并播放剪辑。

出发

应用程序首先调用<body>标签的onLoad属性中的一个函数。这是其他比赛的惯例。init功能执行几项任务。它将初始分值设置为 0。这是必要的,以防播放器重新加载文档;HTML 的一个特点是,浏览器可能不会重置表单数据。该函数从canvas元素中提取值,用于绘制(ctx)和事件处理(canvas1)。这需要在加载整个文档之后发生,因为在此之前canvas元素不存在。该函数绘制三个按钮,并为画布上绘制的文本设置字体和填充样式。之后,除非玩家在三个符号中的一个上点击鼠标按钮,否则什么都不会发生。

既然我们已经研究了用于这个游戏的 HTML5 和 JavaScript 的具体特性,以及一些编程技术,比如数组的数组的使用,那么让我们更仔细地看看代码。

构建应用程序并使之成为您自己的应用程序

基本的石头剪子布应用程序使用样式、全局变量、六个函数和 HTML 标记。表 8-1 中描述了这六种功能。我遵循惯例,函数以小写字母开头,除非函数是程序员定义的对象的构造函数。我首先展示基本的应用程序,然后展示添加音频所需的修改。

表 8-1

基本石头剪刀布应用程序中的函数

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由标签<body>中的onLoad动作调用 | drawall |
| drawall | initchoose | 调用每个对象的draw方法,在这个应用程序中总是在函数drawThrow中 |
| Throw | var用于全局变量的语句 |   |
| drawThrow | drawall使用Throw对象的draw方法 |   |
| choose | 由init中 addEventListener 调用的动作调用 | drawall |
| flyin | choosesetInterval的动作 |   |

从表中可以看出,大多数函数调用都是隐式完成的,例如通过事件处理,而不是一个函数调用另一个函数。在init功能完成设置后,主要工作由choose功能执行。游戏规则的关键信息保存在两个数组中。

表 8-2 显示了基本应用程序的代码,每一行都有注释。

表 8-2

基本石头剪刀布应用程序的完整代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Rock Paper Scissors</title> | 完成title元素。 |
| <style> | 开始style部分。 |
| form { | 为所有form元素指定的样式。这份文件里只有一个。 |
| color: blue; | 文本颜色设置为蓝色,这是已知的 16 种颜色之一。 |
| font-family: Georgia, "Times New``Roman", Times, serif; | 设置要尝试使用的字体。 |
| font-size:16px; | 设置字符的大小。 |
| } | 封闭风格。 |
| input { | 为所有输入元素指定的样式。只有一个。 |
| text-align:right; | 使文本向右对齐,适合数字。 |
| font:inherit ; | 从父级继承任何字体信息,即form。 |
| color:inherit; | 从父级继承文本的颜色,即form。 |
| } | 封闭风格。 |
| </style> | 关闭style元素。 |
| <script > | 开始script元素。 |
| var cwidth = 600; | 画布宽度,用于清除。 |
| var cheight = 400; | 画布高度,用于清除。 |
| var ctx; | Canvas ctx,用于所有绘图。 |
| var everything = []; | 保存三个图形。 |
| var rockbx = 50; | 岩石符号的水平位置。 |
| var rockby = 300; | 岩石符号的垂直位置。 |
| var paperbx = 150; | 纸张符号的水平位置。 |
| var paperby = 300; | 纸张符号的垂直位置。 |
| var scissorsbx = 250; | 剪刀符号的水平位置。 |
| var scissorsby = 300; | 剪刀符号的垂直位置。 |
| var canvas1; | 为画布设置单击事件监听的参考。 |
| var newscore; | 要为新分数设置的值。 |
| var size = 15; | 计算机移动时改变图像的初始大小。 |
| var result; | 作为结果消息显示的值。 |
| var choices = ["rock.jpg",``"paper.gif","scissors.jpg"]; | 符号图像的名称。 |
| var compimg = new Image(); | 用于每次计算机移动的图像元素。 |
| var beats = [ | 保存所有消息的数组声明的开始。 |
| ["TIE: you both threw``rock","You win: computer played rock",``"You lose: computer threw rock"], | 电脑扔石头时的那组信息。 |
| ["You lose: computer``threw paper","TIE: you both threw paper",``"You win: computer threw paper"], | 电脑扔纸时的信息集。 |
| ["You win: computer``threw scissors","You lose: computer``threw scissors","TIE: you both threw``scissors"]]; | 电脑投剪刀时的那组信息。 |
| var points = [ | 开始声明包含分数增量的数组:0 表示平局,1 表示玩家获胜,-1 表示玩家失败。 |
| [0,1,-1], | 电脑扔石头时的一组增量。 |
| [-1,0,1], | 电脑投纸时的一组增量。 |
| [1,-1,0]]; | 电脑投剪刀时的一组增量。 |
| Var inmotion = false; | 当电脑移动出现时,用来阻止对玩家移动的反应。 |
| function Throw(sx,sy, smargin,swidth,``sheight,rectcolor,picture) { | 用于三个游戏符号的构造函数的头。参数包括 x 和 y 坐标、边距、内部宽度和高度、矩形的颜色以及图片文件。 |
| this.sx = sx; | 分配sx属性。 |
| this.sy = sy; | ...sy属性。 |
| this.swidth = swidth; | ...swidth属性。 |
| this.bwidth = swidth + 2*smargin; | 计算并指定外部宽度。这是内部宽度加上两倍的边距。 |
| this.bheight = sheight + 2*smargin; | 计算并指定外部高度。这是内部高度加上两倍的边距。 |
| this.sheight = sheight; | 分配sheight属性。 |
| this.fillstyle = rectcolor; | 分配fillstyle属性。 |
| this.draw = drawThrow; | 将绘制方法指定为drawThrow。 |
| this.img = new Image(); | 创建一个新的Image对象。 |
| this.img.src = picture; | 将其src设置为图片文件。 |
| this.smargin = smargin; | 分配smargin属性。画画还是需要的。 |
| } | 关闭功能。 |
| function drawThrow() { | 用于绘制符号的函数的标题。 |
| ctx.strokeStyle = "rgb(0,0,0)"; | 将矩形轮廓的样式设置为黑色。 |
| ctx.strokeRect(this.sx,this.sy,``this.bwidth,this.bheight); | 绘制矩形轮廓。 |
| ctx.fillStyle = this.fillstyle; | 设置填充矩形的样式。 |
| ctx.fillRect(this.sx,this.sy,``this.bwidth,this.bheight); | 画矩形。 |
| ctx.drawImage(this.img,this.sx+this.``smargin,this.sy+this.smargin,this.swidth,``this.sheight); | 在矩形内绘制图像偏移量。 |
| } | 关闭功能。 |
| function choose(ev) { | 在click事件上调用的函数的标题。 |
| If (!inmotion) { | 仅在计算机移动未出现(运动中)时做出响应。 |
| var compch = Math.floor``(Math.random()*3); | 基于随机处理生成计算机移动。 |
| var compchn = choices[compch]; | 挑选出图像文件。 |
| compimg.src = compchn; | 设置已经创建的Image对象的src。 |
| var mx; | 用于鼠标x。 |
| var my; | 用于鼠标y。 |
| if ( ev.layerX &#124;&#124;  ev.layerX``== 0) { | 检查哪个编码适用于此浏览器。 |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX &#124;&#124;``ev.offsetX == 0) { | 否则检查这个编码是否有效。 |
| mx = ev.offsetX; | 设置mx。 |
| my = ev.offsetY; | 设置my。 |
| } | 关闭子句。 |
| var i; | 用于索引不同的符号。 |
| for (i=0;i<everything.length;i++){ | For标题,用于索引everything数组中的元素,即三个符号。 |
| var ch = everything[i]; | 获取第 I 个元素。 |
| if ((mx>ch.sx)&&(mx<ch.sx+ch``.bwidth)&&(my>ch.sy)&&(my<ch.sy+ch.bheight)) { | 检查mx, my位置是否在该符号的边界内(外部矩形边界)。 |
| drawall(); | 如果是这样,调用drawall函数,该函数将删除所有内容,然后在everything数组中绘制所有内容。 |
| size = 15; | 计算机移动图像的初始大小。 |
| tid = setInterval``(flyin,100); | 设置定时事件。 |
| result = beats``[compch][i]; | 设置结果消息。请参见表格后面的部分,了解音频的添加内容。 |
| newscore =``Number(document.f.score.value); | 获取当前分数,转换成数字。 |
| newscore +=``points[compch][i]; | 添加调整并保存,以便稍后显示。 |
| break; | 离开for循环。 |
| } | 结束if子句。 |
| } | 结束for循环。 |
| } | 结束inmotionfalsetrue类。 |
| } | 结束函数。 |
| function flyin() { | 处理定时间隔事件的函数的标题。 |
| Inmotion = true; | 计算机移动的出现。这被多次设置为true。 |
| ctx.drawImage(compimg, 70,``100,size,size); | 在屏幕上指定的位置和指定的尺寸绘制计算机移动图像。 |
| size +=5; | 通过增加size来改变尺寸值。 |
| if (size>50) { | 使用size变量查看该过程是否持续了足够长的时间。 |
| clearInterval(tid); | 停止计时事件。 |
| ctx.fillText(result,``200,100,250); | 显示消息。 |
| document.f.score.value``= String(newscore); | 显示新的分数。请参见表格后面的部分,了解音频的添加内容。 |
| Inmotion = false; | 设置回初始设置。 |
| } | 关闭if true子句。 |
| } | 关闭该功能。 |
| var rockb = new Throw(rockbx,rockby,8,50,``50,"rgb(250,0,0)","rock.jpg"); | 创建岩石对象。 |
| var paperb = new Throw(paperbx,paperby,8,50,``50,"rgb(0,200,200)","paper.gif"); | 创建纸张对象。 |
| var scib = new Throw(scissorsbx,scissorsby,``8,50,50,"rgb(0,0,200)","scissors.jpg"); | 创建剪刀对象。 |
| everything.push(rockb); | 将岩石对象添加到everything数组中。 |
| everything.push(paperb); | 将纸张对象添加到everything数组中。 |
| everything.push(scib); | 将剪刀对象添加到everything数组中。 |
| function init(){ | 加载文档时调用的函数的标题。 |
| document.f.score.value = "0"; | 将分数设为零。我也可以用... = String(0);(实际上这是不必要的,因为在这种情况下 JavaScript 会将数字转换成字符串)。 |
| ctx = document.getElementById``('canvas').getContext('2d'); | 设置用于所有绘图的变量。 |
| canvas1 = document.getElementById``('canvas'); | 设置用于鼠标点击事件处理的变量。 |
| canvas1.addEventListener``('click',choose,false); | 设置click事件处理。 |
| drawall(); | 画出一切。 |
| ctx.font="bold 16pt Georgia"; | 设置用于结果消息的字体。 |
| ctx.fillStyle = "blue"; | 设置颜色。 |
| } | 关闭该功能。 |
| function drawall() { | 函数的标题。 |
| ctx.clearRect(0,0,cwidth,cheight); | 清理画布。 |
| var i; | 索引变量。 |
| for (i=0;i<everything.length;i++) { | 遍历everything数组。 |
| everything[i].draw(); | 画出各个元素。 |
| } | 关闭for回路。 |
| } | 关闭该功能。 |
| </script> | 关闭script元件。 |
| </head> | 关闭head元件。 |
| <body onLoad="init();"> | 开始body标签。设置对init函数的调用。 |
| <canvas id="canvas" width="600" height=``"400"> | 开始canvas标签。 |
| Your browser doesn't support the HTML5``element canvas. | 针对不兼容浏览器的消息。 |
| </canvas> | 结束标记。 |
| <br/> | 换行。 |
| <form name="f"> | 表单的开始标签,给表单一个名字。 |
| Score: <input name="score" value="0"``size="3"/> | 标签,然后输入字段,初始值和大小。 |
| </form> | form的结束标签。 |
| </body> | body的结束标签。 |
| </html> | HTML 文档的结束标记。 |

音频增强版除了增加了initchooseflyin函数之外,还需要三个全局变量。新的全局变量是

var music = [
   [3,1,0],
   [1,3,2],
   [0,2,3]];
var musicelements;
var musicch;

init函数需要语句

musicelements = document.getElementsByTagName("audio");

document方法getElementsByTagName产生文档中所有音频元素的数组,这正是我们需要的musicelements

下面是choose函数中的子句,新行高亮显示。

if ((mx>ch.sx)&&(mx<ch.sx+ch.bwidth)&&(my>ch.sy)&&(my<ch.sy+ch.bheight)) {
  drawall();
  size = 15;
  tid = setInterval(flyin,100);
  result = beats[compch][i];
  musicch = music[compch][i];
  newscore = Number(document.f.score.value);
  newscore +=points[compch][i];
  break;
}

同样,下面是完整的flyin函数,新行以粗体显示:

function flyin() {
  inmotion = true;
  ctx.drawImage(compimg, 70,100,size,size);
  size +=5;
  if (size>50) {
    clearInterval(tid);
    ctx.fillText(result,200,100,250);
    document.f.score.value = String(newscore);
    musicelements[musicch].play();
    inmotion = false;
  }
}

添加音频增强,就像添加视频一样,提供了一个检查什么需要改变,什么保持不变的练习。首先开发一个基本的应用程序当然是有意义的。

我的想法是为四个结果发声。你也可以为任何赢的玩家鼓掌,为任何输的玩家喝倒彩,或者为平局鼓掌。

有些人喜欢包括额外的可能的动作,用有趣的评论描述什么打败什么,甚至用三种或更多的其他可能性来代替石头、布、剪刀。我的几个学生用不同的语言制作了这个游戏,比如西班牙语。更具挑战性的任务是通过隔离口语成分,以系统的方式使应用程序多语言化。一种方法是将beats数组改为数组的数组,第一个索引对应于语言。包含单词Score的标记中的标签也需要改变,这可以通过使其成为输入字段并使用 CSS 移除其边框来实现。为所谓的本地化准备应用程序已经成为 Web 开发的一个重要领域。

测试和上传应用程序

您需要创建或获取(这是一种礼貌的说法,指找到某样东西并将文件复制到您的计算机上)三个图像来表示石头、布和剪刀。如果您决定通过添加声音来增强应用程序,您需要制作或找到音频剪辑,将它们转换为两种常见格式,然后上传所有声音:这是四个文件乘以两种格式,总共八个文件。

因为这个应用程序涉及到一个随机的元素,所以要齐心协力去做所有的测试。你想测试一个玩家投掷三种可能性中的每一种与三种计算机移动中的每一种。您还想测试分数会随着情况的变化而上下波动,并保持不变。通常,我的测试程序是让石头反复投掷,直到我看到所有三台电脑至少移动两次。然后我转到布,然后剪刀,然后我不断改变我的投掷,说,布,石头,布,剪刀。

测试基本程序,然后决定您希望对演示文稿和评分进行哪些改进。当您在本地计算机上测试了程序并决定将其上传到服务器时,需要上传图像和 HTML 文档。如果你决定用不同的图像来显示计算机移动而不是玩家移动,你将不得不找到并上传更多的图像。有些人喜欢将图像和音频文件放在子文件夹中。如果这样做,不要忘记在代码中使用正确的名称。

摘要

在这一章中,你学习了如何使用 HTML5、JavaScript 和 CSS 的特性以及一般的编程技术来实现一个熟悉的游戏。其中包括

  • 样式,特别是font-family属性

  • 用于显示分数的表单和输入字段

  • 对鼠标点击事件使用addEventListener的事件处理

  • 使用setIntervalclearInterval的动画

  • 用于声音的audio元素和用于不同浏览器的source元素

  • getElementsByTagNameplay用于音频剪辑的具体控制

  • 程序员定义的对象,用于在屏幕上绘制程序员创建的按钮,带有确定鼠标光标是否点击了特定按钮的逻辑

  • 游戏规则的数组,以并行结构组织

下一章描述了另一个熟悉的童年游戏:刽子手。它结合了在画布上绘图和使用您在前面章节中学到的代码创建 HTML 元素的技术,以及一些新的 CSS 和 JavaScript 特性。

九、Hangman

在本章中,我们将介绍

  • CSS 样式

  • 为字母按钮生成标记

  • 对一系列图形使用数组

  • 使用字符串作为密码

  • 为单词列表创建一个外部脚本文件

  • 设置和移除事件处理

介绍

本章的目标是继续演示 HTML5、层叠样式表(CSS)和 JavaScript 的编程技术和特性,结合 HTML 标记的动态创建以及在画布上绘制图形和文本。本章的例子是另一个熟悉的游戏 Hangman 的纸笔游戏。

以防万一你需要温习一下规则,游戏是这样玩的:一个玩家想到一个秘密单词,写下破折号,让另一个玩家知道这个单词有多少个字母。另一个人猜单个字母。如果字母出现在单词中,玩家一用实际的字母替换代表猜测字母的破折号。如果字母没有出现在密语中,第一个玩家画下一步的绞刑简笔画。在图 9-1 所示的例子中,绞刑架已经出现在屏幕上。接下来是头,然后是身体,左臂,右臂,左腿,右腿,最后是绳子。玩家可以就允许多少步达成一致。如果绞刑在单词被猜中之前完成,玩家二输掉游戏。是的,这是一个残忍的游戏,但它很受欢迎,甚至被认为是有教育意义的。

在我们的游戏中,计算机扮演一号玩家的角色,从一个单词列表(在这种情况下是一个公认的非常短的列表)中选择秘密单词。你可以用我的列表。当你制作你自己的游戏时,用你自己的。从小处着手是有意义的,一旦你对你的游戏满意了,就列出一个更长的清单。我为单词列表使用外部文件的技术支持这种方法。

对于用户界面,我选择将字母表中的每个字母放在屏幕上。玩家通过点击一个方块来选择一个字母。选择一个字母后,它的方块会消失。这个决定受到这样一个事实的影响,即大多数玩纸笔游戏的人写出字母表,并在选择字母时划掉字母。

图 9-1 为开启画面。计算机选择了一个有四个字母的单词。请注意,在我们的节目中,绞刑架已经出现在屏幕上。或者,您可以选择将此作为绘图过程的前一步或两步。

img/214814_2_En_9_Fig1_HTML.jpg

图 9-1

打开屏幕

使用小型单词库的一个好处是,我知道现在的单词是什么,即使我的编码使用随机过程来选择单词。这意味着我可以在没有压力的情况下开发游戏。我决定先选一个一个。如图 9-2 所示,这个字母没有出现在密语中,于是屏幕上画出一个椭圆形的头像,字母 a 的方块消失。

img/214814_2_En_9_Fig2_HTML.jpg

图 9-2

猜 a 后截图

通过元音,我猜出一个 e ,结果如图 9-3 所示。

img/214814_2_En_9_Fig3_HTML.jpg

图 9-3

猜中一个 e 后的游戏

接下来,我猜中了一个 i ,导致我第三步走错,如图 9-4 。

img/214814_2_En_9_Fig4_HTML.jpg

图 9-4

三次错误选择后的游戏画面

现在,我猜测一个 o ,这被证明是正确的(因为我有内部消息,所以我知道),一个 o 出现在单词的第三个字母,如图 9-5 所示。

img/214814_2_En_9_Fig5_HTML.jpg

图 9-5

o 的正确猜测

我尝试下一个元音, u ,这也是正确的,如图 9-6 所示。

img/214814_2_En_9_Fig6_HTML.jpg

图 9-6

已经确认了两封信

我现在再做一些猜测,首先是一个 t ,如图 9-7 。

img/214814_2_En_9_Fig7_HTML.jpg

图 9-7

t 又猜错了

然后,我又猜错了,这次是一个 s ,如图 9-8 。

img/214814_2_En_9_Fig8_HTML.jpg

图 9-8

在对 s 的错误猜测之后

图 9-9 显示了另一个错误的猜测。

img/214814_2_En_9_Fig9_HTML.jpg

图 9-9

在对 d 的错误猜测之后

我决定做一个正确的猜测,即 m 。图 9-10 显示了三个识别出的字母和画在屏幕上的人的大部分。

img/214814_2_En_9_Fig10_HTML.jpg

图 9-10

在对 m 的正确猜测之后

在这一点上,我正在努力失去,所以我猜测 b 。这导致了图 9-11 中所示的结果。

img/214814_2_En_9_Fig11_HTML.jpg

图 9-11

智乐

请注意,该图显示了一个套索;完整的秘密单词被揭示;并出现一条消息,告诉玩家游戏失败,并重新加载再试一次。

图 9-12 显示了另一个游戏的截图,计算机已经通过在两个位置显示字母 e 的猜测做出了响应。处理在一个单词中出现不止一次的字母并不困难,但是在我开始编程之前,这一点对我来说并不明显。

img/214814_2_En_9_Fig12_HTML.jpg

图 9-12

在这个游戏中,e 出现在两个地方

我做了一些其他的猜测,最终得到了这个单词的正确答案。同样,从中做出选择的列表不是很长,所以我可以从字母的数量中猜出单词。图 9-13 显示了一个获胜游戏的截图。注意在密语中有两个e’和三个f’s。

img/214814_2_En_9_Fig13_HTML.jpg

图 9-13

赢得比赛

编程技术和语言特性包括操作字符串;使用保存英语字母表的字母的数组;创建标记元素来保存表示秘密单词的字母表和空格,该秘密单词可以由字母替换,也可以不由字母替换;为创建的字母块处理事件;设置一组绘制悬挂步骤的函数;并将函数名放在一个数组中。这个实现还演示了如何使用外部脚本文件来保存单词列表。这个游戏在游戏中有回合,不像石头剪子布,所以程序必须在内部管理游戏状态,并在屏幕上显示出来。

关键要求

和前一章一样,这个游戏的实现使用了前几章演示的许多 HTML5 和 JavaScript 结构,但是它们在这里以不同的方式组合在一起。编程类似于写作。在编程中,你把各种构造放在一起,就像你写由你知道的单词组成的句子,然后把这些放入段落,等等。在阅读本章的时候,回想一下你已经学过的在画布上画直线、弧线和文字的知识;创建新的 HTML 标记;为屏幕上的标记设置鼠标单击事件;并使用iffor语句。

要实现 Hangman,我们需要访问单词列表。创建和测试程序不需要一个很长的列表,以后可以替换它。我决定把单词列表从程序中分离出来作为一个要求。我的单词表保存在文件words1.js中,完整显示如下:

var words = [
     "muon", "blight","kerfuffle","qat"
      ];

玩家移动的用户界面可能以几种方式中的一种表现出来,例如,表单中的输入字段。然而,我认为更好的方法是让界面包含代表字母表字母的图形。有必要让每个图形充当一个可点击的按钮提供了一种方法,使每个字母在被选中后消失。

这个游戏的纸笔版本包括一系列的图画,最终形成一个脖子上套着套索的简笔画。电脑游戏必须显示相同的图纸进展。这些图画可以是简单的线条和椭圆。

密码必须显示在屏幕上,开始时全部为空白,然后填入任何正确识别的字母。我选择使用双线作为空白,因为我希望识别的字母加下划线。另一种可能是问号。

最后,程序必须监控游戏的进程,并正确判断玩家何时输了,何时赢了。游戏状态对玩家来说是可见的,但是程序必须设置和检查内部变量来决定游戏是赢还是输。

HTML5、CSS、JavaScript 特性

现在让我们看看 HTML5、CSS 和 JavaScript 的具体特性,它们提供了我们实现 Hangman 所需的东西。除了基本的 HTML 标签以及函数和变量的工作方式,这里的解释是完整的。然而,这一章的大部分内容重复了前几章给出的解释。和以前一样,您可以选择查看“构建应用程序”一节中的所有代码,如果您需要特定特性的解释,可以返回到这一节。

将单词列表存储为外部脚本文件中定义的数组

Hangman 游戏需要访问一个法律单词列表,这个列表可以称为单词库。可以肯定地说,一种方法是使用数组。我们将在这个初始示例中使用的短数组如下:

var words = [
    "muon", "blight","kerfuffle","qat"
     ];

请注意,这些单词的长度都不同。这意味着我们可以使用最终版本所需的随机处理代码,并且在测试时仍然知道选择了哪个单词。我们将确保代码使用words.length,这样当你替换一个更大的数组时,编码仍然有效。

现在的问题是,如果我们想要引入不同的单词列表,如何使用不同的数组来实现这个目的。当然,更改 HTML 文档是可能的。但是,在 HTML5(或以前版本的 HTML)中,可以包含对外部脚本文件的引用,以代替 HTML 文档中的脚本元素,或者作为其补充。我们可以将声明和定义变量单词的三行代码放在一个名为words1.js的文件中。我们可以使用以下代码行将该文件包含在文档的其余部分中:

<script src="words1.js" defer></script>

当浏览器继续处理基本 HTML 文档的其余部分时,defer方法将加载该文件。如果外部文件包含body的一部分,我们不能同时加载这两个文件,但是在这种情况下它可以工作。

我在为我的课程准备的程序版本中加入了一个更长的列表。这是中学的官方拼字比赛名单。我确实需要在 Excel 中做一些操作来生成 JavaScript。增强程序可以包括多个文件,这些文件带有供玩家从不同级别或语言中选择的代码。

生成并定位 HTML 标记,然后将标记更改为按钮,然后禁用按钮

字母按钮和密码破折号的创建是结合 JavaScript 和 CSS 完成的。

我们将编写代码为程序的两个部分创建 HTML 标记:字母图标和秘密单词的空白。(你可以去第六章中的问答游戏了解更多关于创建 HTML 标记的内容。)在每种情况下,HTML 标记都是使用以下内置方法创建的:

  • document.createElement(x):为新元素类型x创建 HTML 标记

  • document.body.appendChild (d):添加d元素作为body元素的另一个子元素

  • document.getElementById(id):提取 ID 为id值的元素

创建的 HTML 包含每个元素的唯一 ID。该代码涉及设置某些属性:

  • d.innerHTML被设置为保存 HTML

  • thingelem.style.top设置为保持垂直位置

  • thingelem.style.left设置为保持水平位置

有了这个背景,下面是设置字母按钮的代码。我们首先声明一个全局变量alphabet:

var alphabet = "abcdefghijklmnopqrstuvwxyz";

setupgame函数的代码用于制作字母按钮:

var i;
   var x;
   var y;
   var uniqueid;
   var an = alphabet.length;
   for(i=0;i<an;i++) {

      uniqueid = "a"+String(i);
      d = document.createElement('alphabet');
       d.innerHTML = (
         "<div class="letters" id='"+uniqueid+"'>"+alphabet[i]+"</div>");
      document.body.appendChild(d);
      thingelem = document.getElementById(uniqueid);
      x = alphabetx + alphabetwidth*i;
      y = alphabety;
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
      thingelem.addEventListener('click',pickelement,false);
   }

变量i用于迭代字母表字符串。唯一 ID 是与索引值连接的a,索引值从 0 到 25。插入到创建的元素中的 HTML 是一个带有包含字母的文本的div。字符串用双引号括起来,该字符串内的属性用单引号括起来。元素分布在屏幕上,从位置alphabetxalphabety(每个全局变量都在文档中声明过)开始,水平递增alphabetwidth。对于像素,topleft属性需要设置为字符串并以"px"结束。最后一步是设置事件处理,让这些元素充当按钮。

密语元素的创建是类似的。区别在于,每个元素都有两个下划线作为其文本内容。在屏幕上,这两条下划线看起来像一条长下划线。分配给ch(用于选择)是我们的程序如何选择密语。注意,长度是数据类型String的对象以及数组的属性。在这种情况下,我对单词列表使用长度。如果我的列表超过四个元素,这段代码仍然可以工作。

var ch = Math.floor(Math.random()* words.length);
   secret = words[ch];
   for (i=0;i<secret.length;i++) {
      uniqueid = "s"+String(i);
      d = document.createElement('secret');
          d.innerHTML = (
            "<div class="blanks" id='"+uniqueid+"'> __ </div>");
      document.body.appendChild(d);
      thingelem = document.getElementById(uniqueid);
      x = secretx + secretwidth*i;
      y = secrety;
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
   }

在这一点上,你可能会问,字母图标是如何变成有边框的方块中的字母的?答案是我用了 CSS。CSS 的用处远远超出了字体和颜色。这些风格提供了游戏关键部分的外观和感觉。请注意,字母表div元素的类设置为'letters',秘密单词字母div元素的设置为'blanks'。样式部分包含以下两种样式,为了便于阅读,我对它们进行了分组。换行符对浏览器没有意义。

<style>
.letters {
  position:absolute;
  left: 0px; top: 0px;
  border: 2px; border-style: double;
  margin: 5px; padding: 5px;
  font-size: 24px;
  color:#F00; background-color:#0FC;
  font-family:"Courier New", Courier, monospace;
}
.blanks {
  position:absolute;
  left: 0px; top: 0px;
  border:none; margin: 5px; padding: 5px;
  color:#006; background-color:white;
  font-family:"Courier New", Courier, monospace;
  text-decoration:underline;
  color: black; font-size:24px;
}
</style>

后跟名称的点表示该样式适用于该类的所有元素。这与仅仅是一个名称形成对比,比如上一章中的form,其中一个样式被应用于所有的表单元素,或者一个#后跟一个名称,该名称引用文档中具有该名称 ID 的一个元素。请注意,字母的样式包括边框、颜色和背景色。指定字体系列是为任务选择您最喜欢的字体,然后在该字体不可用时指定备份的一种方式。CSS 的这个特性为设计者提供了广阔的空间。我在这里的选择是"Courier New",第二个选择是Courier,第三个选择是任何可用的等宽字体(在等宽字体中,所有的字母都是一样宽的)。我决定使用等宽字体,以便在屏幕上制作大小和空间都相同的图标。margin属性设置为边框外的间距,padding是指文本和边框之间的间距。

我们希望代表字母表中字母的按钮在被点击后消失。pickelement函数中的代码可以使用术语this来指代被点击的对象。这两条语句(可以压缩成一条)通过设置display属性实现了这一点:

var id = this.id;
document.getElementById(id).style.display = "none";

当游戏结束时,无论是赢还是输,我们通过迭代所有元素来删除所有字母的点击事件处理:

for (j=0;j<alphabet.length;j++) {
    uniqueid = "a"+String(j);
    thingelem = document.getElementById(uniqueid);
    thingelem.removeEventListener('click',pickelement,false);
}

removeEventListener事件做的和它听起来的一样:它移除了事件处理。

在画布上创建渐进式绘图

在到目前为止的章节中,你已经了解了绘制矩形、文本、图像和路径。路径由直线和圆弧组成。对于 Hangman 来说,图纸都是路径。对于这个应用程序,代码已经将变量ctx设置为指向画布的 2D 上下文。绘制路径包括通过将ctx.lineWidth设置为一个数值和将ctx.strokeStyle设置为一种颜色来设置线宽。我们将在绘图的不同部分使用不同的线条宽度和颜色。

代码中的下一行是ctx.beginPath();,接下来是一系列绘制线条或弧线或移动虚拟笔的操作。方法ctx.moveTo在不绘图的情况下移动笔,而ctx.lineTo指定从当前笔位置到指示点绘制一条线。请记住,在调用stroke方法之前,不会绘制任何内容。每当调用strokefill方法时,moveTolineToarc命令设置绘制的路径。在我们的绘制函数中,下一步是调用ctx.closePath;,最后一步是调用ctx.stroke();来进行实际的绘制。例如,绞刑架是由以下函数绘制的:

function drawgallows() {
   ctx.lineWidth = 8;
   ctx.strokeStyle = gallowscolor;
   ctx.beginPath();
   ctx.moveTo(2,180);
   ctx.lineTo(40,180);
   ctx.moveTo(20,180);
   ctx.lineTo(20,40);
   ctx.moveTo(2,40);
   ctx.lineTo(80,40);
   ctx.closePath();
   ctx.stroke();

}

头部和套索需要椭圆形。椭圆将基于圆,所以首先我将回顾如何画一个圆。你也可以回到第二章的。使用带有以下参数的ctx.arc命令绘制圆弧:圆心坐标、半径长度、以弧度表示的起始角度、结束角度,以及逆时针的true或顺时针的false。这只对不是完整圆的圆弧有意义,但是当你需要画这样的圆弧时,你需要记住这一点。

弧度是一个完整圆的固有测量值Math.PI*2。(人们熟悉的 360 度圆系统是很久以前人们发明的,并且是任意的。)从角度到弧度的转换是除以Math.PI再乘以180,但是在这个例子中并不需要,因为我们正在画完整的圆弧。

然而,我们想画一个椭圆形来代替圆形的头部(以及后来的套索的一部分)。解决方法是用ctx.scale改变坐标系。在第四章中,我们改变了坐标系来旋转代表大炮的矩形。在这里,我们操纵坐标系来压缩一个维度,使一个圆变成一个椭圆。我们的代码首先使用ctx.save()保存当前坐标系。然后,对于头部,它使用ctx.scale(.6,1);将 x 轴缩短到其当前值的 60 %,并保持 y 轴不变。使用绘制圆弧的代码,然后使用ctx.restore();恢复原来的坐标系。绘制头部的功能如下:

function drawhead() {
   ctx.lineWidth = 3;
   ctx.strokeStyle = facecolor;
   ctx.save();  //before scaling of circle to be oval
   ctx.scale(.6,1);
   ctx.beginPath();
   ctx.arc (bodycenterx/.6,80,10,0,Math.PI*2,false);
   ctx.closePath();
   ctx.stroke();
   ctx.restore();
}

drawnoose函数使用相同的技术,除了对于套索,椭圆是宽的而不是窄的;也就是垂直的被挤压,而不是水平的。

绘图过程中的每一步都由一个函数表示,如drawheaddrawbody。我们将所有这些列在一个名为steps的数组中:

var steps = [
      drawgallows,
      drawhead,
      drawbody,
      drawrightarm,
      drawleftarm,
      drawrightleg,
      drawleftleg,
      drawnoose
      ];

一个变量cur,跟踪当前步骤,当代码确认条件cur等于steps的长度时,游戏结束。

在做了这些实验后,我决定我需要在套索上画一个头和一个脖子。这是通过调用drawnoose函数中的drawheaddrawneck来完成的。顺序很重要。

使用绘图功能作为模型来制作您自己的绘图。一定要改变这些单独的功能。您也可以添加或删除功能。这意味着你将改变游戏进程中的步数,也就是说,玩家在输掉游戏前可以猜错的次数。

小费

如果你还没有这样做(或者即使你已经这样做了),尝试绘画。创建一个单独的文件,只是为了绘制悬挂的步骤。尝试线条和弧线。您也可以包含图像。

维持游戏状态并确定输赢

编码和维护应用程序状态的需求是编程中的常见需求。在第二章中,我们的程序记录下一步是第一次掷骰子还是后续掷骰子。刽子手游戏的状态包括隐藏单词的身份,单词中的哪些字母被正确猜中,字母表中的哪些字母被尝试过,以及绞刑的进行状态。

当玩家点击一个字母块时调用的pickelement函数是关键动作发生的地方,它执行以下任务:

  • 检查保存在变量picked中的玩家的猜测是否与保存在变量secret中的秘密单词中的任何字母相匹配。对于每个match,空白元素中的相应字母通过将textContent设置为该字母来显示。

  • 使用变量lettersguessed记录已经猜出了多少个字母。

  • 通过比较lettersguessedsecret.length来检查游戏是否已经获胜。如果游戏获胜,删除字母按钮的事件处理并显示适当的消息。

  • 如果选择的字母与密语中的任何字母都不匹配(如果变量not仍然是true,则使用变量cur作为数组变量steps的索引来推进悬挂。

  • 通过比较cursteps.length来检查游戏是否已经输了。如果两个值相等,则显示所有字母,移除事件处理,并显示适当的消息。

  • 无论是否匹配,通过将display属性设置为none使点击的字母按钮消失。

这些任务是使用iffor语句执行的。在确定字母被正确猜中之后,检查游戏是否已经获胜。类似地,只有当确定字母没有被正确识别并且悬挂已经进行时,才检查游戏是否已经失败。游戏的状态在代码中由secretlettersguessedcur变量表示。玩家看到秘密单词的下划线和填充字母以及剩余的字母块。

带有逐行注释的整个 HTML 文档的代码在“构建应用程序”一节中。下一部分描述了处理玩家猜测的首要任务。要记住的一个通用策略是,通过为数组的每个成员做一些事情来完成几个任务,即使这对数组的某些元素来说可能是不必要的。例如,当任务是揭示秘密单词中的所有字母时,所有字母的textContent都被改变,即使它们中的一些已经被揭示。类似地,变量not可以多次设置为false

通过设置文本内容检查猜测并显示秘密单词中的字母

玩家通过点击一个字母来移动。pickelement函数被设置为每个字母图标的事件处理程序。因此,在函数中,我们可以使用术语this来指代接收(监听和听到)点击事件的对象。因此,表达式this.textContent将包含选中的字母。因此,声明

var picked = this.textContent;

将玩家正在猜测的字母表中的特定字母赋给局部变量picked。然后,代码遍历保存在变量secret中的秘密单词中的所有字母,并将每个字母与玩家的猜测进行比较。以双下划线开始的创建的标记对应于秘密单词中的字母,因此当有正确的猜测时,对应的元素将被改变;也就是它的textContent会被设置为玩家猜出来的字母,保存在picked:

for (i=0;i<secret.length;i++) {
      if (picked==secret[i]) {
         id = "s"+String(i);
         document.getElementById(id).textContent = picked;
                    not = false;
                    lettersguessed++;
                     ...

当猜测正确时,迭代不会停止;它继续前进。这意味着任何一个字母的所有实例都将被发现和揭示。每当有匹配时,变量not被设置为false。如果同一个字母有两个或更多的实例,这个变量会被设置多次,这不是问题。我加入了单词 kerfuffle 以确保重复的字母被正确处理(除了我喜欢这个单词的事实之外)。您可以在下一节检查所有代码。

构建应用程序并使之成为您自己的应用程序

Hangman 应用程序利用 CSS 样式、JavaScript 创建的 HTML 标记和 JavaScript 编码。有两个初始化和设置函数(initsetupgame)和一个完成大部分工作的函数(pickelement),外加八个绘制悬挂步骤的函数。表 9-1 中描述了这些功能。

表 9-1

由调用调用的函数

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad动作调用 | setupgame |
| setupgame | init | 第一个绘图功能,即drawgallows |
| pickelement | 由setupgameaddEventListener调用的动作调用 | 通过调用steps[cur]()的绘图功能之一 |
| drawgallows | pickelementsteps[cur]()的调用 |   |
| drawhead | pickelementdrawnoosesteps[cur]()的调用 |   |
| drawbody | pickelementsteps[cur]()的调用 |   |
| drawrightarm | pickelementsteps[cur]()的调用 |   |
| drawleftarm | pickelementsteps[cur]()的调用 |   |
| drawrightleg | pickelementsteps[cur]()的调用 |   |
| drawleftleg | pickelementsteps[cur]()的调用 |   |
| drawnoose | pickelementsteps[cur]()的调用 | drawheaddrawneck |
| drawneck | drawnoose |   |

注意大多数函数调用的间接模式。如果您决定改变悬挂进度,这种模式提供了相当大的灵活性。另请注意,您可以删除

steps[cur]();
       cur++;

setupgame函数中,如果你想让玩家从一张白纸开始,而不是从绞刑架的木梁开始。

Hangman 的完整实现如表 9-2 所示。

表 9-2

刽子手的完整实现

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| <head> | 开始head标签。 |
| <title>Hangman</title> | 完成title元素。 |
| <style> | 打开style元素。 |
| .letters {position:absolute;left: 0pxtop: 0px; border: 2px; border-style: double;``margin: 5px; padding: 5px; color:#F00;``background-color:#0FC; font-family:``"Courier New", Courier, monospace; | 用指定的类字母指定任何元素的样式,包括边框、颜色和字体。 |
| } | 结束样式指令。 |
| .blanks {position:absolute;left: 0px;``top: 0px; border:none; margin: 5px;``padding: 5px; color:#006; background-color:``white; font-family:"Courier New", Courier,``monospace; text-decoration:underline; color: black; | 用指定的空白类指定任何元素的样式,包括边框、间距、颜色和字体,并加上下划线。 |
| } | 结束样式指令。 |
| </style> | 关闭style元素。 |
| <script src="words1.js" defer></script> | 要求包含保存在名为words1.js的外部文件中的单词列表的元素,带有与该文档的其余部分同时加载该文件的指令。 |
| <script > | script元素的开始标签。 |
| var ctx; | 用于所有绘图的变量。 |
| var thingelem; | 用于已创建元素的变量。 |
| var alphabet = "abcdefghijklmnopqrstuvwxyz"; | 定义用于字母按钮的字母表的字母。 |
| var alphabety = 300; | 所有字母按钮的垂直位置。 |
| var alphabetx = 20; | 开始字母水平位置。 |
| var alphabetwidth = 25; | 为字母元素分配的宽度。 |
| var secret; | 将持有秘密的话。 |
| var lettersguessed = 0; | 对猜测的字母进行计数。 |
| var secretx = 160; | 机密字的水平起始位置。 |
| var secrety = 50; | 机密字的垂直位置。 |
| var secretwidth = 50; | 显示密码时分配给每个字母的宽度。 |
| var gallowscolor = "brown"; | 绞刑架的颜色。 |
| var facecolor = "tan"; | 脸的颜色。 |
| var bodycolor = "tan"; | 身体的颜色。 |
| var noosecolor = "#F60"; | 套索的颜色。 |
| var bodycenterx = 70; | 身体的水平位置。 |
| var steps = [ | 保存构成向悬挂前进的绘图序列的功能。 |
| drawgallows, | 拉上绞架。 |
| drawhead, | 绘制头部。 |
| drawbody, | 绘制身体。 |
| drawrightarm, | 画右臂。 |
| drawleftarm, | 绘制左臂。 |
| drawrightleg, | 画右腿。 |
| drawleftleg, | 画左腿。 |
| drawnoose | 拉上套索。 |
| ]; | 结束数组步骤。 |
| var cur = 0; | 逐步指向下一个图形。 |
| function drawgallows() { | 绘制绞刑架的函数的头。 |
| ctx.lineWidth = 8; | 设置线条宽度。 |
| ctx.strokeStyle = gallowscolor; | 设置颜色。 |
| ctx.beginPath(); | 开始定义路径。 |
| ctx.moveTo(2,180); | 移动到第一个位置。 |
| ctx.lineTo(40,180); | 画一条线。 |
| ctx.moveTo(20,180); | 移动到下一个位置。 |
| ctx.lineTo(20,40); | 画一条线。 |
| ctx.moveTo(2,40); | 移动到下一个位置。 |
| ctx.lineTo(80,40); | 划清界限。 |
| ctx.closePath(); | 关闭路径。 |
| ctx.stroke(); | 实际上画出了路径。 |
| } | 关闭该功能。 |
| function drawhead() { | 绘制受害者头部的函数的头。 |
| ctx.lineWidth = 3; | 设置线条宽度。 |
| ctx.strokeStyle = facecolor; | 设置颜色。 |
| ctx.save(); | 保存坐标系的当前阶段。 |
| ctx.scale(.6,1); | 应用缩放,即挤压 x 轴。 |
| ctx.beginPath(); | 开始一条路径。 |
| ctx.arc (bodycenterx/.6,80,10,0,``Math.PI*2,false); | 画弧线。请注意,x 坐标被修改为适用于缩放后的坐标系。完整的弧将是椭圆形的。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| ctx.restore(); | 恢复(回到)缩放前的坐标。 |
| } | 关闭该功能。 |
| function drawbody() { | 绘制主体的函数的标头,单行。 |
| ctx.strokeStyle = bodycolor; | 设置颜色。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,90); | 移动到位置(头部正下方)。 |
| ctx.lineTo(bodycenterx,125); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function drawrightarm() { | 绘制右臂的函数的头。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,100); | 移动到位置。 |
| ctx.lineTo(bodycenterx+20,110); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function drawleftarm() { | 绘制左臂的函数的头。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,100); | 移动到位置。 |
| ctx.lineTo(bodycenterx-20,110); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function drawrightleg() { | 绘制右腿的函数的标头。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,125); | 移动到位置。 |
| ctx.lineTo(bodycenterx+10,155); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function drawleftleg() { | 绘制左腿的函数的标头。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,125); | 移动到位置。 |
| ctx.lineTo(bodycenterx-10,155); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function drawnoose() { | 绘制套索的函数的头。 |
| ctx.strokeStyle = noosecolor; | 设置颜色。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx-10,40); | 移动到位置。 |
| ctx.lineTo(bodycenterx-5,95); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| ctx.save(); | 保存坐标系。 |
| ctx.scale(1,.3); | 进行缩放,垂直挤压图像(在 y 轴上)。 |
| ctx.beginPath(); | 开始一条路径。 |
| ctx.arc(bodycenterx,95/.3,8,0,Math.``PI*2,false); | 画一个圆(将变成椭圆形)。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| ctx.restore(); | 恢复保存的坐标系。 |
| drawneck(); | 在套索顶部绘制颈部。 |
| drawhead(); | 将头部画在套索上。 |
| } | 关闭该功能。 |
| function drawneck() { | 用于绘制颈部的函数的标题。 |
| ctx.strokeStyle=bodycolor; | 设置颜色。 |
| ctx.beginPath(); | 开始路径。 |
| ctx.moveTo(bodycenterx,90); | 移动到位置。 |
| ctx.lineTo(bodycenterx,95); | 划清界限。 |
| ctx.closePath(); | 关闭路径定义。 |
| ctx.stroke(); | 进行实际的绘图。 |
| } | 关闭该功能。 |
| function init(){ | 文档加载时调用的函数的头。 |
| ctx = document.getElementById``('canvas').getContext('2d'); | 为画布上的所有绘图设置变量。 |
| setupgame(); | 调用设置游戏的函数。 |
| ctx.font="bold 20pt Ariel"; | 设置字体。 |
| } | 关闭该功能。 |
| function setupgame() { | 设置字母按钮和密码的函数的标题。 |
| var i; | 为迭代创建变量。 |
| var x; | 创建位置变量。 |
| var y; | 创建位置变量。 |
| var uniqueid; | 为每组已创建的 HTML 元素创建变量。 |
| var an = alphabet.length; | 将是 26 岁。 |
| for(i=0;i<an;i++) { | 迭代创建字母按钮。 |
| uniqueid = "a"+String(i); | 创建唯一标识符。 |
| d = document.createElement('alphabet'); | 创建一个类型为alphabet的元素。 |
| d.innerHTML = ( | 定义下一行中指定的内容。 |
| "<div class="letters"``id='"+uniqueid+"'>"+alphabet[i]+"</div>"); | 指定一个具有唯一标识符和文本内容的类别字母div,它是字母表的第i 字母。 |
| document.body.appendChild(d); | 添加到正文。 |
| thingelem = document.getElementById``(uniqueid); | 获取具有 ID 的元素。 |
| x = alphabetx + alphabetwidth*i; | 计算其水平位置。 |
| y = alphabety; | 设置垂直位置。 |
| thingelem.style.top = String(y)+"px"; | 使用样式top,设置垂直位置。 |
| thingelem.style.left = String(x)+"px"; | 使用样式left,设置水平位置 |
| thingelem.addEventListener('click',``pickelement,false); | 为鼠标单击事件设置事件处理。 |
| } | 关闭for循环。 |
| var ch = Math.floor(Math.random()*``words.length); | 随机选择其中一个单词的索引。 |
| secret = words[ch]; | 将全局变量secret设置为这个单词。 |
| for (i=0;i<secret.length;i++) { | 迭代秘密词的长度。 |
| uniqueid = "s"+String(i); | 为单词创建唯一的标识符。 |
| d = document.createElement('secret'); | 为单词创建一个元素。 |
| d.innerHTML = ("<div class="blanks" id='"``+uniqueid+"'> __ </div>"); | 将内容设置为空白类的一个div,其 ID 为刚刚创建的单词uniqueid。文本内容将是下划线。 |
| document.body.appendChild(d); | 将创建的元素作为主体的子元素追加。 |
| thingelem = document.getElementById``(uniqueid); | 获取创建的元素。 |
| x = secretx + secretwidth*i; | 计算元素的水平位置。 |
| y = secrety; | 设置其垂直位置。 |
| thingelem.style.top = String(y)+"px"; | 使用样式top,设置垂直位置。 |
| thingelem.style.left = String(x)+"px"; | 使用样式left,设置水平位置。 |
| } | 关闭for循环。 |
| steps[cur](); | 绘制步骤列表中的第一个函数,绞刑架。 |
| cur++; | 增量cur。 |
| return false; | 返回false来阻止 HTML 页面的任何刷新。 |
| } | 关闭该功能。 |
| function pickelement(ev) { | 作为单击结果调用的函数的标题。 |
| var not = true; | 将not设置为true,可以更改也可以不更改。 |
| var picked = this.textContent; | 从引用的对象中提取文本内容,即字母。 |
| var i; | 迭代。 |
| var j; | 迭代。 |
| var uniqueid; | 用于创建元素的唯一标识符。 |
| var thingelem; | 保存元素。 |
| var out; | 显示一条消息。 |
| for (i=0;i<secret.length;i++) { | 迭代秘密单词中的字母。 |
| if (picked==secret[i]) { | 说,“如果玩家猜对了信就等于这封秘密信……”。 |
| id = "s"+String(i); | 构造此字母的标识符。 |
| document.getElementById(id).``textContent = picked; | 将文本内容更改为字母。 |
| not = false; | 将not设置为false。 |
| lettersguessed++; | 增加正确识别的字母数。 |
| if (lettersguessed==secret.length) { | 他说,“如果整个秘密单词都被猜到了……”。 |
| ctx.fillStyle=gallowscolor; | 设置颜色,使用绞刑架的棕色,但可以是任何颜色。 |
| out = "You won!"; | 设置消息。 |
| ctx.fillText(out,200,80); | 显示消息。 |
| ctx.fillText("Re-load the page to``try again.",200,120); | 显示另一条消息。 |
| for (j=0;j<alphabet.length;j++) { | 遍历整个字母表。 |
| uniqueid = "a"+String(j); | 构造标识符。 |
| thingelem = document.getElementById``(uniqueid); | 获取元素。 |
| thingelem.removeEventListener('click',``pickelement,false); | 移除事件处理。 |
| } | 关闭j进行循环迭代。 |
| } | 关闭if (lettersguessed....),即测试全部完成。 |
| } | 关闭if (picked==secret[i]) true子句。 |
| } | 关闭秘密单词迭代中字母的for循环。 |
| if (not) { | 检查是否没有识别出字母。 |
| steps[cur](); | 进行挂起迭代的下一步。 |
| cur++; | 递增计数器。 |
| if (cur>=steps.length) { | 检查是否所有步骤都已完成。 |
| for (i=0;i<secret.length;i++) { | 开始对秘密单词中的字母进行新的迭代,以显示所有字母。 |
| id = "s"+String(i); | 构造标识符。 |
| document.getElementById(id).textContent``= secret[i]; | 获取对元素的引用,并将其设置为机密字中的那个字母。 |
| } | 关闭迭代。 |
| ctx.fillStyle=gallowscolor; | 设置颜色。 |
| out = "You lost!"; | 设置消息。 |
| ctx.fillText(out,200,80); | 显示消息。 |
| ctx.fillText("Re-load the``page to try again.",200,120); | 显示重新加载消息。 |
| for (j=0;j<alphabet.length;j++) { | 遍历字母表中的所有字母。 |
| uniqueid = "a"+String(j); | 构造唯一标识符。 |
| thingelem = document.getElementById``(uniqueid); | 获取元素。 |
| thingelem.removeEventListener('click',``pickelement,false); | 移除此元素的事件处理。 |
| } | 关闭j迭代。 |
| } | 关闭cur测试,以确定悬挂是否完成。 |
| } | 关闭if (not)测试(玩家的错误猜测)。 |
| var id = this.id; | 提取此元素的标识符。 |
| document.getElementById(id).style.display``= "none"; | 让这个特殊的字母按钮消失。 |
| } | 关闭该功能。 |
| </script> | 关闭脚本。 |
| </head> | 关闭头部。 |
| <body onLoad="init();"> | 建立对init调用的开始标签。 |
| <h1>Hangman</h1> | 用大写字母写游戏的名字。 |
| <p> | 段落的开始标记。 |
| <canvas id="canvas" width="600" height="400"> | canvas元素的开始标记。包括维度。 |
| Your browser doesn't support the HTML5``element canvas. | 给使用不识别画布的浏览器的人的消息。 |
| </canvas> | canvas的结束标签。 |
| </body> | 关闭身体。 |
| </html> | 关闭文档。 |

Hangman 的一个变体是用俗语代替单词。在这个游戏的基础上创造一个对你来说是一个挑战。关键步骤是处理单词和标点符号之间的空白。你可能想立即显示单词和句号之间的空格、逗号和问号,给玩家这些提示。这意味着您需要确保lettersguessed从正确的计数开始。不要担心选择的字母与空格或标点符号进行比较。

另一种变化是改变字母表。我小心翼翼地把26的所有实例都换成了alphabet.length。您还需要更改输赢信息的语言。

游戏的一个合适的改进是制作一个新的单词按钮。要做到这一点,你需要将setupgame按钮的工作分成两个功能:一个功能创建新的字母图标和最长的秘密单词的位置。另一个确保所有的字母图标都是可见的,并为事件处理而设置,然后选择并设置秘密单词的空格,确保适当的数字是可见的。如果你这样做,你可能想显示一个分数和一些游戏。

继续教育的想法,假设你使用不寻常的词,你可能想包括定义。通过在画布上书写文字,可以在最后揭示定义。或者你可以制作一个按钮,点击它来显示定义,作为对玩家的提示。或者,您可以创建一个指向某个站点的链接,例如 Dictionary.com。

测试和上传应用程序

要测试这个应用程序,您可以下载我的单词表或创建自己的单词表。如果你自己创建一个,从一个准备为纯文本的短单词列表开始,命名为words1.js。测试时,不要总是用同样的模式猜测,比如按顺序选择元音。行为不端,游戏结束后还试图继续猜。当您对编码感到满意时,创建一个更长的单词列表,并将其保存在名称words1.js下。HTML 和words1.js文件都需要上传到你的服务器。

摘要

在本章中,您学习了如何使用 HTML5、JavaScript 和 CSS 的功能以及通用编程技术来实现一个熟悉的游戏,其中包括:

  • 使用scale方法改变坐标系,通过前后保存和恢复来绘制一个椭圆形,而不是圆形

  • 动态创建 HTML 标记

  • 使用addEventListenerremoveEventListener为单个元素设置和移除事件处理

  • 使用样式从显示中移除元素

  • 使用函数名数组建立绘图序列

  • 操纵变量来维持游戏的状态,通过计算来确定是赢还是输

  • 创建一个外部脚本文件来保存单词列表以增加灵活性

  • 使用 CSS,包括选择字体的font-familycolordisplay

Hangman 游戏是演示编程概念的一个很有吸引力的例子,我在 press Publishers 的《编程 101:如何和为什么使用处理编程语言 进行编程》中使用了它。

本书的下一章也是最后一章描述了纸牌游戏 21 点的实现,也称为 21 点。它将建立在你所学的基础上,描述一些新的编程技术,HTML5 中添加的元素,以及更多的 CSS。

十、21 点

在本章中,我们将介绍

  • HTML5 的新标签footerheader

  • 捕捉按键

  • 程序员定义的对象

  • 使用一组外部图像文件生成Image元素

  • 洗牌

介绍

本章的目标是结合编程技术、HTML5 和 JavaScript 特性来实现纸牌游戏 21 点,也称为 21 点。实现将使用 HTML5 中引入的新标签,即footerheader。我们将使用页脚给予信用卡图像的来源和我们用于洗牌算法的网站。这些卡是使用程序员定义的对象和Image对象创建的,通过编码生成图像文件的名称。玩家使用按键来移动。

21 点的规则如下:

玩家与庄家(也称为庄家)对弈。玩家和庄家 每人发两张牌。庄家的第一张牌对玩家是隐藏的,但另一张是可见的。一张牌的价值是其有编号的牌的面值,10 代表一张 j、q 或 k,1 或 11 代表一张 a。一手牌的价值是牌的总和。该游戏的目标是在不超过的情况下,让一手牌的值尽可能接近 21,以使其值大于其他人。因此,一张 a 和一张脸牌算作 21,一手赢牌。玩家可以采取的行动是要求另一张卡或持有

由于这是一个双人游戏,我们的玩家将与“计算机”对战,就像石头剪子布一样,我们的任务是生成计算机的动作。然而,我们受赌场惯例的指导—庄家将使用固定的策略。如果这手牌的价值低于 17,我们的庄家将要求另一张牌(赌场中的游戏策略可能稍微复杂一些,可能取决于 a 的存在)。类似地,如果玩家和豪斯的总数都在 21 以下,我们的游戏就会宣布平局;一些赌场可能有不同的做法。

开场截图如图 10-1 所示。

img/214814_2_En_10_Fig1_HTML.jpg

图 10-1

二十一点的开屏

用户按下 n 键后,下一个屏幕看起来会如图 10-2 所示。请记住,这是一个随机的过程,因此不保证每次都会出现相同的一组卡片。

img/214814_2_En_10_Fig2_HTML.jpg

图 10-2

发牌

图 10-2 显示了玩家看到的情况:除了庄家的一张硬牌之外,他或她所有的牌都是自己的。虚拟庄家不知道玩家的手牌。在这种情况下,玩家的手牌值为 2 加 10,总共为 12。庄家显示的是 3。玩家通过按 d 要求另一张牌。图 10-3 显示了结果。

img/214814_2_En_10_Fig3_HTML.jpg

图 10-3

有 20 分的玩家

玩家现在有一手值为 20 的牌,点击 h 停止游戏,看看庄家有什么牌。结果如图 10-4 所示。

img/214814_2_En_10_Fig4_HTML.jpg

图 10-4

玩家赢了 20 元,赌场赢了。

玩家赢了,因为房子倒了,而玩家没有。

玩家可以通过按 n 键或重新加载文档来开始新游戏。重新加载文档将意味着从一副完整的、刚刚洗过的牌开始。按 n 键继续当前的卡片组。任何想练习算牌**的人,都应该选择按 n 键,这是一种记录牌中剩余牌并据此改变你玩法的方法。

*图 10-5 展示了一个新游戏。

img/214814_2_En_10_Fig5_HTML.jpg

图 10-5

新游戏

这次玩家按 h 保持,图 10-6 显示结果。

img/214814_2_En_10_Fig6_HTML.jpg

图 10-6

玩家输了

庄家拿着四张牌,总共 21 张。记住 ace 算 1 或 11。该玩家有 14 张牌,因此输了。

图 10-7 显示了另一场比赛的结果。最初给玩家的牌是两张共 20 张的正面牌。玩家按下 h 保持,庄家又打了两张牌走了过去。

img/214814_2_En_10_Fig7_HTML.jpg

图 10-7

玩家赢了

赌场中庄家的实际做法可能与此不同。这是一个研究的机会!玩家也可以通过查看房子而不暴露它来虚张声势。这可能会导致房子要求另一张卡,并去了。当且仅当玩家点击 h 键保持,从而停止抽牌时,游戏才决定。

您可能希望在按下非 d、h 或 n 键时向玩家提供反馈,如图 10-8 所示。

img/214814_2_En_10_Fig8_HTML.jpg

图 10-8

按错键时的反馈

关键要求

21 点游戏将使用前面游戏中描述的许多 HTML5、CSS 和 JavaScript 特性。

当开始实现时,我遇到的第一个问题是为卡片表面找到一个图像源。我知道我可以画自己的画,但我更喜欢比我能画出来的更精美的东西。

下一个挑战是如何用编程术语设计一张牌,以便我可以实现发牌,显示背面或正面。我还想研究如何洗牌。

另一个挑战是实现玩家玩游戏的方式。我选择使用按键:d 表示发牌,h 表示暂停,n 表示开始新游戏。当然,还有其他选择,例如,用文字或图形显示按钮,或者使用其他键,如箭头键。缺乏清晰、直观的界面使得有必要在屏幕上显示方向。

最后的挑战是维护游戏状态、可视显示和内部信息的一般挑战;生成计算机移动,并遵循规则。

HTML5、CSS 和 JavaScript 特性

现在让我们看看 HTML5、CSS 和 JavaScript 的具体特性,它们提供了实现 21 点纸牌游戏所需的东西。除了基本的 HTML 标签和函数以及变量,这里的解释都是完整的。如果你已经阅读了其他章节,你会注意到这一章的大部分内容重复了前面给出的解释。请记住,您可以直接跳到“构建应用程序”一节来查看游戏的完整代码和注释,然后再回到这一节来获得更多的解释。

卡片正面的图像来源和设置图像对象

在制作第一版的时候,我确实找到了一个很好的 card faces 的来源,它附带了一个 Creative Commons 许可证,我很乐意展示链接和许可证,但是这个网站已经不存在了。我在美国合同桥牌联盟找到了另一个来源。这些数字文件被标记为免费,但我仍然请求并获得了许可,你可以从截图中看到我在网页上注明了数字文件的来源。

将文件复制到计算机后,您需要一种方法来访问 52 个卡面图像文件,而无需编写 52 个不同的语句。(注意卡背图像文件是异地访问的,即init函数。)这是可以实现的,因为文件名遵循一种模式。新卡片图像的模式与原来的略有不同,编码实际上更容易。builddeck功能如下:

function builddeck() {
 var n;
            var si;
            var suitnames =["C","H","S","D"];
            var i;
            i=0;
            var picname;
            var nums=["A","2","3","4","5","6","7","8","9","10","J","Q","K"];
            for (si=0;si<4;si++) {
                for (n=0;n<13;n++) {
                    picname=nums[n]+suitnames[si]+".png";
                    deck[i]=new MCard(n+1,suitnames[si],picname);
                    i++;
                }
            }
   }
 }
}

注意嵌套的for循环。外环处理花色,内环处理花色中的 13 张牌。

在这个函数中,外环管理花色,内环管理每种花色中的牌。picname变量将被设置为我们从源文件下载的文件名。MCard函数是创建MCard对象的构造函数,即我们定义为程序员定义的对象类的对象。n+1会作为牌的数值,对于面牌会有一些调整。

注意

嵌套的 for 循环中的三个语句可以合并成deck[i++]=new MCard(n+1,suitnames[si], suitnames[si]+"-"+nums[n]+".png");

这是因为++迭代运算符发生在为索引卡片组数组生成值之后。但是,我建议在这个学习示例中不要这样做!使用三个语句更容易编写和理解。

为卡片创建程序员定义的对象

正如我们在前面的章节中看到的,例如,弹弓游戏的第四章,JavaScript 为程序员提供了一种创建程序员定义的对象来将数据分组的方法;不同的数据片段被称为属性属性,我们使用点符号来获得不同的属性。也可以通过定义方法将代码与数据关联起来,但是在这个例子中我们不需要这样做。提醒一下,设置新对象的函数称为构造函数函数。对于卡片,我将MCard定义为构造函数,这在前面的章节中已经在builddeck函数中使用过。该函数的定义如下:

function MCard(n, s, picname){
   this.num = n;
   if (n>10) n = 10;
   this.value = n;
   this.suit = s;
   this.picture = new Image();
   this.picture.src = picname;
   this.dealt = 0;
 }

函数的行

   if (n>10) n = 10;

将由牌面触发(杰克、皇后和国王);记住,每个的值是 10。在这些情况下,该行将值更正为 10。

请注意,这条if语句在结构上不同于前面的if语句。有而不是任何左花括号和右花括号来分隔if-true子句。单语句子句是if语句的合法形式。我通常避免这种形式,因为如果我后来决定添加另一个语句,我将需要插入花括号。然而,我认为在这种情况下是可以的,在检查代码时,你会看到这两种变化。注意,当n等于 1 时,没有什么特别的事情发生。ace 的两个可能值的规则在程序的其他地方处理。

MCard对象的属性包括一个新创建的Image对象,它的src属性被设置为传入的picname。最后一个属性dealt,初始化为 0,将根据牌是给玩家还是给庄家而设置为 1 或 2。

开始游戏

对于我的游戏实现,玩家通过按 n 键选择用当前的牌组开始一个新游戏。如果玩家想要从一副新牌开始,玩家重新加载 HTML 文档。事实上,在赌场中,是庄家而不是玩家决定何时使用一副新牌。进行这种更改将是对实现的一个很好的补充。我还应该注意到,一些赌场使用多副牌来阻止一种叫做算牌的做法。我想到可以开发一个应用程序,为玩家提供一种练习算牌的方法。

另一个问题是关于玩家的行为。正如我所透露的,我倾向于假设玩家会表现得很好。如果一个玩家在游戏还没有开始的时候点击 d 表示再发一张牌或者 h 表示等待,应该怎么做?在这种涉及玩家非标准行为的情况下,我们作为应用程序构建者面临的选择包括:显示一条消息;试图猜测玩家想做什么,比如开始一个新游戏;或者什么都不做。我决定显示一条消息。为了跟踪游戏是否已经开始,我使用了一个全局变量gamestart,它被初始化为false。顺便说一下,这种变量的一个术语是标志。它存在于四个函数中(dealdealfromdeckplayerdonenewgame),您可以在代码表的上下文中检查它们。

发牌

函数builddeck构造了MCard对象的deck数组。玩家的手保持在名为playerhand的数组中,其中pi持有下一个位置的索引。类似地,庄家的手保持在名为househand的数组中,其中hi保存下一个位置的索引。playerhand[pi].picture是一个例子,显示了当对象是数组的元素时,引用MCard对象的属性的语法(标点符号)。

dealstart函数的任务是分发前四张牌:两张给玩家,两张给庄家。庄家的一张牌没有显示出来;也就是说,显示卡片的背面。当玩家请求一张新卡时,调用deal功能(见本章下文)。deal功能将向玩家发一张卡,并查看发牌者是否会得到一张新卡。dealstartdeal都通过调用dealfromdeck函数完成实际的发牌,将牌添加到playerhandhousehand数组中,并在画布上绘制牌。从形式上来说,dealfromdeck是一个返回类型为MCard的值的函数。它的调用出现在赋值语句的右边。如果要显示卡片的正面,则绘制的Image对象被卡片引用。如果要显示卡片的背面,则Image对象保存在变量back中。

这里是dealstart函数。卡片被添加到playerhand数组和househand数组中。可以通过两种不同的方式将元素添加到数组中。一种方法是使用推送方法。另一种方法,也就是我在这里演示的,使用一个索引值,其中索引值是数组的当前长度。也就是说,这会将值放在数组中的下一个位置。请注意四组类似的语句:获取卡片、绘制图像、为下一次增加 x 位置以及增加索引变量,pihi,用于分发四张卡片,两张给玩家,两张给赌场。

function dealstart() {
    playerhand[pi] = dealfromdeck(1);
    ctx.drawImage(playerhand[pi].picture,playerxp,playeryp,cardw,cardh);
    playerxp = playerxp+30;
    pi++;
    househand[hi] = dealfromdeck(2);
    ctx.drawImage(back,housexp,houseyp,cardw,cardh);
    housexp = housexp+20;
    hi++;
    playerhand[pi] = dealfromdeck(1);
    ctx.drawImage(playerhand[pi].picture,playerxp,playeryp,cardw,cardh);
    playerxp = playerxp+30;
    pi++;
    househand[hi] = dealfromdeck(2);
    ctx.drawImage(househand[hi].picture,housexp,houseyp,cardw,cardh);
    housexp = housexp+20;
    hi++;
  }

deal功能类似。如果more_to_house返回true,玩家的手牌和房子都会增加一张牌。

function deal() {
    if (gamestart)  {
      playerhand[pi] = dealfromdeck(1);
      ctx.drawImage(playerhand[pi].picture,playerxp,playeryp,cardw,cardh);
      playerxp = playerxp+30;
      pi++;
      if (more_to_house()) {
        househand[hi] = dealfromdeck(2);
        ctx.drawImage(househand[hi].picture,housexp,houseyp,cardw,cardh);
        housexp = housexp+20;
        hi++;
      }
 }
   else{
      alert("Press n to start a new game with the same deck.\n
                 Reload page to start a game with a new deck.");
            }
 }

注意,more_to_house是一个产生truefalse值的函数。该值将基于对经销商总额的计算。如果总数大于等于 17,返回值将是false;否则,它将成为true。函数调用被用作一个if语句的条件,所以如果more_to_house返回true,那么if子句中的语句将被执行。more_to_house代码可以放在deal函数中,但是将大任务分成小任务是很好的实践。这意味着我可以继续处理deal函数,并暂时推迟编写more_to_house函数。如果你想改进more_to_house的计算,你知道在哪里做。

deck确定具体的卡是dealfromdeck函数的任务。同样,我将这个定义明确的任务作为自己的功能。参数是卡的接收者。我们不需要跟踪这个应用程序中的接收者,但是我们会在代码中保存这些信息,以便为构建其他纸牌游戏做准备。关键是牌已经发到了某个人手里。dealt属性从 0 开始变化。注意第return card;行,它做的工作是使MCard对象成为调用函数的结果。

function dealfromdeck(who) {
  var card;
  var ch = 0;
  while ((deck[ch].dealt>0)&&(ch<51)) {
    ch++;
  }
  if (ch>=51) {
    ctx.fillText("NO MORE CARDS IN DECK. Reload. ",200,200);
    ch = 51;
    gamestart = false;
  }
  deck[ch].dealt = who;
  card = deck[ch];
  return card;
}

请记住,卡片组数组的索引是从 0 到 51。while语句是另一种类型的循环结构。在大多数计算机编程语言中,while循环是一个控制流语句,允许代码基于给定的布尔条件重复执行;while循环可以被认为是一个重复的if语句。只要括号内的条件保持为真,花括号内的语句就会执行。程序员有责任确保这种情况会发生——循环不会永远继续下去。我们应用程序中的while循环在识别出一张尚未发牌的牌时停止,也就是说,它的dealt属性为 0。当最后一张牌,即第 51 张牌可用并发出时,这个函数会说没有牌了。如果玩家忽略该消息并再次要求另一张牌,则将再次分发最后一张牌。

作为一个题外话,当发牌者选择将用过的牌收集在一起或者去一副新牌的时候,这个问题对于试图找出剩余牌的算牌者来说是很重要的。在许多赌场,庄家使用多副牌来阻止算牌。我的计划没有赋予众议院这种能力。如果你想用一个程序来练习算牌,你可以用这个程序来模拟这些效果。你可以让玩家控制牌副的数量,使用随机处理,等到剩余牌的数量低于一个固定的数量,或者其他什么。

当玩家请求另一张牌或者当玩家决定持有时,发牌者可以请求另一张牌。如前所述,评估庄家是否要求换牌的函数是more_to_house。计算是将手的值相加。如果有任何 ace,该函数将额外增加 10 点,如果这将使总数为 21 或更少,也就是说,它使 1 ace 计为 11。然后,它评估总和是否小于 17。如果是,它返回true,告诉调用函数请求一张新卡。如果值超过 17,则返回false

function more_to_house(){
  var ac = 0;
  var i;
  var sumup = 0;
    for (i=0;i<hi;i++) {
    sumup += househand[i].value;
    if (househand[i].value==1) {ac++;}
  }
  if (ac>0) {
    if ((sumup+10)<=21) {
       sumup += 10;
    }
  }

  housetotal = sumup;
    if (sumup<17) {
   return true;
  }
  else {
    return false;
  }
}

如果你想为房子尝试不同的策略,more_to_house就是你要改变的功能。

开始一个新游戏对程序员来说是一个挑战。首先要明白重新开始是什么意思。对于 21 点的实现,我为玩家提供了开始一手新牌的选项,这意味着继续使用同一副牌。要从一副没有分发出去的牌开始,玩家必须重新加载文档。我给玩家按下 n 键时调用的函数取的名字是newgame。所需的操作是清除画布,并重置玩家和庄家的指针,以及保存下一张牌的水平位置的变量。这个函数在调用dealstart时结束。

function newgame() {
  if (!gamestart) {
     gamestart = true;
     ctx.clearRect(0,0,cwidth,cheight);
     pi=0;
     hi=0;
     playerxp = 100;
     housexp= 500;
     dealstart();
  }
}

洗牌

集中注意力游戏中的洗牌技术(见第五章)代表了我和我的孩子在玩游戏时所做的一种实现:我们摊开纸牌,抓住一对并交换他们的位置。对于 21 点,一个朋友给我指了一个伊莱·本德斯基( http://eli.thegreenplace.net/2010/05/28/the-intuition-behind-fisher-yates-shuffling/ )的网站,解释了费希尔-耶茨算法 该算法的策略是随机确定牌组中的每个位置,从终点开始,向起点前进。该计算从 0 到当前位置(包括当前位置)确定一个随机位置,并进行交换。主要的shuffle功能如下:

function shuffle() {
  var i = deck.length - 1;
  var s;
  while (i>0) {
      s = Math.floor(Math.random()*(i+1));
      swapindeck(s,i);
      i--;
  }
  }

回想一下,Math.random() * N返回一个从零到不包括N的数。取结果的Math.floor返回一个从零到N的整数。所以如果我们想要一个从0i的数字,我们需要写Math.floor(Math.random()*(i+1))。为了使shuffle函数更容易阅读,我创建了一个名为swapindeck的独立函数,它交换位于函数参数所指示位置的两张卡片。为了执行交换,需要一个额外的位置,这就是变量hold。这个额外的位置是需要的,因为两个赋值语句不能同时完成。

 function swapindeck(j,k) {
    var hold = new MCard(deck[j].num,deck[j].suit,deck[j].picture.src);
    deck[j] = deck[k];
    deck[k] = hold;
 }

捕捉按键

箭头键的使用在第七章的迷宫游戏中有描述。这实质上是对那种解释的重复。

检测键盘上的一个键被按下,并确定哪个键被称为,捕获击键 代码必须设置对按键事件的响应,类似于设置对鼠标事件的响应。编码从调用addEventListener方法开始,这次是调用这个应用程序的window

window.addEventListener('keydown',getkey,false);

这意味着当一个键被按下时,将调用getkey功能。

注意

还有keyupkeypress事件。keydownkeyup只发射一次。如果玩家按住键,一段时间后keypress事件会再次发生。

现在,正如您在这一点上可能预期的那样,获取哪个键的信息的编码涉及不同浏览器的代码。以下代码通过两种方式获得与密钥对应的数字,适用于 Chrome、Firefox 和 Safari:

if(event == null)
  {
    keyCode = window.event.keyCode;
    window.event.preventDefault();
  }
  else
  {
    keyCode = event.keyCode;
    event.preventDefault();
  }

preventDefault函数做的就像它听起来的那样:它阻止任何默认动作,比如与特定键相关联的特殊快捷动作。该应用程序中唯一感兴趣的键是三个键 d、h 和 n。下面的switch语句确定哪个键被按下并调用正确的功能:dealplayerdonenewgame。一条switch语句将括号中的值与术语case之后的值进行比较,并开始执行第一条匹配的语句。break;语句导致执行跳出switch语句。default条款就是它听起来的样子。这不是必需的,但是如果存在,如果没有与提供的 case 值匹配的内容,则执行default:之后的语句。

  switch(keyCode)  {
    case 68:  //d
       deal();
       break;
    case 72:  //h
       playerdone();
       break;
    case 78:  //n
       newgame();
       break;
    default:
       alert ("Press d, h, or n.");
  }

回想一下,您可以通过修改整个switch语句来确定任何键的键码,默认情况下只有下面一行:

      alert(" You just pressed keycode "+keyCode);

做实验,按下按键,写下显示的数字。

警告

像我有时做的那样,如果你在电脑的不同窗口之间移动,你可能会发现当你回到 21 点游戏并按下一个键时,程序没有反应。您需要在包含 21 点文档的窗口上单击鼠标。这允许操作系统恢复对 21 点文档的关注,以便可以进行按键监听。

使用页眉和页脚元素类型

HTML5 增加了一些新的内置元素类型,包括headerfooter。这些和其他新元素(例如,articlenav)背后的基本原理是提供服务于标准目的的元素,以便搜索引擎和其他程序知道如何处理材料,尽管仍然有必要指定格式。以下是我们将在本例中使用的样式:

footer {
    display:block;
    font-family:Tahoma, Geneva, sans-serif;
    text-align: center;
    font-style:oblique;
}
header {
    width:100%;
    display:block;
}

display设置可以是blockinline。将这些设置为block会强制换行。注意,强制换行符对于某些浏览器来说可能是不必要的,但是使用它没有坏处。font-family属性是一种指定字体选择的方式。如果用户的计算机上有 Tahoma,就会使用它。下一个尝试的字体将是 Geneva。如果两者都不存在,浏览器将使用默认设置的无衬线字体。text-alignfont-style设置是它们看起来的样子。width设置将该元素设置为包含元素的整个宽度,在本例中为body。随意实验!

注意,你不能假设页脚在屏幕的底部,页眉也不在顶部。我通过在 HTML 文档中使用定位实现了这一点。

我使用页脚来显示纸牌图像和洗牌算法的源代码。提供信用、显示版权和显示联系信息都是对footer元素的典型使用,但是对于如何使用这些新元素、将它们放在 HTML 文档中的什么位置以及如何格式化它们没有限制。

构建应用程序并使之成为您自己的应用程序

本游戏中使用的函数在表 10-1 中有描述。

表 10-1

二十一点功能

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad函数调用 | builddeckshuffledealstart |
| getkey | 由 init 中的window.addEventListener调用调用 | dealplayerdonenewgame |
| dealstart | init | dealfromdeck四次 |
| deal | getkey | 对dealfromdeck的两次调用和对more_to_house的一次调用 |
| more_to_house | deal, playerdone |   |
| dealfromdeck | dealdealstart, playerdone |   |
| builddeck | init | MCard |
| MCard | builddeck, swapindeck |   |
| add_up_player | playerdone |   |
| playerdone | getkey | more_to_housedealfromdeck showhouseadd_up_player |
| newgame | getkey | dealstart |
| showhouse | playerdone |   |
| shuffle | init | swapindeck |
| swapindeck | shuffle | MCard |

本例中的函数以过程调用模式为特色,只有initgetkey作为事件的结果被调用。请理解这样一个事实,即有许多方法来编写一个应用程序,包括函数的定义。一般来说,将代码分割成小函数是一种好的做法,但这不是必需的。类似的代码行在很多地方重复,所以有机会定义更多的函数。注释文件见表 10-2 。

表 10-2

21 点游戏的注释代码

|

密码

|

说明

|
| --- | --- |
| <html> | 开始html标签。 |
| | 开始head标签。 |
| <title>Black Jack</title> | 完成title元素。 |
| <style> | 开始style标签。 |
| body { | 指定body元素的样式。 |
| background-color:white; | 设置背景颜色。 |
| color: black; | 设置文本的颜色。 |
| font-size:18px; | 设置字体大小。 |
| font-family:Verdana, Geneva, sans-serif; | 设置字体系列。 |
| } | 关闭样式。 |
| footer { | 指定页脚的样式。 |
| display:block; | 将此元素视为一个块。 |
| font-family:Tahoma, Geneva, sans-serif; | 设置字体系列。 |
| text-align: center; | 将文本居中对齐。 |
| font-style:oblique; | 使文本倾斜。 |
| } | 关闭样式。 |
| header { | 指定页眉的样式。 |
| width:100%; | 让它占据了整个窗口。 |
| display:block; | 将其视为一个块。 |
| } | 关闭样式。 |
| </style> | 关闭style元素。 |
| <script> | 启动script元素。 |
| var cwidth = 800; | 设置画布的宽度;清除画布时使用。 |
| var cheight = 500; | 设置画布的高度;清除画布时使用。 |
| var cardw = 75; | 设置每张卡片的宽度。 |
| var cardh = 107; | 设置每张卡的高度。 |
| var playerxp = 100; | 设置玩家手中牌的起始水平位置。 |
| var playeryp = 300; | 设置玩家手中牌的垂直位置。 |
| var housexp = 500; | 设置庄家手中牌的起始水平位置。 |
| var houseyp = 100; | 设置庄家手中牌的垂直位置。 |
| var housetotal; | 庄家手中的总价值。 |
| var playertotal; | 玩家手牌的总价值。 |
| var pi = 0; | 玩家手中下一张牌的索引。 |
| var hi = 0; | 庄家手中下一张牌的索引。 |
| var deck = []; | 掌握所有的牌。 |
| var playerhand = []; | 为玩家拿着牌。 |
| var househand = []; | 为庄家拿着牌。 |
| var back = new Image(); | 用于卡片背面。 |
| var ctx; | 用于保存画布上下文。 |
| var gamestart = false; | 用于检查游戏是否已经开始。 |
| function init() { | 函数体中的onLoad调用该函数来执行初始化任务。 |
| ctx = document.getElementById('canvas').``getContext('2d'); | 设置用于所有绘图的变量。 |
| ctx.font="italic 20pt Georgia"; | 设置字体。 |
| ctx.fillStyle = "blue"; | 设置颜色。 |
| builddeck(); | 调用函数来构建卡片组。 |
| back.src ="cardback.png"; | 指定牌背面的图像(注意,只显示一张背面:庄家的隐藏牌)。 |
| canvas1 = document.getElementById('canvas'); | 为事件处理设置变量。 |
| window.addEventListener('keydown',getkey,false); | 设置keydown按压的事件处理。 |
| shuffle(); | 调用函数来洗牌。 |
| dealstart(); | 调用函数分发前四张牌。 |
| } | 关闭该功能。 |
| function getkey(event) { | 函数来响应 keydown 事件。 |
| var keyCode; | 保存指定密钥的代码。 |
| if(event == null) | 特定于浏览器的代码,用于确定事件是否为空。 |
| { | 开放条款。 |
| keyCode = window.event.keyCode; | 从window.event.keyCode获取键码。 |
| window.event.preventDefault(); | 停止其他按键响应。 |
| } | 关闭子句。 |
| else   { | 条款。 |
| keyCode = event.keyCode; | 从event.keyCode中提取键码。 |
| event.preventDefault(); | 停止其他按键响应。 |
| } | 关闭子句。 |
| switch(keyCode)  { | 基于keyCodeswitch语句的标题。 |
| case 68: | d 键已被按下。 |
| deal(); | 给玩家发另一张牌,也可能给庄家。 |
| break; | 离开开关。 |
| case 72: | h 键已被按下。 |
| playerdone(); | 调用playerdone功能。 |
| break; | 离开开关。 |
| case 78: | 已按下 n 键。 |
| newgame(); | 调用newgame功能。 |
| break; | 离开开关。 |
| default: | 默认选项,如果你觉得玩家使用不可识别的按键时没有必要向他们提供反馈,可以移除。 |
| alert("Press d, h, or n."); | 反馈信息。 |
| } | 关闭开关。 |
| } | 关闭该功能。 |
| function dealstart() { | 最初发牌功能的标题。 |
| playerhand[pi] = dealfromdeck(1); | 获取玩家的第一张牌。 |
| ctx.drawImage(playerhand[pi].picture,``playerxp,playeryp,cardw,cardh); | 在画布上绘制。 |
| playerxp = playerxp+30; | 调整水平指针。 |
| pi++; | 增加玩家的牌数。 |
| househand[hi] = dealfromdeck(2); | 获取庄家的第一张牌。 |
| ctx.drawImage(back,housexp,houseyp,cardw,cardh); | 在画布上绘制一张牌的背面。 |
| housexp = housexp+20; | 调整水平指针。 |
| hi++; | 增加庄家的牌数。 |
| playerhand[pi] = dealfromdeck(1); | 给玩家发第二张卡。 |
| ctx.drawImage(playerhand[pi].picture,``playerxp,playeryp,cardw,cardh); | 在画布上绘制。 |
| playerxp = playerxp+30; | 调整水平指针。 |
| pi++; | 增加玩家的牌数。 |
| househand[hi] = dealfromdeck(2); | 给庄家发第二张牌。 |
| ctx.drawImage(househand[hi].picture,``housexp,houseyp,cardw,cardh); | 在画布上绘制。 |
| housexp = housexp+20; | 调整水平指针。 |
| hi++; | 增加赌场的牌数。 |
| } | 关闭该功能。 |
| function deal() { | 用于处理游戏的函数的标题。 |
| if (gamestart) { | 检查游戏是否已经开始。 |
| playerhand[pi] = dealfromdeck(1); | 给玩家发一张牌。 |
| ctx.drawImage(playerhand[pi].picture,``playerxp,playeryp,cardw,cardh); | 在画布上绘制。 |
| playerxp = playerxp+30; | 调整水平指针。 |
| pi++; | 增加玩家的牌数。 |
| if (more_to_house()) { | if功能是告诉发牌者应该有更多的牌。 |
| househand[hi] = dealfromdeck(2); | 给房子发一张牌。 |
| ctx.drawImage(househand[hi].picture,``housexp,houseyp,cardw,cardh); | 在画布上绘制一张卡片。 |
| housexp = housexp+20; | 调整水平指针。 |
| hi++; | 增加庄家的牌数。 |
| } | 关闭if true子句。 |
| } | 关闭if(gamestart)if true子句。 |
| else{``alert("Press n to start a new game with the same deck.\n Reload page to start a game with a new deck."); | 打印出消息给玩家开始新的游戏或重新加载以获得新的甲板。 |
| } | 为未开始的游戏关闭 else。 |
| } | 关闭该功能。 |
| function more_to_house(){ | 决定庄家移动的函数的标题。 |
| var ac = 0; | 保存 ace 计数的变量。 |
| var i; | 迭代变量 |
| var sumup = 0; | 初始化变量的总和。 |
| for (i=0;i<hi;i++) { | 遍历所有的卡片。 |
| sumup += househand[i].value; | 增加庄家手中牌的价值。 |
| if (househand[i].value==1) {ac++;} | 记录 ace 的数量。 |
| } | 关闭for循环。 |
| if (ac>0) { | if语句来确定是否有 ace。 |
| if ((sumup+10)<=21) { | 如果是这样,会询问使其中一个 ace 的值为 11 是否仍会产生小于 21 的总数。 |
| sumup +=10; | 如果是,就去做。 |
| } | 关闭内部if。 |
| } | 关闭外部if。 |
| housetotal = sumup; | 将全局变量设置为总和。 |
| if (sumup<17) { | 询问总和是否小于 17。 |
| return true; | 如果是,则返回true,意味着可以再得到一张卡。 |
| } | 关闭子句。 |
| else { | 开始else子句。 |
| return false; | 返回false,意味着庄家不会再得到一张牌。 |
| } | 关闭else子句。 |
| } | 关闭该功能。 |
| function dealfromdeck(who) { | 从牌组发牌的函数的标题。 |
| var card; | 拿着卡。 |
| var ch = 0; | 保存下一张未加密卡的索引。 |
| while ((deck[ch].dealt>0)&&(ch<51)) { | 询问这张牌是否已经发出。 |
| ch++; | 增加ch继续下一张卡。 |
| } | 关闭while循环。 |
| if (ch>=51) { | 询问是否没有未打完的牌。 |
| ctx.f illText("NO MORE CARDS IN``DECK. Reload. ",200,250); | 直接在画布上显示消息。 |
| ch = 51; | 将ch设置为 51,使该功能生效。 |
| gamestart = false; | 阻止对任何玩家要求新卡的响应。 |
| } | 关闭if true子句。 |
| deck[ch].dealt = who; | 存储非零值who,因此这张牌被标记为已发。 |
| card = deck[ch]; | 设置卡片。 |
| return card; | 返回一张卡片。 |
| } | 关闭该功能。 |
| function builddeck() { | 构建MCard对象的函数的头。 |
| var n; | 用于内部迭代的变量。 |
| var si; | 用于外层迭代的变量。 |
| var suitnames= ["clubs","hearts",``"spades","diamonds"]; | 西装的名字。 |
| var i; | 跟踪放入卡片组数组的元素。 |
| i=0; | 将数组初始化为 0。 |
| var picname; | 简化编码。 |
| var nums=["a","2","3","4","5","6","7",``"8","9","10","j","q","k"]; | 所有卡片的名字。 |
| for (si=0;si<4;si++) { | 对套装进行迭代。 |
| for (n=0;n<13;n++) { | 迭代花色中的牌。 |
| picname=suitnames[si]+"-"+nums[n]+``"-75.png"; | 构造文件的名称。 |
| deck[i]=new MCard(n+1,suitnames[si],``picname); | 用指定的值构造一个MCard。 |
| i++; | 增量 I |
| } | 关闭内部for循环。 |
| } | 关闭外部for回路。 |
| } | 关闭该功能。 |
| function MCard(n, s, picname){ | 用于创建对象的构造函数的头。 |
| this.num = n; | 设置num值。 |
| if (n>10) n = 10; | 在牌面牌的情况下进行调整。 |
| this.value = n; | 设置值。 |
| this.suit = s; | 设定套装。 |
| this.picture = new Image(); | 创建一个新的Image对象,并将其指定为一个属性。 |
| this.picture.src = picname; | 将该Image对象的src属性设置为图片文件名。 |
| this.dealt = 0; | 将处理的属性初始化为 0。 |
| } | 关闭该功能。 |
| function add_up_player() { | 决定玩家手牌价值的函数的标题。 |
| var ac = 0; | 保存 ace 的计数。 |
| var i; | 用于迭代。 |
| var sumup = 0; | 初始化总和。 |
| for (i=0;i<pi;i++) { | 在玩家手中的牌上循环。 |
| sumup += playerhand[i].value; | 增加玩家手牌的价值。 |
| if (playerhand[i].value==1) | 询问牌是否是 a。 |
| {ac++; | 增加 ace 的计数。 |
| } | 关闭if语句。 |
| } | 关闭for循环。 |
| if (ac>0) { | 问是否有 a。 |
| if ((sumup+10)<=21) { | 如果这还不能让 sum 过去。 |
| sumup +=10; | 让一张 a 变成 11。 |
| } | 关闭内部if。 |
| } | 关闭外部if。 |
| return  sumup; | 返回总数。 |
| } | 关闭该功能。 |
| function playerdone() { | 当玩家说保持时调用的函数的头。 |
| If (gamestart) { | 检查游戏是否已经开始。 |
| while(more_to_house()) { | more_to_house功能指示发牌者应该得到另一张牌。 |
| househand[hi] = dealfromdeck(2); | 给庄家发一张牌。 |
| ctx.drawImage(back,housexp,houseyp,``cardw,cardh); | 在画布上绘制卡片。 |
| housexp = housexp+20; | 调整水平指针。 |
| hi++; | 增加庄家手牌的指数。 |
| } | 关闭while循环。 |
| showhouse(); | 露出庄家的牌。 |
| playertotal = add_up_player(); | 决定玩家的总数。 |
| if (playertotal>21){ | 询问玩家是否结束。 |
| if (housetotal>21) { | 问房子是否结束了。 |
| ctx.fillText("You and house both``went over.",30,100); | 显示一条消息。 |
| } | 关闭内部if语句。 |
| else { | 开始else子句。 |
| ctx.fillText("You went over and lost."``,30,100); | 显示一条消息。 |
| } | 关闭else子句。 |
| } | 关闭外层子句(玩家结束)。 |
| else | else玩家没有结束。 |
| if (housetotal>21) { | 问经销商是否结束了。 |
| ctx.fillText("You won. House went``over.",30,100); | 显示一条消息。 |
| } | 关闭子句。 |
| else | 否则。 |
| if (playertotal>=housetotal) { | 比较两个数量。 |
| if (playertotal>housetotal) { | 执行更具体的比较。 |
| ctx.fillText("You won. ",30,100); | 显示获胜者消息。 |
| } | 关闭内部子句。 |
| else { | 开始else子句。 |
| ctx.fillText("TIE!",30,100); | 显示一条消息。 |
| } | 关闭else子句。 |
| } | 关闭外层子句。 |
| else | 否则。 |
| if (housetotal<=21) { | 检查经销商是否在。 |
| ctx.fillText("You lost. ", 30,100); | 显示一条消息。 |
| } | 关闭子句。 |
| else { | 开始else子句。 |
| ctx.fillText("You won because``house went over."); | 显示一条消息(玩家在下面,房子在上面)。 |
| } | 关闭子句。 |
| gamestart = false; | 重置gamestart。 |
| } | 关闭if(gamestart)if true类。 |
| else{``alert("Press n to start a new game with the same deck.\n Reload for a new deck and then press n to start a game.");``} | 给玩家的消息。 |
| } | 关闭该功能。 |
| function newgame() { | 新游戏的函数头。 |
| ctx.clearRect(0,0,cwidth,cheight); | 清除画布。 |
| pi=0; | 重置播放器的索引。 |
| hi=0; | 重置经销商的索引。 |
| playerxp = 100; | 重置玩家手中第一张牌的水平位置。 |
| housexp= 500; | 重置庄家手牌的水平位置。 |
| dealstart(); | 调用函数开始发牌。 |
| } | 关闭该功能。 |
| function showhouse() { | 显示庄家手牌的函数的标题。 |
| var i; | 用于迭代。 |
| housexp= 500; | 重置水平位置。 |
| for (i=0;i<hi;i++) { | for在手上打圈。 |
| ctx.drawImage(househand[i].picture,``housexp,houseyp,cardw,cardh); | 抽牌。 |
| housexp = housexp+20; | 调整指针。 |
| } | 关闭for循环。 |
| } | 关闭该功能。 |
| function shuffle() { | 洗牌的头。 |
| var i = deck.length - 1; | 将i变量的初始值设置为指向最后一张牌。 |
| var s; | 用于随机选择的变量。 |
| while (i>0) { | 只要i大于零。 |
| s = Math.floor(Math.random()*(i+1)); | 随机挑选。 |
| swapindeck(s,i); | 在 I 位置与卡交换。 |
| i--; | 减量。 |
| } | 关闭while循环。 |
| } | 关闭该功能。 |
| function swapindeck(j,k) { | 用于交换的辅助函数。 |
| var hold = new MCard(deck[j].num,deck[j].``suit,deck[j].picture.src); | 将卡片保存在位置 j。 |
| deck[j] = deck[k]; | 将 k 位置的卡片分配到 j 位置。 |
| deck[k] = hold; | 将暂停分配给 k 位置的卡。 |
| } | 关闭该功能。 |
| </script> | 关闭script元素。 |
| </head> | 关闭head元素。 |
| <body onLoad="init();"> | 开始标记来设置对init的调用。 |
| <header>``Press <b>n</b> for a new game (same deck), <b>d</b> for deal 1 more card, <b>h</b> for hold. Reload for a new deck and then press n for a new game.<br></header> | 包含指令的标题元素。 |
| <canvas id="canvas" width="800" height="500"> | 帆布开瓶器。 |
| Your browser doesn't support the HTML5``element canvas. | 对不兼容浏览器的警告。 |
| </canvas> | 关闭元素。 |
| <footer>Card images obtained courtesy of the American Contract Bridge Association,``<a href="``http://acbl.mybigcommerce.com/52-playing-cards/``<br/> | 打开 footer 元素,它给出了扑克牌图像的来源和链接。 |
| Fisher-Yates shuffle explained at``http://eli.thegreenplace.net``/2010/05/28/the-intuition-behind-``fisher-yates-shuffling | 添加关于洗牌算法的文章的学分。 |
| </footer> | 关闭页脚。 |
| </body> | 关闭身体。 |
| </html> | 关闭 HTML 文件。 |

您可以通过多种方式来改变游戏的外观和感觉,包括为玩家提供不同的方式来请求发一张新卡、用当前手牌持有或请求一手新牌。您可以创建或获取自己的卡片图像集。保持手到手的得分,也许包括某种赌博,将是一个很好的增强。改变庄家玩法的规则是可能的。正如我前面指出的,实现开始一副新牌是在计算机/庄家的控制下,基于一个分数或通过涉及随机处理的计算来完成,是一个值得考虑的想法。另一种增加游戏难度的方法是使用多副牌。记分是一个显而易见的功能,一种方法是添加一个钱包功能,从一些钱开始,在每场比赛中减少,在获胜时增加。分数和/或更完整的结果可以使用localstorage存储在本地计算机上。

测试和上传应用程序

这个程序需要大量的测试。请记住,当你作为测试者获胜时,测试并没有结束。当你经历了许多不同的场景,它就完成了。我第一次测试这个游戏是在没有洗牌的情况下进行的。然后,我进行洗牌,并跟踪测试揭示的案例。我按下 d 键是为了再发一张牌,按下 h 键是为了持有,按下 n 键是为了在不同的情况下玩新游戏。当您想让其他人来测试您的应用程序时,这绝对是一种情况。

上传应用程序需要上传所有图像。如果您使用与我在这里演示的不同的东西,您将需要更改builddeck函数来为文件构造适当的名称。

摘要

在这一章中,你学习了如何使用 HTML5、JavaScript 和 CSS 的特性以及一般的编程技术来实现一个纸牌游戏。其中包括:

  • 基于外部文件的名称生成一组Image对象

  • 为卡片设计一个程序员定义的对象类,包含Image元素、卡片花色和卡片价值

  • 在屏幕上绘制图像和文本

  • 使用forwhileif实现 21 点的逻辑

  • 使用计算和逻辑来生成计算机的移动

  • keydown事件建立事件处理,以便玩家可以指示请求发牌、持有或开始新游戏,并使用switch来区分按键

  • 使用 HTML5 中新出现的headerfooter元素来指示方向并注明出处

这是这本书的最后一章。我希望你把你所学到的东西拿来制作这些游戏和你自己发明的游戏的增强版。好好享受!

我的 HTML5 和 JavaScript 项目书,第 2 版,已经更新,包括一个名为 Add to 15 的游戏的实现,新媒体的使用,以及一个工具的介绍,使你的项目对不同屏幕尺寸和触摸的不同设备做出响应,而不是鼠标事件或对只使用键盘的人可用。就编程技术而言,这是一本适合你的下一本书。如果你想探索一种不同的编程语言,请考虑*编程 101:使用处理编程语言揭示编程的方式和原因。**

posted @ 2024-08-19 15:41  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报