4
\$\begingroup\$

I'm creating a Pong clone for use in teaching JavaScript to beginners. Unfortunately, I am largely a JS beginner myself, so I probably missed something in the below code; however, I do not want to shortchange my students.

Also, the code does work as intended (with the exception of the sound-playing code, since I couldn't embed that in the Stack Snippet).

"use strict";

var canvas = document.getElementById("pongCanvas");
var context = canvas.getContext("2d");
context.fillStyle = "white";

var player1;
var player2;
var winner;
var gameOver;
var collisionSound;

var WIN_POINTS = 10;
var INITIAL_BALL_SPEED = 2;
var SCORE_OFFSET = 80;

var DIVIDER_HEIGHT = 10;
var DIVIDER_WIDTH = DIVIDER_HEIGHT / 3;
var DIVIDER_PADDING = 20;

var BALL_SIZE = 20;

var keyboardState = {
  w: false,
  s: false,
  up: false,
  down: false
};

var ball = {
  width: BALL_SIZE,
  height: BALL_SIZE,

  reset: function() {
    this.x = canvas.width / 2 - this.width / 2;
    this.y = canvas.height / 2 - this.height / 2;

    this.xSpeed = INITIAL_BALL_SPEED;
    this.ySpeed = INITIAL_BALL_SPEED;
    this.acceleration = 0.001;
  },

  update: function() {
    this.bounceOffWalls();
    this.bounceOffPlayers();
    this.accelerate();
    this.updatePosition();
  },

  draw: function() {
    context.beginPath();
    context.rect(this.x, this.y, this.width, this.height);
    context.fill();
    context.closePath();
  },

  bounceOffWalls: function() {
    if (this.y < 0) {
      this.y = 0;
      this.ySpeed *= -1;
      collisionSound.play();
    } else if (this.y + this.height > canvas.height) {
      this.y = canvas.height - this.height;
      this.ySpeed *= -1;
      collisionSound.play();
    }
  },

  bounceOffPlayers: function() {
    if (this.x < player1.offset + player1.width && this.y > player1.y && this.y < player1.y + player1.height) {
      this.x = player1.offset + player1.width;
      this.xSpeed *= -1;
      collisionSound.play();
    } else if (this.x + this.width > canvas.width - player2.offset - player2.width && this.y > player2.y && this.y < player2.y + player2.height) {
      this.x = canvas.width - player2.offset - player2.width - this.width;
      this.xSpeed *= -1;
      collisionSound.play();
    }
  },

  updatePosition: function() {
    this.x += this.xSpeed;
    this.y += this.ySpeed;
  },

  accelerate: function() {
    if (this.xSpeed < 0) {
      this.xSpeed -= this.acceleration;
    } else {
      this.xSpeed += this.acceleration;
    }

    if (this.ySpeed < 0) {
      this.ySpeed -= this.acceleration;
    } else {
      this.ySpeed += this.acceleration;
    }
  }
};

var player = {
  width: 10,
  height: 75,

  offset: 10,

  ySpeed: INITIAL_BALL_SPEED * 2,

  score: 0,

  update: function() {
    this.updatePosition();
    this.constrainPosition();
  },

  draw: function() {
    context.beginPath();
    context.rect(this.x, this.y, this.width, this.height);
    context.fill();
    context.closePath();
  },

  updatePosition: function() {
    if (keyboardState[this.upControl]) {
      this.y -= this.ySpeed;
    } else if (keyboardState[this.downControl]) {
      this.y += this.ySpeed;
    }
  },

  constrainPosition: function() {
    if (this.y < 0) {
      this.y = 0;
    } else if (this.y + this.height > canvas.height) {
      this.y = canvas.height - this.height;
    }
  }
};
player.y = canvas.height / 2 - player.height / 2;

var createSound = function(filename) {
  var sound = document.createElement("audio");
  sound.src = filename;
  sound.setAttribute("preload", "auto");
  sound.setAttribute("controls", "none");
  sound.style.display = "none";
  document.body.appendChild(sound);
  return sound;
};

var drawDivider = function() {
  for (var i = 0; i < canvas.height; i += DIVIDER_HEIGHT + DIVIDER_PADDING) {
    context.beginPath();
    context.rect(canvas.width / 2 - DIVIDER_WIDTH / 2, i, DIVIDER_WIDTH, DIVIDER_HEIGHT);
    context.fill();
    context.closePath();
  }
};

var drawScores = function() {
  context.font = "48px Courier New";

  context.textAlign = "left";
  context.fillText(player1.score.toString(), canvas.width / 2 - SCORE_OFFSET, SCORE_OFFSET);

  context.textAlign = "right";
  context.fillText(player2.score.toString(), canvas.width / 2 + SCORE_OFFSET, SCORE_OFFSET);
};

gameOver = false;

player1 = Object.create(player);
player1.x = player1.offset;
player1.number = 1;
player1.upControl = "w";
player1.downControl = "s";

player2 = Object.create(player);
player2.x = canvas.width - player2.width - player2.offset;
player2.number = 2;
player2.upControl = "up";
player2.downControl = "down";

collisionSound = createSound("collision.ogg");

var update = function() {
  if (!gameOver) {
    checkForScore();
    checkForWin();

    ball.update();
  }

  player1.update();
  player2.update();
};

var draw = function() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  if (!gameOver) {
    ball.draw();
  } else {
    context.textAlign = "center";
    context.font = "30px Courier New";
    context.fillText("Game Over! Winner: Player " + winner.number, canvas.width / 2, canvas.height / 2);
  }

  player1.draw();
  player2.draw();
  drawDivider();
  drawScores();
};

