5
\$\begingroup\$

The game is hosted here.

GitHub repo

This is my first Phaser 3 game/project and I'm still pretty new to Javascript so I'm sure there are many things I could be doing better. The number 1 thing that I would like to improve about my code is the performance. Then code effectiveness and readability, but performance is the top priority.

Your feedback is valuable even if you have no experience with PhaserJS whatsoever, because a lot of things that I could probably be doing better only have to do with pure Javascript.

My JS code:

const width = window.innerWidth;
const height = window.innerHeight;
let hiScore = localStorage['hiScore'] || 0;

const config = {
    width: width,
    height: height,
    backgroundColor: 0x50C875,
    scene: {
        preload,
        create,
        update,
    },
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 50 },
        },
    }
}

const gameState = {
    gameOver: false,
    score: 0,
    scoreText: false,
    player1AnimationStage: 0,
    player2AnimationStage: 0,
    player1SpriteSheet: ['upflap', 'midflap', 'downflap',],
    player2SpriteSheet: ['player2_upflap', 'player2_midflap', 'player2_downflap',],
    player1Y: (height / 2 * 0.5),
    player2Y: (height / 2 * 0.5),
    secondPlayerSpawned: false,
    player1Dead: false,
    player2Dead: false,
}

const game = new Phaser.Game(config, 'root');

game.clearBeforeRender = false;

function preload() {
    this.load.image('background', 'assets/images/background.png');
    this.load.image('ground', 'assets/images/ground.png');
    this.load.image('pipe', 'assets/images/pipe.png');
    this.load.image('upflap', 'assets/images/upflap.png');
    this.load.image('midflap', 'assets/images/midflap.png');
    this.load.image('downflap', 'assets/images/downflap.png');
    this.load.image('player2_upflap', 'assets/images/player2_upflap.png');
    this.load.image('player2_midflap', 'assets/images/player2_midflap.png');
    this.load.image('player2_downflap', 'assets/images/player2_downflap.png');

    this.load.audio('hit', 'assets/audio/hit.mp3');
    this.load.audio('point', 'assets/audio/point.mp3');
    this.load.audio('wing', 'assets/audio/wing.mp3');
    this.load.audio('die', 'assets/audio/die.mp3');
}

