使用 SVG.js 制作 Pong 游戏

Avatar of Ulrich-Matthias Schäfer
Ulrich-Matthias Schäfer 发布

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

每个人都喜欢复古游戏 Pong,对吧?我们当然喜欢。还有什么比自己动手制作更有趣呢?

这就是我们决定使用 SVG.js 创建一个 Pong 游戏的原因——为了突出我们库的一些方面。对于一个小教程来说,这似乎是一个复杂的想法,但正如您将看到的,它比看起来更简单。让我们深入了解一下吧!

这是最终产品

查看 CodePen 上 Wout Fierens (@wout) 编写的 带有效果的完整 Pong 游戏

入门

SVG.js 可通过 GithubnpmbowerCDN.js 获取。有很多方法可以上手使用 SVG.js,选择您最熟悉的方式即可。

首先创建一个新的 HTML 文档,并包含该库。创建一个空的 <div> 作为 SVG 文档的包装器,并赋予它一个 id 属性。对于此项目,pong 应该是一个合适的选择。

<div id="pong"></div>

接下来,通过引用包装器来初始化 SVG.js 实例。在这一点上,为游戏定义一个 widthheight 也是一个好主意,这使得以后更容易修改它们。

// define width and height
var width = 450, height = 300

// create SVG document and set its size
var draw = SVG('pong').size(width, height)

现在您已准备好开始构建游戏。

绘制游戏元素

背景

背景应该覆盖整个文档,所以我们使用一个 <rect> 并赋予它一个中性的灰色。首先,我们将用绿色绘制左边的玩家。然后,我们将克隆左边的玩家并将其涂成粉色来绘制右边的玩家。

// draw background
var background = draw.rect(width, height).fill('#E3E8E6')

我们还需要在中间绘制一条垂直的虚线,以区分玩家的区域。

// draw line
var line = draw.line(width/2, 0, width/2, height)
line.stroke({ width: 5, color: '#fff', dasharray: '5,5' })

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 背景

球拍和球

没有球拍和球,Pong 就不是 Pong。首先,我们将用绿色绘制左边的玩家。然后,我们将克隆左边的玩家并将其涂成粉色来绘制右边的玩家。

var paddleWidth = 20, paddleHeight = 100

// create and position left paddle
var paddleLeft = draw.rect(paddleWidth, paddleHeight)
paddleLeft.x(0).cy(height/2).fill('#00ff99')

// create and position right paddle
var paddleRight = paddleLeft.clone()
paddleRight.x(width-paddleWidth).fill('#ff0066')

对于球,我们将使用一个直径为 20 的圆,并将它放置在球场的中心。

// define ball size
var ballSize = 20

// create ball
var ball = draw.circle(ballSize)
ball.center(width/2, height/2).fill('#7f7f7f')

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 球拍和球

计分板

最后,我们需要一个计分板,我们将把它添加到球场的顶部。

// define initial player score
var playerLeft = playerRight = 0

// create text for the score, set font properties
var scoreLeft = draw.text(playerLeft+'').font({
  size: 32,
  family: 'Menlo, sans-serif',
  anchor: 'end',
  fill: '#fff'
}).move(width/2-10, 10)

// cloning rocks!
var scoreRight = scoreLeft.clone()
  .text(playerRight+'')
  .font('anchor', 'start')
  .x(width/2+10)

就这样!现在我们有了所有游戏元素,让我们继续进行游戏逻辑。

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 计分板

游戏逻辑

我们将从编写一个更新函数开始,该函数将更新我们游戏和游戏元素的状态。

// random velocity for the ball at start
var vx = Math.random() * 500 - 250
  , vy = Math.random() * 500 - 250

// update is called on every animation step
function update(dt) {
  // move the ball by its velocity
  ball.dmove(vx*dt, vy*dt)

  // get position of ball
  var cx = ball.cx()
    , cy = ball.cy()

  // check if we hit top/bottom borders
  if ((vy < 0 && cy <= 0) || (vy > 0 && cy >= height)) {
    vy = -vy
  }

  // check if we hit left/right borders
  if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
    vx = -vx
  }
}

当我们运行它时,什么都不会发生,因为我们还没有调用 update 函数。这将使用 JavaScript 的原生 requestAnimationFrame 功能完成,它将允许我们进行平滑的动画。为了使它工作,注册一个处理程序来定期调用我们的 update 函数

var lastTime, animFrame;

function callback(ms) {
  // we get passed a timestamp in milliseconds
  // we use it to determine how much time has passed since the last call

  if (lastTime) {
    update((ms-lastTime)/1000) // call update and pass delta time in seconds
  }

  lastTime = ms
  animFrame = requestAnimationFrame(callback)
}

callback()

太好了!球在跳动!但是,我们的球拍目前还很无用。所以,让我们做点什么,插入球拍碰撞检测。我们只需要在 x 轴上进行检测。

var paddleLeftY = paddleLeft.y()
  , paddleRightY = paddleRight.y()

// check if we hit the paddle
if ((vx < 0 && cx <= paddleWidth && cy > paddleLeftY && cy < paddleLeftY + paddleHeight) ||
   (vx > 0 && cx >= width - paddleWidth && cy > paddleRightY && cy < paddleRightY + paddleHeight)) {
  // depending on where the ball hit we adjust y velocity
  // for more realistic control we would need a bit more math here
  // just keep it simple
  vy = (cy - ((vx < 0 ? paddleLeftY : paddleRightY) + paddleHeight/2)) * 7 // magic factor

  // make the ball faster on hit
  vx = -vx * 1.05
} else ...

更好的是,现在球能够感知球拍了。但还有一些东西缺失

  • 球碰到边界时,分数没有更新
  • 球拍没有移动
  • 得分后,球应该复位

让我们从上到下逐一解决这些问题。

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 跳动球

更新分数

为了更新分数,我们需要将碰撞检测挂钩到左右墙壁。

// check if we hit left/right borders
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) { ++playerRight }
  else { ++playerLeft }

  vx = -vx

  scoreLeft.text(playerLeft + '')
  scoreRight.text(playerRight + '')
}

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 跳动球

移动玩家控制的球拍

右边的球拍将由键盘控制,这对于 SVG.js 来说轻而易举。

// define paddle direction and speed
var paddleDirection = 0  // -1 is up, 1 is down, 0 is still
  , paddleSpeed = 5      // pixels per frame refresh

// detect if up and down arrows are prssed to change direction
SVG.on(document, 'keydown', function(e) {
  paddleDirection = e.keyCode == 40 ? 1 : e.keyCode == 38 ? -1 : 0
});

// make sure the direction is reset when the key is released
SVG.on(document, 'keyup', function(e) {
  paddleDirection = 0
})

我们在这里做了什么?首先,我们调用 SVG.on,它允许我们将事件监听器绑定到任何节点(不仅仅是 SVG.js 对象)。我们将监听 keydown 事件,以检测 up (38) 或 down (40) 键是否被按下。如果是,paddleDirection 将分别设置为 -11。如果按下其他键,paddleDirection 将为 0。最后,当任何键被释放时,paddleDirection 将被重置为 0

update 函数将根据用户输入,完成移动球拍的实际工作。因此,我们将以下代码添加到 update 函数中。

// move player paddle
var playerPaddleY = paddleRight.y();

if (playerPaddleY <= 0 && paddleDirection == -1) {
  paddleRight.cy(paddleHeight / 2)
} else if (playerPaddleY >= height-paddleHeight && paddleDirection == 1) {
  paddleRight.y(height - paddleHeight)
} else {
  paddleRight.dy(paddleDirection * paddleSpeed)
}

我们通过测试球拍的 y 位置来阻止它超出球场。否则,球拍将使用 dy() 移动一个相对距离。

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 玩家控制的球拍

移动 AI 球拍

一个好的对手会让游戏更有价值。所以,我们将让 AI 玩家跟随球,并设定一个预定义的难度级别。难度越高,AI 球拍的反应速度就越快。

首先定义难度值,定义 AI 的速度

var difficulty = 2

然后将以下代码添加到 update 函数中。

// get position of ball and paddle
var paddleRightCy = paddleRight.cy()

// move the left paddle in the direction of the ball
var dy = Math.min(difficulty, Math.abs(cy - paddleRightCy))
paddleRightCy += cy > paddleRightCy ? dy : -dy

// constraint the move to the canvas area
paddleRight.cy(Math.max(paddleHeight/2, Math.min(height-paddleHeight/2, paddleRightCy)))

查看 CodePen 上 Wout Fierens (@wout) 编写的 Pong 玩家控制的球拍

得分!

等等,不对!即使一方得分,游戏也仍在继续。是时候加入一个 reset 函数,使用动画将所有游戏元素移动到它们初始位置了。

function reset() {
  // reset speed values
  vx = 0
  vy = 0

  // position the ball back in the middle
  ball.animate(100).center(width / 2, height / 2)

  // reset the position of the paddles
  paddleLeft.animate(100).cy(height / 2)
  paddleRight.animate(100).cy(height / 2)
}

如果一方漏接了球,应该调用 reset 函数。为了实现这一点,通过移除 vx = -vx 行并添加 reset() 调用来更改失败检测。

// check if a player missed the ball
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) {
    ++playerRight
  } else {
    ++playerLeft
  }

  // update score
  scoreLeft.text(playerLeft)
  scoreRight.text(playerLeft)

  reset()
}

我们还需要确保初始的 vxvy 值被设置为 0。这样,游戏就不会在没有我们输入的情况下开始。为了能够指示第一次发球,我们将向 SVG 文档添加一个 click 监听器。

draw.on('click', function() {
  if (vx === 0 && vy === 0) {
    vx = Math.random() * 500 - 250
    vy = Math.random() * 500 - 250
  }
})

查看 带有开始和重置的乒乓球,由 Wout Fierens (@wout) 在 CodePen 上创作。

再来一次

当然,游戏还有很多地方可以改进,但本教程的目的是讲解 SVG,尤其是 SVG.js。我们希望向你展示一些视觉效果,为游戏增添趣味。

球的颜色

当球靠近对方时,如果球的颜色可以改变,那将很棒。这可以通过利用 SVG.Color 类上的 morph 方法来实现。我们将检测球的位置,并根据球在 x 轴上的位置,逐渐分配对方球员的颜色。

我们将从初始化 SVG.Color 的新实例开始。

var ballColor = new SVG.Color('#ff0066')

接下来,我们将通过调用 morph() 方法来定义目标颜色。

ballColor.morph('#00ff99')

这将设置一个开始颜色,即 #ff0066,和一个结束颜色,即 #00ff99。使用 SVG.Color 上的 at() 方法,我们可以根据 01 之间的给定位置来过渡颜色。因此,通过在 update 函数中添加以下代码,我们可以在球移动时改变球的颜色。

ball.fill(ballColor.at(1/width*ball.x()))

这并不难,对吧?

砰!

想象一下,当对手没接到球时,会出现巨大的彩色爆炸。这将使赢得一分变得更加有趣。为了实现这一点,我们将使用径向渐变。它将出现在球撞击墙壁的地方,然后迅速淡出。淡出后,承载渐变的物体将从场景中删除。为了实现这一点,我们将添加另一个名为 boom 的函数,其中包含必要的逻辑。

function boom() {
  // detect winning player
  var paddle = vx > width/2 ? paddleLeft : paddleRight

  // create the gradient
  var gradient = draw.gradient('radial', function(stop) {
    stop.at(0, paddle.attr('fill'), 1)
    stop.at(1, paddle.attr('fill'), 0)
  })

  // create circle to carry the gradient
  var blast = draw.circle(300)
  blast.center(ball.cx(), ball.cy()).fill(gradient)

  // animate to invisibility
  blast.animate(1000, '>').opacity(0).after(function() {
    blast.remove()
  })
}

查看 CodePen 上 Wout Fierens (@wout) 编写的 带有效果的完整 Pong 游戏

总结

就这样!你刚刚使用 SVG.js 创建了一个可用的乒乓球游戏。在下一教程中,我们将讲解如何将这段代码块转换成一个可重复使用的 SVG.js 插件,同时为游戏添加新功能和简易配置。


作者:Ulrich-Matthias Schäfer 和 Wout Fierens。