ErTucci674 / snako

Themed as the unforgotten game Snake. This 2D-Game has been programmed in JavaScript so it could be opened with any browser.

Home Page:https://ertucci674.github.io/snako/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

๐Ÿ Snako - 2D Game

About the Project ๐Ÿ“–

Snako is inspired by the unforgotten game Snake. It follows the same mechanics as the original version. Just more colourful and weird! ๐ŸŒˆ

Game Dynamics ๐ŸŽฒ

Just like the original game, the objective is to survive as long as possible without colliding with walls or the snake's own body.

The map is a 25x25 units which can all be setup to contain either a wall or Food. The game starts with the snake being long 3 units.

Every time a fruit is eaten the number of sections increases and so does the player's score. However, if the snake collides with any of the walls or itself the game ends and the final score is shown in a new background.

Built with โŒจ๏ธ

  • HTML/CSS
  • JavaScript

Start Playing ๐ŸŽฎ

You can click the following to open the GitHub web page: https://ertucci674.github.io/snako/

Otherwise, download the repository or clone it, then:

Open the index.html file with any browser and start playing! Preferably, use one of the following ones:

  • Google Chrome
  • Mozilla Firefox
  • Microsoft Edge
  • Safari
  • Opera
  • Brave
  • Internet Explorer 11 and later versions

Other applications might work - Bugs should be expected.

Program and Execute Project ๐Ÿ—”

No compiler is necessary as the game runs on Browsers. JavaScript knowledge is required to modify the program. Just clone the repository in your local machine and start programming:

git clone https://github.com/ErTucci674/snako.git

Files and Code ๐Ÿ“„

Global Constants ๐ŸŒ

Many of the constant variables need to be accessed by different files. Hence, all of the shared constant variables are declared and assigned in the file globals.js.

Game Page ๐Ÿ 

As the game is based and managed by a Browser, the main 'access' file is the index.html file. The graphics and rendering is shown in the <canvas> element. Everything is managed the JavaScript files called in at the top of the index.html file.

<link rel="stylesheet" href="style.css">
<script src="globals.js"></script>
<script src="vector.js"></script>
<script src="button.js"></script>
<script src="quad.js"></script>
<script src="food.js"></script>
<script src="section.js"></script>
<script src="snake.js"></script>
<script src="gameover.js"></script>
<script src="game.js"></script>
<script src="main.js"></script>

Main file โšก

The main.js file is were the whole game performance, dynamics and rendering is managed through the use of other files.

When the page fully loads, the <canvas> element in the index.html file is accessed and the graphic interface is setup by one of the Objects, game.

Whenever the game ends, the page illustrated on <canvas> changes to gameover. This is controlled by the currentPage variable.

window.onload = function () {
    let canvas = document.getElementById("canvas"),
        context = canvas.getContext("2d");

    // Text
    context.font = FONT;

    const game = Game.newGame(canvas, context);
    const gameover = Gameover.newGameover(canvas, context);
    let currentPage = game;
    ...
}

