JavaScript: Intro to Web Game Development – Part 5: ball handling, scoring, and A.I.

In the previous article of the series, I described how to animate the left paddle using mouse movements. See:

In this article, I will detail how to animate the ball, use an AI function to animate the right (computer) paddle, and take care of scoring.

Step 1: Get the ball moving

First I will need to set up some variables for the ball location and speed. ballX and ballY will be used for the ball location and ballsSpeedX and ballSpeedY will be used for the ball movement. When ballX and ballY are initialized, the ball starts out in the upper left hand corner of the screen.

// ball variables
var ballX = offsetA;
var ballY = 0;
var ballSpeedX = 10;
var ballSpeedY = 4;

Where I previously hardcoded the ball location in the drawComponents() function:

// Draw the ball
canvasContext.fillStyle = 'white';
canvasContext.fillRect(200, 300, 10, 10);

I now want to use the ballX and ballY variables. For example:

// Draw the ball
canvasContext.fillStyle = 'white';
canvasContext.fillRect(ballX, ballY, 10, 10);

Next, I need a new function that will control the ball movement and this function needs to be called from inside the game loop. For example, here is the function:

function ballMovement() {
    ballX = ballX + ballSpeedX;
    ballY = ballY + ballSpeedY;
}

For every iteration of the game loop, the ball location is updated by adding the ball speed. The function call in the game loop looks like this:

var framesPerSecond = 30;
setInterval(
function() {
    ballMovement();
    drawComponents();
}, 1000/framesPerSecond);

This is all it takes to get the ball moving, but since there is no ball handling yet, the ball will fly from the upper left corner of the playing area to the right side of the screen and disappear.

Step 2: Add ball handling for top and bottom

When the ball hits a left or right paddle or hits the top or bottom of the screen, we want the ball’s direction to change with some reflection. The direction of the ball is controlled using the ballSpeed variables ballSpeedY and ballSpeedX. If the ball hits the top or bottom wall, we want the ball to bounce off of the wall and move in the other direction. This is accomplished by reversing the speed on the Y axis. For example:

ballSpeedY = -ballSpeedY;

If the ball hits a left or right paddle, we want the reflection on the X axis. For example:

ballSpeedX = -ballSpeedX;

Here is the code to handle these two situations (added to the ballMovement() function):

// top (ceiling)
if(ballY < 0) {
    // bounce off by reflection on the Y axis
    ballSpeedY = -ballSpeedY;
}
// bottom (floor)
if(ballY > canvas_height-10) {
    // bounce off by reflection on the Y axis
    ballSpeedY = -ballSpeedY;
}

Step 3: Add ball handling for left and right

When the ball hits the left or right side of the screen, it will either hit a paddle or miss the paddle. When the ball hits the right or left edge of the screen, we can detect if the ball hits a paddle by checking to see if the vertical ball location “ballY” is between the top of the paddle (paddle1Y) and the bottom of the paddle (paddle1Y + PADDLE_HEIGHT). If it is, we want to reflect the ball on the X axis. For example:

