高级 SVG 动画

SVG动画游戏开发

此主题将介绍有关为网站创建 SVG 动画方面的更高级概念。在学习此教程之前,请掌握基本 SVG 动画中级 SVG 动画,并充分了解 HTML 和 JavaScript。

注意 要查看本主题中的示例,你需要一个支持 SVG 元素的浏览器,如 Windows Internet Explorer 9。

在本主题中,我们将圆形球竞技场(在中级 SVG 动画部分进行了介绍)扩展为以教学为中心的 2D 视频游戏:

SVG 弹跳游戏屏幕快照

游戏的目标很简单 - 用球拍反弹球,以让球弹出圆形竞技场左侧的目标(外墙上的缺口)。球只有在接触球拍之后才能进入目标。球接触球拍后将变“热”,颜色也由“冷”白色变为非白色(随机选择)的“热”颜色。有关详细信息和游戏玩法提示,请参阅游戏概述

要试玩游戏,请单击 SVG 弹球。如游戏概述中所述,在页面上的任何位置单击即开始游戏;垂直上下移动鼠标可上下移动球拍。

现在,你已对游戏有了一些了解,我们将讨论该游戏是如何以实施的顺序构建的:

在下面的讨论中,了解游戏中使用的坐标系是伪笛卡尔坐标非常有用,就是说,游戏中的坐标系的原点在竞技场的中心(与在标准的笛卡尔坐标系中一样),但 y 轴在 x 轴以下为正数,在 x 轴以上为负数。

另外,请注意经常使用极坐标,因为它们可以显著简化许多相关的数学计算:

带有某个 JavaScript 代码的极坐标

图 1

因长度的缘故,本主题中没有显示任何以下示例的源代码。而是为每个示例提供了一个“活动链接”。要查看与示例关联的源代码,请使用浏览器的查看源文件功能。例如,在 Windows Internet Explorer 中,右键单击要查看源代码的网页,然后单击“查看源文件”。请确保阅读本文档其余部分时有适当的源代码。

示例 1 – 静态框架

活动链接: 示例 1

游戏的构建基础为静态游戏框架:

示例 2 代码的屏幕快照

图 2

此框架基于以下标记:

<body id="gamePage" οnlοad="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> 


此标记相对简单,但是以下项可能会很有趣:

  • TimeLevelScore 这三个框(使用 <div> 元素实现)的内容以编程的方式进行更改,而所有四个都使用 position:absolute CSS 在网页的四个角定位框。
  • 默认的 SVG 坐标系已通过 <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.startGame.prototype.play constants.gameDelay 毫秒被调用一次。Game.prototype.play 当前是一个空函数,但最终将包含主要游戏循环。
  • Game.prototype.clock 每秒被调用一次并递减任何剩余的游戏时间。当游戏时间少于或等于 constants.warningTime 时,圆形游戏竞技场的背景将闪烁红色。此效果的实现方式是,首先将竞技场设置为红色,然后设置另一个回调函数(即,Arena.prototype.defaultFloorColor),在很短时间之后将地板的颜色更改为白色。setTimout 之所以用于此目的,是因为其回调函数仅调用一次,这正是我们对竞技场地板进行脉冲所需的行为:每秒一次。

最后,请注意游戏构造函数中的最后四行:

function Game(level)
{
  this.paused = true; 
  this.ended = false; 
  this.level = level; 
  this.score = 0; 
  this.time = constants.playTime; 
  this.arena = new Arena(this); 
  this.goal = new Goal(this); 
  this.paddle = new Paddle(this);
  this.balls = new Balls(this); 
}

这几行允许球、球拍、目标和竞技场对象接触游戏对象。这在一个对象必须接触另一个对象时变得很有用。 例如,在 Arena.prototype.draw 中,我们按如下方式更新时钟:

updateClock(this.game.time);

这里,竞技场对象通过访问其游戏对象属性来访问游戏对象的时间属性。

示例 2 – 球拍移动

活动链接: 示例 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.jsexample2.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 将设置以下事件处理程序:

window.addEventListener('keydown', processKeyPress, false);
window.addEventListener('click', processPaddleClick, false);

转到 helpers.js 后,你会注意到 processKeyPressprocessPaddleClick 函数均直接访问全局变量 game。因为从某种意义上说,这两个函数在逻辑上处于游戏之外,所以它们就成了 helper 函数(与游戏方法相对)。

processKeyPress 很简单 – 在按下正确的键时,它将调用 Paddle.prototype.keyMove,将球拍向适当方向(即向上或向下)移动 constants.paddleDy 个单位。Paddle.prototype.keyMove 还可确保球拍不会越出竞技场外墙。这是通过使用勾股定理实现的:

用于确定球拍最大移动的勾股定理

图 3

从图 3 中,我们看到 r2 = x2 + y2 ⇒ y2 = r2 – x2 ⇒ y = √(r2 – x2),这将得出球拍的最大高度。因此,我们得到:

var maxY = Math.sqrt( (constants.arenaRadius * constants.arenaRadius) - (paddle.x.baseVal.value * paddle.x.baseVal.value) );

移动到 processPaddleClick,在单击鼠标时,此函数的主要目的是添加一个鼠标移动事件侦听程序(在本例中是 Game.prototype.mouseMove)。Game.prototype.mouseMove 根据鼠标的当前垂直位置通过以下方式上下移动球拍:

paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor;

这里的核心问题是,鼠标的 y 位置 (evt.pageY) 位于一个坐标系中,而球拍的 y 位置 ((paddle.y.baseVal.value) 位于另一个坐标系中。首先,我们通过减去用于创建伪笛卡尔系统的变换的 y 轴分量,将鼠标的 y 位置变换为游戏的伪笛卡尔坐标系:

<g id="arena" transform="translate(400, 400)">

Paddle.prototype.mouseMove 中,这是通过较隐密的行得到的:

var arenaTransformForY = arena.transform.baseVal.getItem(0).matrix.f;

此行直接获取上一个 <g> 标记中的第二个 400。我们从这个 400 中减去单击鼠标时定位球拍中心的经验导出常数。现在,鼠标垂直移动时,球拍也会随之垂直移动。

示例 3 – 球的摆放

活动链接: 示例 3

下一步是创建球,将球定位在“甜甜圈”形状的竞技场内以避免球之间或球与球拍之间互相重叠,然后将球附加到 DOM(使其显示在屏幕上)。这是通过以下方法实现的:

Balls.prototype.place = function()
{
  this.create(); 
  this.positionInArena(); 
  this.appendToDOM(); 
}

此代码调用的三种方法相对比较简单,不过可能有以下几种例外情况:

  • 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(竞技场外墙的半径)和 θ = randomThetarandomRadius 是随机选取的,它大于或等于 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,以确定给定的球是否已经与球拍发生碰撞。以下代码示例中所用的方法如下所示:

    1. 检测球是否没有与球拍发生碰撞,如果确实没有,则返回 false
    2. 检测球是否已碰撞到球拍的其中一个平面,以至于球的中心不在角区域内;如果是,则返回 true
    3. 使用勾股定理检测球是否已击中球拍的其中一个角,如果是,则返回 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 – 球的移动

活动链接: 示例 4

球的移动需要大量新代码,因为我们现在必须处理以下问题:

  • 球与球之间的碰撞。
  • 球与竞技场(内墙和外墙)的碰撞。
  • 球与球拍的碰撞。
球与球之间的碰撞

球和球之间的碰撞在中级 SVG 动画中通过示例 4 讨论。有关详细信息,请参阅碰撞响应 (PowerPoint) 的“Have Collision, Will Travel”(有碰撞,就有位移)部分。

球与竞技场的碰撞

球和竞技场之间的碰撞在中级 SVG 动画通过示例 5 讨论。 但是,你会发现对 while ( this.hasCollided(ball) ) 内的 Arena.prototype.processCollision 循环所作的一些更改 – 稍后将在本主题的调试一节进行讨论。此外,已直接将 Arena.prototype.hasCollided 扩展为在球碰到内墙或外墙时返回 true,如下所示:

return (d + r >= constants.arenaRadius) || (d - r <= constants.postRadius);

球与球拍的碰撞

球与球拍的碰撞相对比较明确,因为无论球与球拍所形成的角度如何,球总是以固定的角度弹开,如下图所示:

固定角度回力球弹跳

图 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 中实现,此代码将球是否从左边或右边击中球拍考虑在内,如下所示:

if (ball.v.xc >= 0) // The ball has struck the left (vertical) side of the paddle.
  uAngle = 180 - uAngle;

一旦知道了正确的转向角度后,将通过该角度计算出一个单位矢量 u,来球速度矢量 Vin(参见图 5)的值将传送到产生新的出球速度矢量 Vout 的单位矢量。 即,如果 u 是计算出的单位矢量,Vin 是来球速度矢量,则出球速度矢量 Vout = |Vin|u

结束此部分之前,请注意游戏主循环最终包含了一些内容:

Game.prototype.play = function()
{
  for (var i = 0; i < this.balls.list.length; i++) 
  {
    this.balls.list[i].move(); 
    this.balls.processCollisions(this.balls.list[i]); 
    this.paddle.processCollision(this.balls.list[i]);        
    this.arena.processCollision(this.balls.list[i]); 
  }      
}

球列表中每个球的算法很简单:

  1. 将球移动一点。
  2. 处理球之间的任何碰撞。
  3. 处理球与球拍间的任何碰撞。
  4. 处理球与竞技场间的任何碰撞。

由于 Game.prototype.startsetInterval 调用的原因,推动整个游戏的主循环每 constants.gameDelay 毫秒发生一次:

this.playID = setInterval(function() {gameObject.play();}, constants.gameDelay);

Game.prototype.start 仅在通过按上下箭头键或单击鼠标开始游戏时才会被调用(请参阅 helpers.js 中的 processKeyPressprocessMouseClick)。

示例 5 - 得分和升级

活动链接: 示例 5

最后添加到游戏的主要组件为得分和升级。

计分

游戏概述中我们知道,在球首次击中球拍之后才能得分(并且随后在竞技场墙壁上没有反弹太多次数)。要跟踪哪些球为热(可进入目标),哪些球为冷(无法进入目标),我们为每个球对象添加了自定义属性 hotCount。最初,在球击中球拍之前,球的属性是不明确的,它在 Paddle.prototype.processCollision 中设置为 ball.hotCount = constants.hotBounces。当球碰撞竞技场墙壁时,此属性会递减。hotCount 还用于提供有关某个球留下的热回弹数的视觉提示。当球首次接触球拍时,在 Paddle.prototype.processCollision 中将从“冷”白填充色变为随机的非白“热”填充色,如下所示:

if ( !ball.hotCount )
  ball.style.fill = getRandomColor(); 

如果 ball.hotCountundefined,则认为球是冷的,这适用于首次创建球(在 JavaScript 中,!undefinedtrue)或 ball.hotCount 为零(发生在球已经从竞技场墙壁弹回 constants.hotBounces 或更多次时)的情况。

从竞技场墙壁每弹回一次,球的 hotCount 和不透明度将会在 Arena.prototype.processCollision 中减少(直至最终变为零),如下所示:

if (ball.hotCount > 0)
{
  --ball.hotCount; 
  updateMessagingBox("Ball " + ball.i + ": " + ball.hotCount + " bounces left to score.");

  ball.style.fillOpacity = ball.hotCount / constants.hotBounces;
  if (ball.style.fillOpacity == 0)
    ball.style.fill = constants.coldBallColor; 
}

每个球的 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 – 液态布局

活动链接: 示例 6

添加到游戏的最后功能是“液态布局”。实现液态布局时,随着浏览器窗口大小的变化,游戏的大小将相应地变化。这样可以更方便地在较小屏幕或分辨率的设备上玩游戏。

示例 5Paddle.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

CSS 修改

在 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. */  
}

