How to Make a Bomberman Game in Phaser – Part 2

In the last tutorial we created the basic structure for a Bomberman game. In this tutorial we’re going to add some more content to it and make it more fun to play, with a winning condition. In this tutorial we will add the following:

  • Add targets that must be destroyed by the player to find the goal and advance levels
  • Adding items, that may be found after exploding tiles
  • Controlling the number of bombs the player can drop

To read this tutorial, it is important that you are familiar with the following concepts:

  • Javascript and object-oriented concepts.
  • Basic Phaser concepts, such as: states, sprites, groups and arcade physics
  • Creating maps using Tiled map editor

Learn Phaser by building 15 games

If you want to master Phaser and learn how to publish Phaser games as native games for iOS and Android feel free to check Zenva‘s online course The Complete Mobile Game Development Course – Build 15 Games.

Assets copyright

The assets used in this tutorial were created by Cem Kalyoncu/cemkalyoncu and Matt Hackett/richtaur and made available by “usr_share” through the creative commons license, wich allows commercial use under attribution. You can download them in http://opengameart.org/content/bomb-party-the-complete-set or by downloading the source code.

Source code files

You can download the tutorial source code files here.

New levels

In this tutorial we’ll add new levels to our game. Since the focus of this tutorial is on creating the game, not using Tiled to create the maps, you’re free to create your owns or use the ones provided with the source code. The figures below show the levels I’m going to use. Just remember to add the object and layer properties as explained in the last tutorial.

level1 level2

Showing the player lives

In this tutorial our player will have a starting number of lives, which will decrease every time it dies. When the number of lives reaches zero, it’s game over. In addition we want to show the remaining number of lives in the screen.

First, we will modify the Player prefab to keep tracking of its number of lives. To do that, we start by adding a new property in the constructor. Notice that, since we don’t want to reset the number of lives in each level, we will keep it in the localStorage when changing levels, and the player will get its value from there if available. When the first level is loaded, we clear the localStorage, so the player will reset its number of lives. The methods in TiledState that change the localStorage are “init” and “next_level”, as shown below as well.

Bomberman.TiledState.prototype.init = function (level_data) {
    "use strict";
    var tileset_index;
    this.level_data = level_data;
    
    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.scale.pageAlignHorizontally = true;
    this.scale.pageAlignVertically = true;
    
    // start physics system
    this.game.physics.startSystem(Phaser.Physics.ARCADE);
    this.game.physics.arcade.gravity.y = 0;
    
    // create map and set tileset
    this.map = this.game.add.tilemap(level_data.map.key);
    tileset_index = 0;
    this.map.tilesets.forEach(function (tileset) {
        this.map.addTilesetImage(tileset.name, level_data.map.tilesets[tileset_index]);
        tileset_index += 1;
    }, this);
    
    if (this.level_data.first_level) {
        localStorage.clear();
    }
};

Bomberman.TiledState.prototype.next_level = function () {
    "use strict";
    localStorage.number_of_lives = this.prefabs.player.number_of_lives;
    localStorage.number_of_bombs = this.prefabs.player.number_of_bombs;
    this.game.state.start("BootState", true, false, this.level_data.next_level, "TiledState");
};

We also have to add a “die” method in the Player prefab, which will decrease the player number of lives and check if it’s still greater than 0. If so, it just resets the player to the initial position. Otherwise, it’s game over.  The changes in the Player prefab are shown below.

var Bomberman = Bomberman || {};

Bomberman.Player = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.walking_speed = +properties.walking_speed;
    this.bomb_duration = +properties.bomb_duration;
    
    this.animations.add("walking_down", [1, 2, 3], 10, true);
    this.animations.add("walking_left", [4, 5, 6, 7], 10, true);
    this.animations.add("walking_right", [4, 5, 6, 7], 10, true);
    this.animations.add("walking_up", [0, 8, 9], 10, true);
    
    this.stopped_frames = [1, 4, 4, 0, 1];

    this.game_state.game.physics.arcade.enable(this);
    this.body.setSize(14, 12, 0, 4);

    this.cursors = this.game_state.game.input.keyboard.createCursorKeys();
    
    this.initial_position = new Phaser.Point(this.x, this.y);
    
    this.number_of_lives = localStorage.number_of_lives || +properties.number_of_lives;
};

Bomberman.Player.prototype.die = function () {
    "use strict";
    // decrease the number of lives
    this.number_of_lives -= 1;
    if (this.game_state.prefabs.lives.number_of_lives <= 0) {
        // if there are no more lives, it's game over
        this.game_state.game_over();
    } else {
        // if there are remaining lives, restart the player position
        this.x = this.initial_position.x;
        this.y = this.initial_position.y;
    }
};

To show the player number of lives in the screen we will have a Lives prefab. This prefab will show a heart image with the number of lives inside of it. To do that, we create the text in the prefab constructor with all the necessary properties and in the update method we change it to show the current number of lives of the player. The Lives prefab code is shown below.

var Bomberman = Bomberman || {};

Bomberman.Lives = function (game_state, name, position, properties) {
    "use strict";
    var lives_text_position, lives_text_style, lives_text_properties;
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.fixedToCamera = true;
    
    this.anchor.setTo(0.5);
    this.scale.setTo(0.9);
    
    // create a text prefab to show the number of lives
    lives_text_position = new Phaser.Point(this.position.x - 2, this.position.y + 5);
    lives_text_style = {font: "14px Arial", fill: "#fff"};
    lives_text_properties = {group: "hud", text: this.number_of_lives, style: lives_text_style};
    this.lives_text = new Bomberman.TextPrefab(this.game_state, "lives_text", lives_text_position, lives_text_properties);
    this.lives_text.anchor.setTo(0.5);
};

Bomberman.Lives.prototype = Object.create(Bomberman.Prefab.prototype);
Bomberman.Lives.prototype.constructor = Bomberman.Lives;

Bomberman.Lives.prototype.update = function () {
    "use strict";
    // update to show current number of lives
    this.lives_text.text = this.game_state.prefabs.player.number_of_lives;
};

You can already try playing with the lives to check if everything is working.

lives

Adding targets and the goal

In our game, to advance to the next level the player must destroy some targets in the level. Once all targets have been destroyed, a goal will appear in the level. When the player touches the goal, he advances to the next level.

To do that, we will create a Target prefab, which will have a physical body and is immovable. In the update method we check for overlap with explosions and, if so, we call the kill method. Finally, we overwrite the kill method to besides calling the Phaser.Sprite kill method, checking if this was the last living target. If so, we must create the goal. The code for the Target prefab is shown below:

var Bomberman = Bomberman || {};

Bomberman.Target = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.immovable = true;
};

Bomberman.Target.prototype = Object.create(Bomberman.Prefab.prototype);
Bomberman.Target.prototype.constructor = Bomberman.Target;

Bomberman.Target.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.explosions, this.kill, null, this);
};

Bomberman.Target.prototype.kill = function () {
    "use strict";
    var goal_position, goal_properties, goal;
    Phaser.Sprite.prototype.kill.call(this);
    if (this.game_state.groups.targets.countLiving() === 0) {
        // create goal
        goal_position = new Phaser.Point(this.game_state.game.world.width / 2, this.game_state.game.world.height / 2);
        goal_properties = {texture: "goal_image", group: "goals"};
        goal = new Bomberman.Goal(this.game_state, "goal", goal_position, goal_properties);
    }
};

The Goal prefab is also simple, as you can see in the code below. It will have an immovable physical body too, but it will check for overlaps with the player. When the player touches the goal, it will call the “next_level” method from TiledState, which will advance the level. The “next_level” method was already shown before, since it changes the localStorage. Notice that this method uses a “next_level” property from the JSON file, so we have to add it there accordingly.

var Bomberman = Bomberman || {};

Bomberman.Goal = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    this.scale.setTo(0.5);
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.immovable = true;
};

Bomberman.Goal.prototype = Object.create(Bomberman.Prefab.prototype);
Bomberman.Goal.prototype.constructor = Bomberman.Goal;

Bomberman.Goal.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.players, this.reach_goal, null, this);
};

Bomberman.Goal.prototype.reach_goal = function () {
    "use strict";
    this.game_state.next_level();
};

You can already try playing with the targets and the goal, advancing levels and checking if they’re working properly.

targets

Limiting the number of bombs the player can drop

In this tutorial, we will limit the number of bombs the player can drop simultaneously. To do that we will change the way we control the bomb dropping to work in the following way: we keep track of the total number of bombs the player can drop and the index of the current bomb. When the spacebar is pressed, we first check if it is possible to drop another bomb by comparing the index of the current bomb with the total number of bombs. If so, we drop the bomb only if it does not collide with an already dropped one. Finally, when a bomb is dropped, we have to update the index of the current bomb. The code below shows the changes in the Player prefab.

var Bomberman = Bomberman || {};

Bomberman.Player = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.walking_speed = +properties.walking_speed;
    this.bomb_duration = +properties.bomb_duration;
    
    this.animations.add("walking_down", [1, 2, 3], 10, true);
    this.animations.add("walking_left", [4, 5, 6, 7], 10, true);
    this.animations.add("walking_right", [4, 5, 6, 7], 10, true);
    this.animations.add("walking_up", [0, 8, 9], 10, true);
    
    this.stopped_frames = [1, 4, 4, 0, 1];

    this.game_state.game.physics.arcade.enable(this);
    this.body.setSize(14, 12, 0, 4);

    this.cursors = this.game_state.game.input.keyboard.createCursorKeys();
    
    this.initial_position = new Phaser.Point(this.x, this.y);
    
    this.number_of_lives = localStorage.number_of_lives || +properties.number_of_lives;
    this.number_of_bombs = localStorage.number_of_bombs || +properties.number_of_bombs;
    this.current_bomb_index = 0;
};

Bomberman.Player.prototype.update = function () {
    "use strict";
    var colliding_bombs;
    
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.walls);
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.blocks);
    this.game_state.game.physics.arcade.collide(this, this.game_state.groups.bombs);
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.explosions, this.die, null, this);
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.enemies, this.die, null, this);
    
    if (this.cursors.left.isDown && this.body.velocity.x <= 0) {
        // move left
        this.body.velocity.x = -this.walking_speed;
        if (this.body.velocity.y === 0) {
            // change the scale, since we have only one animation for left and right directions
            this.scale.setTo(-1, 1);
            this.animations.play("walking_left");
        }
    } else if (this.cursors.right.isDown && this.body.velocity.x >= 0) {
        // move right
        this.body.velocity.x = +this.walking_speed;
        if (this.body.velocity.y === 0) {
            // change the scale, since we have only one animation for left and right directions
            this.scale.setTo(1, 1);
            this.animations.play("walking_right");
        }
    } else {
        this.body.velocity.x = 0;
    }

    if (this.cursors.up.isDown && this.body.velocity.y <= 0) {
        // move up
        this.body.velocity.y = -this.walking_speed;
        if (this.body.velocity.x === 0) {
            this.animations.play("walking_up");
        }
    } else if (this.cursors.down.isDown && this.body.velocity.y >= 0) {
        // move down
        this.body.velocity.y = +this.walking_speed;
        if (this.body.velocity.x === 0) {
            this.animations.play("walking_down");
        }
    } else {
        this.body.velocity.y = 0;
    }
    
    if (this.body.velocity.x === 0 && this.body.velocity.y === 0) {
        // stop current animation
        this.animations.stop();
        this.frame = this.stopped_frames[this.body.facing];
    }
    
    // if the spacebar is pressed and it is possible to drop another bomb, try dropping it
    if (this.game_state.input.keyboard.isDown(Phaser.Keyboard.SPACEBAR) && this.current_bomb_index < this.number_of_bombs) {
        colliding_bombs = this.game_state.game.physics.arcade.getObjectsAtLocation(this.x, this.y, this.game_state.groups.bombs);
        // drop the bomb only if it does not collide with another one
        if (colliding_bombs.length === 0) {
            this.drop_bomb();
        }
    }
};

Bomberman.Player.prototype.drop_bomb = function () {
    "use strict";
    var bomb, bomb_name, bomb_position, bomb_properties;
    // get the first dead bomb from the pool
    bomb_name = this.name + "_bomb_" + this.game_state.groups.bombs.countLiving();
    bomb_position = new Phaser.Point(this.x, this.y);
    bomb_properties = {"texture": "bomb_spritesheet", "group": "bombs", bomb_radius: 3};
    bomb = Bomberman.create_prefab_from_pool(this.game_state.groups.bombs, Bomberman.Bomb.prototype.constructor, this.game_state, bomb_name, bomb_position, bomb_properties);
    this.current_bomb_index += 1;
};