if(ballY > paddle1Y && ballY < (paddle1Y + PADDLE_HEIGHT)) {

To make the game more interesting, we also want to add some variability to the reflection angles of the ball based on where the ball made contact with the paddle. This is done by adding some “speed” to the  ballSpeedY variable. For example:

// change ball speed of Y to change the angle of return
var deltaY = ballY - (paddle1Y + PADDLE_HEIGHT/2);
ballSpeedY = deltaY * 0.35;

A hit in the middle of the paddle will result in a gentle, straight return but a hit near the tip of the paddle will return the ball at a steeper angle.

If the ball misses the paddle, then the ball needs to be “reset” by being served again from the middle of the screen and the opposing player should register 1 point. Here is what the ballReset() function looks like:

function ballReset() {
    ballSpeedX = -ballSpeedX;
    ballX = canvas_width/2;
    ballY = canvas_height/2;
}

Its basically a ball reflection plus a resetting of the X and Y positions of the ball to the middle of the screen.

Here is the code for the right and left ball handling, inside the ballMovement() function:

// left wall
if(ballX < offsetA + PADDLE_THICKNESS) {
    // Detect a hit. Check if the ball is between the top and bottom of the paddle
    if(ballY > paddle1Y && ballY < (paddle1Y + PADDLE_HEIGHT)) {
        // console.log("HIT! Ball position: " + ballY)
        ballSpeedX = -ballSpeedX;

        // change ball speed of Y to change the angle of return
        var deltaY = ballY - (paddle1Y + PADDLE_HEIGHT/2);
        ballSpeedY = deltaY * 0.35;

    } else if (ballX < offsetA) {
        ballReset();
    }
}

// right wall
if(ballX > canvas_width-40 - PADDLE_THICKNESS) {
    // Detect a hit. If the ball is between the top and bottom of the paddle
    if(ballY > paddle2Y && ballY < (paddle2Y + PADDLE_HEIGHT)) {
        // console.log("HIT! Ball position: " + ballY)
	ballSpeedX = -ballSpeedX;

        // change ball speed of Y to change the angle of return
	var deltaY = ballY - (paddle2Y + PADDLE_HEIGHT/2 );
	ballSpeedY = deltaY * 0.35;
    } else if (ballX > canvas_width-40) {
        ballReset();
    }
}

Step 4: A.I. for computer controlled paddle

To enable single player, we need the computer to control one of the paddles. Here is a function that handles the computer A.I. for this:

function computerPaddleAI() {
    var paddle2YCenter = paddle2Y + (PADDLE_HEIGHT/2);
    if(paddle2YCenter < ballY - 35) {
	paddle2Y = paddle2Y + 6;
    } else if(paddle2YCenter > ballY + 35) {
	paddle2Y = paddle2Y - 6;
    }
}

Essentially it works by checking the location of the center of the paddle compared to the current location of the ball in each frame of the game loop. If paddle is above or below the ball, then it will move by six units on the Y axis in the direction of the ball.

The computerPaddleAI() function should be called from both the ballMovement() and ballReset() functions.

Step 5: Add scoring

When the ball hits the left or right side of the screen without being hit by a paddle, the opposing side gets 1 point. First we need to set up some variables:

var player1Score = 0;
var player2Score = 0;

In the “else if” blocks of the ballMovement() function, just before the call to ballReset(), we can update the score like so:

player2Score++; // must be BEFORE ballReset()

Then in the drawComponents() function, we should reference the score variables rather than use place holders. For example:

// Place holder for the scores
canvasContext.font="40px monospace";
// Left side score- make room for more digits
if (player1Score < 10) {
    canvasContext.fillText(player1Score, (canvas_width/2)-60, 40);
} else if (player1Score > 99) {
    canvasContext.fillText(player1Score, (canvas_width/2)-100, 40);
} else {
    canvasContext.fillText(player1Score, (canvas_width/2)-80, 40);
}

// Right side score
canvasContext.fillText(player2Score, (canvas_width/2)+35, 40);

Here is the complete code so far:

var canvas;
var canvasContext;
var canvas_width;
var canvas_height;

// offsets to account for the rounded corners
var offsetA = 30;
var offsetB = 60;

// starting y position of the left paddle
var paddle1Y = 100;
var paddle2Y = 100;

// defaults for paddle thickness and height
const PADDLE_THICKNESS = 10;
const PADDLE_HEIGHT = 70;

// ball variables
var ballX = offsetA;
var ballY = 0;
var ballSpeedX = 10;
var ballSpeedY = 4;

var player1Score = 0;
var player2Score = 0;

window.onload = function() {

    canvas = document.getElementById("gameCanvas");
    canvasContext = canvas.getContext("2d");
    canvas_width = canvas.width;
    canvas_height = canvas.height;

    var framesPerSecond = 30;
    setInterval(
        function() {
            ballMovement();
            drawComponents();
        }, 1000/framesPerSecond);

    canvas.addEventListener('mousemove',
    	function(evt) {
    	    var mousePos = calculateMousePos(evt);
    	    paddle1Y = mousePos.y - (PADDLE_HEIGHT/2);
    	});
}

// Function to create rounded corners on the rectangle
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius, fill, stroke) {
    // If stroke argument not provided, provide a stroke by default
    if (typeof stroke == "undefined" ) {
        stroke = true;
    }
    // If no radius is defined, then give it a default of 5
    if (typeof radius === "undefined") {
        radius = 5;
    }

    // Start of shape definition
    this.beginPath();

    this.moveTo(x + radius, y);
    // Draw the top border line
    this.lineTo(x + width - radius, y);

    // Draw the top right curve
    this.quadraticCurveTo(x + width, y, x + width, y + radius);

    // Draw the right border line
    this.lineTo(x + width, y + height - radius);

    // Draw the bottom right curve
    this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);

    // Draw the bottom border line
    this.lineTo(x + radius, y + height);

    // Draw the bottom left curve
    this.quadraticCurveTo(x, y + height, x, y + height - radius);

    // Draw the left border line
    this.lineTo(x, y + radius);

    // Draw the final (top left) curve
    this.quadraticCurveTo(x, y, x + radius, y);

    // End of shape definition
    this.closePath();

    // Draw the shape with a line
    if (stroke) {
        this.stroke();
    }
    // Fill in the shape with color
    if (fill) {
        this.fill();
    }
}

