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.