How to Create a Chrome Extension Game – Maths Solver using Phaser Game Framework

We already covered the basics of developing a Chrome Extension in a previous post – How to build Chrome Extensions? In this post, we’ll discuss how to develop a Chrome Extension Game (Maths Solver) that is about solving maths problems, using the Phaser game framework.

We’ll revisit the basics of developing Chrome Extensions, their key components, and also explore the Phaser game framework, and finally, learn how to create a ‘maths game’ using Phaser.

First, let’s check out the game we’re developing..

https://www.onlycode.in/wp-content/uploads/2024/08/Maths-Solver.mov
What are Chrome Extensions?

Chrome Extensions are small software programs designed to personalize and enhance your browsing experience on Google Chrome. Built using HTML, JavaScript, and CSS, these extensions can customize chrome’s functionality and behavior to suit individual preferences and needs.

Chrome Extensions can modify the user interface, manage cookies, block advertisements, and even style web pages. In addition to enhancing browser functionality, numerous Chrome Extensions also offer entertainment, providing games and interactive experiences that make your browsing time more fun.

These extensions are hosted in the Chrome Web Store and can be easily added to the browser.

Key Components of a Chrome Extension

Every Chrome Extension consists of several essential components:

Manifest File (manifest.json): Every chrome extension has a manifest file, named manifest.json. It’s a JSON-formatted file that tells chrome everything it needs to know about your extension, like its name, version, the permissions it requires, and which scripts to run.

Popup HTML (popup.html): Most extensions have a user interface, often displayed as a popup when you click the extension icon. This HTML file is the structure of that popup.

Scripts (popup.js): Alongside the HTML file, a JavaScript file controls the behavior of the popup and interacts with web pages or the browser.

Background Scripts: These scripts run in the background and can influence web pages or the browser.

Content Scripts: These are JavaScript files that run in the context of web pages loaded by the browser.

Besides these, chrome extensions include css and assets like icons, images and sound files.

This is all about the basics of building a chrome extension. Next, we will learn about the Phaser game framework. But before understanding Phaser, let’s understand what is a game framework?

What is a Game Framework?

A game framework is a collection of pre-written code and tools designed to help developers create games more efficiently. By providing a structured environment and reusable components, game frameworks simplify many of the complex tasks involved in game development, such as rendering graphics, handling user input, and implementing game physics. This allows developers to focus more on the creative and design aspects of game creation rather than reinventing the wheel for common functionalities.

What is Phaser Game Framework? Basics of Phaser

Phaser is a robust 2D game framework used for making HTML5 games for desktop and mobile platforms. It’s open-source and uses both Canvas and WebGL for rendering, making it highly versatile across different device capabilities.

Phaser simplifies game development by handling many of the tedious elements of games, such as sprite animations, game physics, and user input. It’s designed to allow developers to focus on the creative aspects of game development rather than the underlying mechanics.

Phaser’s architecture is based on states, which help in organizing different sections of the game, such as the main menu, game levels, and the game over screen. Each part of the game can be developed and managed independently.

Note: Games developed using Phaser can be deployed to iOS, Android and native desktop apps via 3rd party tools like Apache Cordova and Phonegap.

Creating a Mathematics Game using Phaser

Let’s discuss how you can use Phaser to create a simple maths game, where players solve basic arithmetic problems. Here’s a high-level overview:

HTML Structure (main_game.html):

  • Sets up the game container and includes necessary scripts and stylesheets.

JavaScript Game Logic (main_game.js):

  • Defines game dimensions and mechanics.
  • Loads game assets like sprites and sounds.
  • Handles game interactions and UI updates based on user input.

Game Mechanics:

  • Players interact with numerical buttons to input their answers to maths problems.
  • The game challenges players by increasing difficulty as they progress.
  • Scores are calculated and displayed, with audio feedback for actions like deleting or entering answers.

Here is the complete code for the game structure:

main_game.html
<!DOCTYPE html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <link rel="stylesheet" type="text/css" href="css/main_game.css">

    <script type="text/javascript" src="js/phaser.min.js"></script>
    <script type="text/javascript" src="js/main_game.js"></script>
</head>
<body>
    <div id="game-container">
        <div id="start_game">
        </div>
    </div>
</body>
</html>

Explaining the main parts of the HTML Code

<!DOCTYPE html>