function calculateMousePos(evt) {
    var rect = canvas.getBoundingClientRect();
    var root = document.documentElement;
    var mouseX = evt.clientX - rect.left - root.scrollLeft;
    var mouseY = evt.clientY - rect.top - root.scrollTop;
    return {
        x:mouseX,
        y:mouseY
    };
}

function ballMovement() {
    computerPaddleAI();
    ballX = ballX + ballSpeedX;
    ballY = ballY + ballSpeedY;

    // left wall
    if(ballX < offsetA + PADDLE_THICKNESS) {
        // Detect a hit. Check if the ball is between the top and bottom of the paddle
    	if(ballY > paddle1Y && ballY < (paddle1Y + PADDLE_HEIGHT)) {
            // console.log("HIT! Ball position: " + ballY)
            ballSpeedX = -ballSpeedX;

            // change ball speed of Y to change the angle of return
            var deltaY = ballY - (paddle1Y + PADDLE_HEIGHT/2);
            ballSpeedY = deltaY * 0.35;

    	} else if (ballX < offsetA) {
            player2Score++; // must be BEFORE ballReset()
            ballReset();
        }
    }
    // right wall
    if(ballX > canvas_width-40 - PADDLE_THICKNESS) {
        // Detect a hit. If the ball is between the top and bottom of the paddle
    	if(ballY > paddle2Y && ballY < (paddle2Y + PADDLE_HEIGHT)) {
            // console.log("HIT! Ball position: " + ballY)
    	    ballSpeedX = -ballSpeedX;

            // change ball speed of Y to change the angle of return
    	    var deltaY = ballY - (paddle2Y + PADDLE_HEIGHT/2 );
    	    ballSpeedY = deltaY * 0.35;
    	} else if (ballX > canvas_width-40) {
    	    player1Score++; // must be BEFORE ballReset()
    	    ballReset();
    	}
    }
    // top (ceiling)
    if(ballY < 0) {
        // bounce off by reflection on the Y axis
	ballSpeedY = -ballSpeedY;
    }
    // bottom (floor)
    if(ballY > canvas_height-10) {
        // bounce off by reflection on the Y axis
	ballSpeedY = -ballSpeedY;
    }
}

function computerPaddleAI() {
    var paddle2YCenter = paddle2Y + (PADDLE_HEIGHT/2);
    if(paddle2YCenter < ballY - 35) {
        paddle2Y = paddle2Y + 6;
    } else if(paddle2YCenter > ballY + 35) {
	paddle2Y = paddle2Y - 6;
    }
}

function ballReset() {
    computerPaddleAI();
    ballSpeedX = -ballSpeedX;
    ballX = canvas_width/2;
    ballY = canvas_height/2;
}

function drawComponents() {

    // this line blacks out the background to give the appearance of animation
    canvasContext.fillStyle = 'black';
    canvasContext.roundRect(0, 0, canvas_width, canvas_height, 40, "black", 0);

    // Left paddle
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(offsetA, paddle1Y, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Right paddle
    canvasContext.fillStyle = 'white';
    // canvasContext.fillRect(canvas_width-40, canvas_height-120, PADDLE_THICKNESS, PADDLE_HEIGHT);
    canvasContext.fillRect(canvas_width-40, paddle2Y, PADDLE_THICKNESS, PADDLE_HEIGHT);

    // Draw the ball
    canvasContext.fillStyle = 'white';
    canvasContext.fillRect(ballX, ballY, 10, 10);

    // Place holder for the scores
    canvasContext.font="40px monospace";
    // Left side score- make room for more digits
    if (player1Score < 10) {
        canvasContext.fillText(player1Score, (canvas_width/2)-60, 40);
    } else if (player1Score > 99) {
        canvasContext.fillText(player1Score, (canvas_width/2)-100, 40);
    } else {
        canvasContext.fillText(player1Score, (canvas_width/2)-80, 40);
    }

    // Right side score
    canvasContext.fillText(player2Score, (canvas_width/2)+35, 40);

    drawNet();

}

function drawNet() {
    for(var i=0; i < canvas_height; i += 30) {
        canvasContext.fillStyle = 'white';
        canvasContext.fillRect(canvas_width/2, i, 2, 10, 'white');
    }
}

And here is how the game plays so far: