HTML5 基础知识指南(三)
七、迷宫
在本章中,我们将介绍
-
响应鼠标事件
-
圆与直线碰撞的计算
-
响应箭头键
-
表单输入
-
使用
try和catch对本地存储器中的信息进行编码、保存、解码和恢复,以测试编码是否被识别 -
使用
join和split对信息进行编码和解码 -
在按钮中使用
javascript:来调用函数 -
单选按钮
介绍
在这一章中,我们将继续探索编程技术以及 HTML5 和 JavaScript 特性,这一次我们将使用构建和遍历迷宫的程序。玩家将能够绘制一组墙壁来组成一个迷宫。他们将能够保存和加载他们的迷宫,并使用碰撞检测来遍历它们,以确保它们不会穿越任何墙壁。
一般的编程技术包括为需要在画布上绘制的所有内容使用数组,以及为迷宫中的一组墙壁使用单独的数组。在游戏开始之前,墙的数量是未知的,所以需要一个灵活的方法。一旦迷宫建成,我们将看到如何响应箭头键的按压,以及如何检测游戏棋子(五边形令牌)和墙壁之间的碰撞。使用 HTML5,我们可以处理鼠标事件,这样玩家可以按下鼠标按钮,然后拖动和释放按钮来定义迷宫的每堵墙;响应箭头键来移动令牌;并在本地计算机上保存和检索墙的布局。像往常一样,我们将构建应用程序的多个版本。首先,所有内容都包含在一个 HTML 文件中。也就是说,玩家建造一个迷宫,可以在其中穿行,并可以选择将其保存到本地计算机或恢复先前保存的一组墙壁。在第二个版本中,有一个程序创建迷宫,第二个文件使用单选按钮为玩家提供穿越特定迷宫的选择。也许一个人可以在给定的计算机上建造迷宫,然后请一个朋友试着穿越它们。
HTML5 的本地存储设备只接受字符串,所以我们将看看如何使用 JavaScript 将迷宫信息编码成字符串,然后解码回来重建迷宫的墙壁。保存的信息将保留在计算机上,即使在计算机关闭后。
我们将在本章中讨论的各个功能:构建结构、使用箭头键移动游戏棋子、检查碰撞以及在用户计算机上编码、保存和恢复数据,都可以在各种游戏和设计应用程序中重用。
注意
HTML 文件通常被称为脚本,而术语程序通常是为 Java 或 c 等语言保留的。这是因为 JavaScript 是一种解释型语言:在执行时一次翻译一条语句。相比之下,Java 和 C 程序是编译的,也就是说,一次完全翻译完,结果存储起来以备后用。我们中的一些人并不那么严格,他们使用脚本、程序、应用程序或简单的文件来表示带有 JavaScript 的 HTML 文档。
图 7-1 显示了一体机程序和第二个程序的第一个脚本的开始屏幕。
图 7-1
迷宫游戏的开场画面
图 7-2 显示了在画布上放置了一些相当粗糙的墙壁后的屏幕。
图 7-2
迷宫的墙壁
图 7-3 显示了玩家使用箭头键将令牌移入迷宫后的屏幕。
图 7-3
在迷宫中移动令牌
如果玩家想保存一组墙,他或她输入一个名字并点击按钮。为了检索添加到当前画布上的墙壁,玩家键入名称并按下 GET SAVED WALLS 按钮。如果该名称下没有保存任何内容,则不会发生任何事情。
双脚本应用程序让第二个脚本为玩家提供一个选择。图 7-4 为开启画面。
图 7-4
travelmate 脚本的打开屏幕
这个双脚本应用程序假设有人使用第一个脚本创建并保存了三个迷宫,并在第二个脚本中使用了特定的名称。此外,必须使用相同的浏览器来创建迷宫和进行迷宫活动。我这样做是为了演示 HTML5 的本地存储功能,它类似于 cookie——web 应用程序开发人员存储用户信息的一种方式。
注意
Cookies,以及现在的 HTML5 localStorage,是行为营销的基础。它们给我们带来了便利——我们不必记住密码等某些信息——但它们也是一种被跟踪的方式和销售的目标。我在这里没有立场,只是注意到设施。
图 7-5 显示了一个简单的迷宫。
图 7-5
简单的迷宫
图 7-6 显示了一个稍微复杂一点的迷宫。
图 7-6
中等的迷宫
图 7-7 显示了一个更难的迷宫,更难主要是因为玩家需要从第一个入口点向迷宫底部移动才能通过。当然,迷宫是由玩家/创作者来设计的。
图 7-7
更难的迷宫
一个重要的特性是,在双脚本应用程序中,单击 GET maze 按钮会强制删除当前的迷宫,并绘制新选择的迷宫。这不同于在一体化程序或第二个版本的创建部分中发生的情况,在第二个版本中,旧墙被添加到现有的内容中。和其他例子一样,这些只是程序的存根,用来演示 HTML5 的特性和编程技术。让项目成为你自己的有很多改进的机会。
关键要求
迷宫应用程序需要显示一个不断更新的游戏板,因为新的墙被竖起来了,代币被移动了。
建造迷宫的任务需要响应鼠标事件来收集建造一堵墙所需的信息。应用程序显示正在建造的墙。
走迷宫任务需要响应箭头键来移动令牌。游戏不能让代币穿过任何墙。
保存和检索操作要求程序对墙信息进行编码,将其保存在本地计算机上,然后检索它并使用它来创建和显示保存的墙。迷宫是相当复杂的结构:一组一定数量的墙,每堵墙由起始和结束坐标定义,也就是说,成对的数字代表画布上的 x,y 位置。对于要使用的本地存储设备,这些信息必须转换成一个字符串。
两个文档版本使用单选按钮来选择一个迷宫。
HTML5、CSS 和 JavaScript 特性
现在让我们看看 HTML5 和 JavaScript 的具体特性,它们提供了我们实现迷宫应用程序所需的东西。这是建立在前几章的基础上的:HTML 文档的一般结构;使用程序员定义的函数,包括程序员定义的对象;在canvas元素上绘制由线段组成的路径;程序员对象;和数组。前面的章节已经解决了画布上的鼠标事件(第四章中的炮弹和弹弓游戏以及第五章中的记忆游戏)和 HTML 元素上的鼠标事件(第六章中的问答游戏)。我们将涉及的新特性包括一种不同类型的事件:从玩家按下箭头键中获取输入,称为击键捕获;并且使用本地存储将信息保存在本地计算机上,即使在浏览器已经关闭并且计算机已经关机之后。请记住,您可以跳到“构建应用程序”一节来查看所有带注释的代码,然后返回到这一节来阅读对各个特性和技术的解释。
墙壁和令牌的表示
首先,我们将定义一个函数Wall,用于定义一个墙对象,另一个函数Token,用于定义一个令牌对象。我们将以一种比这个应用程序所要求的更通用的方式来定义这些函数,但是我相信这是没问题的:在性能方面,这种通用性不会影响太多,如果有的话,同时让我们可以自由地将代码用于其他应用程序,例如具有不同游戏棋子的游戏。我选择五边形是因为我喜欢它,并使用mypent作为游戏棋子的变量名。
为墙定义的属性由鼠标动作指定的起点和终点组成。我把这些命名为sx、sy、fx和fy。墙也有一个width和一个strokestyle字符串,一个draw方法被指定为drawAline。这比必要的更普遍的原因是因为所有的墙都有相同的宽度和样式字符串,并且都将使用drawAline函数。当需要将墙保存到本地存储时,我只保存sx、sy、fx和fy值。如果您编写其他程序并需要存储值,您可以使用相同的技术来编码更多的信息。
在迷宫中移动的令牌是通过调用Token函数来定义的。这个函数类似于为多边形记忆游戏定义的Polygon函数。Token函数存储令牌的中心、sx和sy,以及半径(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为真,代码获取鼠标坐标并使用它们来设置curwall的fx和fy值。当玩家按下按钮移动鼠标时,这种情况会反复发生。释放按钮时,调用功能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, sy到fx, 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 之间。
图 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 显示了一个简单的日期保存应用程序的屏幕截图。
图 7-9
一个简单的保存日期应用程序
用户有三个选项:存储当前日期和时间的信息,检索上次保存的信息,以及删除日期信息。图 7-10 显示了第一次使用该应用程序(或在日期被删除后)点击检索日期信息时发生的情况。
图 7-10
尚未保存或删除后的数据
我们的应用程序使用 JavaScript 警告框来显示消息。用户需要单击 OK 按钮从屏幕上删除警告框。
图 7-11 显示了用户点击商店日期信息按钮后的消息。
图 7-11
存储日期信息后
如果用户稍后单击 Retrieve Date Info 按钮,他将看到类似于图 7-12 的消息。
图 7-12
检索存储的日期信息
你可以给你的玩家一个方法,使用删除日期信息按钮来删除存储的信息。图 7-13 显示了结果。
图 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 的最新版本可能已经出来了,这不成问题。)
图 7-14
浏览器无法识别localStorage
JavaScript 还提供了一种避免显示错误的通用机制。复合语句try和catch将尝试执行一些代码,如果不成功,它将转到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 所示的消息。
图 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 文档。其中一个脚本与第一个应用程序相同,可用于构建、穿越和保存迷宫以及在每个迷宫中穿行。第二个脚本只是为了在一个固定的保存迷宫列表中穿行。一组单选按钮允许玩家从简单、中等和困难选项中进行选择,假设有人已经创建并保存了名为 easymaze 、 moderatemaze、和hard masze的迷宫。这些名称可以是您想要的任何名称,数量不限。你只需要在一个程序中创建的东西和在第二个程序中引用的东西保持一致。
现在让我们来解决localStorage只是存储字符串的问题。这里描述的应用程序必须存储足够的关于墙的信息,以便可以将这些墙添加到画布上。在单文档版本中,旧墙实际上被添加到画布上的任何内容中。两个文档的版本删除任何旧的迷宫并加载请求的迷宫。我使用两个表单,每个表单都有一个姓名输入字段和一个提交按钮。玩家选择名称以保存迷宫,并且必须记住它以进行检索。
要存储的数据是一个字符串,即一段文本。我们将通过对每面墙执行以下操作来创建包含一组墙的信息的文本:
-
将
sx, sy, fx, fy组合成一个名为w的单墙数组。 -
使用
join方法,使用w数组生成一个由+符号分隔的字符串。 -
将所有这些字符串添加到一个名为
allw的数组中。 -
再次使用
join方法,使用allw数组产生一个名为sw的字符串。
字符串变量sw将保存所有墙壁的所有坐标(每面墙四个数字)。下一步是使用localStorage.setItem方法将sw存储在玩家给定的名字下。我们使用上一节中解释的try和catch结构来实现这一点。
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 显示了脚本中用于创建、保存、检索和走迷宫的函数。注意,许多函数的调用都是通过事件处理来完成的:调用onLoad、onSubmit、addEventListener。这些并不直接或立即调用函数,而是设置在指示的事件发生时进行调用。
表 7-2
迷宫应用程序中的函数
|功能
|
调用方/被调用方
|
打电话
|
| --- | --- | --- |
| init | 由body标签中的onLoad动作调用 | drawall |
| drawall | initstartwallstretchwallgetkeyAndMovegetwalls | draw用于Wall s 和令牌的方法:drawtoken和drawAline |
| Token | var声明mypent的语句 | |
| Wall | startwall, getwalls | |
| drawtoken | drawall对everything数组中的令牌对象使用draw方法 | |
| movetoken | getkeyAndMove使用moveit方法进行mypent | intersect |
| drawAline | drawall对everything数组中的Wall对象使用draw方法 | |
| startwall | 由init中的addEventListener调用动作调用 | drawall, Wall |
| stretchwall | 由init中的addEventListener调用动作调用 | drawall |
| finish | 由init中的addEventListener调用动作调用 | |
| getkeyAndMove | 由init中的addEventListener调用动作调用 | movetoken使用moveit方法进行mypent |
| savewalls | 由sf form的onSubmit动作调用 | |
| 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 || ev.layerX == 0) { | 我们可以用layerX来确定鼠标的位置吗?有必要,因为浏览器不一样。 |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX➥ || 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 || ev.layerX == 0) { | 我们可以用layerX吗? |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX➥ || 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 和令牌的方法:drawtoken和drawAline |
| Token | var声明mypent的语句 | |
| Wall | startwall, getwalls | |
| drawtoken | drawall对everything数组中的令牌对象使用draw方法 | |
| movetoken | getkeyAndMove使用moveit方法进行mypent | intersect |
| drawAline | drawall对everything数组中的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.html和travelmaze.html。除了标题之外,buildmazesavelocally.html和buildmaze.html文件完全相同。这三个文件都有源代码。请注意,在您创建迷宫并使用本地存储保存到您自己的计算机上之前,travelmaze.html不会起作用。
双脚本版本的两个 HTML 文档可以在本地为现代浏览器工作,但是必须都上传到同一个服务器上,以测试服务器上的构建程序保存的迷宫是否可以被服务器上的旅行程序使用。
有些人可能会限制本地存储和 cookies 的使用。这些结构之间存在差异。在生产应用程序中使用这些都需要大量的工作。最终的退路是使用 PHP 之类的语言将信息存储在服务器上。
如果您打开了多个应用程序,您需要意识到“计算机”,即操作系统,需要确定哪个应用程序来处理任何按键操作。使用的术语是“焦点”。你可能需要用鼠标点击装有迷宫程序的窗口。这将设置焦点,单击箭头键将起作用。
摘要
在这一章中,你学习了如何实现一个程序来支持建造一个墙壁迷宫,并把它存储在本地计算机上。您还学习了如何创建一个迷宫旅行游戏。我们使用了以下编程技术和 HTML5 特性:
-
程序员定义的对象
-
捕捉击键;也就是说,为按键设置事件处理并解密哪个按键被按下
-
用于在玩家的电脑上保存迷宫墙壁的布局
-
try和catch检查某个编码是否可接受 -
用于数组的
join方法和用于字符串的split方法 -
鼠标事件
-
用于确定标记和迷宫墙壁之间碰撞的数学计算
-
单选按钮为玩家提供选择。
对于这个应用程序来说,本地存储的使用相当复杂,需要对迷宫信息进行编码和解码。一个更简单的用途可能是存储任何游戏的最高分数或当前分数。你可以回到前面的章节,看看你是否能加入这个特性。记住localStorage是和浏览器绑定的。在下一章中,你将学习如何实现石头剪子布游戏,以及如何在你的应用程序中加入音频。*
八、石头剪刀布
在本章中,我们将介绍
-
与电脑对战
-
创建用作按钮的图形
-
游戏规则的数组
-
font-family属性 -
继承的样式设置
-
声音的
介绍
本章结合了编程技术和 HTML5 JavaScript 特性来实现大家熟悉的石头剪子布游戏。在这个游戏的校园版中,每个玩家用手的符号来表示三种可能性中的一种:石头、布或剪刀。术语是玩家抛出三个选项中的一个。游戏规则是这样规定的:
-
石头压碎剪刀
-
纸覆盖岩石
-
剪刀剪纸
所以每个符号打败一个其他的符号:石头打败剪刀;纸打败了石头;剪刀打败了布。如果两个玩家扔同样的东西,那就是平局。
由于这是一个双人游戏,我们的玩家将与电脑对战,我们必须创造电脑的移动。我们将生成随机移动,玩家需要相信程序正在这样做,而不是基于玩家的投掷来移动。演示必须加强这种信任。
我们游戏的第一个版本只是使用了你将在这里看到的视觉效果。第二个版本增加了音频、由三个获胜事件控制的四个不同的剪辑以及平局选项。您可以使用源代码提供的声音文件,也可以使用您自己的声音。请注意,您需要更改代码中的文件名,以匹配您使用的声音文件。
在这种情况下,我们希望为玩家的移动使用特殊的图形。图 8-1 显示了应用程序的开始屏幕,包括三个用作按钮的图形,以及一个标有字符串"Score:"的字段,该字段保存初始值零。
图 8-1
石头剪刀布开场画面
玩家通过点击其中一个符号来移动。让我们看一个玩家点击石头图标的例子。我们假设电脑选择了剪刀。在一个简短的动画序列之后,一个剪刀符号开始变小并在屏幕上变大,出现一条文本消息,如图 8-2 所示。在添加了音频的版本中,音频剪辑将播放与石头压碎剪刀相对应的声音。请注意,现在的分数是 1。
图 8-2
玩家扔石头,电脑扔剪刀
接下来在游戏中,玩家和电脑打成平手,如图 8-3 所示。出现平局时分数没有变化,所以分数仍然是 1。
图 8-3
一条领带
后来,游戏已经打平,但玩家输了,分数降到负 1,这意味着玩家落后,如图 8-4 所示。
图 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;
}
函数的参数保存所有信息。名称sx、sy等的选择通过一个简单的修改避免了内置术语:将s放在前面。按钮的位置在sx, sy。矩形的颜色用rectcolor表示。图像的文件名由picture保存。我们认为的内部和外部宽度以及内部和外部高度是基于输入smargin、sheight和swidth计算的。bheight和bwidth中的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绘制一个填充矩形。最后,代码在矩形的顶部绘制一个图像,水平和垂直偏移一定的边距。计算出的bwidth和bheight分别比swidth和sheight大两倍的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;
如果inmotion为true,则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;
}
这部分代码的目标是让变量mx和my在鼠标按钮被点击时分别保存鼠标光标的水平和垂直坐标。某些浏览器将光标信息保存在名为layerX和layerY的ev参数的属性中,而其他浏览器则使用offsetX和offsetY。我们将使用局部变量来确保在所有浏览器中跟踪光标位置。如果ev.layerX对于该浏览器不存在,或者如果它存在并且具有值0,则条件ev.layerX将评估为false。因此,为了检查属性是否存在,我们需要使用复合条件(ev.layerX || ev.layerX == 0)来确保代码在所有情况下都有效。顺便说一句,如果第二个if测试失败,什么都不会发生。这段代码适用于 Chrome、Firefox 和 Safari,但可能最终会适用于所有浏览器。
下一段代码遍历everything的元素(有三个元素,但没有明确提到)来查看光标是否在任何一个矩形上。变量ch保存了对Throw的引用,因此所有的Throw属性,即sx、sy、bwidth和bheight,都可以在比较语句中使用。这是保存在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函数中接下来的几行提取其中一个文件名,并将其分配给图像变量compimg的src属性。
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表示内部数组中的元素,来自玩家棋步。beats和points阵列被称为并行结构。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。
两个变量result和newscore是全局变量。这意味着其他函数也可以使用它们,我们就是这样使用它们的:在一个函数中设置,在另一个函数中引用。
分数是用 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属性来表示字段中的右对齐。标签Score在form元素中。实际分数在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标题以及函数和方法调用中;在if、for、switch、while报表表头;以及用于指定复杂表达式中的运算顺序。花括号用于界定函数的定义以及if、for、switch和while语句的子句。方括号用于定义数组并返回数组的特定成员。级联样式表的语言将每种样式用花括号括起来。HTML 标记包括<和>,通常称为尖括号或尖括号。
使用动画显示结果
你已经在第三章的弹跳球应用程序和第四章的炮弹和弹弓中看到了动画的例子。概括地说,动画是通过快速连续地显示一系列静止图片来制作的。单独的图片被称为帧。在所谓的*计算动画中,*对象在屏幕上的新位置是为每一个连续的帧计算的。制作动画的一种方法是使用setInterval命令设置一个interval事件,如下所示:
tid = setInterval(flyin,100);
这导致每 100 毫秒调用一次flyin函数(每秒 10 次)。用于定时器标识符的变量tid被设置,因此代码可以关闭interval事件。flyin功能将创建尺寸不断增加的Throw对象,并保存相应的图像。当对象达到指定的大小时,代码显示结果并调整分数。这就是为什么变量result和newscore必须是全局变量——它们在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后的画面。
图 8-5
第一次调用 flyin,用一个小图像表示计算机移动
在对代码进行不同的修改后,图 8-6 显示了在后续步骤中暂停的动画。
图 8-6
动画的进一步发展
图 8-7 显示动画已完成,但就在带有结果的文本消息之前。
图 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 所示的内容。
图 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;
你可以查一下music和beats是并联结构,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 | init,choose | 调用每个对象的draw方法,在这个应用程序中总是在函数drawThrow中 |
| Throw | var用于全局变量的语句 | |
| drawThrow | drawall使用Throw对象的draw方法 | |
| choose | 由init中 addEventListener 调用的动作调用 | drawall |
| flyin | choose中setInterval的动作 | |
从表中可以看出,大多数函数调用都是隐式完成的,例如通过事件处理,而不是一个函数调用另一个函数。在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 || ev.layerX``== 0) { | 检查哪个编码适用于此浏览器。 |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } else if (ev.offsetX ||``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循环。 |
| } | 结束inmotion为false的true类。 |
| } | 结束函数。 |
| 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 文档的结束标记。 |
音频增强版除了增加了init、choose和flyin函数之外,还需要三个全局变量。新的全局变量是
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的事件处理 -
使用
setInterval和clearInterval的动画 -
用于声音的
audio元素和用于不同浏览器的source元素 -
getElementsByTagName和play用于音频剪辑的具体控制 -
程序员定义的对象,用于在屏幕上绘制程序员创建的按钮,带有确定鼠标光标是否点击了特定按钮的逻辑
-
游戏规则的数组,以并行结构组织
下一章描述了另一个熟悉的童年游戏:刽子手。它结合了在画布上绘图和使用您在前面章节中学到的代码创建 HTML 元素的技术,以及一些新的 CSS 和 JavaScript 特性。
九、Hangman
在本章中,我们将介绍
-
CSS 样式
-
为字母按钮生成标记
-
对一系列图形使用数组
-
使用字符串作为密码
-
为单词列表创建一个外部脚本文件
-
设置和移除事件处理
介绍
本章的目标是继续演示 HTML5、层叠样式表(CSS)和 JavaScript 的编程技术和特性,结合 HTML 标记的动态创建以及在画布上绘制图形和文本。本章的例子是另一个熟悉的游戏 Hangman 的纸笔游戏。
以防万一你需要温习一下规则,游戏是这样玩的:一个玩家想到一个秘密单词,写下破折号,让另一个玩家知道这个单词有多少个字母。另一个人猜单个字母。如果字母出现在单词中,玩家一用实际的字母替换代表猜测字母的破折号。如果字母没有出现在密语中,第一个玩家画下一步的绞刑简笔画。在图 9-1 所示的例子中,绞刑架已经出现在屏幕上。接下来是头,然后是身体,左臂,右臂,左腿,右腿,最后是绳子。玩家可以就允许多少步达成一致。如果绞刑在单词被猜中之前完成,玩家二输掉游戏。是的,这是一个残忍的游戏,但它很受欢迎,甚至被认为是有教育意义的。
在我们的游戏中,计算机扮演一号玩家的角色,从一个单词列表(在这种情况下是一个公认的非常短的列表)中选择秘密单词。你可以用我的列表。当你制作你自己的游戏时,用你自己的。从小处着手是有意义的,一旦你对你的游戏满意了,就列出一个更长的清单。我为单词列表使用外部文件的技术支持这种方法。
对于用户界面,我选择将字母表中的每个字母放在屏幕上。玩家通过点击一个方块来选择一个字母。选择一个字母后,它的方块会消失。这个决定受到这样一个事实的影响,即大多数玩纸笔游戏的人写出字母表,并在选择字母时划掉字母。
图 9-1 为开启画面。计算机选择了一个有四个字母的单词。请注意,在我们的节目中,绞刑架已经出现在屏幕上。或者,您可以选择将此作为绘图过程的前一步或两步。
图 9-1
打开屏幕
使用小型单词库的一个好处是,我知道现在的单词是什么,即使我的编码使用随机过程来选择单词。这意味着我可以在没有压力的情况下开发游戏。我决定先选一个一个。如图 9-2 所示,这个字母没有出现在密语中,于是屏幕上画出一个椭圆形的头像,字母 a 的方块消失。
图 9-2
猜 a 后截图
通过元音,我猜出一个 e ,结果如图 9-3 所示。
图 9-3
猜中一个 e 后的游戏
接下来,我猜中了一个 i ,导致我第三步走错,如图 9-4 。
图 9-4
三次错误选择后的游戏画面
现在,我猜测一个 o ,这被证明是正确的(因为我有内部消息,所以我知道),一个 o 出现在单词的第三个字母,如图 9-5 所示。
图 9-5
对 o 的正确猜测
我尝试下一个元音, u ,这也是正确的,如图 9-6 所示。
图 9-6
已经确认了两封信
我现在再做一些猜测,首先是一个 t ,如图 9-7 。
图 9-7
t 又猜错了
然后,我又猜错了,这次是一个 s ,如图 9-8 。
图 9-8
在对 s 的错误猜测之后
图 9-9 显示了另一个错误的猜测。
图 9-9
在对 d 的错误猜测之后
我决定做一个正确的猜测,即 m 。图 9-10 显示了三个识别出的字母和画在屏幕上的人的大部分。
图 9-10
在对 m 的正确猜测之后
在这一点上,我正在努力失去,所以我猜测 b 。这导致了图 9-11 中所示的结果。
图 9-11
智乐
请注意,该图显示了一个套索;完整的秘密单词被揭示;并出现一条消息,告诉玩家游戏失败,并重新加载再试一次。
图 9-12 显示了另一个游戏的截图,计算机已经通过在两个位置显示字母 e 的猜测做出了响应。处理在一个单词中出现不止一次的字母并不困难,但是在我开始编程之前,这一点对我来说并不明显。
图 9-12
在这个游戏中,e 出现在两个地方
我做了一些其他的猜测,最终得到了这个单词的正确答案。同样,从中做出选择的列表不是很长,所以我可以从字母的数量中猜出单词。图 9-13 显示了一个获胜游戏的截图。注意在密语中有两个*e’和三个f’*s。
图 9-13
赢得比赛
编程技术和语言特性包括操作字符串;使用保存英语字母表的字母的数组;创建标记元素来保存表示秘密单词的字母表和空格,该秘密单词可以由字母替换,也可以不由字母替换;为创建的字母块处理事件;设置一组绘制悬挂步骤的函数;并将函数名放在一个数组中。这个实现还演示了如何使用外部脚本文件来保存单词列表。这个游戏在游戏中有回合,不像石头剪子布,所以程序必须在内部管理游戏状态,并在屏幕上显示出来。
关键要求
和前一章一样,这个游戏的实现使用了前几章演示的许多 HTML5 和 JavaScript 结构,但是它们在这里以不同的方式组合在一起。编程类似于写作。在编程中,你把各种构造放在一起,就像你写由你知道的单词组成的句子,然后把这些放入段落,等等。在阅读本章的时候,回想一下你已经学过的在画布上画直线、弧线和文字的知识;创建新的 HTML 标记;为屏幕上的标记设置鼠标单击事件;并使用if和for语句。
要实现 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。字符串用双引号括起来,该字符串内的属性用单引号括起来。元素分布在屏幕上,从位置alphabetx、alphabety(每个全局变量都在文档中声明过)开始,水平递增alphabetwidth。对于像素,top和left属性需要设置为字符串并以"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方法之前,不会绘制任何内容。每当调用stroke或fill方法时,moveTo、lineTo和arc命令设置绘制的路径。在我们的绘制函数中,下一步是调用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函数使用相同的技术,除了对于套索,椭圆是宽的而不是窄的;也就是垂直的被挤压,而不是水平的。
绘图过程中的每一步都由一个函数表示,如drawhead和drawbody。我们将所有这些列在一个名为steps的数组中:
var steps = [
drawgallows,
drawhead,
drawbody,
drawrightarm,
drawleftarm,
drawrightleg,
drawleftleg,
drawnoose
];
一个变量cur,跟踪当前步骤,当代码确认条件cur等于steps的长度时,游戏结束。
在做了这些实验后,我决定我需要在套索上画一个头和一个脖子。这是通过调用drawnoose函数中的drawhead和drawneck来完成的。顺序很重要。
使用绘图功能作为模型来制作您自己的绘图。一定要改变这些单独的功能。您也可以添加或删除功能。这意味着你将改变游戏进程中的步数,也就是说,玩家在输掉游戏前可以猜错的次数。
小费
如果你还没有这样做(或者即使你已经这样做了),尝试绘画。创建一个单独的文件,只是为了绘制悬挂的步骤。尝试线条和弧线。您也可以包含图像。
维持游戏状态并确定输赢
编码和维护应用程序状态的需求是编程中的常见需求。在第二章中,我们的程序记录下一步是第一次掷骰子还是后续掷骰子。刽子手游戏的状态包括隐藏单词的身份,单词中的哪些字母被正确猜中,字母表中的哪些字母被尝试过,以及绞刑的进行状态。
当玩家点击一个字母块时调用的pickelement函数是关键动作发生的地方,它执行以下任务:
-
检查保存在变量
picked中的玩家的猜测是否与保存在变量secret中的秘密单词中的任何字母相匹配。对于每个match,空白元素中的相应字母通过将textContent设置为该字母来显示。 -
使用变量
lettersguessed记录已经猜出了多少个字母。 -
通过比较
lettersguessed和secret.length来检查游戏是否已经获胜。如果游戏获胜,删除字母按钮的事件处理并显示适当的消息。 -
如果选择的字母与密语中的任何字母都不匹配(如果变量
not仍然是true,则使用变量cur作为数组变量steps的索引来推进悬挂。 -
通过比较
cur和steps.length来检查游戏是否已经输了。如果两个值相等,则显示所有字母,移除事件处理,并显示适当的消息。 -
无论是否匹配,通过将
display属性设置为none使点击的字母按钮消失。
这些任务是使用if和for语句执行的。在确定字母被正确猜中之后,检查游戏是否已经获胜。类似地,只有当确定字母没有被正确识别并且悬挂已经进行时,才检查游戏是否已经失败。游戏的状态在代码中由secret、lettersguessed和cur变量表示。玩家看到秘密单词的下划线和填充字母以及剩余的字母块。
带有逐行注释的整个 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 编码。有两个初始化和设置函数(init和setupgame)和一个完成大部分工作的函数(pickelement),外加八个绘制悬挂步骤的函数。表 9-1 中描述了这些功能。
表 9-1
由调用调用的函数
|功能
|
调用/调用者
|
打电话
|
| --- | --- | --- |
| init | 由<body>标签中的onLoad动作调用 | setupgame |
| setupgame | init | 第一个绘图功能,即drawgallows |
| pickelement | 由setupgame中addEventListener调用的动作调用 | 通过调用steps[cur]()的绘图功能之一 |
| drawgallows | pickelement中steps[cur]()的调用 | |
| drawhead | pickelement、drawnoose中steps[cur]()的调用 | |
| drawbody | pickelement中steps[cur]()的调用 | |
| drawrightarm | pickelement中steps[cur]()的调用 | |
| drawleftarm | pickelement中steps[cur]()的调用 | |
| drawrightleg | pickelement中steps[cur]()的调用 | |
| drawleftleg | pickelement中steps[cur]()的调用 | |
| drawnoose | pickelement中steps[cur]()的调用 | drawhead,drawneck |
| 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: 0px;top: 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 标记
-
使用
addEventListener和removeEventListener为单个元素设置和移除事件处理 -
使用样式从显示中移除元素
-
使用函数名数组建立绘图序列
-
操纵变量来维持游戏的状态,通过计算来确定是赢还是输
-
创建一个外部脚本文件来保存单词列表以增加灵活性
-
使用 CSS,包括选择字体的
font-family、color和display
Hangman 游戏是演示编程概念的一个很有吸引力的例子,我在 press Publishers 的《编程 101:如何和为什么使用处理编程语言 、 进行编程》中使用了它。
本书的下一章也是最后一章描述了纸牌游戏 21 点的实现,也称为 21 点。它将建立在你所学的基础上,描述一些新的编程技术,HTML5 中添加的元素,以及更多的 CSS。