每个人都喜欢复古游戏 Pong,对吧?我们当然喜欢。还有什么比自己动手制作更有趣呢?
这就是我们决定使用 SVG.js 创建一个 Pong 游戏的原因——为了突出我们库的一些方面。对于一个小教程来说,这似乎是一个复杂的想法,但正如您将看到的,它比看起来更简单。让我们深入了解一下吧!
这是最终产品
查看 CodePen 上 Wout Fierens (@wout) 编写的 带有效果的完整 Pong 游戏。
入门
SVG.js 可通过 Github、npm、bower 或 CDN.js 获取。有很多方法可以上手使用 SVG.js,选择您最熟悉的方式即可。
首先创建一个新的 HTML 文档,并包含该库。创建一个空的 <div>
作为 SVG 文档的包装器,并赋予它一个 id
属性。对于此项目,pong
应该是一个合适的选择。
<div id="pong"></div>
接下来,通过引用包装器来初始化 SVG.js 实例。在这一点上,为游戏定义一个 width
和 height
也是一个好主意,这使得以后更容易修改它们。
// 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
将分别设置为 -1
或 1
。如果按下其他键,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()
}
我们还需要确保初始的 vx
和 vy
值被设置为 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()
方法,我们可以根据 0
到 1
之间的给定位置来过渡颜色。因此,通过在 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。
非常好!我一直都在寻找一个相对简单的示例,可以用来做实验(在一个响应式实时应用程序的背景下)。这对我很有效。请查看 https://pong.definite-software.com。谢谢!