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.