HTML5 基础知识指南(二)
四、炮弹和弹弓
在本章中,我们将介绍
-
维护要在屏幕上绘制的对象列表
-
旋转屏幕上绘制的对象
-
鼠标拖放操作
-
模拟弹道运动(重力效应)和碰撞的计算
介绍
本章演示了动画的另一个例子,在这种情况下,弹道模拟,也称为抛射体运动。球或类似球的物体保持恒定的水平(x)位移,垂直位移由于重力而改变。产生的运动是一个弧。当球(实际上)碰到地面或目标时就会停止。您将看到的代码使用演示球在盒子中弹跳的相同技术来生成动画。该代码重新定位球,并以固定的间隔重绘场景。我们将看三个例子。
-
一个非常简单的弹道模拟。球在击中目标或地面之前起飞并沿弧线飞行。飞行的参数是水平和初始垂直速度,由玩家使用表单输入字段设置。当球碰到目标或地面时,它就停止了。
-
一种改进的炮弹,用一个长方形代表倾斜一定角度的大炮。飞行的参数是出炮速度和炮角度。同样,这些是由玩家使用表单输入字段设置的。该程序计算初始水平和垂直位移值。
-
一个弹弓。飞行的参数是由玩家拖动,然后释放,一个球的形状拴在一个代表弹弓的木棒上来决定的。速度是由球到弹弓上一个地方的距离决定的。角度是弹弓这部分与水平面的夹角。
图 4-1 显示了简单的(无加农炮)应用。
图 4-1
球落在了地上
图 4-2 显示了第二个应用程序的开始屏幕。目标是一个Image,代表大炮的矩形可以旋转。注意控件引用了一个角度和一个初速度。
图 4-2
以图像为目标的旋转加农炮
图 4-3 为成功命中后的场景。请注意,加农炮被旋转了,目标的原始图像被替换为新图像。
图 4-3
在开炮并击中目标后
弹弓应用程序的打开屏幕如图 4-4 所示。这个应用程序类似于大炮,但飞行的参数是由玩家使用鼠标在球(代表弹弓中的岩石)上拖动来设置的,目标现在是一只鸡。
图 4-4
弹弓应用程序的打开屏幕
对于弹弓,我决定让球一直飞下去,直到它落地。但是,如果鸡被打了,我想换成羽毛,如图 4-5 。请注意,当松开鼠标按钮,球飞起来的时候,弹弓的线还在原来的位置。我发现我需要更多的时间来观察琴弦,以便计划我的下一次拍摄。如果您愿意,您可以更改游戏,使字符串弹回到它们的原始位置,或者创建一个新的游戏按钮。在我的例子中,通过重新加载 HTML 文件来重放游戏。
图 4-5
球击中鸡后落在地上,那里只剩下羽毛
这些应用程序的编程使用了许多在弹跳球应用程序中演示的相同技术。球在飞行中的重新定位只是因为它需要模拟由于重力而改变的垂直位移的效果。slingshot 应用程序为玩家提供了一种新的与应用程序交互的方式,使用鼠标的拖放操作。
带加农炮的炮弹和弹弓使用加农炮和弹弓的绘图功能以及原始目标和命中目标的外部图像文件。如果你想改变目标,你需要找到图像文件,然后用应用程序上传。
关键要求
我们的第一个要求是通过设置一个事件以固定的时间间隔发生来制作动画,然后设置一个函数通过重新定位球和检查碰撞来处理该事件。我们在前一章的弹跳球应用程序中讨论了这一点。这里新增的是模拟重力的计算。由简单物理模型指示的计算基于以恒定量改变垂直位移,然后计算旧的和新的位移的平均值来计算新的位置,计算出新的垂直位移。
-
水平位移(由变量
dx保存)是水平速度(horvelocity),不变。代码:dx = horvelocity; -
间隔开始时的垂直速度是
verticalvel1 -
间隔结束时的垂直速度是
verticalvel1加上加速量(gravity)。代码:verticalvel2 = verticalvel1 + gravity; -
井段(
dy)的垂直位移是verticalvel1和verticalvel2的平均值。代码: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 中定义的对象。对于弹道应用程序,我定义了自己的对象,特别是Ball、Picture、Myrectangle和Sling。这些不同对象中的每一个都包括一个draw方法的定义以及指示位置和维度的属性。我这样做是为了能画出每一个事物的清单。适当的draw方法访问属性来决定画什么和在哪里画。我还包括了旋转单个对象的方法。
定义一个对象很简单:我简单地为Ball、Picture和Myrectangle定义了一个名为构造函数的函数,并使用这些函数和操作符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.draw和this.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改变了应用程序放置对象的位置。该函数改变对象的sx和sy属性的值,当它下一次显示时,这些新值用于绘图。
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)");
Picture、Myrectangle和Sling的功能类似,稍后将进行解释。它们各自指定了一个draw方法。对于这个应用程序,我只为cball使用了moveit,但是我为其他对象定义了moveit,以防我以后想要在这个应用程序上构建。变量cannon和ground将被设置为保存一个new Myrectangle,变量target和htarget将被设置为保存一个new Picture。
小费
程序员编的名字是任意的,但是拼写和大小写保持一致是个好主意。HTML5 似乎不考虑大小写,这与 XHTML 版本形成对比。许多语言将大写字母和小写字母视为不同的字母。我一般使用小写,但是我将Ball、Picture、Slingshot和Myrectangle的第一个字母大写,因为按照惯例,打算作为对象构造函数的函数应该以大写字母开头。
使用数组方法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 所示。
图 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 中的图不是我想要的。
图 4-7
绘制和旋转矩形
问题是旋转点在原点,(0,0),而不是在红色矩形的角上。因此,我需要编写代码来执行一个转换,以重置原点,然后旋转,然后转换回来,以便在正确的位置绘制下一个项目。我可以使用 HTML5 的特性来做到这一点。画布上的所有绘图都是根据一个坐标系完成的,我可以使用save和restore操作来保存当前的坐标系——轴的位置和方向——然后恢复它以制作更多的绘图。这是代码。
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 所示。
图 4-8
保存、翻译、旋转、翻译、恢复
我再次敦促你修改这个例子,以帮助你理解转换和弧度。做一些小的改变,一次一个陈述。
顺便说一下,我们不能期望我们的玩家用弧度来输入角度。他们,还有我们,太习惯于度(90 度是直角,180 度是你掉头时的弧度等等。).程序必须完成工作。从角度到弧度的转换要乘以 pi/180。
注意
大多数编程语言在三角函数和旋转操作中使用弧度表示角度。
在这种背景下,我向everything数组中的信息添加指示,指示是否有旋转,如果有,则指示所需的平移点。这是我的主意。这与 HTML5 或 JavaScript 无关,本来可以用不同的方式完成。底层任务是创建和维护模拟场景中对象的信息。HTML5 的 canvas 特性提供了一种画图和显示图像的方式,但是它并没有保留对象的信息!
第二个和第三个应用程序的everything数组中的项目本身就是数组。第一个(0 th index)值指向对象。第二个(1 st index)是true或false。值true意味着旋转角度值和平移的 x 和 y 值跟随其后。实际上,这意味着内部数组要么有两个值,最后一个是false,要么有五个值。
注意
此时,你可能会想:她设置了一个通用系统,只是为了旋转大炮。为什么不为大炮装些东西呢?答案是我们可以,但是一般的系统确实可以工作,而且仅仅为加农炮编写的东西可能有同样多的代码。
第一个应用程序使用从表单中提取的水平和垂直位移值。玩家必须考虑这两个不同的值。对于第二个应用程序,播放器再次输入两个值,但它们是不同的。一个是出炮口的速度,一个是炮的角度。剩下的工作由程序来完成。初始不变的水平位移和初始垂直位移是根据玩家的输入计算出来的:出炮速度和一个角度。计算基于标准的三角学。幸运的是,JavaScript 提供了 trig 函数作为内置方法的Math类的一部分。
图 4-9 显示了玩家指定的炮外位移值和角度值的计算。竖线的负号是由于 JavaScript 屏幕坐标的 y 值随着屏幕向下增加而产生的。
图 4-9
计算水平*垂直位移
在这一点上,您可能想跳过阅读炮弹应用程序的实现。然后你可以回来阅读弹弓需要什么。
绘制线段
对于 slingshot 应用程序,我通过定义两个函数Sling和drawsling添加了一个新的对象类型。我的理想化弹弓由四个位置表示,如图 4-10 。请理解,我们可以用许多不同的方式来做这件事。
图 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设置为每当draw与Sling对象结合使用时调用的函数。虽然在当前的应用程序中没有发生,movesling将被调用,如果你或我在这个应用程序上移动弹弓的位置。
绘制弹弓包括基于四个点绘制四条线段。这个点将会改变,我将在下一节描述。HTML5 让我们绘制线段作为路径的一部分。我们已经用路径来画圆了。您可以将路径绘制为笔画或填充。对于圆圈,我们使用了fill方法,但是对于弹弓,我只想要线条。画一条线可能涉及两个步骤:移动到线的一端,然后画它。HTML5 提供了moveTo和lineTo方法。在调用stroke或fill方法之前,不会绘制路径。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,by到s1x,s1y的线 -
向路径添加一条从
bx,by到s2x,s2y的线 -
向路径添加一条从
s1x,s1y到s2x,s2y的线 -
向路径添加一条从
s2x,s2y到s3x,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。如果这两个都不起作用,mx和my将不会被设置,我应该添加另一个else子句来告诉玩家代码在他的浏览器中不起作用。
现在,下一步是确定(mx,my)点是否在球上。我在重复自己的话,但重要的是要明白,球现在相当于画布上的墨水或颜料,如果不确定(mx,my)点是否在球的顶部,我们就无法继续下去。我们如何做到这一点?我们可以计算出(mx,my)离球的中心有多远,看看它是否小于球的半径。平面距离有一个标准公式。我的代码是这个想法的一个微小的变化。它通过计算距离的平方并将其与球半径的平方进行比较来做出决定。我这样做是为了避免计算平方根。
如果鼠标点击在球上,即在球中心的半径距离内,该函数将全局变量inmotion设置为true。findball函数以调用drawall()结束。
每当鼠标移动时,就会调用moveit函数,在这里我们检查inmotion是否是true。如果不是,什么都不会发生。如果是,使用与前面相同的代码来获取鼠标坐标和球的中心,并将弹弓的bx,by值设置为鼠标坐标。这有拖动球和拉伸弹弓弦的效果。
当释放鼠标按钮时,我们调用finish函数,如果inmotion不是true,该函数不做任何事情。这什么时候会发生?如果玩家在球上的而不是周围移动鼠标并按下和释放按钮。
如果inmotion是true,该函数立即将其设置为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中设置的内容。我所做的是跟踪原来的target在everything数组中的位置,然后移除它并替换htarget。类似地,我将球从everything数组中移除。对于弹弓操作,我没有移除目标,而是将它的img属性改为feathers。注意,在代码中,chicken和feathers是Image对象。每个都有一个指向文件的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 | 由init、fire、change直接调用 | 调用everything数组中所有对象的draw方法。这些是drawball、drawrects的功能 |
| fire | 由表单中的onSubmit属性的动作调用 | drawall |
| change | 由在fire中调用的setInterval函数的动作调用 | drawall,调用cball的moveit方法,也就是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 增加了两行用于Picture和drawAnImage。
表 4-3
第二个炮弹应用程序中的函数
|功能
|
调用方/被调用方
|
打电话
|
| --- | --- | --- |
| init | body标签中onLoad的动作 | drawall |
| drawall | 由init、fire、change直接调用 | 调用everything数组中所有对象的draw方法。这些是功能drawball和drawrects |
| fire | 由表单中的onSubmit属性的动作调用 | drawall |
| change | 由在fire中调用的setInterval函数的动作调用 | drawall,调用cball的moveit方法,也就是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); | 将cball的x设定在将要旋转的炮口。 |
| cball.sy = cannony+cannonht*.5 - cannonlength*Math.sin(angleradians); | 将cball的y设定在将要旋转的炮口。 |
| 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方法。这些是功能drawball、drawrects、drawsling和drawAnImage |
| findball | 由mousedown事件的init中的addEventListener动作调用 | drawall and distsq |
| distsq | 由findball调用 | |
| moveit | 由mousemove事件的init中的addEventListener动作调用 | drawall |
| finish | 由mouseup事件的init中的addEventListener动作调用 | drawall和distsq |
| change | 由在finish中调用的setInterval函数的动作调用 | drawall,调用cball的moveit方法,也就是moveball |
| Ball | 由代码在var语句中直接调用 | |
| Myrectangle | 由代码在var语句中直接调用 | |
| drawball | 通过调用一个Ball对象的draw方法来调用 | |
| drawrects | 通过调用target对象的draw方法来调用 | |
| moveball | 通过调用一个Ball对象的moveit方法来调用 | |
| Picture | 由代码在var语句中直接调用 | |
| drawAnImage | 通过调用Picture对象的draw方法来调用 | |
| Sling | 由代码在var语句中直接调用 | |
| drawsling | 通过调用mysling的draw方法来调用 | |
表 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 || ev.layerX == 0) { | ev.layerX没事。 |
| mx= ev.layerX; | 用于mx。 |
| my = ev.layerY; } | 用layerY代替my。 |
| else if (ev.offsetX || 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 || ev.layerX == 0) { | layerX管用吗? |
| mx= ev.layerX; | 用于mx。 |
| my = ev.layerY; | ev.layerY为my。 |
| } else if (ev.offsetX || 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; | 基数outofcannon与bx,by到s1x,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进行玩家输入 -
处理鼠标事件(
mousedown、mousemove和mouseup),用addEventListener获取玩家输入 -
在画布上绘制圆弧、矩形、直线和图像
程序员定义的对象和使用对象数组来显示的技术将在后面的章节中再次出现。下一章集中在一个熟悉的游戏,被称为记忆或注意力。它使用了不同的计时事件以及第一章中介绍的Date功能。
五、记忆游戏(又名集中注意力)
在本章中,我们将介绍
-
绘制多边形
-
将文本放置在画布上
-
表示信息的编程技术
-
编程暂停
-
计算运行时间
-
洗牌一组卡对象的一种方法
介绍
这一章演示了两个版本的纸牌游戏,分别被称为记忆或专注。纸牌面朝下出现,玩家一次翻两张(通过点击它们)试图找到匹配的对子。该程序从棋盘上移除匹配的牌,但(实际上)会将不匹配的牌翻回来。当玩家完成所有匹配后,游戏会显示经过的时间。
我描述的游戏的第一个版本使用多边形作为面卡;第二种用全家福。您会注意到其他差异,这些差异是为了说明几个 HTML5 特性,但我也敦促您考虑一下这些版本的共同点。
图 5-1 显示版本一的开启画面。当玩家完成游戏时,记录比赛的表单也会显示经过的时间。
图 5-1
记忆游戏第一版的开场画面
图 5-2 显示玩家点击两张牌(紫色方块)后的结果。描绘的多边形不匹配,所以暂停后,程序用卡片背面的图像替换它们,使卡片看起来翻转了。
图 5-2
两张卡片正面:不匹配
当两张卡匹配时,应用程序移除它们,并在表格中记录匹配情况(参见图 5-3 )。
图 5-3
应用程序已移除两张匹配的卡
如图 5-4 所示,当玩家结束时,游戏显示结果——在本例中,36 秒内有 6 场比赛。
图 5-4
玩家完成游戏后的第一个版本
在游戏的第二个版本中,卡片正面显示的是人物的照片,而不是多边形。请注意,尽管许多记忆游戏认为图像只有在完全相同时才是相同的,但这个游戏类似于一副扑克牌中红心 2 与方块 2 的匹配。为了说明编程要点,我们将把一个匹配定义为同一个人,即使是在不同的图片中。这需要一种对我们用来确定匹配状态的信息进行编码的方法。游戏的第二版还演示了在画布上书写文本,如图 5-5 所示,它描绘了开始的屏幕。
图 5-5
记忆游戏,第二版,开屏
要查看在我们的新游戏中点击两张卡的一种可能结果,请看图 5-6 。
图 5-6
此屏幕显示不匹配的照片
因为结果显示了两个不同的人——在暂停让玩家观看两张照片之后——应用程序将卡片翻转过来,让玩家再试一次。图 5-7 显示了一个成功的选择——同一个人的两个图像(尽管在不同的图片中)。
图 5-7
这张截图显示了一场比赛(不同的场景,但同一个人)
应用程序从板上删除匹配的图像。当所有的牌都被移除时,完成游戏所用的时间会出现,同时会显示如何再次玩游戏的说明,如图 5-8 所示。
图 5-8
游戏最终画面(照片版);所有图像都已匹配,因此不会出现卡片
你可以使用源代码中的照片来玩这个游戏,但是使用你自己的照片会更有趣。你可以从少量照片开始,比如两三对照片,然后逐步增加到整个家庭、班级或俱乐部的照片。对于游戏的第一个版本,你可以用自己的设计替换多边形。
关键要求
游戏的数字版本需要用不同的多边形或照片来表现卡片的背面(都是一样的)和正面。应用程序还必须能够告诉哪些卡匹配,以及卡在棋盘上的什么位置。此外,玩家需要反馈。在现实世界的游戏中,参与者翻转两张卡片并寻找匹配(这需要一些时间)。如果没有,他们就把牌翻过来。
电脑程序必须显示所选牌的正面,并在显示第二张牌后暂停,以便玩家有时间看到两张正面。这种停顿是计算机实现所需要的东西的一个例子,当人们玩游戏时,这种停顿或多或少是自然发生的。该应用程序还应该显示当前找到的配对数量,以及当游戏完成时,参与者找到所有配对所花费的时间。该程序的多边形和照片版本使用不同的方法来完成这些任务。
下面总结一下两个游戏版本必须做的事情:
-
把牌抽回来。
-
在玩家做出初始选择之前洗牌,这样就不会每次都出现相同的选择。
-
检测玩家点击卡片的时间,并区分第一次和第二次点击。
-
在检测到点击时,在游戏版本 1 的情况下,通过绘制多边形来显示适当的卡面,或者在版本 2 中显示正确的照片。
-
移除匹配的配对。
-
即使那些讨厌的玩家做了意想不到的事情,比如点击同一张卡两次,或者点击之前被卡占据的空白区域,也要适当地操作。
HTML5、CSS、JavaScript 特性
让我们回顾一下具体的 HTML5 和 JavaScript 特性,它们提供了我们实现游戏所需的东西。我们将建立在之前的材料之上:HTML 文档的一般结构;如何在一个canvas元素上画矩形、图像、线段组成的路径;程序员定义的和内置的函数;程序员对象;form元素;和数组。
新的 HTML5 和 JavaScript 特性包括超时事件,使用Date对象计算运行时间,在画布上书写和绘制文本,以及一些有用的编程技术,您会发现这些技术在未来的应用程序中很有价值。
和前面几章一样,这一节概括地描述了 HTML5 的特性和编程技术。您可以在“构建应用程序”一节中看到上下文中的所有代码。如果您愿意,您可以跳到该部分来查看代码,然后返回到这里来解释这些特性是如何工作的。
代表卡片
当我们手里拿着一张实体卡时,我们可以看到它是什么。有卡面和背面,背面都一样。我们可以清楚地确定纸牌在游戏棋盘上的位置,以及它们是正面还是背面出现。要实现一个电脑游戏,我们必须表现所有的信息。编码是创建许多计算机应用程序的基本部分,不仅仅是游戏。
在这一章(以及整本书),我描述了一种完成任务的方法。但是请记住,实现应用程序的一个特性很少只有一种方法。也就是说,构建应用程序的不同策略可能会有一些共同的技术。
我们处理卡片的方法将使用程序员定义的对象。在 JavaScript 中创建程序员定义的对象涉及到编写构造函数;在这种情况下,我们称之为Card。使用程序员定义的对象的优点是 JavaScript 提供了访问通用类型对象的信息和代码所需的点符号。我们在第四章中为炮弹和弹弓游戏做了这个。
我们将赋予Card对象属性来保存卡片的位置(sx和sy)和尺寸(swidth和sheight),一个指向为卡片绘制背面的函数的指针,以及对于每种情况,指定适当正面的信息(info)。
在多边形的情况下,info的值将指示要绘制的边数。(在后面的部分中,我们将讨论绘制它的代码。)对于照片卡的正面,该值将是对我们创建的Image对象的引用img。该对象将保存一个特定的图像文件和一个编号(info),该编号将匹配的图片联系在一起。为了绘制文件的图像,我们将使用内置的drawImage方法。
不用说,卡片并不是以物理实体的形式存在,而是有两面的。应用程序在画布上玩家希望看到的地方绘制卡片的正面或背面。函数flipback绘制卡片背面。为了给出一张被移除的牌的外观,flipback通过绘制一个矩形来有效地擦除一张牌,该矩形是棋盘的颜色。
两个应用程序都使用名为makedeck的函数来准备卡片组,这个过程包括创建Card对象。对于游戏的多边形版本,我们在Card对象中存储边数(从 3 到 8)。但是,应用程序在设置过程中没有绘制多边形。photos 版本设置了一个名为pairs的数组,列出照片的图像文件名。你可以按照这个例子来创建自己的家庭或团体记忆游戏。
小费
如果您使用在线代码玩游戏,如前所述,您可以下载图像文件。要制作你自己的游戏,你需要上传图片,然后修改代码来引用你的文件。代码指出了您需要更改的内容。
makedeck函数创建Image对象,并使用pairs数组将src属性设置为image对象。当代码创建Card对象时,它放入控制pairs数组的索引值,以便匹配的照片具有相同的值。与多边形版本一样,应用程序在创建卡片组的过程中不在画布上绘制图像。在屏幕上,所有的牌看起来都一样;然而,信息是不同的。这些牌在固定的位置——洗牌在后面。
对于Card和Polygon,代码对位置信息、sx和sy属性的解释有所不同。在第一种情况下,信息指的是左上角。在第二种情况下,该值标识多边形的中心。不过,你可以从另一个中计算出一个。
使用日期计时
我们需要一种方法来确定玩家花了多长时间来完成所有的匹配。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);
这个代码
-
创建一个
new Date对象并将其存储在变量now中。 -
使用
getTime提取时间,将其转换为Number,并将其赋给变量nt。这意味着nt保存了从 1970 年开始直到代码调用Date时的毫秒数。然后程序从当前时间nt中减去保存的开始时间starttime。 -
除以 1000 得到秒。
-
添加
.5并调用Math.floor将结果向上或向下舍入到整秒。我们希望小数部分等于或大于 0.5 的数字向上取整,小于 0.5 的数字向下取整。
如果您需要比秒提供的精度更高的精度,请省略或修改最后一步。
每当需要计算程序中两个事件之间经过的时间时,都可以使用这个代码。
提供暂停
当我们用真正的卡片玩记忆游戏时,我们不会有意识地在翻开不匹配的卡片之前暂停。但是如前所述,我们的计算机实现必须提供暂停,以便玩家有时间看到两张不同的卡。你可能还记得第三章和第四章中的动画应用——弹跳球、炮弹和弹弓——使用 JavaScript 函数setInterval在固定的时间间隔设置事件。我们可以在记忆游戏中使用一个相关的函数setTimeout。(要查看上下文中的完整代码,请转到“构建应用程序”一节。)让我们看看如何设置事件,以及暂停时间用完时会发生什么。
setTimeout函数设置了一个事件,我们可以用它来强制暂停。当玩家点击画布时调用的choose函数首先检查firstpick变量,以确定这个人是做了第一个还是第二个选择。在这两种情况下,程序都在画布上与卡片背面相同的位置绘制卡片正面。如果点击是第二个选择,并且两张卡片匹配,代码将变量matched设置为true或false,这取决于卡片是否匹配。如果应用程序确定游戏还没有结束,代码就会调用
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 所示的屏幕截图。
图 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 用一个三角形说明了这一点。
图 5-10
将三角形表示为辐条几何形状有助于阐明绘制多边形的代码开发;箭头指示绘图路径中的第一点
为了确定辐条之间的角度,我们将数量2*Math.PI (representing a complete circle)除以多边形的边数。我们使用角度值和moveTo方法来绘制路径的点。源代码有一个简单的 HTML 程序画一个三角形,也就是设置一个变量n为3。你可以通过改变声明和初始化n的语句来修改它以绘制其他正多边形。
程序将多边形绘制成一条填充路径,该路径始于由angle值的一半指定的点(如图 5-10 中的箭头所示)。为了说明问题,我们使用了moveTo方法以及半径、Math.sin和Math.cos。然后,我们使用lineTo的方法,以顺时针方向进行 n-1 个点。对于三角形来说,n-1 就是多了两个点。对于八角形,它将是七个以上。在用lineTo点运行完一个for循环后,我们调用fill方法来产生一个填充的形状。要查看完整的带注释的代码,请转到“构建应用程序”一节。
注意
绘制和重绘多边形需要时间,但这不会给这个应用程序带来问题。如果一个程序有大量复杂的设计,提前准备好图片可能是有意义的。然而,这种方法要求用户下载文件,这可能需要相当长的时间。您需要进行试验,看看哪种方法总体上更好。
洗牌
如前所述,记忆游戏要求程序在每一轮之前洗牌,因为我们不希望牌一次又一次地出现在同一个位置。改变价值观的最佳方式是广泛研究的主题。在第十章中,描述了一种叫做 21 点的纸牌游戏,你会找到一篇文章的参考资料,该文章描述了一种据称是产生洗牌最有效方法的技术。
对于记忆力/专注力,还是按照我小时候玩游戏的方式来实现吧。我和其他人会摊开所有的卡片,然后拿起并交换配对。当我们认为我们已经做了足够多的次数,我们就会开始玩。在本节中,我们将探索这种方法背后的一些概念。(要检查shuffle函数,您可以跳到“构建应用程序”一节。)
要为洗牌的交换方法编写 JavaScript,我们首先需要定义“足够的次数”让我们把这一副牌中的牌数增加三倍,我们已经在数组变量deck中表示过了。但是既然没有卡片,只有代表卡片的数据,我们交换什么呢?答案是唯一定义每张卡的信息。对于多边形记忆游戏,这是属性info。对于图片游戏,是info和img。
为了得到一张随机的牌,我们使用表达式Math.floor(Math.random()*dl),其中dl代表牌组长度,代表该副牌中牌的数量。我们这样做两次,以获得要(虚拟地)交换的卡对。这可能会产生相同的数字,这意味着一张卡与自己交换,但这不是真正的问题。如果发生了,这个过程中的这一步就没有作用了。代码要求进行大量的交换,因此一次交换不做任何事情是可以的。
实现交换是下一个挑战,它需要一些临时存储。我们将使用一个变量holder作为游戏的多边形版本,两个变量holderimg和holderinfo作为图片版本。
实现在卡片上点击
下一步是解释我们如何实现玩家的移动,也就是玩家点击一张卡片。在 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。如果两者都不起作用,mx和my就不会得到设置。
因为卡片是矩形的,所以使用鼠标光标坐标(mx、my)、左上角的位置以及每张卡片的宽度和高度,浏览卡片组并进行比较操作相对容易。我们是这样构造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 true和else子句,但是这里我使用了简化的格式来表示单个语句,这种格式也是因为它看起来足够清晰。
现在让我们继续构建我们的两个记忆游戏。
构建应用程序并使之成为您自己的应用程序
本节介绍了游戏两个版本的完整代码。因为应用程序包含多个函数,所以该部分为每个游戏提供了一个表,说明每个函数调用的内容和被调用的方式。
表 5-1 是记忆游戏多边形版本的函数列表。注意,一些函数的调用是基于事件完成的。
表 5-1
记忆游戏的多边形版本中的函数
|功能
|
调用方/被调用方
|
打电话
|
| --- | --- | --- |
| init | 响应于body标签中的onLoad而被调用, | makedeck``shuffle |
| choose | 响应init中的addEventListener时调用。 | Polycard``drawpoly(作为多边形的draw方法调用)。 |
| flipback | 响应choose中的setTimeout调用而调用。 | |
| drawback | 在makedeck和flipback中作为抽牌方法调用。 | |
| Polycard | 在choose中调用。 | |
| shuffle | 在init中调用。 | |
| makedeck | 在init中调用。 | |
| Card | 由makedeck调用。 | |
| drawpoly | 在choose中称为Polygon的draw方法。 | |
表 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 || ev.layerX == 0) { | 可以用layerX和layerY吗? |
| mx= ev.layerX; | 设置mx。 |
| my = ev.layerY; | 设置my。 |
| } | 如果为真,则关闭。 |
| else if (ev.offsetX || ev.offsetX == 0) { | 可以用offsetX和offset吗? |
| 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)|| (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 | 在makedeck和flipback中作为卡片的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; | 把k的info放到i里。 |
| deck[i].img = deck[k].img; | 把k的img放到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 || ev.layerX == 0) { | 提醒:这是处理三种浏览器之间差异的代码。 |
| mx= ev.layerX; | |
| my = ev.layerY; | |
| } else if (ev.offsetX || 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)|| (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,所以你不需要通过代码来改变一副牌中的牌数。不过,您可能需要调整cardwidth和cardheight变量的值,以便在屏幕上排列卡片。
当然,另一种可能性是使用一副标准的 52 张牌(或 54 张带玩笑的牌)。关于使用扑克牌的例子,请跳到第十章,它将带你创建一个 21 点游戏。对于任何匹配游戏,您都需要开发一种方法来表示定义哪些卡片匹配的信息。
测试和上传应用程序
当我们,开发人员,检查我们的程序时,我们倾向于每次都做同样的事情。然而,用户、玩家和顾客经常做奇怪的事情。这就是为什么让别人来测试我们的应用程序是一个好主意。所以请朋友来测试你的游戏。您应该总是让没有参与构建应用程序的人来测试它。你可能会发现你没有发现的问题。
记忆游戏多边形版本的 HTML 文档包含了完整的游戏,因为程序可以动态地绘制和重绘多边形。游戏的照片版需要你上传所有的图片。您可以通过使用网页上的图像文件(在您自己的网页之外)来改变这个游戏。注意,pairs数组需要有完整的地址。
摘要
在本例中,您学习了如何使用编程技术和 HTML5 特性实现游戏的两个版本,即 memory 或 concentration。其中包括:
-
程序员定义的函数和对象的例子
-
如何使用
moveTo和lineTo以及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 为开启画面。
图 6-1
测验的开始画面
这给了我一个评论这个游戏的机会。我能按顺序背诵总统,所以能很好地玩这个游戏。这种情况有问题,因为我需要确保当玩家给出错误的答案或在其他方面表现不佳时,测试可以正常进行,我稍后会解释。本章的目的是介绍 HTML、CSS 和 JavaScript 的特性和通用技术,您可以用它们来构建自己的测验,选择自己的题目。请记住,你可能不是在为自己打造游戏。
顺便说一下,对于美国总统来说,我需要提供一些方法来解决格罗弗·克利夫兰(Grover Cleveland)的问题,他是唯一一位连任两届非连续总统的人。我选择将格罗弗·克利夫兰和 ?? 列入名单。也许你需要对你的主题采取类似的步骤。
玩家点击连续的选项。我开始了一个新游戏。图 6-2 显示了玩家选择她认为(知道)是这一套中最早的总统后的屏幕。请注意,数字 2 出现在您的订单下,约翰·昆西·亚当斯盒子现在是金色的。
图 6-2
玩家选择她认为是这一组 4 个中最早的总统
无论正确与否,任何被点击的方块都会变成金色。我不会试图犯任何错误。图 6-3 显示了两种选择以及出现在您订单下的数字 2 1。
图 6-3
玩家点击了约翰·昆西·亚当斯,然后点击了马丁·范·布伦
我完成了测验。图 6-4 显示的是车窗,有些挤压。此时显示的是一个视频剪辑和一个音频文件。我有一个自由女神像附近烟火的视频剪辑。这段视频的配乐是纽约,纽约。我决定找一个免费版的《褶边与华丽》(也叫《向酋长致敬》)。稍后,您将会看到我将视频和音频结合起来所需的一些小步骤。
图 6-4
在成功订购总统组之后
让我调用一个新游戏,现在输入一个错误的顺序。图 6-5 显示了做错订单的结果。显示玩家的订单,并出现消息WRONG。
图 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被设置为absolute,top和left包括可以通过代码改变的值。
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";
这里,my和mx是数字。设置style.top和style.left需要一个字符串,所以我们的代码将数字转换成字符串,并在字符串末尾添加"px"。
响应玩家的移动
在pickelement函数中,你会看到响应和跟踪玩家动作的代码。pickelement标题有一个称为ev的单一参数。然而,还有一种我们称之为隐含参数的东西。调用该函数是因为对特定元素的操作。代码中的术语this指的是该元素。
在代码中,this指的是当前实例,即玩家点击的元素。我们为每个元素设置了事件监听,因此当执行pickelement时,代码可以引用使用this听到点击的特定元素。当玩家点击一个写有约翰·昆西·亚当斯名字的方块时,代码知道它,通过“知道”我比我想要的更拟人化了程序。换句话说,同样的pickelement函数将被调用于我们在屏幕上放置的所有方块,但是,通过使用this,代码可以引用玩家每次点击的特定方块。pickelement代码从textContent中的元素和第一个字符中提取 ID。ID 中的信息用于填充一个名为slots的数组,该数组将用于检查玩家的订单。来自textContent的字符,1 或 2 或 3 或 4,将用于向玩家显示已经做出的选择。
我们想在玩家点击盒子时改变它的颜色。我们可以这样做,就像改变top和left来重新定位模块一样。然而,JavaScript 的属性名与 CSS 中的略有不同:没有破折号。
this.style.backgroundColor = "gold";
gold是一组已确定的颜色之一,包括red、white、blue等。那可以用名字来指代。或者,您可以使用 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;}
您可能还会问,为什么我不编写代码来动态创建video和audio元素,而是将它们放在 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。当然,如果您想向玩家呈现不同数量的对子,您可以更改该值。其他变量用于块的原始位置和保存状态信息,比如是第一次点击还是第二次点击。
我为这个应用程序创建了四个函数:init、setupgame、pickelement和checkorder。我本可以将init和setupgame合并,将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函数进行检查。它显示WRONG或CORRECT,如果顺序正确,使音频控制和视频可见并开始播放。
注意,我的程序中有多余的代码。我这样做是为了减轻重复播放的负担,而不需要重新加载或“重做”。
表 6-4 提供了代码的逐行解释。
表 6-4
总统测验的完整代码
| `` | HTML 标签。 | | `` | 定义字符集,在这种情况下是一种 Unicode 形式。可以省略,我确实在很多例子中省略了,但是在这里包含它是为了让你看到。 | | `` | 头部标签。 | | `Ordering Quiz with Rewards` | 完整的标题元素。 | | `` | 样式标签。 | | `.thing {position:absolute; left: 0px; top: 0px; border: 2px; border-style: double; background-color: white; margin: 5px; padding: 5px; }` | 我称之为总统名字的区块的格式。 | | `audio {visibility: hidden;}` | 隐藏启动音频控制。默认定位。 | | `video {visibility: hidden; display: none; position:absolute;}` | 视频开始时隐藏。 | | `` | 关闭样式元素。 | | `` | 关闭`script`元素。 | | `` | 关闭`head`元素。 | | `` | `Body`标记。注意`onload`的设置。 | | `` | `Audio`标记。 | | `` | MP3 源。 | | `` | OGG 来源 | | `Your browser does not accept the audio tag.` | 适用于旧浏览器。 | | `` | 关闭`audio`元素。 | | `` | 视频标签。注意到`muted`属性。 | | `` | WEBM 来源。 | | `` | MP4 来源。 | | `` | OGG 来源。 | | `Your browser does not accept the video tag.` | 对于较旧的浏览器。 | | `` | 关闭`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.createElement、document.getElementById和document.body.appendChild在运行时创建 HTML -
使用
addEventListener设置鼠标click事件的事件处理 -
使用
removeEventListener移除鼠标click事件的事件处理 -
使用代码更改 CSS 设置来更改屏幕上对象的颜色
-
创建一个数组的数组来保存测验内容
-
使用
for循环迭代数组 -
使用
do-while循环随机选择一个未使用的问题集 -
使用
substring提取检查中使用的字符串 -
使用
Number函数将字符串转换成数字 -
使用
video和audio元素显示以不同浏览器可接受的格式编码的视频和音频
您可以使用动态创建和重新定位的 HTML 以及在前面章节中学习的画布上的绘制。第九章中描述的 Hangman 的实现就是这么做的。你可以像这里一样,把视频和音频作为应用程序的一小部分,或者作为网站的主要部分。在下一章中,我们将回到在画布上画一个迷宫,然后在迷宫中穿行而不穿过墙壁。