Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Totem Destroyer game, Box2D, Game development, HTML5, Javascript and Phaser.

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.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.