All the events (user's input) are taken care in this file and the values are shared across the Objects/Functions that make use of them.

Each object has two main functions in common: render() and update(). These are called in equivalent functions in the main.js file and looped in a constant time change stored in the timePassed variable. The time check makes sure that no matter the refresh rate of the user's monitor, the game speed is always going to be the same. The "rendering/animation" is requested to the browser by the function requestAnimationFrame().

function main() {
    // Control Frame Rate
    const timeNow = performance.now();
    const timePassed = timeNow - timeLast;

    if (timePassed >= rr) {
        render();
        update();
        timeLast = timeNow - (timePassed % rr);
    }

    requestAnimationFrame(main);
}

The render() and update() functions run the code depending on the currentPage value.

Game Control ๐ŸŽฎ

Setup Canvas ๐Ÿ–ฝ

The game objects takes care of the graphics and user's interactions. At the declaration, the <canvas> is setup to take the current window's size, and the game settings are initiated (map, score, snake, etc.).

setup: function () {
    // Window Size
    this.width = this.canvas.width = window.innerWidth;
    this.widthCenter = this.width * 0.5;
    this.height = this.canvas.height = window.innerHeight;
    this.heightCenter = this.height * 0.5;

    // Adjust scaling if game is larger than screen
    if (rectSide > this.height) {
        this.scale = (this.height / rectSide) * 0.9;

        this.widthCenter /= this.scale;
        this.heightCenter /= this.scale;
    }

    // Map
    this.setGrid();
    this.rectStartX = this.widthCenter - rectHalfSide;
    this.rectStartY = this.heightCenter - rectHalfSide;
    this.map.src = "pictures/map.png";
    this.wall.src = "pictures/wall.png";

    // Snake
    this.setupSnake();

    // Food
    this.setupFood();

    // Text
    this.context.font = FONT;
},

The main element is the this.canvas variable which stores and changes the attributes of the <canvas> element. This is used to take control of the "drawing frame", distances and sizes of everything in the game.

The map has a standard size of 800x800 pixels. Hence, a screen of 720px would not fit the game in the window, here the scale attribute comes in handy. This is the value used to set the context scale of the game depending on the screen height. The ratio between the screen height and the window is taken and then decreased by 10% (multiplying by 0.9);

Important - The scaling changes the rendering of all of the pictures, hence the quality lowers and 'renderig issues' might occurs (e.g, different colour shading, blurring).

Setup Map ๐Ÿ—บ๏ธ

The map is just 'png' file and on top of it walls and food is placed. The map is divided into 25x25 units stored in a 2D-array of corresponding dimensions, grid. The value stored in each element of the array affects the what is going to be placed on that section of the map. The equivalent values can be found in the globals.js file.

In the setGrid() function the grid array is manually set to include walls in the desired shape. The 'manual-change' decision has been taken at last since security issues have been met during the game development. The first idea was to prepare a picture of the same dimensions of the map where the wall would be drawn as black squared while keeping the rest of the backgorund white. The picture would have then been analysed and used as reference to change the values in the grid array. The getImageData().data should have been the main tool to be used. However, browsers kept coming back with an error that the function was trying to read an external source, which apparently goes against security safety...

In the maps.txt file there are two grid's values can be copied and pasted in the grid's values assignment.

Quads ๐Ÿงฉ

Some of the pictures/frames have been drawn on the same 'png' file. This avoids extra memory usage. <canvas> does not include any quad declaring function. However, with the simple drawImage() function, specific sections of a picture can be taken and rendered.

The values needed to display the correct section of the pictures are stored as quad objects.

let Quad = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,

    newQuad: function (x, y, width, height) {
        let obj = Object.create(this);
        obj.x = x;
        obj.y = y;
        obj.width = width;
        obj.height = height;
        return obj;
    },
}

Rendering ๐Ÿ–ผ๏ธ

The render() function in the game.js file takes care of all of the rendering in the game.

render: function () {
    this.context.clearRect(0, 0, this.width, this.height);

    this.context.save()
    this.context.scale(this.scale, this.scale);
    this.renderGrid();
    this.renderScore();
    this.snake.render(this.context, this.rectStartX, this.rectStartY);
    this.context.restore();
},

All the rendering functions are contained in between save() and restore() so that the scale can be temporarily set and every adjusted to the right size in the frame loading.

The renderGrid() function in the game.js file goes through the grid array by using for loops and checks what needs to be rendered on part of the map.

renderGrid: function () {
    this.context.fillStyle = "#FFF";
    //Draw Background
    this.context.drawImage(this.map, this.widthCenter - rectHalfSide, this.heightCenter - rectHalfSide, rectSide, rectSide);
    // Draw items
    for (let c = 0; c < gridCols; c++) {
        for (let r = 0; r < gridRows; r++) {
            if (this.grid[r][c] == FOOD) {
                let quad = this.foodQuads[this.currentFood];
                this.context.drawImage(this.food, quad.x, quad.y, quad.width, quad.height,
                    this.widthCenter - rectHalfSide + cellWidth * c,
                    this.heightCenter - rectHalfSide + cellHeight * r,
                    quad.width, quad.height);
            }
            else if (this.grid[r][c] == WALL) {
                this.context.drawImage(this.wall, this.widthCenter - rectHalfSide + cellWidth * c, this.heightCenter - rectHalfSide + cellWidth * r);
            }
        }
    }
    this.context.stroke();
}

The function makes sure that the picture of the map is loaded first and then all the walls and fruits are drawn on top of it. The position of the individual objects are assigned with a "Cartesian Coordinate System" taken from the grid array which is then multiplied by the size of the cells so it can be shifted to the correct position on the canvas. The same technique is used to render the snake.

Keyboard Controls ๐Ÿ–ฎ

The main.js file takes all the keyboard inputs and gives them as parameters to the game.js file's function eventListener(). The snake moves when the arrow keys are pressed down.

