Exploring Phaser 3 with a Game: Adding Multiple Scenes

In part one we had made peg solitaire game in Phaser 3 which was a single scene game. We will further explore Phaser 3 and use multiple scenes. Most of the times games would have a menu screen and a separate game screen. Many games would also keep a separate screen once the game is over so let us make changes in our game to add another scene which will be called once the game is over (either game is finished or there are no further moves).

We are going to define our “game over” scene as following

class GameOver extends Phaser.Scene {
 
    constructor() {
        super("GameOver");
    }

    preload() {
        this.load.image("restart", "images/restart.png");
    }
 
    create() {
        var gamedata = this.registry.get('gamedata');

        this.add.text(140, 100, 'Game Over', { font: '42px Courier', fill: '#000000' });
        this.add.text(155, 160, 'Moves: ' + gamedata.movesCount, { font: '42px Courier', fill: '#000000' });
        if (gamedata.remainingPegs > 1) {
            this.remainingPegs = this.add.text(30, 220, 'Remaining Pegs: ' + gamedata.remainingPegs, { font: '42px Courier', fill: '#000000' });
        }
        var btn = this.add.image(175, 300, 'restart');
        btn.setInteractive();
        btn.setOrigin(0);
        btn.on('pointerup', this.startGame, this);
    }

    startGame() {
        this.scene.remove('GameOver');
        let theGame = new TheGame('TheGame');
        this.scene.add('TheGame', theGame, true);
    }
}

Game over scene simply displays a “game over” message and total number of moves it took to finish. In case of “no win” we can also display no of pegs remaining. In “preload” method of this scene we are loading “restart” image which will be used for restart button and attached with the “pointerup” event. Once that event is called, we will remove the current scene and begin the game scene. The scene will be automatically started for which the third parameter in this.scene.add method will be passed as “true”.

Also notice the way we pass data between the scenes. We use global registry and save data in the “game scene” which can be retrieved using the same key in the “game over” scene (we are using ‘gamedata’ as key here).

Now we need to make some changes in the game code as well to check for “game over”, and then start “game over” scene once the game is over.

let loadGame = function () {
    let config = {
        type: Phaser.AUTO,
        width: 500,
        height: 500,
        backgroundColor: 0xFF0000,
        scene: TheGame
    }

    let game = new Phaser.Game(config);
};

if (document.readyState === "complete" || (document.readyState !== "loading" && !document.documentElement.doScroll)) {
    loadGame();
} else {
    document.addEventListener("DOMContentLoaded", loadGame);
}

class TheGame extends Phaser.Scene {

    constructor() {
        super("TheGame");
    }

    preload() {
        this.load.spritesheet("pegs", "images/pegs.png", {
            frameWidth: 60,
            frameHeight: 60
        });
    }

    create() {
        this.boardDef = [
            [-1, -1, 1, 1, 1, -1, -1],
            [-1, -1, 1, 1, 1, -1, -1],
            [1, 1, 1, 1, 1, 1, 1],
            [1, 1, 1, 0, 1, 1, 1],
            [1, 1, 1, 1, 1, 1, 1],
            [-1, -1, 1, 1, 1, -1, -1],
            [-1, -1, 1, 1, 1, -1, -1]
        ];

        //  If a Game Object is clicked on, this event is fired.
        //  We can use it to emit the 'clicked' event on the game object itself.
        this.input.on('gameobjectup', function (pointer, gameObject) {
            gameObject.emit('clicked', gameObject);
        }, this);

        // add our sprites
        this.board = [];
        this.selectedPeg = null;
        this.movesCount = 0;

        for (let i = 0, len = this.boardDef.length; i < len; i++) {
            let r = this.boardDef[i];
            let row = [];
            this.board.push(row);
            for (let j = 0, cnt = r.length; j < cnt; j++) {
                let c = r[j];
                if (c >= 0) {
                    let cell = this.add.image(30 + i * 60, 30 + j * 60, "pegs");
                    cell.setFrame(c > 0 ? 1 : 0);
                    cell.setOrigin(0);

                    // enable input events
                    cell.setInteractive();
                    cell.on('clicked', this.clickPeg, this);
                    cell.gridX = i;
                    cell.gridY = j;
                    row.push(cell);
                } else {
                    row.push(null);
                }
            }
        }

        this.movesLabel = this.add.text(0, 0, 'Moves: ' + this.movesCount, { font: '24px Courier', fill: '#000000' });
    }

    clickPeg(peg) {
        if (peg.frame.name === 0) {
            // if we have not selected a peg to jump then no need to move any further
            if (!this.selectedPeg)
                return;

            let clickedX = peg.gridX;
            let clickedY = peg.gridY;
            let selectedX = this.selectedPeg.gridX;
            let selectedY = this.selectedPeg.gridY;

            if ((clickedX + 2 === selectedX || clickedX - 2 === selectedX) && clickedY === selectedY) {
                // move horizontal
                let pegToRemove = this.board[(selectedX + clickedX) / 2][clickedY];
                if (pegToRemove.frame.name === 0)
                    return;

                this.updateMoves(++this.movesCount);
                pegToRemove.setFrame(0);
                peg.setFrame(1);
                this.selectedPeg.setFrame(0);
                this.selectedPeg = null;
                this.checkForGameOver();

            } else if ((clickedY + 2 === selectedY || clickedY - 2 === selectedY) && clickedX === selectedX) {
                // move vertical
                let pegToRemove = this.board[clickedX][(selectedY + clickedY) / 2];
                if (pegToRemove.frame.name === 0)
                    return;

                this.updateMoves(++this.movesCount);
                pegToRemove.setFrame(0);
                peg.setFrame(1);
                this.selectedPeg.setFrame(0);
                this.selectedPeg = null;
                this.checkForGameOver();
            }

        } else {
            if (this.selectedPeg) {
                if (peg === this.selectedPeg) {
                    peg.setFrame(1);
                    this.selectedPeg = null;
                } else {
                    this.selectedPeg.setFrame(1);
                    this.selectedPeg = peg;
                    peg.setFrame(2);
                }
            } else {
                this.selectedPeg = peg;
                peg.setFrame(2);
            }
        }
    }

    updateMoves(movesCount) {
        this.movesLabel.setText('Moves: ' + movesCount);
    }

    checkForGameOver() {
        if (!this.isAnyValidMove()) {
            let timedEvent = this.time.addEvent({
                delay: 3000,
                callbackScope: this,
                callback: function () {
                    this.gameOver();
                }
            });
        }
    }

    gameOver() {
        this.registry.set('gamedata', { movesCount: this.movesCount, remainingPegs: this.remainingPegs() });
        this.scene.remove('TheGame');
        let gameOver = new GameOver('GameOver');
        this.scene.add('GameOver', gameOver, true);
    }

    isAnyValidMove() {
        let colsCount = this.board.length;
        for (let i = 0; i < colsCount; i++) {
            let col = this.board[i];
            for (let j = 0, endIndex = col.length - 3; j <= endIndex; j++) {
                let c1 = col[j];
                let c2 = col[j + 1];
                let c3 = col[j + 2];

                if (c1 && c2 && c3) {
                    if (c1.frame.name !== 0 && c2.frame.name !== 0 && c3.frame.name === 0) return true;
                    if (c1.frame.name === 0 && c2.frame.name !== 0 && c3.frame.name !== 0) return true;
                }
            }
        }

        var rowsCount = this.board[0].length;
        for (let i = 0, len = colsCount - 3; i <= len; i++) {
            let r1 = this.board[i];
            let r2 = this.board[i + 1];
            let r3 = this.board[i + 2];
            for (let j = 0; j < rowsCount; j++) {
                let c1 = r1[j];
                let c2 = r2[j];
                let c3 = r3[j];

                if (c1 && c2 && c3) {
                    if (c1.frame.name !== 0 && c2.frame.name !== 0 && c3.frame.name === 0) return true;
                    if (c1.frame.name === 0 && c2.frame.name !== 0 && c3.frame.name !== 0) return true;
                }
            }
        }
        return false;
    }

    remainingPegs() {
        var pegs = 0;
        for (let i = 0, len = this.board.length; i < len; i++) {
            let col = this.board[i];
            for (let j = 0, cnt = col.length; j < cnt; j++) {
                let cell = col[j];
                if (cell && cell.frame.name !== 0) {
                    pegs++
                }
            }
        }
        return pegs;
    }
}

In the game scene we have added a text label to display number of moves made in the game and also a check for a valid move in the game. If no valid move is found then we simply call the gameover method which does similar thing which we did in the “game over” scene. It removes current scene and then starts “game over” scene. On gameover we also save data to the registry which we need to pass to the next scene. Note that we don’t have to start all scenes at once. We can start a scene when needed and we also don’t have to keep scenes in the memory. Scenes can be removed and then launched again when needed.

So far the code can be seen at work here


Leave A Comment

Your email address will not be published.