Talking about Concentration game, Box2D, Game development, HTML5, Javascript, Phaser and TypeScript.
Planck.js is a great JavaScript library to add Box2D physics to your HTML5 games, and I already published some examples which you can find in the Box2D section of the blog.
This new example improves the physics driven Concentration prototype by rewriting it to TypeScript and using images rather than texts.
Let’s have a look at the prototype:
Click or tap on two covered boxes to uncover them, if pictures match, both boxes will be removed.
Just like in the original prototype, keep in mind that at the moment there is not game over condition – it should be when the stack of boxes becomes too high – and there’s no object pooling.
Do you like those cute animal icons? Then you can absolutely visit Kenney’s website, which provides a lot of free high quality art.
You can include Planck.js in your project with
npm install planck -dev
If you don’t know what I am talking about, then follow this tutorial and the second step and, why not, the 3rd step too.
Ok, now let’s have a look at the resources used in the game:
tiles.png: a sprite sheet containing all tiles
wall.png: an image to render game walls.
Now, let’s have a look at the completely commented source code, which consists in one html file, one css file and 7 TypeScript files:
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">
<style type = "text/css">
* {
padding: 0;
margin: 0;
}
body{
background: #000;
}
canvas {
touch-action: none;
-ms-touch-action: none;
}
</style>
<script src = "main.js"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
style.css
The style sheet of the main web page.
* {
padding : 0;
margin : 0;
}
canvas {
touch-action : none;
-ms-touch-action : none;
}
gameOptions.ts
Configurable game options.
// CONFIGURABLE GAME OPTIONS
export const GameOptions = {
// world scale to convert Box2D meters to pixels. 1 meter = 30 pixels
worldScale: 30,
// game gravity
gameGravity: 8,
// amount of boxes when the game starts
startingBoxes: 16,
// box size, in pixels
boxSize: 100,
// delay between two boxes, in milliseconds
boxDelay: 1500,
// time before box value is hidden, in milliseconds
timeBeforeHide: 2000
}
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 : 700,
height : 1244
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x1dc8fc,
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 sprite sheet and the image to be used for the walls.
// 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 a sprite sheet
this.load.spritesheet('tiles', 'assets/tiles.png', {
frameWidth: 136,
frameHeight: 136
});
// this is how we preload an image
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');
}
}
playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
import * as planck from 'planck';
import { GameOptions } from './gameOptions';
import { PhysicsBox } from './physicsBox';
import { PhysicsWall } from './physicsWall';
import { toMeters, toPixels } from './planckUtils';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
// Box2d world
world : planck.World;
// variable to store game width, in pixels
gameWidth: number;
// variable to store game height, in pixels
gameHeight : number;
// array where to store the two selected boxes
selectedBoxes : planck.Body[];
// constructor
constructor() {
super({
key: 'PlayGame'
});
}
// method to be executed when the scene has been created
create() : void {
// save game width and height in a variable
this.gameWidth = this.game.config.width as number;
this.gameHeight = 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, GameOptions.gameGravity);
// this is how we create a Box2D world
this.world = new planck.World(gravity);
// add static physics boxes
new PhysicsWall(this, this.world, this.gameWidth / 2, this.gameHeight - 20, this.gameWidth, 40);
new PhysicsWall(this, this.world, 20, this.gameHeight / 2, 40, this.gameHeight);
new PhysicsWall(this, this.world, this.gameWidth - 20, this.gameHeight / 2, 40, this.gameHeight);
// time event to place the first boxes
let firstTimeEvent : Phaser.Time.TimerEvent = this.time.addEvent({
// event delay, in milliseconds
delay : 200,
// how many times do we repeat the event?
repeat : GameOptions.startingBoxes,
// callback function
callback: () => {
// add a new physics box
new PhysicsBox(this, this.world, Phaser.Math.Between(100, this.gameWidth - 100), -100, GameOptions.boxSize, GameOptions.boxSize, GameOptions.timeBeforeHide);
// is this the last time we have to repeat the event?
if (firstTimeEvent.repeatCount == 0) {
// time event to place the remaining boxes
this.time.addEvent({
// event delay, in milliseconds
delay : GameOptions.boxDelay,
// callback function
callback : () => {
// add a new physics box
new PhysicsBox(this, this.world, Phaser.Math.Between(100, this.gameWidth - 100), -100, 100, 100, GameOptions.timeBeforeHide);
},
// repeat the event forever
loop : true
});
}
}
});
// array where to store the two selected boxes
this.selectedBoxes = [];
// input listener
this.input.on('pointerdown', this.selectBox, this);
}
// method to select a box
selectBox(event : Phaser.Input.Pointer) : void {
// did we select less than 2 boxes?
if (this.selectedBoxes.length < 2) {
// loop through all bodies
for (let body : planck.Body = this.world.getBodyList() as planck.Body; body; body = body.getNext() as planck.Body) {
// loop through all fixtures
for (let fixture : planck.Fixture = body.getFixtureList() as planck.Fixture; fixture; fixture = fixture.getNext() as planck.Fixture) {
// if the fixture contains the input coordinate...
if (fixture.testPoint(new planck.Vec2(toMeters(event.x), toMeters(event.y)))) {
// get body userData
let userData : any = body.getUserData();
// if the body is dynamic and covered
if (body.isDynamic() && userData.covered) {
// show actual box face
userData.sprite.setFrame(userData.value);
// the box is no longer covered
userData.covered = false;
// push the box in selectedBoxes array
this.selectedBoxes.push(body);
// does selectedBoxes array contain two boxes?
if (this.selectedBoxes.length == 2) {
// wait 1/2 seconds
this.time.addEvent({
// event delay, in milliseconds
delay : 500,
// callback function
callback : () => {
// get userData of both boxes
let userData : any[] = [this.selectedBoxes[0].getUserData(), this.selectedBoxes[1].getUserData()];
// do boxes have the same value?
if (userData[0].value == userData[1].value) {
// destroy the sprites
userData[0].sprite.destroy();
userData[1].sprite.destroy();
// destroy the bodies
this.world.destroyBody(this.selectedBoxes[0]);
this.world.destroyBody(this.selectedBoxes[1]);
}
// do boxes have different values?
else {
// hide boxes images
userData[0].sprite.setFrame(10);
userData[1].sprite.setFrame(10);
// set boxes as covered
userData[0].covered = true;
userData[1].covered = true;
}
// empty selectedBoxes array
this.selectedBoxes = [];
}
})
}
}
}
}
}
}
}
// method to be executed at each frame
update() : void {
// 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();
// iterate through all bodies
for (let body : planck.Body = this.world.getBodyList() as planck.Body; body; body = body.getNext() as planck.Body) {
// get body position
let bodyPosition : planck.Vec2 = body.getPosition();
// get body angle, in radians
let bodyAngle : number = body.getAngle();
// get body user data, the graphics object
let userData : any = body.getUserData();
// adjust graphic object position and rotation
userData.sprite.x = toPixels(bodyPosition.x);
userData.sprite.y = toPixels(bodyPosition.y);
userData.sprite.rotation = bodyAngle;
}
}
}
planckUtils.ts
Just a couple of functions to convert pixels, used by Phaser, to meters, used by Planck, and to convert pixels to meters.
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;
}
physicsWall.ts
Custom class for the physics walls, a static Box2D body.
import * as planck from 'planck';
import { toMeters } from './planckUtils';
// this class extends Phaser Sprite class
export class PhysicsWall extends Phaser.GameObjects.Sprite {
constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, width : number, height : number) {
super(scene, posX, posY, 'wall');
// adjust sprite display width and height
this.displayWidth = width;
this.displayHeight = height;
// add sprite to scene
scene.add.existing(this);
// this is how we create a generic Box2D body
let box : planck.Body = world.createBody();
// a body can have one or more fixtures. This is how we create a box fixture inside a body
box.createFixture(planck.Box(toMeters(width / 2), toMeters(height / 2)));
// now we place the body in the world
box.setPosition(planck.Vec2(toMeters(posX), toMeters(posY)));
// time to set mass information
box.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
});
// a body can have anything in its user data, normally it's used to store its sprite
box.setUserData({
sprite : this,
});
}
}
physicsBox.ts
Custom class for the physics box, a dynamic Box2D body.
import * as planck from 'planck';
import { toMeters } from './planckUtils';
// this class extends planck Phaser Sprite class
export class PhysicsBox extends Phaser.GameObjects.Sprite {
constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, width : number, height : number, hideAfter : number) {
super(scene, posX, posY, 'tiles');
// adjust sprite display width and height
this.displayWidth = width;
this.displayHeight = height;
// add sprite to scene
scene.add.existing(this);
// this is how we create a generic Box2D body
let box : planck.Body = world.createBody();
// Box2D bodies are created as static bodies, but we can make them dynamic
box.setDynamic();
// a body can have one or more fixtures. This is how we create a box fixture inside a body
box.createFixture(planck.Box(toMeters(width / 2), toMeters(height / 2)));
// now we place the body in the world
box.setPosition(planck.Vec2(toMeters(posX), toMeters(posY)));
// time to set mass information
box.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
});
// initial random value
let randomValue : number = -1;
// set a random value
randomValue = Phaser.Math.Between(0, 9);
// set sprite frame to randomValue
this.setFrame(randomValue);
// set a timed event to hide box image
let timedEvent : Phaser.Time.TimerEvent = scene.time.addEvent({
// event delay
delay : hideAfter,
// optional arguments: the sprite itself
args : [this],
// callback function
callback : () => {
// set frame to 10 (cover)
this.setFrame(10);
// get box user data
let userData : any = box.getUserData();
// set box as covered
userData.covered = true;
}
});
// a body can have anything in its user data, normally it's used to store its sprite
box.setUserData({
sprite : this,
value : randomValue,
covered : false,
event : timedEvent
});
}
}
And now you can have your physics driven Concentration game. Remember to add a game over condition and a pooling system to save resources. Download the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.