Pong with SVG.js

Everybody loves the vintage game Pong, right? We sure do. What's more fun? Building it yourself!

That's why we decided to create one with SVG.js - to highlight some aspects of our library. It might seem like a complex idea for a small tutorial, but as you'll see, it's simpler than it sounds. Let's dive into it!

Here's the finished product:

See the Pen Fully functional Pong game with effects by Wout Fierens (@wout) on CodePen.

Getting started

SVG.js is available through Github, npm, bower or CDN.js. There are plenty of options for getting your hands on SVG.js, so use whatever you are most comfortable with.

Start out by creating a new HTML document and include the library. Create an empty <div> to serve as a wrapper for the SVG document, and give it an id attribute. Something like pong should be suitable for this project:

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

Next, initialize the SVG.js instance by referencing the wrapper. At this point, it's also a good idea to define a width and height for the game which makes it easier to modify them later on.

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

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

Now you're ready to start building the game.

Drawing Game Elements

The Background

The background should cover the whole document, so we're using a <rect> and give it a neutral grayish color. First, we'll draw the left player in green. Then, we'll draw the right one by cloning the left one and coloring it pink.

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

We'll also need a vertical, dashed line in the middle to distinguish the player fields.

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

See the Pen Pong Background by Wout Fierens (@wout) on CodePen.

Paddles and the Ball

Pong wouldn't be Pong without paddles and a ball. First, we'll draw the left player in green. Then, we'll draw the right one by cloning the left one and coloring it pink.

var paddleWidth = 20, paddleHeight = 100

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

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

For the ball we're going to use a circle with a diameter of 20 and place it in the center of the court:

// define ball size
var ballSize = 20

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

See the Pen Pong Paddels and Ball by Wout Fierens (@wout) on CodePen.

Score board

Finally, we will need a scoreboard which we will add at the top of the court.

// 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)

That's all! Now we have all game elements, let's move on to game logic.

See the Pen Pong Scoreboard by Wout Fierens (@wout) on CodePen.

Game logic

We'll start out by writing an update function which will update the state of our game and game elements.

// 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
  }
}

When we run this, nothing will happen, because we didn't call the update function yet. This will be done using JavaScript's native requestAnimationFrame feature, which will allow us to do smooth animations. To make this work, a handler is registered to periodically call our update function:

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()

Yay! The ball is jumping around! But, our paddles are still pretty useless at the moment. So, let's do something about that and insert paddle collision detection. We'll only need it on the x-axis:

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 ...

Better, now the ball is aware of the paddles. But a few other things are still missing:

  • the score is not updating when the border is hit
  • paddles are not moving
  • the ball should reset when a point was scored

Let's work through this list from top to bottom.

See the Pen Pong jumping ball by Wout Fierens (@wout) on CodePen.

Update the score

To update our score, we need to hook into collision detection to the left or right wall:

// 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 + '')
}

See the Pen Pong jumping ball by Wout Fierens (@wout) on CodePen.

Moving the user-controlled paddle

The right paddle will be controlled by the keyboard, and that's a piece of cake with 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
})

So what are we doing here? First, we call SVG.on, which lets us bind an event listener to any node (not only SVG.js objects). We'll listen to the keydown event to detect if either the up (38) or the down (40) key is pressed. If that's the case, the paddleDirection will be set to -1 or 1 respectively. If another key is pressed, the paddleDirection will be 0. Lastly, when any key is released, the paddleDirection will be reset to 0.

The update function will do the actual work of moving the paddle, based on the user input. So we'll add the following code to the update function:

// 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)
}

We prevent the paddle from exiting the court by testing its y position. Otherwise, the paddle will be moved by a relative distance using dy().

See the Pen Pong user controlled paddle by Wout Fierens (@wout) on CodePen.

Moving the AI paddle

A good opponent will make the game worthwhile. So we'll have the AI player follow the ball, with a predefined difficulty level. The higher the difficulty, the faster the AI paddle will respond.

First define the difficulty value, defining the AI's speed:

var difficulty = 2

Then add the following code to the update function:

// 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)))

See the Pen Pong user controlled paddle by Wout Fierens (@wout) on CodePen.

Score!

Wait, this isn't right! The game goes on even after one of the players scored. Time to include a reset function to move all game elements to their initial position using animations:

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)
}

The reset function should be called if one of the players misses the ball. To make that happen, change the failure detection by removing the vx = -vx line and adding the reset() call:

// 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()
}

We also need to make sure the initial vx and vy values are set to 0. So the game does not start without our input. To be able to indicate the first serve, we'll add a click listener to the SVG document:

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

See the Pen Pong with start and reset by Wout Fierens (@wout) on CodePen.

Encore

Of course, there is a lot left to improve on the game, but the purpose of this tutorial is to teach about SVG and in particular about SVG.js. We want to leave you with some visual effects to spice up the game.

Ball color

It would be nice to have the color of the ball change while approaching the opposite opponent. This is done by leveraging the power of the morph method on the SVG.Color class. We'll detect the position of the ball, and gradually assign the color of the opposite opponent, based on the position of the ball on the x-axis.

We'll start by initializing a new instance of SVG.Color:

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

Next, we'll define the target color by calling the morph() method:

ballColor.morph('#00ff99')

This will set a start color, being #ff0066 and an end color, being #00ff99. Using the at() method on SVG.Color, we can tween the color based on a given position between 0 and 1. So by adding the following code to our update function, we can change the color of the ball while it moves:

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

That wasn't hard at all, right?

Boom!

Imagine a huge color blast when the opponent missed the ball. That would make it even more fun to win a point. To achieve this, we'll use a radial gradient. It will appear where the ball has hit the wall and then quickly fade out. Once faded out, the object carrying the gradient will be deleted from the scene. To achieve this, we'll add another function called boom including the required logic:

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()
  })
}

See the Pen Fully functional Pong game with effects by Wout Fierens (@wout) on CodePen.

Conclusion

That's it! You just created a working Pong game using SVG.js. In the next tutorial, we will cover how to convert this blob of code into a reusable SVG.js plug-in, while adding new features and easy configuration to the game.


Written by Ulrich-Matthias Schäfer & Wout Fierens.