In addition, we have to change the Bomb prefab to decrease the index of the current bomb when it explodes. This modification is shown in the code below.

Bomberman.Bomb.prototype.explode = function () {
    "use strict";
    this.kill();
    var explosion_name, explosion_position, explosion_properties, explosion, wall_tile, block_tile;
    explosion_name = this.name + "_explosion_" + this.game_state.groups.explosions.countLiving();
    explosion_position = new Phaser.Point(this.position.x, this.position.y);
    explosion_properties = {texture: "explosion_image", group: "explosions", duration: 0.5};
    // create an explosion in the bomb position
    explosion = Bomberman.create_prefab_from_pool(this.game_state.groups.explosions, Bomberman.Explosion.prototype.constructor, this.game_state,
                                                      explosion_name, explosion_position, explosion_properties);
    
    // create explosions in each direction
    this.create_explosions(-1, -this.bomb_radius, -1, "x");
    this.create_explosions(1, this.bomb_radius, +1, "x");
    this.create_explosions(-1, -this.bomb_radius, -1, "y");
    this.create_explosions(1, this.bomb_radius, +1, "y");
    
    this.game_state.prefabs.player.current_bomb_index -= 1;
};

Try playing now and see if you can control the maximum number of dropped bombs. Try changing it and check if it works properly.

bombs

Adding items

When a tiled is destroyed by a bomb, eventually an item should appear to the player. To do that, first we will create a generic Item prefab, as shown below. This prefab will have an immovable physical body, which overlaps with explosions and the player. When it collides with an explosion, the item is destroyed, and when it collides with a player, it is collected. We also write a default “collect_item” method, which just kills it.

var Bomberman = Bomberman || {};

Bomberman.Item = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.immovable = true;
    
    this.scale.setTo(0.75);
};

Bomberman.Item.prototype = Object.create(Bomberman.Prefab.prototype);
Bomberman.Item.prototype.constructor = Bomberman.Item;

Bomberman.Item.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.players, this.collect_item, null, this);
};

Bomberman.Item.prototype.collect_item = function () {
    "use strict";
    // by default, an item is destroyed when collected
    this.kill();
};

Now, we are going to create other prefabs that will extend Item and will overwrite the “collect_item” method. We are going to create two items:
1) Life item, which will increase the player number of lives
2) Bomb item, which will increase the number of bombs the player can drop

The LifeItem prefab code is shown below. Since we already have our generic Item prefab, we only have to overwrite the “collect_item” method, which increase the player number of lives.

var Bomberman = Bomberman || {};

Bomberman.LifeItem = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Item.call(this, game_state, name, position, properties);
};

Bomberman.LifeItem.prototype = Object.create(Bomberman.Item.prototype);
Bomberman.LifeItem.prototype.constructor = Bomberman.LifeItem;

Bomberman.LifeItem.prototype.collect_item = function (item, player) {
    "use strict";
    Bomberman.Item.prototype.collect_item.call(this);
    player.number_of_lives += 1;
};

The BombItem prefab code is similar and shown below. In the “collect_item” method we only have to increase the player number of bombs, limited by a maximum number.

var Bomberman = Bomberman || {};

Bomberman.BombItem = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Item.call(this, game_state, name, position, properties);
    
    this.MAXIMUM_NUMBER_OF_BOMBS = 5;
};

Bomberman.BombItem.prototype = Object.create(Bomberman.Item.prototype);
Bomberman.BombItem.prototype.constructor = Bomberman.BombItem;

Bomberman.BombItem.prototype.collect_item = function (item, player) {
    "use strict";
    Bomberman.Item.prototype.collect_item.call(this);
    // increases the player number of bombs, limited by a maximum
    player.number_of_bombs = Math.min(player.number_of_bombs + 1, this.MAXIMUM_NUMBER_OF_BOMBS);
};

Finally, we have to create the items when a tile is destroyed. First, we will add a property in our TiledState containing all the items probabilities and properties, as shown below. One important thing is that the items are ordered by their probabilities, with the lower probability item being the first one.

