此主题将介绍有关为网站创建 SVG 动画方面的更高级概念。在学习此教程之前,请掌握基本 SVG 动画、中级 SVG 动画,并充分了解 HTML 和 JavaScript。
注意 要查看本主题中的示例,你需要一个支持 SVG 元素的浏览器,如 Windows Internet Explorer 9。
在本主题中,我们将圆形球竞技场(在中级 SVG 动画部分进行了介绍)扩展为以教学为中心的 2D 视频游戏:
游戏的目标很简单 - 用球拍反弹球,以让球弹出圆形竞技场左侧的目标(外墙上的缺口)。球只有在接触球拍之后才能进入目标。球接触球拍后将变“热”,颜色也由“冷”白色变为非白色(随机选择)的“热”颜色。有关详细信息和游戏玩法提示,请参阅游戏概述。
要试玩游戏,请单击 SVG 弹球。如游戏概述中所述,在页面上的任何位置单击即开始游戏;垂直上下移动鼠标可上下移动球拍。
现在,你已对游戏有了一些了解,我们将讨论该游戏是如何以实施的顺序构建的:
在下面的讨论中,了解游戏中使用的坐标系是伪笛卡尔坐标非常有用,就是说,游戏中的坐标系的原点在竞技场的中心(与在标准的笛卡尔坐标系中一样),但 y 轴在 x 轴以下为正数,在 x 轴以上为负数。
另外,请注意经常使用极坐标,因为它们可以显著简化许多相关的数学计算:
图 1
因长度的缘故,本主题中没有显示任何以下示例的源代码。而是为每个示例提供了一个“活动链接”。要查看与示例关联的源代码,请使用浏览器的查看源文件功能。例如,在 Windows Internet Explorer 中,右键单击要查看源代码的网页,然后单击“查看源文件”。请确保阅读本文档其余部分时有适当的源代码。
活动链接: 示例 1
游戏的构建基础为静态游戏框架:
图 2
此框架基于以下标记:
<body id="gamePage" onload="game.init();"> <div id="rules" class="roundCorners"> <p><a href="rules.html" target="_blank">Game Overview</a></p> </div> <div id="clock" class="roundCorners"> <p>Seconds Remaining:</p> </div> <div id="score" class="roundCorners"> <p>Score: 0</p> </div> <div id="level" class="roundCorners"> <p>Level: 1</p> </div> <div id="messagingBox" class="roundCorners"> <h3>SVG Ball Bounce</h3> <p>Dare to get your balls in the goal, to score!</p> </div> <div id="arenaWrapper"> <svg id="arenaBox" width="800px" height="800px" viewBox="0 0 800 800"> <g id="arena" transform="translate(400, 400)"> <circle id="floor" x="0" y="0" /> <path id="wall" /> <circle id="post" x="0" y="0" /> <rect id="paddle" x="300" y="-50" width="8" height="100" rx="2" ry="2"/> </g> </svg> </div> </body>
此标记相对简单,但是以下项可能会很有趣:
Time
、Level
和 Score
这三个框(使用 <div>
元素实现)的内容以编程的方式进行更改,而所有四个都使用 position:absolute
CSS 在网页的四个角定位框。 <g id="arena" transform="translate(400, 400)">
中的变换属性修改为伪笛卡尔坐标系(如前所述)。 从 JavaScript 的角度来看,游戏的基本逻辑流程如下:
Game.prototype.init
来绘制竞技场 (Arena.prototype.draw
) 并开始游戏 (Game.prototype.start
)。 Game.prototype.start
调用 setInterval
两次,一次设置回调函数,该函数将执行大多数工作 (Game.prototype.play
),一次设置另一个回调函数,该函数用于更新右上角框中的游戏锁 (Game.prototype.clock
),每秒钟调用一次。请注意该要求,在 Game.prototype.start
内,创建 JavaScript 闭包(即 var gameObject = this;
),以便让 setInterval
调用的函数访问正确的 this
指针。 Game.prototype.start
,Game.prototype.play
每 constants.gameDelay
毫秒被调用一次。Game.prototype.play
当前是一个空函数,但最终将包含主要游戏循环。 Game.prototype.clock
每秒被调用一次并递减任何剩余的游戏时间。当游戏时间少于或等于 constants.warningTime
时,圆形游戏竞技场的背景将闪烁红色。此效果的实现方式是,首先将竞技场设置为红色,然后设置另一个回调函数(即,Arena.prototype.defaultFloorColor
),在很短时间之后将地板的颜色更改为白色。setTimout
之所以用于此目的,是因为其回调函数仅调用一次,这正是我们对竞技场地板进行脉冲所需的行为:每秒一次。 最后,请注意游戏构造函数中的最后四行:
这几行允许球、球拍、目标和竞技场对象接触游戏对象。这在一个对象必须接触另一个对象时变得很有用。 例如,在 Arena.prototype.draw 中,我们按如下方式更新时钟:
这里,竞技场对象通过访问其游戏对象属性来访问游戏对象的时间属性。
活动链接: 示例 2
在此示例中,可通过按上下箭头键或垂直移动鼠标移动球拍。
要开始,请注意示例 1 中使用的所有 JavaScript helper 函数已移动到外部文件 helpers.js
(因此 <script src="helpers.js" type="text/javascript"></script>
用于示例 2)。你可能还注意到 helpers.js
中存在其他的 helper 函数 - 这些将用在以后的示例中。
注意 要查看外部 JavaScript 文件的内容,请在浏览器中放置指向 JavaScript 文件的合适路径,然后将其打开或保存。例如,要查看 helpers.js
,我们从 <script src="helpers.js" type="text/javascript"></script>
中可以看到 helpers.js
和 example2.html
处于相同的目录。因此,如果 example2.html
在 http://samples.msdn.microsoft.com/Workshop/samples/svg/svgAnimation/advanced/example2.html 中,打开 helpers.js
的所需路径是 http://samples.msdn.microsoft.com/Workshop/samples/svg/svgAnimation/advanced/helpers.js。请注意,要查看 JavaScript 文件,可能需要首先本地保存文件,然后在记事本(或类似工具)中打开它。
在示例 1 中,游戏启动时无任何用户输入。在示例 2 中,游戏仅在按上箭头或下箭头或者单击鼠标时才启动。这项新功能的基本逻辑如下所示:
页面完全加载后,Game.prototype.init
将设置以下事件处理程序:
转到 helpers.js
后,你会注意到 processKeyPress
和 processPaddleClick
函数均直接访问全局变量 game
。因为从某种意义上说,这两个函数在逻辑上处于游戏之外,所以它们就成了 helper 函数(与游戏方法相对)。
processKeyPress
很简单 – 在按下正确的键时,它将调用 Paddle.prototype.keyMove
,将球拍向适当方向(即向上或向下)移动 constants.paddleDy
个单位。Paddle.prototype.keyMove
还可确保球拍不会越出竞技场外墙。这是通过使用勾股定理实现的:
图 3
从图 3 中,我们看到 r2 = x2 + y2 ⇒ y2 = r2 – x2 ⇒ y = √(r2 – x2),这将得出球拍的最大高度。因此,我们得到:
移动到 processPaddleClick
,在单击鼠标时,此函数的主要目的是添加一个鼠标移动事件侦听程序(在本例中是 Game.prototype.mouseMove
)。Game.prototype.mouseMove
根据鼠标的当前垂直位置通过以下方式上下移动球拍:
这里的核心问题是,鼠标的 y 位置 (evt.pageY
) 位于一个坐标系中,而球拍的 y 位置 ((paddle.y.baseVal.value
) 位于另一个坐标系中。首先,我们通过减去用于创建伪笛卡尔系统的变换的 y 轴分量,将鼠标的 y 位置变换为游戏的伪笛卡尔坐标系:
在 Paddle.prototype.mouseMove
中,这是通过较隐密的行得到的:
此行直接获取上一个 <g> 标记中的第二个 400
。我们从这个 400 中减去单击鼠标时定位球拍中心的经验导出常数。现在,鼠标垂直移动时,球拍也会随之垂直移动。
活动链接: 示例 3
下一步是创建球,将球定位在“甜甜圈”形状的竞技场内以避免球之间或球与球拍之间互相重叠,然后将球附加到 DOM(使其显示在屏幕上)。这是通过以下方法实现的:
此代码调用的三种方法相对比较简单,不过可能有以下几种例外情况:
在 Balls.prototype.create
中,helper 函数 getRandomArenaPosition(ballElement.r.baseVal.value)
使用极坐标将球定位在竞技场内:
function getRandomArenaPosition(ballRadius) { var p = new Point(0, 0); // This constructor defined in vector.js var r = constants.arenaRadius; var allowableRandomRadius; var randomTheta; allowableRandomRadius = getRandomInteger(constants.postRadius + ballRadius, r - ballRadius); randomTheta = getRandomReal(0, 2*Math.PI); p.x = allowableRandomRadius * Math.cos(randomTheta); p.y = allowableRandomRadius * Math.sin(randomTheta); return p; }
以下图表有助于解释此代码示例:
图 4
在图 4 中,a = constants.postRadius
、b = ballRadius
、r = r(竞技场外墙的半径)和 θ = randomTheta
。 randomRadius
是随机选取的,它大于或等于 a + b,小于或等于 r – b。randomTheta
是随机选取的,位于 0 和 2π 弧度(360 度)之间。这得到一个随机点 (r, θ),参见图 1,该点位于由图 4 中点 (p.x, p.y) 指示的内虚线圆和外虚线圆之间。 点 (r, θ) 然后使用标准方程式 x = r cos(θ) 和 y = r sin(θ) 转换为矩形的坐标。
注意,vector.js
在示例 3 中引入,因为 getRandomArenaPosition
返回类型为 point
的对象 p
(尤其是 function Point(x_coordinate, y_coordinate)
)。此外,vector.js
包含了将用于随后示例的常用矢量运算的实现。请参阅矢量以了解有关这些常见矢量运算的更多信息。
在 Balls.prototype.positionInArena
中,将会调用 Paddle.prototype.hasCollided
,以确定给定的球是否已经与球拍发生碰撞。以下代码示例中所用的方法如下所示:
false
。 true
。 true
。 对于下面的示例代码,请注意 ball_cx(ball)
、ball_cy(ball)
和 ball_r(ball)
分别返回球中心的 x 坐标、y 坐标和球的半径:
Paddle.prototype.hasCollided = function(ball) /* Returns true if a ball has collided with the paddle, false otherwise. */ { var paddle = document.getElementById('paddle'); // Needed for Firefox. Not needed for IE or Chrome. var p = new Object(); // To save on typing, create a generic object to hold assorted paddle related values ("p" stands for "paddle"). p.x = paddle.x.baseVal.value; // The x-coordinate for the upper left-hand corner of the paddle rectangle. p.y = paddle.y.baseVal.value; // The y-coordinate for the upper left-hand corner of the paddle rectangle. p.w = paddle.width.baseVal.value; // The width of the paddle rectangle. p.h = paddle.height.baseVal.value; // The height of the paddle rectangle. p.delta_x = Math.abs( ball_cx(ball) - p.x - p.w/2 ); // The distance between the center of the ball and the center of the paddle, in the x-direction. p.delta_y = Math.abs( ball_cy(ball) - p.y - p.h/2 ); // The distance between the center of the ball and the center of the paddle, in the y-direction. // See if the ball has NOT collided with the paddle in the x-direction and the y-direction: */ if ( p.delta_x > (p.w/2 + ball_r(ball)) ) { return false; } if ( p.delta_y > (p.h/2 + ball_r(ball)) ) { return false; } // See if the ball HAS collided with the paddle in the x-direction or the y-direction: */ if ( p.delta_x <= (p.w/2) ) { return true; } if ( p.delta_y <= (p.h/2) ) { return true; } // If we've gotten to this point, check to see if the ball has collided with one of the corners of the paddle: */ var corner = new Object(); // A handy object to hold paddle corner information. corner.delta_x = p.delta_x - p.w/2; corner.delta_y = p.delta_y - p.h/2; corner.distance = Math.sqrt( (corner.delta_x * corner.delta_x) + (corner.delta_y * corner.delta_y) ); return corner.distance <= ball_r(ball); }
活动链接: 示例 4
球的移动需要大量新代码,因为我们现在必须处理以下问题:
球和球之间的碰撞在中级 SVG 动画中通过示例 4 讨论。有关详细信息,请参阅碰撞响应 (PowerPoint) 的“Have Collision, Will Travel”(有碰撞,就有位移)部分。
球和竞技场之间的碰撞在中级 SVG 动画通过示例 5 讨论。 但是,你会发现对 while ( this.hasCollided(ball) )
内的 Arena.prototype.processCollision
循环所作的一些更改 – 稍后将在本主题的调试一节进行讨论。此外,已直接将 Arena.prototype.hasCollided
扩展为在球碰到内墙或外墙时返回 true
,如下所示:
球与球拍的碰撞相对比较明确,因为无论球与球拍所形成的角度如何,球总是以固定的角度弹开,如下图所示:
图 5
该固定的后弹角度 (Vout) 是球最初与球拍接触点的函数,通过简单的线性方程进行计算:
图 6
x 轴代表球拍的垂直长度,顶部为 0,底部出现在 paddle.height.baseVal.value
处。y 轴代表球从球拍(垂直表面)弹开的角度。
如果我们将线性方程表示为函数 ƒ,从图 6 中我们可以看出 ƒ(0) = 45。在此情况下,球击中球拍顶部 (x = 0) 并偏转 45 度 (y = 45),与球的进入角度无关(如图 5 中所示)。
同样,ƒ(paddle.height.baseVal.value
) = -45。这里,球击中球拍底部,并偏转 -45 度。
如果球击中了球拍中心,我们将得到 x = paddle.height.baseVal.value
/2 和 ƒ(x) = 0,这意味着球将以 0 度(水平)离开球拍。
线性方程 ƒ 假定球从右而来击中球拍。如果球自左方而来并击中球拍顶部或底部,则球将按下图所示进行转向:
图 7
ƒ 在 Paddle.prototype.verticalBounce
中实现,此代码将球是否从左边或右边击中球拍考虑在内,如下所示:
一旦知道了正确的转向角度后,将通过该角度计算出一个单位矢量 u,来球速度矢量 Vin(参见图 5)的值将传送到产生新的出球速度矢量 Vout 的单位矢量。 即,如果 u 是计算出的单位矢量,Vin 是来球速度矢量,则出球速度矢量 Vout = |Vin|u
结束此部分之前,请注意游戏主循环最终包含了一些内容:
球列表中每个球的算法很简单:
由于 Game.prototype.start
中 setInterval
调用的原因,推动整个游戏的主循环每 constants.gameDelay
毫秒发生一次:
而 Game.prototype.start
仅在通过按上下箭头键或单击鼠标开始游戏时才会被调用(请参阅 helpers.js
中的 processKeyPress
和 processMouseClick
)。
活动链接: 示例 5
最后添加到游戏的主要组件为得分和升级。
从游戏概述中我们知道,在球首次击中球拍之后才能得分(并且随后在竞技场墙壁上没有反弹太多次数)。要跟踪哪些球为热(可进入目标),哪些球为冷(无法进入目标),我们为每个球对象添加了自定义属性 hotCount
。最初,在球击中球拍之前,球的属性是不明确的,它在 Paddle.prototype.processCollision
中设置为 ball.hotCount = constants.hotBounces
。当球碰撞竞技场墙壁时,此属性会递减。hotCount
还用于提供有关某个球留下的热回弹数的视觉提示。当球首次接触球拍时,在 Paddle.prototype.processCollision
中将从“冷”白填充色变为随机的非白“热”填充色,如下所示:
如果 ball.hotCount
是 undefined
,则认为球是冷的,这适用于首次创建球(在 JavaScript 中,!undefined
为 true
)或 ball.hotCount
为零(发生在球已经从竞技场墙壁弹回 constants.hotBounces
或更多次时)的情况。
从竞技场墙壁每弹回一次,球的 hotCount
和不透明度将会在 Arena.prototype.processCollision
中减少(直至最终变为零),如下所示:
每个球的 hotCount
属性还用于确定球是否可进入目标:
Goal.prototype.processCollision = function(ball) { if ( this.hasCollided(ball) && ball.hotCount > 0 && !ball.inactive) { updateMessagingBox("Score! (ball " + ball.i + " gone)"); ++this.game.score; updateScoreBox(this.game.score); ball.poof(); ball.inactive = true if ( this.game.balls.allInactive() ) this.game.nextLevel(); } }
随着级数的增长,球列表中的球数目也随之增加(每级别增加一个球)。在包含多个球的级别(2 级及以上)上,球进入目标后需要以某种方式从游戏中删除。最初的方法只是简单地将得分的球从球列表中删除,但是这样做会造成代码同步问题。为避免重写大部分代码,可以将每个得分球标记为非活动,并通过以下方式将这些球从游戏区域删除:首先将这些球移出竞技场外墙,然后慢慢将球的速度和半径设为零(这由 ball.poof()
负责,如之前所示)。请注意,使用此方法时,许多游戏循环内存在附加检查(在 JavaScript 代码中搜索“inactive”以查找这些循环)。
如果球列表中全是非活动的球(时钟上仍有时间),我们知道该进入下一个级别(并重置时钟)了。由于 Goal.prototype.processCollision
是检测此状态的第一种方法,我们将此代码放入其中,如之前的代码段中最后两行所示:
活动链接: 示例 6
添加到游戏的最后功能是“液态布局”。实现液态布局时,随着浏览器窗口大小的变化,游戏的大小将相应地变化。这样可以更方便地在较小屏幕或分辨率的设备上玩游戏。
在示例 5 的 Paddle.prototype.mouseMove
方法中,我们将处理两个坐标系:
<svg id="arenaBox" width="800px" height="800px" viewBox="0 0 800 800"> <g id="arena" transform="translate(400, 400)"> <circle id="floor" cx="0" cy="0" /> <!-- The arena floor --> <path id="wall" /> <!-- The arena wall less the goal hole. --> <circle id="post" cx="0" cy="0" /> <!-- The central post in the middle of the arena. --> <rect id="paddle" x="300" y="-50" width="8" height="100" rx="2" ry="2"/> <!-- Ball circle elements are appended here via JavaScript. --> </g> </svg>
在 Paddle.prototype.mouseMove
中,我们使用 paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor
将鼠标的(垂直)位置 evt.pageY
(在屏幕坐标中)映射到与竞技场(包括球拍)关联的坐标系。由于竞技场大小是固定的 (800x800),arenaTransformForY
(在竞技场坐标中)的含意相对于 evt.pageY
保持不变,因此我们可以摆脱此坐标转换的麻烦。不过,当布局变为液态时,我们将不能进行此假设。而是需要使用常规方法将一个坐标系(屏幕坐标)转换为另一坐标系(竞技场坐标)。
幸运的是,SVG 提供了一个执行此操作的相对简单的方法。该技术在 SVG 坐标转换中介绍,应先了解此技术再继续。在理解此技术之后,对示例 5 进行了以下更改,以生成其液态版本示例 6:
在 base.css 中,添加了以下基于百分比的值:
html { padding: 0; margin: 0; height: 100%; /* Required for liquidity. */ } body { padding: 0; margin: 0; background-color: #CCC; height: 100%; /* Required for liquidity. */ } body#gamePage svg { margin-top: 0.8%; /* Used to provide a liquid top margin for the SVG viewport. */ min-height: 21em; /* Don't let the playing arena get preposterously small. */ } body#gamePage #arenaWrapper { text-align: center; height: 80%; /* Required for liquidity. */ }
html
的 height: 100%
基本上可确保页面始终与浏览器的窗口一样大。body
的 height: 100%
可确保页面的内容始终与其容器(html
元素)一样大。如注释中所提到,body#gamePage svg
的 margin-top: 0.8%
为 SVG 视区提供了一个液态顶边距,body#gamePage #arenaWrapper
上的 height: 80%
提供了模拟的底部边距。
对于 svg
元素,我们按如下所示将 width
和 height
属性值从 800
更改为 100%
:
此更改结合以前的 CSS 更改,允许 SVG 元素的视区扩展到浏览器的窗口大小。
在 helpers.js 中,添加了一个坐标转换函数:
如 SVG 坐标转换中所述,此函数将鼠标的位置 (screenPoint
) 映射到与 someSvgObject
关联的坐标系,在本例中是竞技场的(因此也是球拍的)坐标系。
最后,在 example6.html 中,对 Paddle.prototype.mouseMove
进行了如下修改(注意注释):
Paddle.prototype.mouseMove = function(evt) { if (this.game.ended || this.game.paused) return; var paddle = document.getElementById('paddle'); var arena = document.getElementById('arena'); var maxY = Math.sqrt( (constants.arenaRadius * constants.arenaRadius) - (paddle.x.baseVal.value * paddle.x.baseVal.value) ); var arenaBox = document.getElementById('arenaBox'); var point = arenaBox.createSVGPoint(); // Create an SVG point object so that we can access its matrixTransform() method in function coordinateTransform(). point.x = evt.pageX; // Transfer the mouse's screen position to the SVG point object. point.y = evt.pageY; point = coordinateTransform(point, arena); // Transform the mouse's screen coordinates to arena coordinates. var paddleHeight = paddle.height.baseVal.value; paddle.y.baseVal.value = point.y - (paddleHeight / 2); // Change the position of the paddle based on the position of the mouse (now in arena coordinates). var paddleTop = paddle.y.baseVal.value; if (paddleTop <= -maxY) { paddle.y.baseVal.value = -maxY; return; } var paddleBottom = paddleTop + paddleHeight; if (paddleBottom >= maxY) { paddle.y.baseVal.value = maxY - paddleHeight; return; } }
由于球拍使用竞技场的坐标系,我们首先通过调用 coordinateTransform
helper 函数,将鼠标的屏幕坐标位置转换为其在竞技场坐标系中的模拟位置。在转换鼠标的位置之后,基于鼠标当前位置更改球拍位置就变得不微不足道:paddle.y.baseVal.value = point.y - (paddleHeight / 2)
。paddleHeight/2
常量可确保球拍从其中心移动(与其顶点相对)。
SVG 弹球是在 Internet Explorer 中进行开发的,IE 的 F12 开发人员工具在跟踪和解决 JavaScript Bug 方面极为有用。
例如,遇到的较为复杂的 Bug 之一是球在几乎同时击中球拍和竞技场外墙的特定部位时发生的似乎随机的游戏锁定。
此问题可能与 while
、Arena.prototype.processCollision
和 Paddle.prototype.processCollision
中的“hacky”(和潜在无限的)SVGCircleElement.prototype.collisionResponse
循环有关。
在每个可疑 F12Log()
循环和游戏主循环 (while
) 内放置对 Game.prototype.play
的调用,以在 F12 开发人员工具的控制台窗口中监控它们的输出,提供确定何时何处出现游戏锁定的方法。这些 F12Log()
调用仍在代码中出现,但带有注释(搜索“F12Log”)。
游戏进行几分钟后便锁定,这时即清楚地知道代码确实在 while
内的可疑 Arena.prototype.processCollision
循环中陷入了无限循环。
很明显,在某些情况下,ball.move()
没有达到预期的效果,而是使得 this.hasCollided(ball)
变为 true
,从而导致无限循环。一个简单的解决方案是检测此“无限循环”的状态,然后直接将有问题的球从该情形中“删除”,如以下代码示例所示:
这里,我们做一项简单的测试以查看 while
循环是否“过多”,如果是的话,我们将“挑出该球”,然后将其向竞技场中心稍微移动一点。这样可以打破无限循环,而不会过度影响对玩家所显示的物理特性。
另外,在游戏开发中,防错性程序设计的值也很有用。例如,在 helper 函数 affirmingMessage
中,你会发现下面的一行:
你会发现代码中遍布着类似的警告。对于 affirmingMessage
函数,错误地使用了 level = level % 7 + 1
来确保 level
保持在 1 和 7 之间。没有此防御警报,找出这个编码错误会困难得多(正确语句是 level = ((level-1) % 7) + 1
)。
SVG 弹球是使用 Internet Explorer 9 进行开发的。当游戏在 Internet Explorer 9 中完成并且能够完全运行后,我们对代码进行了相应调整以使其能在 Firefox 和 Chrome 中运行。幸运地是,只有几个跨浏览器的问题需要解决:
在 Paddle.prototype.mouseMove
原型中,evt.y
用于确定鼠标的当前 y 坐标位置。将 evt.y
更改为 evt.pageY
可提供相同的信息,但是跨浏览器兼容性更好。即:
更改为:
请考虑以下假设函数:
假定 paddle
已存在,此函数在 Internet Explorer 9 和 Chrome 中均能正常工作。但是,Firefox 却需要(在撰写本文之时)以下 getElementById
调用:
这解释了为何代码内存在大量“反常的”getElementById
调用。
在 Paddle.prototype.processCollision
中,以下代码用于检测何时将冷白球变为非白色的热颜色:
尽管在最初创建时,每个球的填充属性都设置为 constants.coldBallColor ("white"),但是上述条件测试在不同的浏览器中会发生失败,因为字符串在一些浏览器中 "white"
转变为 #ffffff
,而在另一些浏览器中转变为 rgb(255, 255, 255)
。所采用的解决方案是放弃该冷色测试,转而依靠球的 hotCount
属性:
在 Paddle.prototype.processCollision
中,此测试用于仅在以下条件下给予冷球实心的非白色:
ball.hotCount
是 0(热球已变为冷球) ball.hotCount
是 undefined
(新创建的球击中球拍导致其变为热球) 如之前所述,热球由于过多次地从竞技场墙壁弹回而变冷时,ball.hotCount
为 0;就在球首次创建好(并且默认全部为冷球)之后,ball.hotCount
为 undefined
。这种方法很有效,因为在 JavaScript 中,!undefined
为 true
。
若要完全理解本主题中出现的代码,建议进行下列游戏改进:
其他更具挑战性的改进可能包括将矩形球拍更改为椭圆形。这样可以提供引导球向目标移动的看起来更加精确的方法。此示例中,检测球与球拍间碰撞的方法之一可能是同时解决与球的圆形和球拍的椭圆形关联的方程系统。