HTML5 physics Word game prototype using Phaser and Planck.js

Read all posts about "" game

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.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers