JavaScript Game Programming Tutorial: Making a Retro Wall Tennis Game Complete Version

Game

Let’s make a simple game using just HTML and JavaScript, without any game engines or libraries.

This is for people who:

Want to try making a game but have no idea where to start.
Just want to make a simple game without diving into complex tools like Unity or Unreal Engine.

This guide is aimed at those who already have some programming knowledge.

What’s Covered in This Article

There’s no extra flavor to this wall tennis game, but give it a try!

Prerequisites

This article is a continuation of the previous one. If you haven't read it yet, please check it out first.

Full Source Code

<html>
<body>
    <canvas id="gameCanvas" width="800" height="400"
            style="border:1px solid #000000; background-color: #000;"></canvas>
    <script>
  JavaScript
  </script>
</body>
</html>

Except for the JavaScript part, everything else remains the same as before. I'll extract just the JavaScript code.

const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');

const State = {
    STANBY: 0,
    GAME: 1,
    GAME_OVER: 2
}

// Paddle
let padX = canvas.width / 2;
const padY = canvas.height - 60;
const padWidth = 100;
const padHeight = 20;
let padSpeed = 0;

// Ball
const ballRadius = 10;
let ballX;
let ballY;
let ballSpeedX;
let ballSpeedY;

// Game State
let gameState = State.STANBY;
let score = 0;
let high_score = 0;

// Input event handler
document.addEventListener('keydown', keyDownHandler, false);
document.addEventListener('keyup', keyUpHandler, false);

function drawBall() {
    context.fillStyle = '#fff';
    context.fillRect(ballX-ballRadius/2, ballY-ballRadius/2, ballRadius*2, ballRadius*2);
}

function drawPaddle() {
    context.fillStyle = '#fff';
    context.fillRect(padX, padY, padWidth, padHeight);
}

function keyDownHandler(event) {
    if (event.key === 'Left' || event.key === 'ArrowLeft') {
        padSpeed = -10;
    } 

    if (event.key === 'Right' || event.key === 'ArrowRight') {
        padSpeed = +10;
    }

    if (event.key == 's') {
        if (gameState == State.STANBY) gameState = State.GAME;
    }
    if (gameState == State.GAME_OVER) gameState = State.STANBY; 
}

function keyUpHandler(event) {
}

function isHitPaddle() {
    if (ballX+ballRadius < padX) {
        return false;
    }
    if (ballX-ballRadius > padX+padWidth) {
        return false;
    }
    if (ballY+ballRadius < padY) {
        return false;
    }
    if (ballY-ballRadius  > padY+padHeight) {
        return false;
    }
    return true;
}

function update() {

    // update the ball position
    ballX += ballSpeedX;
    ballY += ballSpeedY;

    // It's game over if the ball gone beyond the bottom boundary
    if (ballY + ballRadius > canvas.height) {
        gameState = State.GAME_OVER;
    }

    // Handling when the ball goes beyond the top or bottom boundaries.
    if(ballY - ballRadius < 0) {
        ballSpeedY = -ballSpeedY;
        score += 1;
    }

    // Handling when the ball goes beyond the left or right boundaries.
    if (ballX - ballRadius < 0 || ballX + ballRadius > canvas.width) {
        ballSpeedX = -ballSpeedX;
    }

    padX += padSpeed;
    if (padX < 0) padX = 0;
    if (padX + padWidth > canvas.width) padX = canvas.width - padWidth;
    if (padSpeed > 0) padSpeed -= 1;
    if (padSpeed < 0) padSpeed += 1;

    if (isHitPaddle()) {
        if (padSpeed != 0) {
            dx = padSpeed > 0 ? 1 : -1;
            if (Math.abs(ballSpeedX+dx) >= 1 && Math.abs(ballSpeedX+dx) <= 3) 
                ballSpeedX += dx; 
        }
        if (ballY >= padY && ballY <= padY+padHeight) {
            ballSpeedX = -ballSpeedX;
        }
        ballSpeedY = -ballSpeedY;
    }

}

