Learn Javascript through a Game

If you Google “Javascript”, hundreds of millions of results pop up. That’s how popular it is. Almost all Web applications use Javascript. As a JS developer, there are a lot of options when it comes to frameworks, React, Node, Vue or whatever. In this vast sea of frameworks, we often become oblivious to our good old friend, Vanilla JS, the purest form of Javascript.

Preliminary knowledge

There is no preparation for this project if you are willing to learn and do it. But a little programming knowledge doesn’t hurt, does it?

project

Since we will cover all aspects of the project, this article will be a long one. Therefore, for clarity and ease of understanding, the whole project is divided into the following chapters:

What we’re going to do

Before we put code in, we need to plan exactly what we’re going to build. We need to make a snake, which is represented by a head and tail, and consists of many pieces. We also need to produce some kind of food at random places on the screen for the snake to eat and the snake to grow. We’ll keep track of the player’s score, and we’ll add pause.

skeleton

Create a separate folder for this game. Create two files in the folder named index.html and game.js. The index.html file will contain regular HMTL boilerplate code and have a very special element, Canvas, where our game will wake up.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Snake Game</title>
</head>
<body>

    <canvas id="game-area"></canvas>
    <script type="text/javascript" src="game.js"></script>

</body>
</html>
Copy the code

The HTML Canvas tag is used for drawing with Javascript. It has built-in functions for drawing simple shapes such as arcs, rectangles and lines. It can also display text and pictures. We use the script tag to add a reference to the game.js file, which specifies the logic of the game.

Before we start, we need to add the following style tag to the HTML file’s head tag:

<style type="text/css">* {margin: 0;
        padding: 0;
        overflow: hidden;
        box-sizing: border-box;
    }
    canvas{
        background-color: # 333;
    }
</style>
Copy the code

To override the default Settings on the browser elements, we write a custom CSS style for the page and set margin and padding to 0. The border-box attribute adds the consideration to the border of the element and fits it within the element. The Overflow property is set to Hidden to disable and hide the element scroll bar. Finally, we set the canvas background color for the game.

Initialize the

Here we go to the game.js file. First, we need to declare some global variables that are referenced throughout the game. These variables represent specific attributes that will control the behavior of the game. We’re going to initialize these properties with a function called init. A function is equivalent to executing a particular task by running some statements, in which case the variable is initialized.

Initially add the following code to the game.js file:

let width;
let height;
let tileSize;
let canvas;
let ctx;

// The gameobject is initialized.
function init() {

    tileSize = 20;

    // Dynamically control canvas size.
    width = tileSize * Math.floor(window.innerWidth / tileSize);
    height = tileSize * Math.floor(window.innerHeight / tileSize);

    canvas = document.getElementById("game-area");
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");

}
Copy the code

The variables width and height hold the width and height of the canvas. The Canvas variable holds a reference to the HTML Canvas element. CTX is short for Canvas Context, which determines the coordinate system in which we work. In our example, we will use 2D coordinates.

The tileSize variable is an integral part of the game. It’s the base unit size on the screen. To achieve perfect alignment of snakes and food, we split the entire screen into grids, with each size corresponding to tileSize. This is why the width and height of the canvas are closest to tileSize multiples.

food

We need a reference to the food that will be eaten by the snake. We’ll consider it as an object with specific properties and behaviors, much like real-world objects. To do this, we’ll dabble in some basic OOP (Object Oriented Programming).

We will create a class called Food as follows:

// Treat food as an object
class Food {

    // The object property is initialized
    constructor(pos, color) {

        this.x = pos.x;
        this.y = pos.y;
        this.color = color;

    }

    // Draw food on canvas
    draw() {

        ctx.beginPath();
        ctx.rect(this.x, this.y, tileSize, tileSize);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3; ctx.stroke(); ctx.closePath(); }}Copy the code

Classes in JS consist of the constructor method, which initializes properties based on its object, and idiom functions, which define behavior.

Here we use parameterized constructors to provide food objects with location and color information. The position pos in turn has properties x and y that determine the x and y coordinates on the canvas. The this keyword is used to point to an instance (or object) of the current class, that is, to the property of the object under consideration. It will be clearer when we create the object.

The member function used here is draw, which is responsible for drawing food on the canvas. The draw function can have any piece of code that draws food on the canvas but for simplicity we use a red square for food that has positions X and y and tileSize width and height. All the code that’s written in the function is responsible for doing exactly that, drawing a red square on the canvas.

Finally, we need to add the food object to the global variable sequence and create the food object in the init function as follows:

// Other global variables.

let food;
Copy the code

The init function:

// Initialize the object in the game
function init() {

    tileSize = 20;

    // Dynamically control canvas size
    width = tileSize * Math.floor(window.innerWidth / tileSize);
    height = tileSize * Math.floor(window.innerHeight / tileSize);

    canvas = document.getElementById("game-area");
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");

    food = new Food(spawnLocation(), "red");
}
Copy the code

You may be wondering what spawnLocation is. It is a function that returns a random position on the canvas for food generation. The code is as follows:

// Determine a random generation position in the grid.
function spawnLocation() {

    // Divide the canvas into blocks
    let rows = width / tileSize;
    let cols = height / tileSize;

    let xPos, yPos;

    xPos = Math.floor(Math.random() * rows) * tileSize;
    yPos = Math.floor(Math.random() * cols) * tileSize;

    return { x: xPos, y: yPos };

}
Copy the code

The snake

Snakes are probably the most important aspect of the game. Similar to the Food object, based on the Food class, we will create a class named Snake, which contains the attributes and behaviors of the Snake. The Snake class is as follows:

class Snake {

    // The object property is initialized.
    constructor(pos, color) {

        this.x = pos.x;
        this.y = pos.y;
        this.tail = [{ x: pos.x - tileSize, y: pos.y }, { x: pos.x - tileSize * 2.y: pos.y }];
        this.velX = 1;
        this.velY = 0;
        this.color = color;

    }

    // Draw a snake on the canvas.
    draw() {

        // Draw the snake head.
        ctx.beginPath();
        ctx.rect(this.x, this.y, tileSize, tileSize);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.closePath();

        // Draw the snake's tail.
        for (var i = 0; i < this.tail.length; i++) {

            ctx.beginPath();
            ctx.rect(this.tail[i].x, this.tail[i].y, tileSize, tileSize);
            ctx.fillStyle = this.color;
            ctx.fill();
            ctx.strokeStyle = "black";
            ctx.lineWidth = 3; ctx.stroke(); ctx.closePath(); }}// Move the snake in an updated position
    move() {

        // The snake tail moves.
        for (var i = this.tail.length - 1; i > 0; i--) {

            this.tail[i] = this.tail[i - 1];

        }

        // Update the first element of the tail to get the header position
        if (this.tail.length ! =0)
            this.tail[0] = { x: this.x, y: this.y };

        // The snake head moves
        this.x += this.velX * tileSize;
        this.y += this.velY * tileSize;

    }

    // Change the direction of the snake
    dir(dirX, dirY) {

        this.velX = dirX;
        this.velY = dirY;

    }

    // Determine whether the snake has eaten the food
    eat() {

        if (Math.abs(this.x - food.x) < tileSize && Math.abs(this.y - food.y) < tileSize) {

            // Add to snake tail
            this.tail.push({});
            return true;
        }

        return false;

    }

    // Check if the snake is dead
    die() {

        for (var i = 0; i < this.tail.length; i++) {

            if (Math.abs(this.x - this.tail[i].x) < tileSize && Math.abs(this.y - this.tail[i].y) < tileSize) {
                return true; }}return false;

    }

    border() {

        if (this.x + tileSize > width && this.velX ! = -1 || this.x < 0 && this.velX ! =1)
            this.x = width - this.x;

        else if (this.y + tileSize > height && this.velY ! = -1 || this.velY ! =1 && this.y < 0)
            this.y = height - this.y; }}Copy the code

This class contains a lot of code, so we start with methods one by one.

First, we have parameterized constructors that initialize the x and y coordinates of the snake head with variables X and y, initialize the color of the snake with color, and determine the velocities in the x and y directions with velX and velY. We also have the tail variable, which is a list of objects that holds the snippet of the snake’s tail. The tail of a snake is initially set to have two segments whose X and Y coordinates are determined by its X and Y attributes.

Now, let’s focus on the different member methods of the class:

  • Draw function: The draw function is similar to the one in Food. It’s responsible for drawing snakes on a canvas. Again, we can represent snakes with anything, but for simplicity, we use tileSize sized green squares to represent the head and each tail fragment. What the code in this function is actually doing is drawing some green squares on the canvas.

  • Move function: The main challenge of snake movement is proper tail movement. We need places where we can store different segments of the snake’s tail, so that the snake can follow a certain path. This is done by assigning the snake tail fragment to the location of the previous fragment. So the tail of the snake followed the path taken by its head. The position of the snake is increased by velocity velX and velY multiplied by tileSize, the basic unit of the grid.

  • Dir function: The purpose of dir is to change the direction of the snake’s head. We’ll get to that in a minute.

  • Eat: The eat function checks whether the snake has eaten a piece of food. This is done by looking for overlap between the snake head and the food. Since tileSize corresponds to grid size, we can check whether the difference in head and food position corresponds to tileSize and return true or false accordingly. On this basis, we also added a section on the snake’s tail to increase its length.

  • Die function: our snake will die only if it bites a part of its tail. That’s what we’re checking in this function, which is if the head and tail overlap at some point. Therefore, we return true or false in response.

  • Border function: The border function checks whether the snake is inside the screen boundary. It would be strange if the snake disappeared from one side of the screen. Here we can do either of two things; We could either end the game there or have the snake magically appear on the other side of the screen, similar to the classic Snake game. We chose the second method, which is the code inside the function.

We need to do one last thing for the snake. We will declare the following snake object under the sequence of global variables:

let snake;
Copy the code

And initialize as follows in init function:

