From Tiled to HTML5 game with Box2D physics using Phaser and Planck.is – “Totem Destroyer” example

Read all posts about "" game

If you followed my latest post on the Totem Destroyer series, you probably noticed I used a lot of methods with some hardcoded values to build the level:

// totem creation
this.createBox(game.config.width / 2, game.config.height - 20, game.config.width, 40, false, TERRAIN, 0x049b15);
this.createBox(game.config.width / 2 - 60, game.config.height - 60, 40, 40, true, BREAKABLE, 0x6e5d42);
this.createBox(game.config.width / 2 + 60, game.config.height - 60, 40, 40, true, BREAKABLE, 0x6e5d42);
this.createBox(game.config.width / 2, game.config.height - 100, 160, 40, true, BREAKABLE, 0x6e5d42);
this.createBox(game.config.width / 2, game.config.height - 140, 80, 40, true, UNBREAKABLE, 0x3b3b3b);
this.createBox(game.config.width / 2 - 20, game.config.height - 180, 120, 40, true, BREAKABLE, 0x6e5d42);
this.createBox(game.config.width / 2, game.config.height - 240, 160, 80, true, UNBREAKABLE, 0x3b3b3b);
this.idol = this.createBox(game.config.width / 2, game.config.height - 320, 40, 80, true, IDOL, 0xfff43a);

This is quite annoying, because it’s very hard to modify these values, giving them a sense, if we want to change the level.

It would be better to draw the level elsewhere, then import the level and have a method to build it block by block.

This is where Tiled comes into play. Look at the picture:

I created an Object Layer, then used the Insert Rectangle tool to draw the totem, and the Object Types Editor to give blocks a type and a color.

Actually the color in Tiled editor is not exported, it’s used just to let you highlight blocks.

The exported JSON of this stuff is:

{ "compressionlevel":0,
 "editorsettings":
    {
     "export":
        {
         "format":"json",
         "target":"levels.json"
        }
    },
 "height":15,
 "infinite":false,
 "layers":[
        {
         "draworder":"topdown",
         "id":2,
         "name":"Object Layer 1",
         "objects":[
                {
                 "height":40,
                 "id":2,
                 "name":"",
                 "rotation":0,
                 "type":"Breakable",
                 "visible":true,
                 "width":40,
                 "x":440,
                 "y":520
                }, 
                {
                 "height":40,
                 "id":4,
                 "name":"",
                 "rotation":0,
                 "type":"Breakable",
                 "visible":true,
                 "width":160,
                 "x":320,
                 "y":480
                }, 
                {
                 "height":40,
                 "id":5,
                 "name":"",
                 "rotation":0,
                 "type":"Unbreakable",
                 "visible":true,
                 "width":80,
                 "x":360,
                 "y":440
                }, 
                {
                 "height":80,
                 "id":8,
                 "name":"",
                 "rotation":0,
                 "type":"Idol",
                 "visible":true,
                 "width":40,
                 "x":380,
                 "y":240
                }, 
                {
                 "height":40,
                 "id":10,
                 "name":"",
                 "rotation":0,
                 "type":"Breakable",
                 "visible":true,
                 "width":120,
                 "x":320,
                 "y":400
                }, 
                {
                 "height":40,
                 "id":11,
                 "name":"",
                 "rotation":0,
                 "type":"Breakable",
                 "visible":true,
                 "width":40,
                 "x":320,
                 "y":520
                }, 
                {
                 "height":80,
                 "id":12,
                 "name":"",
                 "rotation":0,
                 "type":"Unbreakable",
                 "visible":true,
                 "width":160,
                 "x":320,
                 "y":320
                }, 
                {
                 "height":40,
                 "id":13,
                 "name":"",
                 "rotation":0,
                 "type":"Terrain",
                 "visible":true,
                 "width":800,
                 "x":0,
                 "y":560
                }],
         "opacity":1,
         "type":"objectgroup",
         "visible":true,
         "x":0,
         "y":0
        }],
 "nextlayerid":3,
 "nextobjectid":14,
 "orientation":"orthogonal",
 "renderorder":"right-down",
 "tiledversion":"1.3.2",
 "tileheight":40,
 "tilesets":[],
 "tilewidth":40,
 "type":"map",
 "version":1.2,
 "width":20
}

There still are a lot of numbers, but I am not writing numbers anymore, Tiled does all the hard work and I only have to draw my level on the editor.

What should we do with this JSON? We import it in Phaser, obviously, but first have a look at the result:

Click on a light brick to destroy it. Light bricks are the only ones which can be destroyed. Don’t let the idol hit the ground.

And this is the completely commented source code, for you to compare with the previous one to see how easy it was to build the totem, and how simple would be to change something in the level without gettin mad with hardcoded numbers:

let game;

let gameOptions = {

    // conversion unit from pixels to meters. 30 pixels = 1 meter
    worldScale: 30
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor:0x87ceea,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 800,
            height: 600
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

// constants to store block types
const TERRAIN = 0;
const IDOL = 1;
const BREAKABLE = 2;
const UNBREAKABLE = 3;

// constant to store all block types imported from Tiled
const BLOCKTYPES = {
    "Terrain": {
        color: 0x049b15,
        dynamic: false,
        type: TERRAIN
    },
    "Breakable": {
        color: 0x6e5d42,
        dynamic: true,
        type: BREAKABLE
    },
    "Unbreakable": {
        color: 0x3b3b3b,
        dynamic: true,
        type: UNBREAKABLE
    },
    "Idol": {
        color: 0xfff43a,
        dynamic: true,
        type: IDOL
    }
}

class playGame extends Phaser.Scene {

    constructor() {
        super("PlayGame");
    }

    preload() {

        // load tiled map
        this.load.tilemapTiledJSON("level", "levels.json");
    }

    create() {

        // world gravity, as a Vec2 object. It's just a x, y vector
        let gravity = planck.Vec2(0, 3);

        // this is how we create a Box2D world
        this.world = planck.World(gravity);

        // add the tilemap
        let map = this.add.tilemap("level");

        // select all objects in Object Layer zero, the first - and only, at the moment - level
        let blocks = map.objects[0].objects;

        // looping through all blocks and execute addBlock method
        blocks.forEach(blocks => this.addBlock(blocks));

        // input listener to call destroyBlock method
        this.input.on("pointerdown", this.destroyBlock, this);

    }

    // method to add a totem block
    addBlock(block) {

        // get block object
        let blockObject = BLOCKTYPES[block.type];

        // we store block coordinates inside a Phaser Rectangle just to get its center
        let rectangle = new Phaser.Geom.Rectangle(block.x, block.y, block.width, block.height);

        // create the Box2D block with old createBox method
        let box2DBlock = this.createBox(rectangle.centerX, rectangle.centerY, block.width, block.height, blockObject.dynamic, blockObject.type, blockObject.color);

        // is this block the idol?
        if (blockObject.type == IDOL) {

            // assign it to idol variable
            this.idol = box2DBlock;
        }
    }

    // method to destroy a block
    destroyBlock(e) {

        // convert pointer coordinates to world coordinates
        let worldX = this.toMeters(e.x);
        let worldY = this.toMeters(e.y);
        let worldPoint = planck.Vec2(worldX, worldY);

        // query for the world coordinates to check fixtures under the pointer
        this.world.queryAABB(planck.AABB(worldPoint, worldPoint), function(fixture) {

            // get the body from the fixture
            let body = fixture.getBody();

            // get the userdata from the body
            let userData = body.getUserData();

            // is a breakable body?
            if (userData.blockType == BREAKABLE) {

                // destroy the sprite
                userData.sprite.destroy();

                // destroy the body
                this.world.destroyBody(body);
            }
        }.bind(this));
    }

    // simple function to convert pixels to meters
    toMeters(n) {
        return n / gameOptions.worldScale;
    }

    // totem block creation
    createBox(posX, posY, width, height, isDynamic, blockType, color) {

        // 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 borderColor = Phaser.Display.Color.IntegerToColor(color);
        borderColor.darken(20);

        let userData = {
            blockType: blockType,
            sprite: this.add.graphics()
        }
        userData.sprite.fillStyle(color);
        userData.sprite.fillRect(- width / 2, - height / 2, width, height);
        userData.sprite.lineStyle(4, borderColor.color)
        userData.sprite.strokeRect(- width / 2 + 2, - height / 2 + 2, width - 4, height - 4);

        // a body can have anything in its user data, normally it's used to store its sprite
        box.setUserData(userData);
        return box;
    }

    update(t, dt) {

        // advance world simulation
        this.world.step(dt / 1000 * 2);

        // crearForces  method should be added at the end on each step
        this.world.clearForces();

        // get idol contact list
        for (let ce = this.idol.getContactList(); ce; ce = ce.next) {

            // get the contact
            let contact = ce.contact;

            // get the fixture from the contact
            let fixtureA = contact.getFixtureA();
            let fixtureB = contact.getFixtureB();

            // get the body from the fixture
            let bodyA = fixtureA.getBody();
            let bodyB = fixtureB.getBody();

            // the the userdata from the body
            let userDataA = bodyA.getUserData();
            let userDataB = bodyB.getUserData();

            // did the idol hit the terrain?
            if (userDataA.blockType == TERRAIN || userDataB.blockType == TERRAIN) {

                // oh no!!
                this.cameras.main.setBackgroundColor(0xa90000)
            }
        }

        // iterate through all bodies
        for (let b = this.world.getBodyList(); b; b = b.getNext()) {

            // get body position
            let bodyPosition = b.getPosition();

            // get body angle, in radians
            let bodyAngle = b.getAngle();

            // get body user data, the graphics object
            let userData = b.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;
        }
    }
}

Next time we’ll see the complete prototype with all levels, it’s about a time to start playing Totem Destroyer again. Download the source code, Tiled level included.

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