function create() {
    gameState.hitSound = this.sound.add('hit');
    gameState.pointSound = this.sound.add('point');
    gameState.wingSound = this.sound.add('wing');
    gameState.dieSound = this.sound.add('die');

    // Hide Score Table
    document.getElementById('hiScoreTable').style.display = 'none';

    const colliderTile = this.physics.add.staticGroup();
    gameState.colliderTile = colliderTile.create(50, 0, 'pipe').setScale(0.1, 80).refreshBody();
    gameState.colliderTile2 = colliderTile.create(1, 0, 'pipe').setScale(0, 80).refreshBody();
    gameState.bgTile = this.add.tileSprite(0, height, width, height, 'background').setScale(2);
    gameState.ground = this.physics.add.staticGroup();
    gameState.ground.create(0, height, 'ground').setScale((8.6, 1)).refreshBody();
    gameState.groundTile = this.add.tileSprite(0, height, width, null, 'ground').setScale(8.6, 1);
    gameState.gameOver = false;
    gameState.player1 = this.physics.add.sprite(100, gameState.player1Y, 'midflap').setScale(2);
    gameState.player1.body.acceleration.y = 1500;
    gameState.pipes = this.physics.add.group();
    gameState.scoreText = this.add.text((width / 2) - 100, 100, `Score: ${gameState.score}`, { fontSize: '40px', fontWeight: 'bold', });
    gameState.secondPlayerSpawned = false;

    // Layers
    gameState.groundTile.setDepth(1);
    gameState.pipes.setDepth(2);
    gameState.scoreText.setDepth(3);

    gameState.playSoundMethod = (sound) => {
        this.sound.play(sound);
    }

    const addRowOfPipes = () => {
        const hole = Math.floor(Math.random() * 7) + 3;

        for (let i = 0; i < 17; i++) {
            if (i !== hole && i !== hole + 1 && i !== hole + 2) {
                let pipe = gameState.pipes.create(width - 60, i * 50 + 25, 'pipe');
                pipe.body.setVelocityX(-200);
                pipe.outOfBoundsKill = true;
                pipe.body.allowGravity = false;
                pipe.body.immovable = true;

                this.physics.add.collider(pipe, gameState.colliderTile2, (item) => {
                    if (i === 16) {
                        gameState.pointSound.play();
                        gameState.score++;

                        if (gameState.scoreText)
                            gameState.scoreText.destroy();

                        gameState.scoreText = this.add.text((width / 2) - 100, 100, `Score: ${gameState.score}`, { fontSize: '40px', fontWeight: 'bold', });
                    }
                    item.destroy();
                })

                if (i === 16) {
                    pipe.onWorldBounds = true;
                }
            }

        }
    }



    gameState.fallDown = () => {
        if (gameState.player1Dead) {
            gameState.player1.y += 5;

            if (gameState.player1.y > height)
                gameState.player1fallDownCaller.destroy();
        }
        if (gameState.player2Dead) {
            gameState.player2.y += 5;

            if (gameState.player2.y > height)
                gameState.player2fallDownCaller.destroy();
        }


    }

    addRowOfPipes();

    gameState.gameOverMethod = () => {
        this.physics.pause();
        gameState.scoreText.destroy();

        if (gameState.score > hiScore)
            localStorage['hiScore'] = gameState.score;

        hiScore = localStorage.getItem('hiScore');

        document.getElementById('hiScoreTable').style.display = 'initial';
        document.getElementById('score').innerHTML = gameState.score;
        document.getElementById('hiScore').innerHTML = hiScore;

        birdAnimation.destroy();
        gameState.gameOver = true;
        this.add.text();
        pipeGen.destroy();
        gameState.player1.setVelocityY(150);
        gameState.player1.setVelocityX(0);

        if (gameState.secondPlayerSpawned) {
            gameState.player2.setVelocityY(150);
            gameState.player2.setVelocityX(0);
        }



        if (gameState.score > 10) {
            if (gameState.score > 20) {
                if (gameState.score > 30) {
                    displayMedal('gold');
                }
                displayMedal('silver');
            }
            displayMedal('bronze');
        }



        function displayMedal(medal) {
            let medalColor;
            document.getElementById('medalContainer').style.display = 'initial';

            switch (medal) {
                case 'bronze':
                    medalColor = '#cd7f32';
                    break;
                case 'silver':
                    medalColor = '#c0c0c0';
                    break;
                case 'gold':
                    medalColor = '#ccac00';
                    break;
            }

            document.getElementById('medal').style.backgroundColor = medalColor;
        }

        gameState.score = 0;
    }

    gameState.fallDownCaller = (player) => {
        if (player === 'player1') {
            gameState.player1fallDownCaller = this.time.addEvent({
                delay: 10,
                callback: gameState.fallDown,
                loop: true,
            })
        } else {
            gameState.player2fallDownCaller = this.time.addEvent({
                delay: 10,
                callback: gameState.fallDown,
                loop: true,
            })
        }

    }

    gameState.collisionMethod = (player) => {
        if (player === 'player1') {
            gameState.player1Dead = true;
            if ((gameState.player1Dead && gameState.player2Dead) || !gameState.secondPlayerSpawned) {
                gameState.gameOverMethod();
            }
            gameState.fallDownCaller(player);
        } else {
            gameState.player2Dead = true;
            if (gameState.player1Dead && gameState.player2Dead) {
                gameState.gameOverMethod();
            }
            gameState.fallDownCaller(player);
        }
    }

    // Colliders
    gameState.player1.setCollideWorldBounds(true);
    this.physics.add.collider(gameState.player1, gameState.ground, () => {
        gameState.dieSound.play();
        gameState.collisionMethod('player1')
    });
    this.physics.add.collider(gameState.player1, gameState.pipes, () => {
        gameState.hitSound.play();
        gameState.collisionMethod('player1');
    });

    // Initialize input keys
    gameState.cursors = this.input.keyboard.createCursorKeys();

    const pipeGen = this.time.addEvent({
        callback: addRowOfPipes,
        delay: 1500,
        callbackScope: this,
        loop: true,
    })

    // Animation
    const animateBird = () => {
        gameState.player1AnimationStage++;
        if (gameState.player1AnimationStage > 2)
            gameState.player1AnimationStage = 0;

        if (gameState.secondPlayerSpawned) {
            gameState.player2AnimationStage++;
            if (gameState.player2AnimationStage > 2)
                gameState.player2AnimationStage = 0;
        }

        gameState.player1.setTexture(gameState.player1SpriteSheet[gameState.player1AnimationStage]);
        if (gameState.secondPlayerSpawned)
            gameState.player2.setTexture(gameState.player2SpriteSheet[gameState.player2AnimationStage]);

    }
    const birdAnimation = this.time.addEvent({
        callback: animateBird,
        delay: 100,
        callbackScope: this,
        loop: true,
    })

}

