<Emanuele Feronato/>
home

HTML5 physics Word game prototype using Phaser and Planck.js

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.

230 games covered