function draw_text_center(text, font='30px Consolas') {
    context.font = font;
    context.textAlign = 'left';
    text_w = context.measureText(text).width;
    context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
}

function draw_scores() {
    context.font = '20px Consolas';
    context.textAlign = 'left';

    score_text = 'Score: ' + score;
    context.fillText(score_text, 10, canvas.height-10);

    hscore_text = 'High Score: ' + high_score;
    context.fillText(hscore_text, canvas.width-200, canvas.height-10);
}

function init() {
    // initialize position of paddle
    padX = canvas.width / 2 - padWidth/2;
    // initialize position of ball
    ballX = padX + padWidth/2; 
    ballY = padY - ballRadius*2;
    ballSpeedX = 2;
    ballSpeedY = -2;
    // initialize the score
    if (high_score < score) high_score = score;
    score = 0;
}

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    draw_scores();
    drawBall();
    drawPaddle();
    switch (gameState) {
    case State.STANBY:
        init();
        draw_text_center("Press 'S' Key to Start");
        break;
    case State.GAME:
        update();
        break;
    case State.GAME_OVER:
        draw_text_center("Game Over!!");
        break;
    }
}

setInterval(draw, 10);

Thinking game rules

The game's rules are straightforward:

  • Hit the ball against the top wall to score +1 point.
  • If the ball touches the bottom of the screen, it's game over.

The ball's movement is generally simple and doesn't change much, so you can keep playing for a long time.

However, compared to the previous implementation, I've made some improvements to the paddle movement and collision detection with the ball. When the ball hits the paddle while it's moving left or right, the movement amount in that direction is factored in within a certain range. This helps to somewhat reduce the monotony.

There are plenty of ways to increase the game's complexity. At the end of this article, I’ll share some ideas for how to expand on this.

Game State Transitions

This game has three states:

  1. Standby (Waiting to start)
  2. Game (In progress)
  3. Game Over

Following chart shows the game state transitions.

How to implement the state transition in JavaScript? Look the draw() function:

function draw() {
   ・・・
    switch (gameState) {
    case State.STANBY:
        init();
        draw_text_center("Press 'S' Key to Start");
        break;
    case State.GAME:
        update();
        break;
    case State.GAME_OVER:
        draw_text_center("Game Over!!");
        break;
    }
}

The draw() function is called every 10 milliseconds. At that time, it checks the current state and performs the corresponding actions. You just need to write the appropriate code for each state. It's very simple.

State changes are handled by modifying the gameState variable. Let's go through the relevant parts in the code.

The initial state is STANDBY.

let gameState = State.STANBY;

The state transitions when a specific key is pressed while in a certain state.

function keyDownHandler(event) {
   ・・・
    if (event.key == 's') {
        if (gameState == State.STANBY) gameState = State.GAME;
    }
    if (gameState == State.GAME_OVER) gameState = State.STANBY; 
}

It's game over if the ball gone beyond the bottom boundary and following code is that implementation.

function update() {
    ・・・
    // It's game over if the ball gone beyond the bottom boundary 
    if (ballY + ballRadius > canvas.height) {
        gameState = State.GAME_OVER;
    }
    ・・・
}

Now, let's take a look at the definition of State. It's defined as an object with properties, and each property holds a numerical value.

const State = {
    STANBY: 0,
    GAME: 1,
    GAME_OVER: 2
}

Now, what's next

With this, the game is complete for now. Even though it's a minimal game, it includes all the essential elements:

  • Drawing and moving objects on the screen
  • Responding to user input to interact with the game objects
  • Managing game state transitions and changing behavior based on the state

These core components are common to almost all action games.

The "Wall Tennis" game we created this time is as simple as it gets, but there are still many variations you can consider. For example:

  • Change the way the ball bounces depending on the position of the wall
  • Increase the ball's speed as time passes
  • Limit the time the paddle can move continuously (like a stamina parameter for the player)

If you want to add more objects to the game, the variations are almost limitless depending on your ideas. You can make it a two-player game by adding more paddles, or place obstacles on the screen. Feel free to modify it as you like!


コメント

Copied title and URL