Doctype Declaration: The <!DOCTYPE html> declaration is the first line in any HTML document. It is used to tell the web browser that the document is written in HTML5, the latest version of HTML. This ensures that the browser uses standards-compliant rendering.

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

Viewport Meta Tag: This is crucial for responsive design. It controls the layout on mobile browsers. width=device-width sets the width of the page to follow the screen-width of the device, and initial-scale=1.0 sets the initial zoom level when the page is first loaded.

    <link rel="stylesheet" type="text/css" href="css/main_game.css">

External CSS Link: This line links an external CSS file (main_game.css) located in a css folder. This stylesheet is expected to contain all the custom styles for the game’s user interface, like positioning and dimensions of the game container, buttons, and other visual elements.

    <script type="text/javascript" src="js/phaser.min.js"></script>
    <script type="text/javascript" src="js/main_game.js"></script>

JavaScript Includes: These lines import two JavaScript files:

  • phaser.min.js: The minified version of the Phaser game framework library, which is essential for creating the game.
  • main_game.js: This is the custom script where the game’s logic is defined, including setup, game mechanics, and Phaser configuration.

Note: We’re using Phaser CE v2.20.0, which was released on 13th December 2022. You could download it from here: Phaser CE v2.20.0

<body>
    <div id="game-container">
        <div id="start_game">
        </div>
    </div>
</body>

Body Content:

  • Game Container: The div element with an id of game-container acts as the placeholder where the Phaser game will render. This is where all the graphical output of the game is displayed.
  • Start Game: Inside the game container, there is another div with an id of start_game. This could be used to trigger the start of the game.

This HTML provides a skeleton for a Phaser-based game. The structure is kept minimal to focus entirely on setting up and running the game, ensuring that the game scales correctly on different devices and loads the necessary resources efficiently.

main_game.js
var GameAreaWidth = window.outerWidth * 0.30; // setting the game area dimensions based on screen dimensions;
var GameAreaHeight = window.outerHeight * 0.85;
var coordinate_factor = 11.0; // use to align game elements
var maxValChangerConst = 2;   // use to increase the difficulty level of game
var mathOperationMap = {1: 'plus', 2: 'subtract', 3: 'multiply', 4: 'divide'};
var operatorsCount = 4;
var score = 0;   // current score
var final_score; // score after game is over
var answer = 0;  // answer provided by the player
var scoreText;
var displayText;
var clicks;


var mainState = {
    
    preload: function() {
        game.load.image('coin', 'assets/coin.svg');
        game.load.image('plus', 'assets/plus.svg');
        game.load.image('subtract', 'assets/subtract.svg');
        game.load.image('multiply', 'assets/multiply.svg');
        game.load.image('divide', 'assets/divide.svg');
        game.load.image('zero', 'assets/zero.svg');
        game.load.image('one', 'assets/one.svg');
        game.load.image('two', 'assets/two.svg');
        game.load.image('three', 'assets/three.svg');
        game.load.image('four', 'assets/four.svg');
        game.load.image('five', 'assets/five.svg');
        game.load.image('six', 'assets/six.svg');
        game.load.image('seven', 'assets/seven.svg');
        game.load.image('eight', 'assets/eight.svg');
        game.load.image('nine', 'assets/nine.svg');
        game.load.image('enter', 'assets/enter.svg');
        game.load.image('display', 'assets/display.svg');
        game.load.image('delete', 'assets/delete.svg');
        game.load.image('score_display', 'assets/score_display.svg');

        game.load.audio('click', 'assets/sounds/click.mp3');
        game.load.audio('delete', 'assets/sounds/delete.mp3');
        game.load.audio('enter', 'assets/sounds/coin_drop.mp3');
    },

    create: function() {
        // This function is called after the preload function
        // Here we set up the game, display sprites, etc.
        game.stage.backgroundColor = '#000000';
        game.physics.startSystem(Phaser.Physics.ARCADE);
        this.createUI();
    },

    createUI: function(){
        var gameObjects = this.getGameAssetsHelper();
        var buttons= [];

        answer = 0;

        // Score Display
        var scoreDisplay =  game.add.sprite(0, 0, 'score_display');
        scoreDisplay.width = GameAreaWidth;
        scoreText = game.add.text(GameAreaWidth/coordinate_factor - 25, 5, "US $" + score.toString(), 
        {font: "25px Arial", fill: "#000000"});

        // Answer Display
        game.add.sprite(GameAreaWidth/coordinate_factor + (GameAreaWidth*0.09), 2*(GameAreaHeight/3) - 20, 'display');
        displayText = game.add.text(GameAreaWidth/coordinate_factor + (GameAreaWidth*0.65), 
            2*(GameAreaHeight/3) - 15, answer.toString(), {font: "25px Arial", fill: "#000000"});

        // Input Numerals Buttons
        game.add.button(GameAreaWidth/coordinate_factor + 1.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.028), 'zero', this.calculate, {key: 0});
        game.add.button(GameAreaWidth/coordinate_factor + 3.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.028), 'one', this.calculate, {key: 1});
        game.add.button(GameAreaWidth/coordinate_factor + 5.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.028), 'two', this.calculate, {key: 2});
        game.add.button(GameAreaWidth/coordinate_factor + 7.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.028), 'three', this.calculate, {key: 3});
        game.add.button(GameAreaWidth/coordinate_factor + 9.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.028), 'four', this.calculate, {key: 4});
        game.add.button(GameAreaWidth/coordinate_factor + 1.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.10), 'five', this.calculate, {key: 5});
        game.add.button(GameAreaWidth/coordinate_factor + 3.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.10), 'six', this.calculate, {key: 6});
        game.add.button(GameAreaWidth/coordinate_factor + 5.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.10), 'seven', this.calculate, {key: 7});
        game.add.button(GameAreaWidth/coordinate_factor + 7.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.10), 'eight', this.calculate, {key: 8});
        game.add.button(GameAreaWidth/coordinate_factor + 9.0*(GameAreaWidth*0.07), 
        2*(GameAreaHeight/3) + (GameAreaHeight*0.10), 'nine', this.calculate, {key: 9});


        // Binding keyboard keys to the game actions
        buttons[0] = game.input.keyboard.addKey(Phaser.Keyboard.ZERO);
        buttons[0].onDown.add(this.calculate, {key: 0});
        buttons[1] = game.input.keyboard.addKey(Phaser.Keyboard.ONE);
        buttons[1].onDown.add(this.calculate, {key: 1});
        buttons[2] = game.input.keyboard.addKey(Phaser.Keyboard.TWO);
        buttons[2].onDown.add(this.calculate, {key: 2});
        buttons[3] = game.input.keyboard.addKey(Phaser.Keyboard.THREE);
        buttons[3].onDown.add(this.calculate, {key: 3});
        buttons[4] = game.input.keyboard.addKey(Phaser.Keyboard.FOUR);
        buttons[4].onDown.add(this.calculate, {key: 4});
        buttons[5] = game.input.keyboard.addKey(Phaser.Keyboard.FIVE);
        buttons[5].onDown.add(this.calculate, {key: 5});
        buttons[6] = game.input.keyboard.addKey(Phaser.Keyboard.SIX);
        buttons[6].onDown.add(this.calculate, {key: 6});
        buttons[7] = game.input.keyboard.addKey(Phaser.Keyboard.SEVEN);
        buttons[7].onDown.add(this.calculate, {key: 7});
        buttons[8] = game.input.keyboard.addKey(Phaser.Keyboard.EIGHT);
        buttons[8].onDown.add(this.calculate, {key: 8});
        buttons[9] = game.input.keyboard.addKey(Phaser.Keyboard.NINE);
        buttons[9].onDown.add(this.calculate, {key: 9});


        // Enter Button
        game.add.button(GameAreaWidth/coordinate_factor + (GameAreaWidth*0.175), 2*(GameAreaHeight/3) + (GameAreaHeight*0.22), 
        'enter', this.update_score, {first: gameObjects[0], second: gameObjects[1], operator_number: gameObjects[2]});
        buttons[10] =  game.input.keyboard.addKey(Phaser.Keyboard.ENTER);
        buttons[10].onDown.add(this.update_score, {first: gameObjects[0], second: gameObjects[1],
            operator_number: gameObjects[2]});


        // Delete Button
        game.add.button(GameAreaWidth/coordinate_factor + (GameAreaWidth*0.75), 2*(GameAreaHeight/3) - 20, 
        'delete', this.delete, this);
        buttons[11] =  game.input.keyboard.addKey(Phaser.Keyboard.BACKSPACE);
        buttons[11].onDown.add(this.delete, this);


        // Sounds
        clicks = game.add.audio('click');
        clicks.volume = 1.5;
        deletes = game.add.audio('delete');
        deletes.volume = 0.5;
        enters = game.add.audio('enter');
        enters.volume = 0.5;
    },

    getCoin: function (x, y, speed, value) {
        this.coin = game.add.sprite(x, y, 'coin');
        game.physics.arcade.enable(this.coin);

        // Add ‘gravity’ to the coin to make it fall
        this.coin.body.gravity.y = speed;
        var coinValue = value;

        var textXCoord = 34 - (coinValue.toString().length - 1)*4;
        var textYCoord = 125;
        var text = game.add.text(textXCoord, textYCoord, coinValue, {font: "25px Arial", fill: "#FFFFFF"});
        
        this.coin.addChild(text);
        return coinValue;
    },

    getGameAssetsHelper: function() {
        var speed = 2.5;
        var maxCoinValue = 10;
        var coinFirstX = GameAreaWidth/coordinate_factor + GameAreaWidth*0.07;
        var mathOperatorX = GameAreaWidth/coordinate_factor + GameAreaWidth*0.34;
        var coinSecondX = GameAreaWidth/coordinate_factor + GameAreaWidth*0.55;
        var coinY = 10;
        var mathOperatorY = 25;

        // Increases the difficulty level of game
        if (score >= 10 && score <= 30) {
            maxValChangerConst = 3;
        } else if (score >= 30 && score <= 60) {
            maxValChangerConst = 4;
        } else if (score > 60){
            maxValChangerConst = 5;
        }

        maxCoinValue = maxCoinValue*maxValChangerConst;
        var valueFirst = Math.floor(Math.random()*maxCoinValue);
        var valueSecond = Math.floor(Math.random()*maxCoinValue);
        
        if (valueFirst < valueSecond) {            // For non-negative subtraction
            valueFirst = valueFirst + valueSecond
            valueSecond = valueFirst - valueSecond;
            valueFirst = valueFirst - valueSecond;
        }
        
        var valueFirst = this.getCoin(coinFirstX, coinY, speed, valueFirst);
        var valueSecond = this.getCoin(coinSecondX, coinY, speed, valueSecond);
        var operator = this.getOperator(mathOperatorX, mathOperatorY, speed, valueFirst, valueSecond);
        return [valueFirst, valueSecond, operator];
    },

    getOperator: function (x, y, speed, valueFirst, valueSecond) {
        var operatorNumber = Math.floor(Math.random()*operatorsCount)+1;
        var iterator = 0;

        // For avoiding two-digit multiplication and performing perfect division ie. division only by a factor
        while (((valueFirst >= 10 || valueSecond >= 10) && (operatorNumber == 3)) ||
        ((valueFirst % valueSecond != 0)  && operatorNumber == 4)) {
            iterator++;
            operatorNumber = Math.floor(Math.random() * operatorsCount) + 1;
            if (iterator > 5) {
                operatorNumber = 1;
                break;
            }
        }

        this.mathOperator = game.add.sprite(x, y, mathOperationMap[operatorNumber]);
        game.physics.arcade.enable(this.mathOperator);
        this.mathOperator.body.gravity.y = speed;
        return operatorNumber;
    },

    // This function is called as many times as the framework can execute it.
    // Default frame rate in Phaser games is usually set to 60 frames per second (fps)
    update: function() {
        // End the game if no answer is provided until 
        // coin was inside the gameplay window 
        // (adjust the window size using, GameAreaHeight * 0.30)
        if (this.coin && (this.coin.y < 0 || this.coin.y > GameAreaHeight * 0.30)) {
            answer = 0;
            window.location = "game_over.html?score="+score;
        }

    },

    calculate: function() {
        clicks.play();
        answer = 10*answer + this.key;
        var displayX = GameAreaWidth/coordinate_factor + GameAreaWidth*0.65 - (answer.toString().length - 1)*13;
        displayText.x = displayX;   // used to set the x-coordinate of displayText
        displayText.setText(answer.toString());
    },

    update_score: function() {
        enters.play();
        var result = false;

        if (this.operator_number == 1 && (this.first+this.second == answer)) {
            result = true;
        } else if (this.operator_number == 2 && (Math.abs(this.first-this.second) == answer)) {
            result = true;
        } else if (this.operator_number == 3 && (this.first*this.second == answer)) {
            result = true;
        } else if (this.operator_number == 4 && (this.first/this.second == answer)) {
            result = true;
        }

        if (result) {
            score += 1;
        } else {
            final_score = score;
            score = 0;
        }

        scoreText.setText("US $" + score.toString());
        answer = 0;

        if (score == 0) {
            window.location = "game_over.html?score="+final_score;
        }

        game.state.start('main');
    },

    delete: function () {
        answer = Math.floor(answer/10);
        var displayX = GameAreaWidth/coordinate_factor + GameAreaWidth*0.65 - (answer.toString().length - 1)*13  ;
        displayText.x = displayX;   // used to set the x-coordinate of displayText
        displayText.setText(answer.toString());
        deletes.play();
    }

};

var game = null;

// returns the value of input parameter 'parameter' in URL,
// window.location.search returns 'parameter part' of current url
function getQueryParam(parameter) {
    const urlParams = new URLSearchParams(window.location.search);
    return urlParams.get(parameter);
}

window.onload = function() {
    // Initialize game only if main_game.html has been loaded
    const startGame = getQueryParam('start') === 'true';
    if (startGame) {
        initializeGame();
    }
}


// All initialization code for the game
function initializeGame() {
        // Initialize Phaser, and create a GameAreaWidth px by GameAreaHeight px game view
        game = new Phaser.Game(GameAreaWidth, GameAreaHeight, Phaser.AUTO, 'game-container');
        
        // Add the 'mainState' and call it 'main'
        game.state.add('main', mainState);

        // Start the state to actually start the game
        game.state.start('main');
}

The above code snippet covers a variety of functionalities, from initializing the game environment to handling game dynamics such as user inputs and scoring. Below, we’ll understand the key sections of this code to provide a better understanding of its components and how they work together in the context of the game.

The script defines several variables to control game settings and mechanics, sets up the game state with preload, create, and update methods, and manages user interaction and game progression.

Key Variables and Their Purpose

  • GameAreaWidth and GameAreaHeight: These variables set the dimensions of the game area based on the window size, making the game responsive.
  • coordinate_factor: Used to position game elements uniformly.
  • maxValChangerConst: Adjusts the difficulty level of the game by changing values the player will work with as they progress.
  • mathOperationMap: Maps numeric values to mathematical operations to facilitate random operation selection during the game.
  • score and final_score: Track the current and final scores of the game.
  • answer: Stores the player’s current input as they solve the maths problem.

