Sitemap / Advertise

Introduction

This particular game is an iteration of a well-known and over-played Android game genre - obstacle avoidance with cars - on browsers in JavaScript.


Tags

Share

JavaScript Game: Obstacle Avoidance with Cars

Advertisement:


read_later

Read Later



read_later

Read Later

Introduction

This particular game is an iteration of a well-known and over-played Android game genre - obstacle avoidance with cars - on browsers in JavaScript.

Tags

Share





Advertisement

Advertisement





Introduction

I am still improving my skills in developing and designing browser games in JavaScript to see the extent of it. And, in this project series named JavaScript Game, I share games I develop with their tutorials from basic to advanced to convey my game developing experience in JavaScript. This particular game is an iteration of a well-known and over-played Android game genre - obstacle avoidance with cars - on browsers. As my previous games, you can see this game design as a starting point in your developing journey in its genre. Thus, I developed the game in a comprehensible but not perplexing design by which you can elucidate the source code without any extra steps.

I wanted to add reward tokens besides obstacles, although you do not have to catch them to continue the game but level-up. There are three levels in the game. In Level 1, you must escape from meteors as obstacles and catch fuel icons to increase your score to level-up. In Level 2, you need to avoid meteors and spaceships as obstacles. And, in Level 3, you must refrain from crashing to skyscrapers. As a bonus, you can fire blasters in Level 3 to obliterate obstacles before hitting you. But, it costs you to lose 200 score point for each blast :)

You need to collect 5000 score points to reach Level 2 and 10000 score points for Level 3. Each fuel icon increases your total score by

- 100 in Level 1

- 200 in Level 2

- 500 in Level 3

As I implied, each blast in Level 3 costs 200 score points so do not squander your points to exceed your previous scores :)

It is a competitive game: check out your newest score on the game over screen after playing.

Click 'Preview' to Play.

For more information about the code and asset files, please visit the Source Code section below.

Step 1: Features

- Move the car using arrow keys or the mobile keyboard to escape from obstacles and catch fuel icons (score points).

project-image
Figure - 44.1

- Pause or resume the game using the escape key or the P button when you need a break.

project-image
Figure - 44.2

- In Level 1, escape meteors as obstacles and collect fuels to increment your score by 100 points.

project-image
Figure - 44.3

- In Level 2, dodge meteors and spaceships as obstacles and collect fuels to increment your score by 200 points.

project-image
Figure - 44.4

- In Level 3, avoid obstacles and refrain from crashing to skyscrapers. And, collect fuels to increase your score by 500 points.

project-image
Figure - 44.5

- As a bonus, in Level 3, shoot blasts to obliterate obstacles by the cost of 200 points minus from your score.

project-image
Figure - 44.6


project-image
Figure - 44.7

- If the car is hindered by an obstacle or hit to skyscrapers, the game is over. You can display your total score on the game over screen.

project-image
Figure - 44.8


project-image
Figure - 44.9

Step 2: Index

- Import the Game class.

- Get the canvas element.

- Create the 2d context to draw.

- Define the game width and the game height.

- Define the game object.

- Create the loop function to update and draw the game using requestAnimationFrame(loop);.

project-image
Figure - 44.10

Step 3: Game

- Import Car, Obstacles, Collision, and InputHandler classes.

- Define game states.

- Create and define game objects. And, attach the class itself to the created game objects.

- Update each game object if the game state is not equal to paused or gameover.

- Print the game score and the current game level.

- Proceed after drawing each object unless there is not a change in the game state.

- Define pause() and gameOver() functions.

project-image
Figure - 44.11


project-image
Figure - 44.12

Step 4: Car

- Get the objects from the game class.

- Define the properties of the car - width, height, speed, accelerate, position, barrier, and blast (diameter, speed, and number).

- Define the blasts array.

- Define the movement functions and the attack function.

- In the attack() function, add a new blast element to the blast array, decrease the total score by 200 points, and play the blast sound (MP3) in the assets folder.

- Draw the background, the car, and each blast as a circle.

- Update the position of the car and blasts. If a blast is out of the canvas, remove it from the blasts array by using splice() and indexOf() functions.

project-image
Figure - 44.13


project-image
Figure - 44.14


project-image
Figure - 44.15

Step 5: Obstacles

- Get the objects from the game class.

- Define the properties of obstacles (meteor and spaceship) and the reward token (fuel) - width, height, and speed.

- Define the properties of the skyscraper for Level 3 - position, width, height, and speed.

- Define arrays and element numbers for each game mechanism (obstacles and fuel).

- Draw fuels and meteors for each level.

- Add spaceships in Level 2.

- Show the skyscraper background to constraint the car in Level 3.

- Update fuels and obstacles randomly in the given range.

- Update positions of each occurring element on the canvas - fuels, meteors, spaceships, and skyscraper - depending on the current level.

project-image
Figure - 44.16


project-image
Figure - 44.17

Step 6: Collision

- Get the objects from the game class.

- Define the game properties - LEVEL and score.

- In the detect_fuel_collision() function, check whether the car hits a fuel icon and increase the total score depending on the current level - 100, 200, 500 - and level-up depending on the total score.

- In the detect_obstacles_collision() function, check whether the given obstacle type - meteor or spaceship - crashed to the car. And, if it did, execute the gameOver() function.

- In the detect_blast_collision() function, detect whether a blast fired by the car smashes to the given obstacle type - meteor or spaceship. And, if it did, remove the crashed obstacle on the canvas, with the cost of 200 score points.

- For each meteor, spaceship, and fuel element, run its related function mentioned above.

- In Level 3, check whether the car crashes to the skyscraper background. And, if it did, execute the gameOver() function.

- For each blast element, execute the detect_blast_collision() function with obstacles - meteor and spaceship.

project-image
Figure - 44.18


project-image
Figure - 44.19


project-image
Figure - 44.20

Step 7: InputHandler

- Get objects and variables from the Game class.

- If the right arrow key or button is pressed, execute the moveRight() function.

- If the left arrow key or button is pressed, execute the moveLeft() function.

- If the up arrow key or button is pressed, execute the moveUp() function.

- If the down arrow key or button is pressed, execute the moveDown() function.

- In Level 3, if the space key or button is pressed, execute the attack() function to shoot blasts.

- To avoid the page navigation while pressing arrow keys, use event.preventDefault();

- If the escape key or the P button is pressed, pause or resume the game depending on the previous game state.

- If the Enter key or the P button is pressed, restart the game in the GAMEOVER state.

- On keyup, stop the car by executing the stop() function.

project-image
Figure - 44.21

Source Code

Please sign in to download source code files and the game, including index.css, index.html, and assets (background.png, blast.mp3, etc.), in a zipped folder to play and try the game on your computer.

Zip Folder

Download


Index

Download



import Game from './game.js';

let canvas = document.getElementById("game");
let ctx = canvas.getContext("2d");

const GAME_WIDTH = 420;
const GAME_HEIGHT = 325;

let game = new Game(GAME_WIDTH, GAME_HEIGHT);

function loop(timestamp){
	// Define if needed.
	let deltaTime;
	
	ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
    game.update(deltaTime);
	game.draw(ctx);
	
	requestAnimationFrame(loop);
}

requestAnimationFrame(loop);


Game

Download



import Car from './car.js';
import InputHandler from './inputhandler.js';
import Obstacles from './obstacles.js';
import Collision from './collision.js';

export default class Game{
	constructor(gameWidth, gameHeight){
		this.gameWidth = gameWidth;
		this.gameHeight = gameHeight;
		
		this.STATE = {
			PAUSED: 0,
			RUNNING: 1,
			GAMEOVER: 2
		};
		
	    this.car = new Car(this);
        this.obstacles = new Obstacles(this);
		this.collision = new Collision(this);
		new InputHandler(this);
		this.objects = [this.car, this.obstacles, this.collision];

	}
	
	update(deltaTime){
		if(this.gameState == this.STATE.PAUSED || this.gameState == this.STATE.GAMEOVER) return;
		this.objects.forEach((object) => object.update(deltaTime));
	}
	
	draw(ctx){
		this.objects.forEach((object) => object.draw(ctx));
		
		ctx.font = "bold 20px Times";
		ctx.fillStyle = "#eb2e00";
		ctx.fillText("SCORE: " + this.collision.properties.score, 10, 25);
		ctx.fillText("LEVEL: " + this.collision.properties.LEVEL, this.gameWidth - 100, 25);
		
		if(this.gameState == this.STATE.PAUSED){
			ctx.fillStyle = "rgba(78, 88, 81, 0.6)";
			ctx.fillRect(0, 0, this.gameWidth, this.gameHeight);
			ctx.font = "bold 30px Times";
			ctx.fillStyle = "white";
			ctx.textAlign = "center";
			ctx.fillText("PAUSED", this.gameWidth / 2, this.gameHeight / 3);
			ctx.textAlign = "left";
		}
		
		if(this.gameState == this.STATE.GAMEOVER){
			ctx.fillStyle = "#1F2020";
			ctx.fillRect(0, 0, this.gameWidth, this.gameHeight);
			ctx.font = "bold 30px Times";
			ctx.fillStyle = "white";
			ctx.textAlign = "center";
			ctx.fillText("GAME OVER !!!", this.gameWidth / 2, this.gameHeight / 2);
		}

	}
	
	pause(){
		if(this.gameState == this.STATE.PAUSED){
			this.gameState = this.STATE.RUNNING
		}else{
			this.gameState = this.STATE.PAUSED;
		}
	}
	
	gameOver(){
		this.gameState = this.STATE.GAMEOVER;
		alert("GAME OVER !!! \n\nSCORE = " + this.collision.properties.score);
		
	}
}




export default class Car{
	constructor(game){
		this.game = game;
		
		this.gameWidth = game.gameWidth;
		this.gameHeight = game.gameHeight;
		
		this.properties = {
			width: 60,
			height: 114,
			speed: {x: 0, y: 0},
			accelerate: 10,
			position: {x : this.gameWidth / 2, y: this.gameHeight - 114},
			barrier: 50,
			blast: {diameter: 60, speed: 5, number: 0}
		};
		
		this.blasts = [];
		
	}
	
	moveRight(){
		this.properties.speed.x = -this.properties.accelerate;
		this.properties.speed.y = 0;
	}
	
	moveLeft(){
		this.properties.speed.x = this.properties.accelerate;
		this.properties.speed.y = 0;
	}
	
	moveUp(){
		this.properties.speed.x = 0;
		this.properties.speed.y = -this.properties.accelerate;
	}
	
	moveDown(){
		this.properties.speed.x = 0;
		this.properties.speed.y = this.properties.accelerate;
	}
	
	stop(){
		this.properties.speed.x = 0;
		this.properties.speed.y = 0;
	}
	
	attack(){
		if(this.game.collision.properties.score > 200){
		    this.blasts.push({x: this.properties.position.x + (this.properties.width / 2), y: this.properties.position.y - this.properties.blast.diameter / 2 + 20});
		    this.properties.blast.number++;
			this.game.collision.properties.score = this.game.collision.properties.score - 200;
			// MP3
			var audio = new Audio("assets/blast.mp3");
			audio.play();
		}
	}
	
	draw(ctx){
		ctx.drawImage(document.getElementById("background"), 0, 0, this.gameWidth, this.gameHeight);
		
		ctx.drawImage(document.getElementById("car"), this.properties.position.x, this.properties.position.y, this.properties.width, this.properties.height);
		
		if(this.properties.blast.number > 0){
		    this.blasts.forEach((object) => {
				ctx.beginPath();
                ctx.arc(object.x, object.y, this.properties.blast.diameter / 2, 0, 2 * Math.PI);
				ctx.fillStyle = 'cyan';
                ctx.fill();
                ctx.lineWidth = 5;
                ctx.strokeStyle = '#006666';
                ctx.stroke();
		    });
		}

	}
	
	update(deltaTime){
		this.properties.position.x += this.properties.speed.x;
		this.properties.position.y += this.properties.speed.y;
		
	    if(this.properties.position.x > this.gameWidth - this.properties.width - this.properties.barrier) this.properties.position.x = this.gameWidth - this.properties.width - this.properties.barrier;
		if(this.properties.position.x < this.properties.barrier) this.properties.position.x = this.properties.barrier;
	    if(this.properties.position.y > this.gameHeight - this.properties.height) this.properties.position.y = this.gameHeight - this.properties.height;
	    if(this.properties.position.y < 0) this.properties.position.y = 0;
		
		// BLAST
		if(this.properties.blast.number > 0){
			this.blasts.forEach((object) => {
				object.y -= this.properties.blast.speed;
				if(object.y < 0) this.blasts.splice(this.blasts.indexOf(object), 1);
			});
		}

	}
}


Obstacles

Download



export default class Obstacles{
	constructor(game){
		this.game = game;
		
		this.gameWidth = game.gameWidth;
		this.gameHeight = game.gameHeight;
		
		this.properties = {
			fuel: {width: 30, height: 30, speed: 8},
			meteor: {width: 25, height: 25, speed: 5},
			spaceship: {width: 30, height: 40, speed: 2},
			skyscraper: {position: {x: 0, y: -150}, width: this.gameWidth, height: 150, speed: 3}
		};
	    
		this.fuel = [];
		this.obstacles_meteor = [];
		this.obstacles_spaceship = [];
		
		this.fuelNumber = 0;
		this.meteorNumber = 0;
		this.spaceshipNumber = 0;

	}
	
	draw(ctx){
		// DRAW FUEL and OBSTACLES
		if(this.fuelNumber > 0){
		    this.fuel.forEach((object) => {
			   ctx.drawImage(document.getElementById("fuel"), object.x, object.y, this.properties.fuel.width, this.properties.fuel.height);
		    });
		}
		// LEVEL 1
	    if(this.meteorNumber > 0){
		    this.obstacles_meteor.forEach((object) => {
			   ctx.drawImage(document.getElementById("meteor"), object.x, object.y, this.properties.meteor.width, this.properties.meteor.height);
		    });
		}
		// LEVEL 2
		if(this.spaceshipNumber > 0){
		    this.obstacles_spaceship.forEach((object) => {
			   ctx.drawImage(document.getElementById("spaceship"), object.x, object.y, this.properties.spaceship.width, this.properties.spaceship.height);
		    });
		}
		// LEVEL 3
		if(this.game.collision.properties.LEVEL == 3){
			ctx.drawImage(document.getElementById("skyscraper"), this.properties.skyscraper.position.x, this.properties.skyscraper.position.y, this.properties.skyscraper.width, this.properties.skyscraper.height);
		}

	}
		
	update(deltaTime){
		// FUEL
		this.fuelNumber++;
		if(this.fuelNumber < 11){
			this.fuel.push({x: Math.floor(Math.random() * 10 * 39), y: 0});
		}else if(this.fuelNumber > 50){
			this.fuelNumber = 0;
		}
		// LEVEL 1
		this.meteorNumber++;
		if(this.meteorNumber < 4){
			this.obstacles_meteor.push({x: Math.floor(Math.random() * 10 * 39), y: 0});
		}else if(this.meteorNumber > 50){
			this.meteorNumber = 0;
		}
		// LEVEL 2
		if(this.game.collision.properties.LEVEL == 2 || this.game.collision.properties.LEVEL == 3){
			this.spaceshipNumber++;
			if(this.spaceshipNumber < 5){
			   this.obstacles_spaceship.push({x: Math.floor(Math.random() * 10 * 39), y: 0});
		    }else if(this.spaceshipNumber > 100){
			   this.spaceshipNumber = 0;
		    }
		}
	
        // UPDATE POSITIONS
		this.fuel.forEach((object) => {
			object.y += this.properties.fuel.speed;
			if(object.y > this.gameHeight) this.fuel.splice(this.fuel.indexOf(object), 1);
		});
		this.obstacles_meteor.forEach((object) => {
			object.y += this.properties.meteor.speed;
			if(object.y > this.gameHeight) this.obstacles_meteor.splice(this.obstacles_meteor.indexOf(object), 1);
		});
		if(this.game.collision.properties.LEVEL == 2 || this.game.collision.properties.LEVEL == 3){
			this.obstacles_spaceship.forEach((object) => {
			    object.y += this.properties.spaceship.speed;
			    if(object.y > this.gameHeight) this.obstacles_spaceship.splice(this.obstacles_spaceship.indexOf(object), 1);
		    });
		}
		if(this.game.collision.properties.LEVEL == 3 && this.properties.skyscraper.position.y < 0){
			this.properties.skyscraper.position.y += this.properties.skyscraper.speed;
		}
		
	}
}


Collision

Download



export default class Collision{
	constructor(game){
        // GET ALL INFO FROM THE GAME CLASS
		this.game = game;
		
		// DEFINE LEVEL AND SCORE
		this.properties = {
			LEVEL: 1,
			score: 0
		};
	}
	
	detect_fuel_collision(fuel, car){
		let bottomOfFuel = fuel.y + this.game.obstacles.properties.fuel.height;
        let leftOfFuel = fuel.x;
        let rightOfFuel = fuel.x + this.game.obstacles.properties.fuel.width;
        let topOfFuel = fuel.y;
        
        let bottomOfCar = car.y + this.game.car.properties.height;
        let leftOfCar = car.x;		
        let rightOfCar = car.x + this.game.car.properties.width;	
        let topOfCar = car.y;
		
		if(leftOfCar < rightOfFuel && rightOfCar > leftOfFuel && topOfCar < bottomOfFuel && bottomOfCar > topOfFuel){
			this.game.obstacles.fuel.splice(this.game.obstacles.fuel.indexOf(fuel), 1);
			
			if(this.properties.LEVEL == 1) this.properties.score += 100;
			if(this.properties.LEVEL == 2) this.properties.score += 200;
			if(this.properties.LEVEL == 3) this.properties.score += 500;
			
			if(this.properties.score > 5000) this.properties.LEVEL = 2;
			if(this.properties.score > 10000) this.properties.LEVEL = 3;
		}
        
	}
	
	detect_obstacles_collision(obstacle, car, type){
		let bottomOfObstacle = obstacle.y + type.height - 10;
        let leftOfObstacle = obstacle.x + 10;
        let rightOfObstacle = obstacle.x + type.width - 10;
        let topOfObstacle = obstacle.y + 10;
        
        let bottomOfCar = car.y + this.game.car.properties.height - 20;
        let leftOfCar = car.x + 20;		
        let rightOfCar = car.x + this.game.car.properties.width - 20;	
        let topOfCar = car.y + 20;
		
		if(leftOfCar < rightOfObstacle && rightOfCar > leftOfObstacle && topOfCar < bottomOfObstacle && bottomOfCar > topOfObstacle){
			this.game.gameOver();
		}
        
	}
	
	detect_blast_collision(blast, obstacle_array, type){
		obstacle_array.forEach((object) => {
	    	let bottomOfObstacle = object.y + type.height;
            let leftOfObstacle = object.x;
            let rightOfObstacle = object.x + type.width;
            let topOfObstacle = object.y;
			 
			let bottomOfBlast = blast.y + this.game.car.properties.blast.diameter;
            let leftOfBlast = blast.x;		
            let rightOfBlast = blast.x + this.game.car.properties.blast.diameter;	
            let topOfBlast = blast.y;
			 
			if(leftOfBlast < rightOfObstacle && rightOfBlast > leftOfObstacle && topOfBlast < bottomOfObstacle && bottomOfBlast > topOfObstacle){
				obstacle_array.splice(obstacle_array.indexOf(object), 1);
		    }   
		});
        
	}
	
