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.