function update() {
    if (!gameState.gameOver) {
        gameState.bgTile.tilePositionX += 0.1;
        gameState.groundTile.tilePositionX += 1;
    }

    // Press spacebar to fly up
    if (gameState.cursors.space.isDown) {
        if (gameState.gameOver) {
            this.scene.restart();
        } else {
            gameState.wingSound.play();
            gameState.player1.setVelocityY(-350);
        }
    }

    const spawnSecondPlayer = () => {
        gameState.secondPlayerSpawned = true;
        gameState.player2 = this.physics.add.sprite(100, gameState.player2Y, 'player2_midflap').setScale(2);
        gameState.player2.body.acceleration.y = 1500;
        gameState.player2.setCollideWorldBounds(true);
        this.physics.add.collider(gameState.player2, gameState.ground, () => {
            gameState.dieSound.play();
            gameState.collisionMethod('player2')
        });
        this.physics.add.collider(gameState.player2, gameState.pipes, () => {
            gameState.hitSound.play();
            gameState.collisionMethod('player2');
        });
    }

    if (!gameState.secondPlayerSpawned) {
        if (gameState.cursors.shift.isDown) {
            spawnSecondPlayer();
        }
    } else {
        if (gameState.cursors.shift.isDown) {
            if (gameState.gameOver) {
                this.scene.restart();
            } else {
                gameState.player2.setVelocityY(-350);
            }

        }
    }

}
\$\endgroup\$
7
\$\begingroup\$

What if you want to add the functionality for up to 3 players?

You'd have to create a 'player3AnimationStage, player3SpriteSheet etc. It's also inside the 'gameState', which arguably makes sense, but still could be separated into it's own class.

For example:

class Player
{
    constructor(spriteSheet, animationStage)
    {
        this.SpriteSheet = spriteSheet;
        this.AnimationStage = animationStage;
    }
}

const gameState = {
    player1: new Player(...);
    player2: new Player(...);

Or better yet, have an array of Players. Try to code your game so that it does not matter how many players there are. (E.g iterate through the list of players).

Your colours could be made into an ENUM, or a class with score, colorName, colorCode.

I'd suggest declaring some variables at the top, to make maintenance easier. Such as key div elements (hiScoreTable). (Or even just the ids of the elements). Images.

Try to avoid 'maigc numbers' by using named variables. For example, what is '17' here?:

for (let i = 0; i < 17; i++) 

Avoiding 'magicNumbers' also decreases code duplication and makes maintenance easier. For example, to increase player speed currently we'd have to change it in at least 2 places.

\$\endgroup\$
1
\$\begingroup\$

This code makes great use of the keywords let and const where appropriate. Some of the functions are a little on the long side, so those could potentially be split up into smaller functions. The repeated calls to functions like this.load.image could be done in a loop over arrays if pre-defined arrays were setup.


Did you consider using element references, which was mentioned in blindman’s answer to your previous question Tip Calculator in pure JS?


In the method gameState.collisionMethod() there is a line in both cases that could be pulled outside of the conditional blocks: gameState.fallDownCaller(player);


In the method gameState.fallDownCaller() the object passed to this.time.adddEvent() is the same and could be declared in one spot above (or else in a separate function).


Instead of using a switch statement in displayMedal(), a mapping of color names to hex values could be used.

For example:

const colorToHexMap = {
    bronze: '#cd7f32',
    silver: '#c0c0c0',
    gold:   '#ccac00',
};

Then use it with the in operator:

if (color in colorToHexMap) {
    medalColor = colorToHexMap[color];
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.