HTML5 hyper casual game like “Bouncing Ball” step 3: measuring distance – commented source code available

Read all posts about "" game

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.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers