A Brick-Breaking Game in JavaScript

Below is a simple brick-breaking game implemented in JavaScript. Its gameplay is similar to the classic games Breakout and Arkanoid. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

UPDATE 2018/01/16 – An online version of this code is now available at the URL “https://thiscouldbebetter.neocities.org/brickbreakinggame.html“.

BrickBreakingGame.png


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var displaySize = new Coords(100, 200);

	var display = new Display(displaySize);

	var world = World.random(displaySize);

	Globals.Instance.initialize
	(
		10, // ticksPerSecond
		display,
		world
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.remove = function(element)
	{
		var elementIndex = this.indexOf(element);
		if (elementIndex >= 0)
		{
			this.splice(elementIndex, 1);
		}
		return this;
	}
}

// classes

function Activity(perform)
{
	this.perform = perform;
}
{
	Activity.Instances = new Activity_Instances()
	
	function Activity_Instances()
	{
		this.DoNothing = new Activity(function perform() {});
		this.UserInputAccept = new Activity
		(
			function perform(world, actor)
			{
				var inputHelper = Globals.Instance.inputHelper;
				var inputsActive = inputHelper.keysPressed;

				for (var i = 0; i < inputsActive.length; i++)
				{
					var inputActive = inputsActive[i];
					if (inputActive == "ArrowLeft")
					{
						actor.vel.x -= actor.accelPerTick;
					}
					else if (inputActive == "ArrowRight")
					{
						actor.vel.x += actor.accelPerTick;
					}
				}
			}
		);
	}
}

function Actor(pos, activity)
{
	this.pos = pos;
	this.activity = activity;

	this.color = "Gray";
	this.radius = 8;

	this.vel = new Coords(0, 0);

	this.accelPerTick = .005;
	this.speedMax = .25;

	this.projectileRadius = this.radius / 4;
	this.projectileSpeed = .3;

	// Helper variables.

	this.coordsTemp = new Coords();
	this.vertices = 
	[
		new Coords(), new Coords(), new Coords()
	];
}
{
	Actor.prototype.updateForTimerTick = function(world)
	{
		this.activity.perform(world, this);

		var speed = this.vel.magnitude();
		if (speed >= this.speedMax)
		{
			this.vel.normalize().multiplyScalar(this.speedMax);
		}

		this.pos.add(this.vel);
		this.pos.trimToRangeMinMax
		(
			world.actorPosMin,
			world.actorPosMax
		);

		this.vel.multiplyScalar(.98); // friction
	}

	// drawable

	Actor.prototype.drawToDisplay = function(display)
	{
		display.drawCircle
		(
			this.pos, this.radius, "Gray"
		);
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;	
		return this;
	}

	Coords.prototype.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;	
		return this;
	}

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.random = function()
	{
		this.x = Math.random();
		this.y = Math.random();
		return this;
	}

	Coords.prototype.right = function()
	{
		var temp = this.x;
		this.x = 0 - this.y;
		this.y = temp;
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

	Coords.prototype.trimToRangeMinMax = function(min, max)
	{
		if (this.x < min.x)
		{
			this.x = min.x;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}

		while (this.y < min.y)
		{
			this.y = min.y;
		}
		while (this.y > max.y)
		{
			this.y = max.y;
		}

		return this;
	}
}

function CollisionHelper()
{
	this.displacement = new Coords();
	this.edgeForward = new Coords();
	this.edgeRight = new Coords();
}
{
	CollisionHelper.Instance = new CollisionHelper();

	CollisionHelper.prototype.doCirclesCollide = function
	(
		circle0Center, circle0Radius, circle1Center, circle1Radius
	)
	{
		var distanceBetweenCenters = this.displacement.overwriteWith
		(
			circle1Center
		).subtract
		(
			circle0Center
		).magnitude();

		var sumOfRadii = circle0Radius + circle1Radius;

		var returnValue = (distanceBetweenCenters < sumOfRadii);

		return returnValue;
	}
}

function Display(size)
{
	this.size = size;

	this.colorBack = "White";
	this.colorFore = "Gray";
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);
	}

	// drawing

	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.size.x, this.size.y
		);

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.size.x, this.size.y
		);
	}

	Display.prototype.drawCircle = function(center, radius, colorBorder)
	{
		this.graphics.strokeStyle = colorBorder;

		this.graphics.beginPath();
		this.graphics.arc(center.x, center.y, radius, 0, Polar.RadiansPerTurn);
		this.graphics.stroke();
	}

	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}

	Display.prototype.drawText = function(text, height, pos, color)
	{
		this.graphics.strokeStyle = this.colorBack;
		this.graphics.strokeText(text, pos.x, pos.y + height);

		this.graphics.fillStyle = color;
		this.graphics.fillText(text, pos.x, pos.y + height);
	}
}

function Globals()
{
	// Do nothing.
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(timerTicksPerSecond, display, world)
	{
		this.display = display;
		this.display.initialize();

		this.world = world;

		this.inputHelper = new InputHelper();
		
		var millisecondsPerTimerTick = Math.floor(1000 / this.timerTicksPerSecond);
		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this), 
			millisecondsPerTimerTick
		);

		this.inputHelper.initialize();
	}

	// events

	Globals.prototype.handleEventTimerTick = function()
	{
		this.world.drawToDisplay(this.display);
		this.world.updateForTimerTick();
	}
}

function InputHelper()
{
	this.keysPressed = [];
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	InputHelper.prototype.removeKey = function(key)
	{
		if (this.keysPressed[key] != null)
		{
			this.keysPressed.splice(this.keysPressed.indexOf(key), 1);
			delete this.keysPressed[key];
		}
	}

	// events 

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var key = event.key;
		if (this.keysPressed[key] == null)
		{
			this.keysPressed.push(key);
			this.keysPressed[key] = key;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.removeKey(event.key);
	}

}

function Polar(azimuthInTurns, radius)
{
	this.azimuthInTurns = azimuthInTurns;
	this.radius = radius;
}
{
	Polar.RadiansPerTurn = Math.PI * 2;

	Polar.prototype.toCoords = function(coords)
	{
		var azimuthInRadians = this.azimuthInTurns * Polar.RadiansPerTurn; 
		coords.x = Math.cos(azimuthInRadians) * this.radius;
		coords.y = Math.sin(azimuthInRadians) * this.radius;
		return coords;
	}

	Polar.prototype.trimAzimuthToRangeMinMax = function(min, max)
	{
		if (this.azimuthInTurns < min)
		{
			this.azimuthInTurns = min;
		}
		else if (this.azimuthInTurns > max)
		{
			this.azimuthInTurns = max;
		}
		return this;
	}
}

function Obstacle(radius, pos)
{
	this.radius = radius;
	this.pos = pos;
}
{
	// drawable

	Obstacle.prototype.drawToDisplay = function(display)
	{
		display.drawCircle(this.pos, this.radius, "Gray");
	}
}

function Projectile(pos, vel)
{
	this.pos = pos;
	this.vel = vel;

	this.radius = 2;
	this.color = "Gray";
}
{
	Projectile.prototype.updateForTimerTick = function(world)
	{
		this.pos.add(this.vel);

		if (this.pos.x < 0 || this.pos.x > world.size.x)
		{
			this.vel.x *= -1;
		}

		if (this.pos.y < 0)
		{
			this.vel.y *= -1;
		} 
		else if (this.pos.y > world.size.y)
		{
			world.projectiles.length = 0;
		}

		this.updateForTimerTick_Collisions(world);
	}

	Projectile.prototype.updateForTimerTick_Collisions = function(world)
	{
		var collisionHelper = CollisionHelper.Instance;

		var obstacles = world.obstacles;

		for (var i = 0; i < obstacles.length; i++)
		{
			var obstacle = obstacles[i];
			var doProjectileAndObstacleCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				obstacle.pos, obstacle.radius
			);

			if (doProjectileAndObstacleCollide == true)
			{
				this.collideWithOther(obstacle);
				obstacles.remove(obstacle);
				i--;
				break;
			}
		}
	
		var actors = world.actors;

		for (var i = 0; i < actors.length; i++)
		{
			var actor = actors[i];
			var doProjectileAndActorCollide = collisionHelper.doCirclesCollide
			(
				this.pos, this.radius,
				actor.pos, actor.radius
			);
			if (doProjectileAndActorCollide == true)
			{
				this.collideWithOther(actor);
			}
		}
	}

	// drawable

	Projectile.prototype.drawToDisplay = function(display)
	{
		display.drawLine
		(
			this.pos, 
			this.pos.clone().subtract(this.vel), 
			this.color
		);

		display.drawCircle
		(
			this.pos, this.radius, this.color
		);
	}

	// collidable

	Projectile.prototype.collideWithOther = function(other)
	{
		var posAfterCollision = new Coords();
		var velAfterCollision = new Coords();
		var displacementBetweenCenters = new Coords();
		var velocityRelative = new Coords();
  
		var sumOfBodyRadii = this.radius + other.radius; 
 
		displacementBetweenCenters.overwriteWith
		(
			other.pos
		).subtract
		(
			this.pos
		);
 
		var distanceBetweenCenters = displacementBetweenCenters.magnitude();
 
		var normalAtCollision = displacementBetweenCenters.divideScalar
		(
			distanceBetweenCenters
		);
 
		var velocityAlongNormal = normalAtCollision.multiplyScalar
		(
			this.vel.dotProduct
			(
				normalAtCollision
			)
		);

		this.vel.add
		(
			velocityAlongNormal.multiplyScalar(-2)
		)
	}
}

function World(size, actor, obstacles, projectiles)
{
	this.size = size;
	this.actors = [ actor ];
	this.obstacles = obstacles;
	this.projectiles = projectiles;

	this.actorPosMin = new Coords(actor.radius, actor.pos.y);
	this.actorPosMax = new Coords(this.size.x - actor.radius, actor.pos.y);
}
{
	World.random = function(size)
	{
		var obstacleGridSizeInCells = new Coords(8, 8);
		var spaceBetweenObstacles = new Coords(1, 1).multiplyScalar
		(
			size.x / (obstacleGridSizeInCells.x + 1)
		);

		var actorPos = new Coords
		(
			size.x / 2,
			size.y - spaceBetweenObstacles.y
		);

		var actor = new Actor
		(
			actorPos,
			Activity.Instances.UserInputAccept
		);

		var obstacleRadius = Math.floor(spaceBetweenObstacles.x / 2);

		var obstacles = [];
		for (var y = 1; y <= obstacleGridSizeInCells.y; y++)
		for (var x = 1; x <= obstacleGridSizeInCells.x; x++)
		{
			var pos = new Coords(x, y).multiply(spaceBetweenObstacles);

			var obstacle = new Obstacle
			(
				obstacleRadius, pos
			);

			obstacles.push(obstacle);
		}

		var projectile = new Projectile
		(
			actor.pos.clone().subtract
			(
				new Coords(0, actor.radius + actor.projectileRadius)
			),
			new Coords(1, -1).normalize().multiplyScalar
			(
				actor.projectileSpeed
			)
		);
		
		var projectiles = [ projectile ];

		var returnValue = new World
		(
			size,
			actor,
			obstacles,
			projectiles
		);

		return returnValue;
	}

	// instance methods

	World.prototype.updateForTimerTick = function()
	{		
		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.updateForTimerTick(this);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.updateForTimerTick(this);
		}
	}

	// drawable

	World.prototype.drawToDisplay = function(display)
	{
		display.clear();

		for (var i = 0; i < this.obstacles.length; i++)
		{
			var obstacle = this.obstacles[i];
			obstacle.drawToDisplay(display);
		}

		for (var i = 0; i < this.actors.length; i++)
		{
			var actor = this.actors[i];
			actor.drawToDisplay(display);
		}

		for (var i = 0; i < this.projectiles.length; i++)
		{
			var projectile = this.projectiles[i];
			projectile.drawToDisplay(display);
		}
	}
}

// run

main();

</script>
</body>
</html>

This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s