Phaser Game State

  • preload(): This method loads all the assets such as images and sounds needed for the game, ensuring they are available before the game starts.
  • create(): Sets up the initial game environment, configuring settings like the background color and initializing the physics system. It also calls a custom method createUI(), which sets up the user interface elements such as buttons and displays.
  • update(): Regularly executed at the framework’s refresh rate (commonly 60 fps), this method handles the game’s dynamic aspects, such as checking if the game should end based on certain conditions (e.g., the coin moving out of the play area).

User Interface and Interaction

The game uses sprites and text to display scores and the current maths problem. Buttons are added for each numeral, allowing the player to input their answer. Additionally, these buttons are binded to corresponding keyboard keys, allowing the player to interact with the game using the keyboard.

Event handlers are attached to these buttons to handle interactions:

  • calculate(): Updates the answer based on button presses, playing a sound each time.
  • update_score(): Checks if the answer is correct when the ‘enter’ button is pressed, updates the score accordingly, and resets the game state.
  • delete(): Allows the player to correct their input by removing the last digit.

Initialization and Utility Functions

  • getQueryParam(): A utility function to parse URL parameters. This is used to check if the game should start based on a specific query parameter, allowing the game to be conditionally initialized.
  • initializeGame(): Sets up the Phaser game instance and links it to the DOM, and starts the main game state.

The Phaser game needs a point of entry into the HTML document, typically provided as a div element identified by an id (game-container here). In the setup phase, Phaser is directed to render the game inside this div and using this div element as the canvas for the game, overlaying its graphics and user interface components onto it.

You can find the complete code for the Maths Solver Chrome Extension here – onlycodeblog/mathsolver

Developing a Chrome Extension game using Phaser is a great way to explore both game development and web development. This project will enhance your skills in JavaScript and Phaser while also helping you develop a fully functional app. Keep in mind that web development is an iterative process. Continuous effort is essential for creating engaging, functional, and well-integrated apps.

Happy coding, and crunch numbers while having fun!

Leave a Reply

Your email address will not be published. Required fields are marked *