Bomberman.TiledState = function () {
    "use strict";
    Phaser.State.call(this);
    
    this.prefab_classes = {
        "player": Bomberman.Player.prototype.constructor,
        "enemy": Bomberman.Enemy.prototype.constructor,
        "target": Bomberman.Target.prototype.constructor,
        "life_item": Bomberman.LifeItem.prototype.constructor,
        "bomb_item": Bomberman.BombItem.prototype.constructor
    };
    
    // define available items
    this.items = {
        life_item: {probability: 0.1, properties: {texture: "life_item_image", group: "items"}},
        bomb_item: {probability: 0.3, properties: {texture: "bomb_item_image", group: "items"}}
    };
};

Now, in the Bomb prefab, after we destroy a tile we have to check if a item must be created. To do that, we generate a random item using Phaser random data generator (for more information, you can check the documentation), and iterate through all the available items comparing the generated number with the item probability. If the generated number is lower than the probability of an item, we create it and stop the loop. Since the items are ordered by probability, the less likely ones will have priority. The Bomb prefab modifications are shown below. Notice that we call the “check_item” method at the end of “create_explosions”, ane we use a pool of objects to create the items.

Bomberman.Bomb.prototype.create_explosions = function (initial_index, final_index, step, axis) {
    "use strict";
    var index, explosion_name, explosion_position, explosion, explosion_properties, wall_tile, block_tile;
    explosion_properties = {texture: "explosion_image", group: "explosions", duration: 0.5};
    for (index = initial_index; Math.abs(index) <= Math.abs(final_index); index += step) {
        explosion_name = this.name + "_explosion_" + this.game_state.groups.explosions.countLiving();
        // the position is different accoring to the axis
        if (axis === "x") {
            explosion_position = new Phaser.Point(this.position.x + (index * this.width), this.position.y);
        } else {
            explosion_position = new Phaser.Point(this.position.x, this.position.y + (index * this.height));
        }
        wall_tile = this.game_state.map.getTileWorldXY(explosion_position.x, explosion_position.y, this.game_state.map.tileWidth, this.game_state.map.tileHeight, "walls");
        block_tile = this.game_state.map.getTileWorldXY(explosion_position.x, explosion_position.y, this.game_state.map.tileWidth, this.game_state.map.tileHeight, "blocks");
        if (!wall_tile && !block_tile) {
            // create a new explosion in the new position
            explosion = Bomberman.create_prefab_from_pool(this.game_state.groups.explosions, Bomberman.Explosion.prototype.constructor, this.game_state, explosion_name, explosion_position, explosion_properties);
        } else {
            if (block_tile) {
                // check for item to spawn
                this.check_for_item({x: block_tile.x * block_tile.width, y: block_tile.y * block_tile.height},
                                    {x: block_tile.width, y: block_tile.height});
                this.game_state.map.removeTile(block_tile.x, block_tile.y, "blocks");
            }
            break;
        }
    }
};

Bomberman.Bomb.prototype.check_for_item = function (block_position, block_size) {
    "use strict";
    var random_number, item_prefab_name, item, item_probability, item_name, item_position, item_properties, item_constructor, item_prefab;
    random_number = this.game_state.game.rnd.frac();
    // search for the first item that can be spawned
    for (item_prefab_name in this.game_state.items) {
        if (this.game_state.items.hasOwnProperty(item_prefab_name)) {
            item = this.game_state.items[item_prefab_name];
            item_probability = item.probability;
            // spawns an item if the random number is less than the item probability
            if (random_number < item_probability) {
                item_name = this.name + "_items_" + this.game_state.groups[item.properties.group].countLiving();
                item_position = new Phaser.Point(block_position.x + (block_size.x / 2), block_position.y + (block_size.y / 2));
                console.log(item_position);
                item_properties = item.properties;
                item_constructor = this.game_state.prefab_classes[item_prefab_name];
                item_prefab = Bomberman.create_prefab_from_pool(this.game_state.groups.items, item_constructor, this.game_state, item_name, item_position, item_properties);
                break;
            }
        }
    }
};

Now you can try playing to see if the items are being created correctly, and if they’re having the correct effect in the game.

item

Finishing the game

And now we finished this tutorial! We added some nice content making it more fun to play. In the next tutorial we will make it multiplayer, adding another player to it! Let me know your opinion of this tutorial and what you would like to see in future ones.