Talking about Block it game, Box2D, Game development, HTML5, Javascript, Phaser and TypeScript.
Finally the HTML5 “Block it” prototype has a score system and a particle explosion when the game is over.
The prototype uses Box2D to handle the physics, powered by Planck.js. I also used some advanced features like compound objects and custom contact listeners.
If you are new to this tutorial series, let’s make a small recap:
In first step I built the basic prototype with compound objects and listeners, but the compound object was rendered using a graphic object.
In second step some sprites have been used to display the compound object.
To make the prototype somehow playable, in third step I added an energy system.
Now I fixed the energy system which had a little bug, turned the squared canvas into a portrait one, ready to be ported on mobile devices, added a scoring system and a particle explosion when the game is over.
Have a look at the example:
Tap the canvas to activate the walls when the ball is about to hit them, but don’t waste too much energy.
Remember to activate walls before the ball hits them, or it’s game over.
On the left, you can see the energy. On the right, the score.
Let’s see the source code, made of one html file, one css file and 7 TypeScript files. Since previous example, I only edited main.ts to change the resolution and the background color, playGame.ts to add the new game logic and gameOptions.ts to change some gameplay paramters.
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="style.css">
</style>
<script src="main.js"></script>
</head>
<body>
<div id="thegame"></div>
</body>
</html>
style.css
The cascading style sheets of the main web page.
* {
padding : 0;
margin : 0;
}
canvas {
touch-action : none;
-ms-touch-action : none;
}
gameOptions.ts
Configurable game options. It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game.
// CONFIGURABLE GAME OPTIONS
export const GameOptions = {
// unit used to convert pixels to meters and meters to pixels
worldScale : 30,
// ball radius, in pixels
ballRadius : 15,
// ball start speed, in meters/second
ballStartSpeed : 3,
// ball speed increase at each collision, in meters/second
ballSpeedIncrease : 0.1,
// wall width, in pixels
wallTickness : 20,
// wall edge distance from center, in pixels
wallDistanceFromCenter : 250,
// game energy, that is the amount in milliseconds of active wall time
energy : 5000
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT,
autoCenter : Phaser.Scale.CENTER_BOTH,
parent : 'thegame',
width : 540,
height : 960
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x2c3e50,
scale : scaleObject,
scene : [PreloadAssets, PlayGame]
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Here we preload all assets to be used in the game, such as the sprites used for the ball and the wall.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be execute during class preloading
preload(): void {
// this is how we preload an image
this.load.image('ball', 'assets/ball.png');
this.load.image('wall', 'assets/wall.png');
}
// method to be called once the instance has been created
create(): void {
// call PlayGame class
this.scene.start('PlayGame');
}
}
planckUtils.ts
Just a couple of functions because Box2D works with meters while Phaser and most frameworks work with pixels. Here is where we convert pixels to meters and meters to pixels.
import { GameOptions } from './gameOptions';
// simple function to convert pixels to meters
export function toMeters(n : number) : number {
return n / GameOptions.worldScale;
}
// simple function to convert meters to pixels
export function toPixels(n: number) : number {
return n * GameOptions.worldScale;
}
playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
// enum to represent the game states
enum GameState {
Waiting,
Playing,
Over
}
import * as planck from 'planck';
import { GameOptions } from './gameOptions';
import { PlanckCompound } from './planckCompound';
import { PlanckBall } from './planckBall';
import { toPixels } from './planckUtils';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
// Box2d world
world : planck.World;
// the ball
theBall : PlanckBall;
// planck compound object
theCompound : PlanckCompound;
// graphics objects to render the compound object
compoundGraphics : Phaser.GameObjects.Graphics;
// keep track of the number of bounces to restart the demo at 50
bounces : number;
// variable to store current game state
gameState : GameState;
// variable to save the time the player touched the input to activate the wall
downTime : number;
// walls energy
energy : number;
// text object to display the energy
energyText : Phaser.GameObjects.Text;
// text object to display the amount of bounces, the score of this game
bounceText : Phaser.GameObjects.Text;
// constructor
constructor() {
super({
key: 'PlayGame'
});
}
// method to be executed when the scene has been created
create() : void {
// game state is set to "Waiting" (for player input)
this.gameState = GameState.Waiting;
// fill the energy according to game options
this.energy = GameOptions.energy;
// zero bounces at the beginning
this.bounces = 0
// just a couple of variables to store game width and height
let gameWidth : number = this.game.config.width as number;
let gameHeight : number = this.game.config.height as number;
// world gravity, as a Vec2 object. It's just a x, y vector
let gravity = new planck.Vec2(0, 0);
// this is how we create a Box2D world
this.world = new planck.World(gravity);
// add the planck ball
this.theBall = new PlanckBall(this, this.world, gameWidth / 2, gameHeight / 2, GameOptions.ballRadius, 'ball');
// add the planck compound object
this.theCompound = new PlanckCompound(this, this.world, gameWidth / 2, gameHeight / 2, GameOptions.wallDistanceFromCenter, GameOptions.wallTickness);
// add simulation graphics
this.compoundGraphics = this.add.graphics();
// add an event listener waiting for a contact to solve
this.world.on('post-solve', () => {
// handle ball bounce increasing speed
this.theBall.handleBounce(this.theBall.angleToReflect, GameOptions.ballSpeedIncrease);
// increase number of bounces
this.bounces ++;
// update bounce text
this.updateBounceText();
});
// add an event listener waiting for a contact to pre solve
this.world.on('pre-solve', (contact : planck.Contact) => {
// if the compound is not active or if it's game over...
if (!this.theCompound.active || this.gameState == GameState.Over) {
// do not enable the contact
contact.setEnabled(false);
// if it's not game over...
if (this.gameState != GameState.Over) {
// ...now it is! :)
this.gameState = GameState.Over
// create a particle manager with the same image used for the ball
let particleEmitter : Phaser.GameObjects.Particles.ParticleEmitterManager = this.add.particles('ball');
// create a particle emitter
let emitter = particleEmitter.createEmitter({
// particle life span, in milliseconds
lifespan : 1500,
// particle speed range
speed : {
min : 10,
max : 30
},
// particle alpha tweening
alpha : {
start : 0.7,
end : 0.2
},
// particle scale tweening
scale : {
start : 0.1,
end : 0.05
}
});
// let the emitter fire 100 particles at ball position
emitter.explode(100, this.theBall.x, this.theBall.y);
// hide the ball
this.theBall.visible = false;
// add a time event
this.time.addEvent({
// start in 2 seconds
delay : 2000,
// callback function scope
callbackScope : this,
// callback function
callback: () => {
// restart the game
this.scene.start('PlayGame');
}
});
}
// exit the method
return;
}
// get edge fixture in this circle Vs edge contact
let edgeFixture : planck.Fixture = this.getEdgeFixture(contact);
// get edge body
let edgeBody : planck.Body = edgeFixture.getBody();
// get edge shape
let edgeShape : planck.Edge = edgeFixture.getShape() as planck.Edge;
// did the ball just collided with this edge?
if (this.theBall.sameEdgeCollision(edgeShape)) {
// disable the contact
contact.setEnabled(false);
// exit callback function
return;
}
// get edge shape vertices
let worldPoint1 : planck.Vec2 = edgeBody.getWorldPoint(edgeShape.m_vertex1);
let worldPoint2 : planck.Vec2 = edgeBody.getWorldPoint(edgeShape.m_vertex2);
// transform the planck edge into a Phaser line
let worldLine : Phaser.Geom.Line = new Phaser.Geom.Line(toPixels(worldPoint1.x), toPixels(worldPoint1.y), toPixels(worldPoint2.x), toPixels(worldPoint2.y));
// determine bounce angle
this.theBall.determineBounceAngle(worldLine);
});
// add the text object to show the amount of energy
this.energyText = this.add.text(10, 10, this.energy.toString(), {
font : '64px Arial'
});
// add the text object to show the amount of bounces
this.bounceText = this.add.text(gameWidth - 10, 10, this.bounces.toString(), {
font : '64px Arial'
});
// set bounce text registration point
this.bounceText.setOrigin(1, 0);
// wait for input start to call activateWalls method
this.input.on('pointerdown', this.activateWalls, this);
// wait for input end to call deactivateWalls method
this.input.on('pointerup', this.deactivateWalls, this);
}
// method to update energy text
updateEnergyText(energy : number) : void {
// set energy text to energy value itself or zero
this.energyText.setText(Math.max(0, energy).toString());
}
updateBounceText() : void {
this.bounceText.setText(this.bounces.toString());
}
// method to activate compound walls
activateWalls(e : Phaser.Input.Pointer) : void {
// is the game state set to "Waiting" (for player input)?
if (this.gameState == GameState.Waiting) {
// give the ball a random velocity
this.theBall.setRandomVelocity(GameOptions.ballStartSpeed);
// game state is now "Playing"
this.gameState = GameState.Playing;
return;
}
// do we have energy and are we still in game?
if (this.energy > 0 && this.gameState != GameState.Over) {
// set compound walls to active
this.theCompound.setActive(true);
// save the current timestamp
this.downTime = e.downTime;
}
}
// method to deactivate compound walls
deactivateWalls(e : Phaser.Input.Pointer) : void {
// if game state is set to "Waiting" the exit the function
if (this.gameState == GameState.Waiting) {
return;
}
// are we playing?
if (this.gameState == GameState.Playing) {
// set compound walls to inactive
this.theCompound.setActive(false);
// determine how long the input has been pressed
let ellapsedTime : number = Math.round(e.upTime - e.downTime);
// subtract the ellapsed time from energy
this.energy -= ellapsedTime;
// update energy text
this.updateEnergyText(this.energy);
}
}
// method to get the edge fixture in a edge Vs circle contact
getEdgeFixture(contact : planck.Contact) : planck.Fixture {
// get first contact fixture
let fixtureA : planck.Fixture = contact.getFixtureA();
// get first contact shape
let shapeA : planck.Shape = fixtureA.getShape();
// is the shape an edge? Return the first contact fixture, else return second contact fixture
return (shapeA.getType() == 'edge') ? fixtureA : contact.getFixtureB();
}
// method to be called at each frame
update(time : number) : void {
// is the compound object active?
if (this.theCompound.active) {
// determine remaining energy subtracting from current energy the amount of time we are pressing the input
let remainingEnergy : number = this.energy - Math.round(time - this.downTime);
// if remaining energy is less than zero...
if (remainingEnergy <= 0) {
// set energy to zero
this.energy = 0;
// turn off the compound
this.theCompound.setActive(false);
}
// update energy text
this.updateEnergyText(remainingEnergy);
}
// advance the simulation by 1/30 seconds
this.world.step(1 / 30);
// crearForces method should be added at the end on each step
this.world.clearForces();
// update ball position
this.theBall.updatePosition();
// draw walls of compound object
this.theCompound.drawWalls(this.compoundGraphics);
}
}
planckBall.ts
Custom class extending Phaser.GameObjects.Sprite to define the planck ball as a Phaser sprite.
import * as planck from 'planck';
import { toMeters, toPixels } from './planckUtils';
// this class extends planck Phaser Sprite class
export class PlanckBall extends Phaser.GameObjects.Sprite {
// planck body
planckBody : planck.Body;
// ball speed
speed : number;
// last collided edge, to prevent colliding with the same edge twice
lastCollidedEdge : planck.Edge;
// angle to reflect after a collision
angleToReflect : number;
constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, radius : number, key : string) {
super(scene, posX, posY, key);
// adjust sprite display width and height
this.displayWidth = radius * 2;
this.displayHeight = radius * 2;
// add sprite to scene
scene.add.existing(this);
// this is how we create a generic Box2D body
this.planckBody = world.createBody();
// set the body as bullet for continuous collision detection
this.planckBody.setBullet(true)
// Box2D bodies are created as static bodies, but we can make them dynamic
this.planckBody.setDynamic();
// a body can have one or more fixtures. This is how we create a circle fixture inside a body
this.planckBody.createFixture(planck.Circle(toMeters(radius)));
// now we place the body in the world
this.planckBody.setPosition(planck.Vec2(toMeters(posX), toMeters(posY)));
// time to set mass information
this.planckBody.setMassData({
// body mass
mass : 1,
// body center
center : planck.Vec2(),
// I have to say I do not know the meaning of this "I", but if you set it to zero, bodies won't rotate
I : 1
});
}
// method to set a random velocity, given a speed
setRandomVelocity(speed : number) : void {
// save the speed as property
this.speed = speed;
// set a random angle
let angle : number = Phaser.Math.Angle.Random();
// set ball linear velocity according to angle and speed
this.planckBody.setLinearVelocity(new planck.Vec2(this.speed * Math.cos(angle), this.speed * Math.sin(angle)))
}
// method to check if an edge is the one we already collided with
sameEdgeCollision(edge : planck.Edge) : boolean {
// check if current edge is the same as last collided edge
let result : boolean = this.lastCollidedEdge == edge;
// update last collided edge
this.lastCollidedEdge = edge;
// return the result
return result;
}
// method to handle bounce increasing speed number
handleBounce(angle : number, speedIncrease : number) : void {
// increase speed
this.speed += speedIncrease;
// set linear velocity according to angle and speed
this.planckBody.setLinearVelocity(new planck.Vec2(this.speed * Math.cos(angle), this.speed * Math.sin(angle)));
}
// method to determine bounce angle against a line
determineBounceAngle(line : Phaser.Geom.Line) : void {
// get linear velocity
let velocity : planck.Vec2 = this.planckBody.getLinearVelocity();
// transform linear velocity into a line
let ballLine : Phaser.Geom.Line = new Phaser.Geom.Line(0, 0, velocity.x, velocity.y);
// calculate reflection angle between two lines
this.angleToReflect = Phaser.Geom.Line.ReflectAngle(ballLine, line);
}
// method to update ball position
updatePosition() : void {
// get ball planck body position
let ballBodyPosition : planck.Vec2 = this.planckBody.getPosition();
// update ball position
this.setPosition(toPixels(ballBodyPosition.x), toPixels(ballBodyPosition.y));
}
}
planckCompound.ts
Custom class to define the planck compound object.
import * as planck from 'planck';
import { toMeters, toPixels } from './planckUtils';
export class PlanckCompound {
// planck body
planckBody : planck.Body;
// flag to check if the compound is active
active : boolean;
// sprites to render the compound
sprites : Phaser.GameObjects.Sprite[];
constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, offset : number, tickness : number) {
// this is how we create a generic Box2D body
let compound : planck.Body = world.createKinematicBody();
// set body position
compound.setPosition(new planck.Vec2(toMeters(posX), toMeters(posY)))
// add edge fixtures to body
compound.createFixture(planck.Edge(new planck.Vec2(toMeters(0), toMeters(-offset)), new planck.Vec2(toMeters(offset), toMeters(0))));
compound.createFixture(planck.Edge(new planck.Vec2(toMeters(offset), toMeters(0)), new planck.Vec2(toMeters(0), toMeters(offset))));
compound.createFixture(planck.Edge(new planck.Vec2(toMeters(0), toMeters(offset)), new planck.Vec2(toMeters(-offset), toMeters(0))));
compound.createFixture(planck.Edge(new planck.Vec2(toMeters(-offset), toMeters(0)), new planck.Vec2(toMeters(0), toMeters(-offset))));
// give the compound object a random angle
compound.setAngle(Phaser.Math.Angle.Random());
// give the compound object an angular velocity, to make it rotate a bit
compound.setAngularVelocity(0.1);
// assign the body to planckBody property
this.planckBody = compound;
// calculate edge length with Pythagorean theorem
let edgeLength : number = Math.sqrt(offset * offset + offset * offset);
// create sprites to render the compound
this.sprites = [
scene.add.sprite(0, 0, 'wall'),
scene.add.sprite(0, 0, 'wall'),
scene.add.sprite(0, 0, 'wall'),
scene.add.sprite(0, 0, 'wall')
]
// loop through all sprites
this.sprites.forEach((sprite : Phaser.GameObjects.Sprite) => {
// adjust display size
sprite.setDisplaySize(edgeLength + tickness * 2, tickness);
// set sprite origin to horizontal center, vertical bottom
sprite.setOrigin(0.5, 1);
// set sprite to semi transparent
sprite.setAlpha(0.3);
});
// the compound is not active at the beginning
this.active = false;
}
// method to set the compound walls to active or non active
setActive(isActive : boolean) : void {
// loop through all sprites
this.sprites.forEach((sprite : Phaser.GameObjects.Sprite) => {
// set sprite to semi transparent if the compound is not active, fully opaque otherwise
sprite.setAlpha(isActive ? 1 : 0.3);
});
// set acrive property according to method argument
this.active = isActive;
}
// method to draw compount walls in a Graphics game object
drawWalls(graphics : Phaser.GameObjects.Graphics) : void {
// clear the graphics
graphics.clear();
// set line style to one pixel black
graphics.lineStyle(1, 0x000000);
// just a counter to see how many fixtures we processed so far
let fixtureIndex : number = 0;
// loop through all body fixtures
for (let fixture : planck.Fixture = this.planckBody.getFixtureList() as planck.Fixture; fixture; fixture = fixture.getNext() as planck.Fixture) {
// get fixture edge
let edge : planck.Edge = fixture.getShape() as planck.Edge;
// get edge vertices
let lineStart : planck.Vec2 = this.planckBody.getWorldPoint(edge.m_vertex1);
let lineEnd : planck.Vec2 = this.planckBody.getWorldPoint(edge.m_vertex2);
// turn the planck edge into a Phaser line
let drawLine : Phaser.Geom.Line = new Phaser.Geom.Line(
toPixels(lineStart.x),
toPixels(lineStart.y),
toPixels(lineEnd.x),
toPixels(lineEnd.y)
);
// get line mid point
let lineMidPoint : Phaser.Geom.Point = Phaser.Geom.Line.GetMidPoint(drawLine);
// get line angle
let lineAngle : number = Phaser.Geom.Line.Angle(drawLine);
// stroke the line shape
graphics.strokeLineShape(drawLine);
// place the sprite in the middle of the edge
this.sprites[fixtureIndex].setPosition(lineMidPoint.x, lineMidPoint.y);
// rotate the sprite according to edge angle
this.sprites[fixtureIndex].setRotation(lineAngle);
// increase the counter processed texture
fixtureIndex ++;
}
}
}
Now you should just find a way to let the player get some extra time. I have a couple of ideas, you will see them in the final version, meanwhile download the source code.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.