Talking about Concentration game, Box2D, Game development, HTML5, Javascript and Phaser.
Did you enjoy the HTML5 physics Word game prototype using Phaser and Planck.js?
Adding physics to old game concepts is a great idea to give old gameplays a new life. I already added physics to a classic game in SamePhysics, my first Box2D game whose source code is available at this page.
Today we are going to add physics to one of the oldest game concepts: Concentration, which I already ported in some languages.
Look at the game:
Click or tap on two covered boxes to uncover them, if numbers match, both boxes will be removed.
Just like in Word game example, 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.
And here is the completely commented source code:
let game;
let gameOptions = {
// world scale to convert Box2D meters to 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
}
window.onload = function() {
let gameConfig = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 750,
height: 1334
},
scene: playGame
}
game = new Phaser.Game(gameConfig);
window.focus();
}
class playGame extends Phaser.Scene {
constructor() {
super("PlayGame");
}
create() {
// world gravity, as a Vec2 object. It's just a x, y vector
let gravity = planck.Vec2(0, 8);
// this is how we create a Box2D world
this.world = planck.World(gravity);
// createBox is a method I wrote to create a box
this.createBox(game.config.width / 2, game.config.height - 20, game.config.width, 40, false);
this.createBox(20, game.config.height / 2, 40, game.config.height, false);
this.createBox(game.config.width - 20, game.config.height / 2, 40, game.config.height, false);
// time event to place the first boxes
this.startingTimer = this.time.addEvent({
delay: 200,
callbackScope: this,
callback: function() {
this.createBox(Phaser.Math.Between(100, game.config.width - 100), -100, gameOptions.boxSize, gameOptions.boxSize, true);
if (this.startingTimer.repeatCount == 0) {
// time event to place the remaining boxes
this.inGameBoxes = this.time.addEvent({
delay: gameOptions.boxDelay,
callbackScope: this,
callback: function() {
this.createBox(Phaser.Math.Between(100, game.config.width - 100), -100, 100, 100, true);
},
loop: true
});
}
},
repeat: gameOptions.startingBoxes
});
// array where to store the two selected boxes
this.selectedBoxes = [];
// input listener
this.input.on("pointerdown", this.selectBox, this);
}
selectBox(event) {
// did we select less than 2 boxes?
if (this.selectedBoxes.length < 2) {
// loop through all bodies
for (let body = this.world.getBodyList(); body; body = body.getNext()) {
// loop through all fixtures
for (let fixture = body.getFixtureList(); fixture; fixture = fixture.getNext()) {
// if the fixture contains the input coordinate...
if (fixture.testPoint(new planck.Vec2(this.toWorldScale(event.x), this.toWorldScale(event.y)))) {
// get body userData
let userData = body.getUserData();
// if the sprite has a "?" on it...
if (body.isDynamic() && userData.text.text == "?") {
// show box value
userData.text.text = userData.value;
// 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({
delay: 500,
callbackScope: this,
callback: function() {
// get userData of both boxes
let userData = [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 texts
userData[0].text.destroy();
userData[1].text.destroy();
// destroy the bodies
this.world.destroyBody(this.selectedBoxes[0]);
this.world.destroyBody(this.selectedBoxes[1]);
}
// do boxes have different values?
else {
// hide boxes values
userData[0].text.text = "?";
userData[1].text.text = "?";
}
// empty selectedBoxes array
this.selectedBoxes = [];
}
})
}
}
}
}
}
}
return;
}
// simple function to convert pixels to meters
toWorldScale(n) {
return n / gameOptions.worldScale;
}
// here we go with some Box2D stuff
// arguments: x, y coordinates of the center, with and height of the box, in pixels
// we'll conver pixels to meters inside the method
createBox(posX, posY, width, height, isDynamic) {
// this is how we create a generic Box2D body
let box = this.world.createBody();
if (isDynamic) {
// Box2D bodies born 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(width / 2 / gameOptions.worldScale, height / 2 / gameOptions.worldScale));
// now we place the body in the world
box.setPosition(planck.Vec2(posX / gameOptions.worldScale, posY / gameOptions.worldScale));
// time to set mass information
box.setMassData({
mass: 1,
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
});
// now we create a graphics object representing the body
let color = new Phaser.Display.Color();
// draw the square
let debugDraw = this.add.graphics();
// a box has no value text by default
let valueText = null;
// initial random value
let randomValue = -1;
// if isDynamic is set to true
if (isDynamic) {
// set a random value
randomValue = Phaser.Math.Between(0, 9);
// text to write a random value on the box
valueText = this.add.text(0, 0, randomValue, {
font: "64px arial",
fill: "#000000"
})
valueText.setOrigin(0.5);
// assign the box a random color
color.random();
color.brighten(50).saturate(100);
}
// if isDynamic is set to true, assign the box a grey color
else {
color.setTo(128, 128, 128);
}
// draw the rectangle
debugDraw.fillStyle(color.color, 1);
debugDraw.fillRect(- width / 2, - height / 2, width, height);
// set a timed event to hide box value
let timedEvent = this.time.addEvent({
delay: gameOptions.timeBeforeHide,
args: [valueText],
callback: function(valueText) {
if (valueText) {
valueText.text = "?"
}
}
});
// set box user data
let userData = {
sprite: debugDraw,
text: valueText,
value: randomValue,
event: timedEvent
}
// a body can have anything in its user data, normally it's used to store its sprite
box.setUserData(userData);
}
update() {
// advance the simulation by 1/20 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 = this.world.getBodyList(); body; body = body.getNext()) {
// get body position
let bodyPosition = body.getPosition();
// get body angle, in radians
let bodyAngle = body.getAngle();
// get body user data, the graphics object
let userData =body.getUserData();
// adjust graphic object position and rotation
userData.sprite.x = bodyPosition.x * gameOptions.worldScale;
userData.sprite.y = bodyPosition.y * gameOptions.worldScale;
userData.sprite.rotation = bodyAngle;
// if there is a text on the box...
if (userData.text) {
// adjust text position and rotation
userData.text.x = userData.sprite.x;
userData.text.y = userData.sprite.y;
userData.text.rotation = bodyAngle;
}
}
}
};
Which old game would you improve by adding physics? Download the source code and let me know.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.