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.