eventListener: function (event) {
    if (this.snakeTurn) {
        const speed = this.snake.velocity;
            switch (event.key) {
                case "ArrowUp":
                    if (speed.y != 1) {
                        speed.x = 0;
                        speed.y = -snakeSpeed;
                        this.snakeDir = UP;
                        this.snakeTurn = false;
                    }
                    break;

                    ...
            }

            ...
    }

In the code above, the moving up example is shown. The snakeTurn variable is what allows the user to make the snake changing direction. This allows the turning to happen only when the snake finishes the previous change in directoin and is ready to read the next command.

Snake ๐Ÿ

The snake is made up of multiple sections stored in the sections array. The head (first section) is the 'leader'. This is the one that is constantly updated with the direction and movement. Whilst the rest of the body takes the position of the section before on each update. The position is stored and managed as a Cartesian Coordinate System, just like the one in the grid array.

Vectors are used to control the position and velocity of the snake in the move() function.

move: function () {
    for (let i = this.sectionsNum - 1; i > 0; i--) {
        const current = this.sections[i];
        const next = this.sections[i - 1];

        // Change the position values to the one of the following section
        current.position.changeTo(next.position);

        // Change the direction value to the one of the following section
        current.dir = next.dir;
    }
    this.head.position.addTo(this.velocity);
    this.contain();
},

The contain() function makes sure the snake is always contained in the map. This is also what is takes it from one side to the other when the player crosses the map's margins.

The render() function in the snake.js file checks in which direction each individual section is pointing to and temporarily rotates the canvas so it looks like the overall snake is turning.

render: function (context, rectStartX, rectStartY) {

    ...

    // Shifting the canvas to the center point of the section
    // Rotating the canvas (hence the section)
    // Render the picture
    // Restore the original canvas settings (position and rotation)
    context.save();
    context.translate(rectStartX + section.position.x * section.size + half, rectStartY + section.position.y * section.size + half);
    context.scale(scaleX, scaleY);
    context.rotate((section.dir + section.partTurning) * scaleX * scaleY);
    context.drawImage(this.image, quad.x, quad.y, quad.width, quad.height, -half * scaleX, -half * scaleY, section.size * scaleX, section.size * scaleY);
    context.restore();
},

Additionally, the body and tail quads need to change to their corresponding "turning-frames" whenever they change direction. This is determined by the function setBodyDirection().

Directions are stored as angle-in-radians values so when the canvas needs to be rotated, that angle can be used. The scaleX and scaleY are used instead to flip the picture on one of the two axis depending on the current orientation of the snake (otherwise, the snake section quad might illustrate the snake turning towards the opposite way).

The collision() function constantly checks if the head position meets a wall or another section of the snake. In such eventuality, it returns true. This value is then used to reset the snake and the game through their corresponding functions.

collision: function (grid) {
    let x = this.head.position.x,
        y = this.head.position.y;

    // Wall Collision
    if (grid[y][x] == WALL) {
        return true;
    }

    // Body Collision
    for (let i = 1; i < this.sectionsNum; i++) {
        if (x == this.sections[i].position.x && y == this.sections[i].position.y) {
            return true;
        }
    }

    return false;
},

The function takes the grid array as a parameter and check if anything is on the unit where the snake's head is currently situated.

The functions checkFood() and increase() work together when a fruit is eaten. checkFood() checks if the snake's head reaches the food's position. If that's the case, increse() is called and the number of snake's sections is increased by one by adding a Body Section just after the head every time.

// Increase the snake length if food is eaten
checkFood: function (grid) {
    const x = this.head.position.x;
    const y = this.head.position.y;
    if (grid[y][x] == FOOD) {
        this.incCheck = true;
        grid[y][x] = EMPTY;
        this.biteAudio.play();
        return true;
    }
},

// Increase Snake's Sections
increase: function () {
    const last = this.sections[this.sectionsNum - 1];
    const section = Section.newSection(last.position.x, last.position.y, this.size, last.dir);
    section.part = last.part;
    this.sections.push(section);
    this.sectionsNum += 1;
    this.sections[this.sectionsNum - 2].part = BODY;
},

Ending ๐Ÿ”š

When the game ends, two functions in the game file come in: pageChange() and reset(). These make sure to reset the main values needed if the game is started again and to 'tell' the main.js file that the page needs to be changed to gameover.

The gameover page illustrates just the player's final score and a button with the text "Play Again" written on top of it. The buttons has its own dedicated button attributes which trigger animation, set its text and its position on the canvas. The score is taken from the number of sections of the snake minus the initial ones (3). The same technique is used on the game.render() function to show the player's current score on the top-left corner of the screen.

When the button is clicked, the page goes back to the game where everything has already been setup. Here the game starts again right away.

Reference links ๐Ÿ”—

2D Drawings Designed with: - Piskelapp

Bite Sound - Pixabay

Licence ๐Ÿ–‹๏ธ

This project is licensed under the terms of the GNU General Public License, version 3.0.

About

Themed as the unforgotten game Snake. This 2D-Game has been programmed in JavaScript so it could be opened with any browser.

https://ertucci674.github.io/snako/

License:GNU General Public License v3.0


Languages

Language:JavaScript 94.0%Language:HTML 3.7%Language:CSS 2.3%