snake = new Snake({ x: tileSize * Math.floor(width / (2 * tileSize)), y: tileSize * Math.floor(height / (2 * tileSize)) }, "#39ff14");
Copy the code

The game loop

Before we go any further, we need to define a function that runs the game. So we define it as follows:

// The actual game function.
function game() {

    init();

}
Copy the code

In this function, we call the init function, which only handles global variable initialization. How do you draw objects on the Canvas and keep running the game? That’s where the game loop comes in.

The game loop or repeated logic is written down in a function called Update. The update function is defined as follows:

// Update positions and redraw objects in the game.
function update() {

        if (snake.die()) {
            alert("GAME OVER!!!");
            window.location.reload();
        }

        snake.border();

        if (snake.eat()) {
            food = new Food(spawnLocation(), "red");
        }

        // Redraw the clean canvas.
        ctx.clearRect(0.0, width, height);

        food.draw();
        snake.draw();
        snake.move();

}
Copy the code

This update function will process the update game logic frame by frame, drawing snakes, food, and moving snakes. It also tests if the snake has eaten food or if it is dead. If the snake dies, we reload the game, as the logic says.

Now we are left with the task of calling the update function repeatedly after some specified interval. Before anything else, we need to talk about FPS or frames per second. Loosely defined, this is the number of times a game screen renders per second. Traditional Snake games have a low frame rate, around 10 FPS, and we’ll stick with that.

We define a variable called FPS under a sequence of global variables and initialize it to 10 in our init function.

Then we update the game function as follows:

// The actual game function. function game() { init(); // Game loop. interval = setInterval(update,1000/fps); }Copy the code

The setInterval function periodically calls a function after a specified number of milliseconds. We save this reference in a variable called interval.

Finally, when the snake dies, we need to clear the interval by calling clearInterval as follows:

if (snake.die()) {
     alert("GAME OVER!!!");
     clearInterval(interval);
     window.location.reload();
}
Copy the code

So our game loop is ready.

logistics

Now that we have the game loop ready, we need a system that counts player scores and provides pause.

We will define two global variables score and isPaused and initialize them inside the init function as follows:

score = 0;
isPaused = false;
Copy the code

We then define two functions to display the score and game state on the canvas as follows:

// Display player score.
function showScore() {

    ctx.textAlign = "center";
    ctx.font = "25px Arial";
    ctx.fillStyle = "white";
    ctx.fillText("SCORE: " + score, width - 120.30);

}

// Show if the game is paused
function showPaused() {

    ctx.textAlign = "center";
    ctx.font = "35px Arial";
    ctx.fillStyle = "white";
    ctx.fillText("PAUSED", width / 2, height / 2);

}
Copy the code

We add the following code to the beginning of the udpate function:

if(isPaused){
   return;
}
Copy the code

ShowScore is called at the end of the update as follows:

showScore();
Copy the code

Add under snake. Eat in the update function:

score += 10;
Copy the code

Keyboard control

The player needs to be able to interact with the game. To do this, we need to add event listeners to the code. These listeners will have callback functions that look for buttons and execute code to control the game, as follows:

// Add an event listener for the key.
window.addEventListener("keydown".function (evt) {
    if (evt.key === "") { evt.preventDefault(); isPaused = ! isPaused; showPaused(); }else if (evt.key === "ArrowUp") {
        evt.preventDefault();
        if(snake.velY ! =1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(0, -1);
    }
    else if (evt.key === "ArrowDown") {
        evt.preventDefault();
        if(snake.velY ! = -1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(0.1);
    }
    else if (evt.key === "ArrowLeft") {
        evt.preventDefault();
        if(snake.velX ! =1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(-1.0);
    }
    else if (evt.key === "ArrowRight") {
        evt.preventDefault();
        if(snake.velX ! = -1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(1.0); }});Copy the code

The dir function code above specifies the direction in which the snake moves. We make the following agreements;

Moving up and down correspond to -1 and 1 for the Y velocity, respectively, and moving left and right are represented by -1 and 1 for the X velocity, respectively. The evt.key property represents the name of the key being pressed, on the listener. Thus, we can now control the snake with the D-pad and pause the game with the space bar.

End and spend

Now that everything is in place, we’ll add the final feature pieces to our code. We load the game when the HTML document is finished loading in the browser. To do this, we’ll add another event listener that checks whether the document is loaded or not. The code is as follows:

Window.addeventlistener ("load",function(){game(); });Copy the code

Look! Our game should start running when we launch the index.html file in the browser.

resources

Soupaul / Snake-Game

An HTML and pure JS version of the classic Snake.

The updated repository branch includes some additions to the code to make the game more beautiful, robust, and smooth. We also added some checks to avoid unknown bugs.

You can play the game here.

We hope you find insights.

Visit our website to learn more about us and also follow us:

  • Facebook
  • Instagram
  • LinkedIn

Also, don’t forget to like and comment below if you’re interested in learning more about game development using Javascript. Feel free to ask questions and suggest improvements.

At that time,

Stay safe and may the source be with you!