var checkForScore = function() {
  if (ball.x > canvas.width || ball.x + ball.width < 0) {
    if (ball.x > canvas.width / 2) {
      ++player1.score;
    } else {
      ++player2.score;
    }

    ball.reset();
  }
};

var checkForWin = function() {
  if (player1.score == WIN_POINTS) {
    gameOver = true;
    winner = player1;
  } else if (player2.score == WIN_POINTS) {
    gameOver = true;
    winner = player2;
  }
};

var isArrowKey = function(keyCode) {
  return keyCode >= 37 && keyCode <= 40;
};

var changeKeyboardState = function(event, status) {
  if (isArrowKey(event.keyCode)) {
    switch (event.keyCode) {
      case 38:
        keyboardState.up = status;
        break;
      case 40:
        keyboardState.down = status;
        break;
    }
  } else {
    keyboardState[String.fromCharCode(event.keyCode).toLowerCase()] = status;
  }
};

document.addEventListener("keydown", function(event) {
  changeKeyboardState(event, true);
});
document.addEventListener("keyup", function(event) {
  changeKeyboardState(event, false);
});

ball.reset();

var game = function() {
  update();
  draw();
  requestAnimationFrame(game);
};

game();
* {
    padding: 0;
    margin: 0;
}

canvas {
    background: black;
    display: block;
    margin: 0 auto;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Pong</title>
</head>

<body>
  <canvas id="pongCanvas" width=600 height=400></canvas>
</body>

</html>

\$\endgroup\$
1
\$\begingroup\$
if(this.x < player1.offset + player1.width && this.y > player1.y && this.y < player1.y + player1.height)

Conditions like this might scare your students, and definitely take away from the readability. Ideally, it would be nice to be able to make a generalized function to check whether the ball is bouncing off either player, but unfortunately both checks are rather different, so that might be difficult. At the very least, to make your code more self-documenting, I'd wrap the checks in a function:

function ballTouchedPlayerOne() {
    return this.x < player1.offset + player1.width
        && this.y > player1.y
        && this.y < player1.y + player1.height
}

And just a matter of personal taste, but I found your if...else constructs a little hard to read:

if (this.y < 0) {
      this.y = 0;
      this.ySpeed *= -1;
      collisionSound.play();
} else if (this.y + this.height > canvas.height) {
      this.y = canvas.height - this.height;
      this.ySpeed *= -1;
      collisionSound.play();
}

I've made a habit of adding a new line at the end of each block to help separate it:

if (this.y < 0) {
      this.y = 0;
      this.ySpeed *= -1;
      collisionSound.play();

} else if (this.y + this.height > canvas.height) {
      this.y = canvas.height - this.height;
      this.ySpeed *= -1;
      collisionSound.play();
}

I've found that it really helps readability.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.