Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Bouncing Ball game, Game development, HTML5, Javascript and Phaser.

In this 3rd step of Bouncing Ball series we are going to cover how to measure distance traveled by the ball.

In first step we built a basic prototype of the game, in second step we added a mandatory bonus, and now it’s time to measure distance traveled by the ball.

The only problem is the ball does not cover any distance because actually it does not move.

In most endless runner games, player does not run, it’s the entire environment which moves towards the player.

So we are going to determine distance traveled by the ball by calculating the distance traveled by obstacles.

Since we know obstacle speed, in pixels per second, it’s quite easy to do it.

Have a look at the example where a marker is placed every 1000 pixels:

Tap or click the game to increase ball speed at the right time, avoid black bars.

Finally we have the source code fully commented, so you can learn and change it as you want to build your own bouncing ball game:

var game;
var gameOptions = {

    // bounce height fromthe ground, in pixels
    bounceHeight: 300,

    // ball gravity. Affects ball descending speed
    ballGravity: 1200,

    // ball power, used to boost the ball
    ballPower: 1200,

    // obstacle speed, that is the actual speed of the game
    obstacleSpeed: 250,

    // distance range between two obstacles, in pixels
    obstacleDistanceRange: [100, 200],

    // obstacle height range, in pixels
    obstacleHeightRange: [20, 80],

    // local storage name, where to save high scores
    localStorageName: 'bestballscore',

    // bonus ratio, in %. No bonus in this case, just obstacles
    bonusRatio: 0,

    // distance, in pixels,
    distanceStep: 1000
}
window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor:0x87ceeb,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: 'thegame',
            width: 750,
            height: 500
        },
        physics: {
            default: 'arcade'
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super('PlayGame');
    }
    preload(){
        this.load.image('ground', 'ground.png');
        this.load.image('ball', 'ball.png');
        this.load.image('distance', 'distance.png');
        this.load.spritesheet('obstacle', 'obstacle.png', {
            frameWidth: 20,
            frameHeight: 40
        })
    }
    create(){

        // we have to measure the force of the first bounce.
        // this is the only way we have to boost the ball while
        // keeping the same force when bouncing
        this.firstBounceForce = 0;

        // add the ground and set it immovable
        this.ground = this.physics.add.sprite(game.config.width / 2, game.config.height / 4 * 3, 'ground');
        this.ground.setImmovable(true);

        // add the ball, set its gravity, give it full restitution and define it as a circle
        this.ball = this.physics.add.sprite(game.config.width / 10 * 2, game.config.height / 4 * 3 - gameOptions.bounceHeight, 'ball');
        this.ball.body.gravity.y = gameOptions.ballGravity;
        this.ball.setBounce(1);
        this.ball.setCircle(25);

        // add physics group which will contain all obstacles
        this.obstacleGroup = this.physics.add.group();

        // first obstacle will be placed at the right edge of the screen
        let obstacleX = game.config.width;

        // add 20 obstacles. More than enough to allow object pooling
        for(let i = 0; i < 20; i++){

            // create an obstacle, give it random height, set it immovable, ad adjust its frame if it's a bonus
            let obstacle = this.obstacleGroup.create(obstacleX, this.ground.getBounds().top, 'obstacle');
            obstacle.displayHeight = Phaser.Math.Between(gameOptions.obstacleHeightRange[0], gameOptions.obstacleHeightRange[1]);
            obstacle.setOrigin(0.5, 1);
            obstacle.setImmovable(true);
            obstacle.setFrame((Phaser.Math.Between(0, 99) < gameOptions.bonusRatio) ? 0 : 1);

            // then set new obstacle position according to distance range
            obstacleX += Phaser.Math.Between(gameOptions.obstacleDistanceRange[0], gameOptions.obstacleDistanceRange[1])
        }

        // move the entire obstacle group towards the player
        this.obstacleGroup.setVelocityX(-gameOptions.obstacleSpeed);

        // set score, retrieve top score and display them
        this.score = 0;
        this.topScore = localStorage.getItem(gameOptions.localStorageName) == null ? 0 : localStorage.getItem(gameOptions.localStorageName);
        this.scoreText = this.add.text(10, 10, '');
        this.updateScore(this.score);

        // set distance
        this.distance = 0;

        // calculate where to place next distance marker
        this.distanceMarker = gameOptions.distanceStep;

        // add the distance bar, invisible at the moment
        this.distanceBar = this.physics.add.sprite(0, this.ground.getBounds().top, 'distance');
        this.distanceBar.setOrigin(0, 1);
        this.distanceBar.visible = false;

        // also add a distance text. We can't add arcade physics texts
        this.distanceText = this.add.text(0, 200, '');
        this.distanceText.visible = true;

        // wait for player input
        this.input.on('pointerdown', this.boost, this);
    }

    // update score and display it
    updateScore(inc){
        this.score += inc;
        this.scoreText.text = 'Score: ' + this.score + '\nBest: ' + this.topScore;
    }

    // boost the ball, if it's not the first bounce
    // we have to calculate the force of the first bounce to make the game run
    boost(){
        if(this.firstBounceForce != 0){
            this.ball.body.velocity.y = gameOptions.ballPower;
        }
    }

    // method to get the rightmost obstacle
    getRightmostObstacle(){
        let rightmostObstacle = 0;
        this.obstacleGroup.getChildren().forEach(function(obstacle){
            rightmostObstacle = Math.max(rightmostObstacle, obstacle.x);
        });
        return rightmostObstacle;
    }

    // update the obstacle, adding 1 to the score, and moving it to its new position.
    // height and frame are also updated
    updateObstacle(obstacle){
        this.updateScore(1);
        obstacle.x = this.getRightmostObstacle() + Phaser.Math.Between(gameOptions.obstacleDistanceRange[0], gameOptions.obstacleDistanceRange[1]);
        obstacle.displayHeight = Phaser.Math.Between(gameOptions.obstacleHeightRange[0], gameOptions.obstacleHeightRange[1]);
        obstacle.setFrame((Phaser.Math.Between(0, 99) < gameOptions.bonusRatio) ? 0 : 1);
    }

    // method to be executed at each frame
    // the two arguments represent respectively the total amount of time since the game started
    // and the amount of time since last update, both in milliseconds
    update(totalTime, deltaTime){

        // determine total distance
        this.distance += gameOptions.obstacleSpeed * (deltaTime / 1000);

        // it's time to make the distance bar enter the game from the right edge of the screen
        if(this.distance + game.config.width + 200 > this.distanceMarker && !this.distanceBar.visible){
            this.distanceBar.visible = true;
            this.distanceBar.x = this.distanceMarker - this.distance + this.ball.x;
            this.distanceBar.visible = true;
            this.distanceBar.setVelocityX(-gameOptions.obstacleSpeed);
            this.distanceText.visible = true;
            this.distanceText.setText(this.distanceMarker);
        }

        // it's time to hide distance bar as it left the screen to the left edge
        if(this.distanceBar.x < 0){
            this.distanceBar.setVelocityX(0);
            this.distanceBar.visible = false;
            this.distanceText.visible = false;
            this.distanceMarker += gameOptions.distanceStep;
        }

        // update distance text position if distance bar is visible
        if(this.distanceText.visible = true){
            this.distanceText.x = this.distanceBar.x + 10;
        }

        // check collision between the ball and the ground
        this.physics.world.collide(this.ground, this.ball, function(){

            // if this is the first bounce, then get ball bounce force...
            if(this.firstBounceForce == 0){
                this.firstBounceForce = this.ball.body.velocity.y;
            }
            else{

                // ... to use it in future bounces
                this.ball.body.velocity.y = this.firstBounceForce;
            }
        }, null, this);


        // check for collision between the ball and the obstacles/bonuses
        this.physics.world.overlap(this.ball, this.obstacleGroup, function(ball, obstacle){
            if(obstacle.frame.name == 1){
                localStorage.setItem(gameOptions.localStorageName, Math.max(this.score, this.topScore));
                this.scene.start('PlayGame');
            }
            else{
                this.updateObstacle(obstacle);
            }
        }, null, this);

        // reuse obstacles when they leave the screen to the left edge
        this.obstacleGroup.getChildren().forEach(function(obstacle){
            if(obstacle.getBounds().right < 0){
                if(obstacle.frame.name == 0){
                    localStorage.setItem(gameOptions.localStorageName, Math.max(this.score, this.topScore));
                    this.scene.start('PlayGame');
                }
                else{
                    this.updateObstacle(obstacle);
                }
            }
        }, this)
    }
}

There is a lot of room for customization, by playing with gameOptions object, so download the source code and start creating.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.