	draw(ctx){
		
	}
	
	update(deltaTime){
		// DETECT FUEL COLLISION
		this.game.obstacles.fuel.forEach((object) => {
			this.detect_fuel_collision(object, this.game.car.properties.position);
		});
		// DETECT LEVEL 1 COLLISION
		this.game.obstacles.obstacles_meteor.forEach((object) => {
			this.detect_obstacles_collision(object, this.game.car.properties.position, this.game.obstacles.properties.meteor);
		});
		// DETECT LEVEL 2 COLLISION
		if(this.properties.LEVEL != 1){
		  this.game.obstacles.obstacles_spaceship.forEach((object) => {
			  this.detect_obstacles_collision(object, this.game.car.properties.position, this.game.obstacles.properties.spaceship);
		  });
	    }
		// DETECT LEVEL 3 COLLISION
		if(this.properties.LEVEL == 3){
			if(this.game.car.properties.position.y <= this.game.obstacles.properties.skyscraper.position.y + this.game.obstacles.properties.skyscraper.height - 20){
				this.game.gameOver();
			}
		}
		// DETECT BLAST COLLISION
		if(this.game.car.properties.blast.number > 0){
			this.game.car.blasts.forEach((object) => {
				this.detect_blast_collision(object, this.game.obstacles.obstacles_meteor, this.game.obstacles.properties.meteor);
				this.detect_blast_collision(object, this.game.obstacles.obstacles_spaceship, this.game.obstacles.properties.spaceship);
			});
		}

	}
}


InputHandler

Download



export default class InputHandler{
	constructor(game){
	    // GET ALL INFO FROM THE GAME CLASS
		this.game = game;
		// KEYBOARD
		document.addEventListener("keydown", (event) => {
			let key = event.keyCode || event.which;
			switch(key){
				case 37:
				this.game.car.moveRight();
				break;
				case 38:
				event.preventDefault(); this.game.car.moveUp();
				break;
				case 39:
				this.game.car.moveLeft();
				break;
				case 40:
				event.preventDefault(); this.game.car.moveDown();
				break;
				case 32:
				event.preventDefault();
				if(this.game.collision.properties.LEVEL == 3 && this.game.gameState != this.game.STATE.PAUSED && this.game.gameState != this.game.STATE.GAMEOVER) this.game.car.attack();
				break;
				case 27:
				if(this.game.gameState != this.game.STATE.GAMEOVER) this.game.pause();
				break;
				case 13:
				if(this.game.gameState == this.game.STATE.GAMEOVER) location.reload();
				break;
				
			}
		});
		
		document.addEventListener("keyup", (event) => {
			let key = event.keyCode || event.which;
			switch(key){
				case 37:
				this.game.car.stop();
				break;
				case 38:
				this.game.car.stop();
				break;
				case 39:
				this.game.car.stop();
				break;
				case 40:
				this.game.car.stop();
				break;
			}
		});
		
		// MOBILE KEYBOARD
		document.getElementById("RIGHT").addEventListener("click", () => {
			this.game.car.moveRight();
		});
		document.getElementById("UP").addEventListener("click", () => {
			this.game.car.moveUp();
		});
		document.getElementById("LEFT").addEventListener("click", () => {
			this.game.car.moveLeft();
		});
		document.getElementById("DOWN").addEventListener("click", () => {
			this.game.car.moveDown();
		});
		document.getElementById("SPACE").addEventListener("click", () => {
			if(this.game.collision.properties.LEVEL == 3 && this.game.gameState != this.game.STATE.PAUSED && this.game.gameState != this.game.STATE.GAMEOVER) this.game.car.attack();
		});
		document.getElementById("PAUSE").addEventListener("click", () => {
			if(this.game.gameState != this.game.STATE.GAMEOVER){this.game.pause();}
			else{location.reload();}
		});
	}
}