htmlheight: 100% 基本上可确保页面始终与浏览器的窗口一样大。bodyheight: 100% 可确保页面的内容始终与其容器(html 元素)一样大。如注释中所提到,body#gamePage svgmargin-top: 0.8% 为 SVG 视区提供了一个液态顶边距,body#gamePage #arenaWrapper 上的 height: 80% 提供了模拟的底部边距。

标记修改

对于 svg 元素,我们按如下所示将 widthheight 属性值从 800 更改为 100%

<svg id="arenaBox" width="100%" height="100%" viewBox="0 0 800 800">

此更改结合以前的 CSS 更改,允许 SVG 元素的视区扩展到浏览器的窗口大小。

JavaScript 修改

在 helpers.js 中,添加了一个坐标转换函数:

function coordinateTransform(screenPoint, someSvgObject)
{
  var CTM = someSvgObject.getScreenCTM();

  return screenPoint.matrixTransform( CTM.inverse() );
}

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 之一是球在几乎同时击中球拍和竞技场外墙的特定部位时发生的似乎随机的游戏锁定。

此问题可能与 whileArena.prototype.processCollision Paddle.prototype.processCollision 中的“hacky”(和潜在无限的)SVGCircleElement.prototype.collisionResponse 循环有关。

在每个可疑 F12Log() 循环和游戏主循环 (while) 内放置对 Game.prototype.play 的调用,以在 F12 开发人员工具的控制台窗口中监控它们的输出,提供确定何时何处出现游戏锁定的方法。这些 F12Log() 调用仍在代码中出现,但带有注释(搜索“F12Log”)。

游戏进行几分钟后便锁定,这时即清楚地知道代码确实在 while 内的可疑 Arena.prototype.processCollision 循环中陷入了无限循环。

while ( this.hasCollided(ball) )
{
  F12Log("In Arena.prototype.processCollision while loop...");
  ball.move(); // Normally move the ball away from the arena wall.
}

很明显,在某些情况下,ball.move() 没有达到预期的效果,而是使得 this.hasCollided(ball) 变为 true,从而导致无限循环。一个简单的解决方案是检测此“无限循环”的状态,然后直接将有问题的球从该情形中“删除”,如以下代码示例所示:

var loopCount = 0;
while ( this.hasCollided(ball) )
{
  // F12Log("In Arena.prototype.processCollision while loop...");
  if (++loopCount > constants.arenaDeadlock) 
    ball.moveTowardCenter(); 
  else
    ball.move(); 
}

这里,我们做一项简单的测试以查看 while 循环是否“过多”,如果是的话,我们将“挑出该球”,然后将其向竞技场中心稍微移动一点。这样可以打破无限循环,而不会过度影响对玩家所显示的物理特性。

另外,在游戏开发中,防错性程序设计的值也很有用。例如,在 helper 函数 affirmingMessage 中,你会发现下面的一行:

alert("Error in affirmingMessage(" + level + ")");

你会发现代码中遍布着类似的警告。对于 affirmingMessage 函数,错误地使用了 level = level % 7 + 1 来确保 level 保持在 1 和 7 之间。没有此防御警报,找出这个编码错误会困难得多(正确语句是 level = ((level-1) % 7) + 1)。

跨浏览器支持

SVG 弹球是使用 Internet Explorer 9 进行开发的。当游戏在 Internet Explorer 9 中完成并且能够完全运行后,我们对代码进行了相应调整以使其能在 Firefox 和 Chrome 中运行。幸运地是,只有几个跨浏览器的问题需要解决:

  • 鼠标事件对象
  • getElementById
  • 颜色字符串(颜色值序列化)
鼠标事件对象

Paddle.prototype.mouseMove 原型中,evt.y 用于确定鼠标的当前 y 坐标位置。将 evt.y 更改为 evt.pageY 可提供相同的信息,但是跨浏览器兼容性更好。即:

paddle.y.baseVal.value = evt.y - arenaTransformForY - constants.paddleFudgeFactor;

更改为:

paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor;

getElementById

请考虑以下假设函数:

function notUsedInGame()
{
  paddle.y.baseVal.value = 40;
}

假定 paddle 已存在,此函数在 Internet Explorer 9 和 Chrome 中均能正常工作。但是,Firefox 却需要(在撰写本文之时)以下 getElementById 调用:

function notUsedInGame()
{
  var paddle = document.getElementById('paddle');
  paddle.y.baseVal.value = 40;
}

这解释了为何代码内存在大量“反常的”getElementById 调用。

颜色字符串

Paddle.prototype.processCollision 中,以下代码用于检测何时将冷白球变为非白色的热颜色:

if (ball.style.fill == constants.coldBallColor) 
  ball.style.fill = getRandomColor(); 


尽管在最初创建时,每个球的填充属性都设置为 constants.coldBallColor ("white"),但是上述条件测试在不同的浏览器中会发生失败,因为字符串在一些浏览器中 "white" 转变为 #ffffff,而在另一些浏览器中转变为 rgb(255, 255, 255)。所采用的解决方案是放弃该冷色测试,转而依靠球的 hotCount 属性:

if ( !ball.hotCount )
  ball.style.fill = getRandomColor(); 


Paddle.prototype.processCollision 中,此测试用于仅在以下条件下给予冷球实心的非白色:

  • ball.hotCount 是 0(热球已变为冷球)
  • ball.hotCountundefined(新创建的球击中球拍导致其变为热球)

如之前所述,热球由于过多次地从竞技场墙壁弹回而变冷时,ball.hotCount 为 0;就在球首次创建好(并且默认全部为冷球)之后,ball.hotCountundefined。这种方法很有效,因为在 JavaScript 中,!undefinedtrue

建议的练习

若要完全理解本主题中出现的代码,建议进行下列游戏改进:

  • 提供一种方法(通过按按钮或其他方法)提高慢速移动的球的速度。例如,按下“s”键可提高所有球的速度,其中相关的速度矢量值低于某个阈值。
  • 确保每个球都给定了初始速度矢量,以避免出现“单调”的反弹模式(如竞技场内墙和外墙间的“锁定”反弹)。
  • 添加声音效果 - 参见基本 SVG 动画示例 8

其他更具挑战性的改进可能包括将矩形球拍更改为椭圆形。这样可以提供引导球向目标移动的看起来更加精确的方法。此示例中,检测球与球拍间碰撞的方法之一可能是同时解决与球的圆形和球拍的椭圆形关联的方程系统。

相关主题

如何向网页添加 SVG 基本 SVG 动画 中级 SVG 动画 SVG 坐标转换 HTML5 图形 Scalable Vector Graphics (SVG)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值