HTML5 physics Word game prototype using Phaser and Planck.js
Talking about Word Game game, Box2D, Game development, HTML5, Javascript and Phaser.
If you liked the series about word games and want to see how you can build a prototype of a game like worDrop, which was played more than 4 million times during Flash era, here is a quick HTML5 prototype.
Keep in mind that at the moment I am not recognizing words, but you can easily do it following this post, there is not game over condition – it should be when the stack of boxes becomes too high – and there’s no object pooling.
Moreover, letters appear in a random way but you should make some letters to appear more frequently, according to the language of your game.
But it’s a good start to build physics driven word games.
Look at the prototype:
Click on letters to build a word, and click on the word itself, or the bottom of the canvas, to remove letters.
And this is the source code, completely commented:
let game;
let gameOptions = {
// world scale to convert Box2D meters to pixels
worldScale: 30,
gameGravity: 8,
// amount of letters when the game starts
startingLetters: 16,
// letter size, in pixels
letterSize: 100,
// delay between two letters, in mmilliseconds
letterDelay: 1500
}
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, see how it works at line 55
this.createBox(game.config.width / 2, game.config.height - 80, game.config.width, 160, 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 letters
this.startingTimer = this.time.addEvent({
delay: 200,
callbackScope: this,
callback: function() {
this.createBox(Phaser.Math.Between(100, game.config.width - 100), -100, gameOptions.letterSize, gameOptions.letterSize, true);
if (this.startingTimer.repeatCount ==0) {
// time event to place the remaining letters
this.inGameLetters = this.time.addEvent({
delay: gameOptions.letterDelay,
callbackScope: this,
callback: function() {
this.createBox(Phaser.Math.Between(100, game.config.width - 100), -100, 100, 100, true);
},
loop: true
});
}
},
repeat: gameOptions.startingLetters
});
// text to display the word
this.wordText = this.add.text(game.config.width / 2, game.config.height - 80, "", {
font: "64px arial",
fill: "#000000"
});
this.wordText.setOrigin(0.5)
// input listener
this.input.on("pointerdown", this.writeWord, this);
}
writeWord(event) {
// if we are clicking on the bottom of the canvas...
if (event.y > game.config.height - 160) {
// delete the word
this.wordText.text = "";
// loop through all bodies
for (let body = this.world.getBodyList(); body; body = body.getNext()) {
// get body userData
let userData = body.getUserData();
// if the body sprite is semi transparent...
if (userData.sprite.alpha == 0.25) {
// destroy the sprite
userData.sprite.destroy();
// destroy the letter
userData.letter.destroy();
// destroy the body itself
this.world.destroyBody(body);
}
}
}
// if we are NOT clicking on the bottom of the canvas...
else {
// 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 is fully opaque and the body is dynamic (this means it's a letter)
if (userData.sprite.alpha == 1 && body.isDynamic()) {
// set the sprite to semi transparent
userData.sprite.alpha = 0.25;
// update text string
this.wordText.text += userData.letter.text;
}
}
}
}
}
}
// 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 letter by default
let letter = null;
// if isDynamic is set to true
if (isDynamic) {
// assign the box a random letter
letter = this.add.text(0, 0, String.fromCharCode(65+Math.floor(Math.random() * 26)), {
font: "64px arial",
fill: "#000000"
})
letter.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 box user data
let userData = {
sprite: debugDraw,
letter: letter
}
// 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 letter on the box...
if (userData.letter) {
// adjust letter position and rotation
userData.letter.x = userData.sprite.x;
userData.letter.y = userData.sprite.y;
userData.letter.rotation = bodyAngle;
}
}
}
};
My worDrop game begins to take shape, let’s see if I manage to achieve 4 millions more plays, 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.