How to Make a Bomberman Game in Phaser – Part 3

In the last tutorial, we added content to our Bomberman game, such as lives, items and more levels. In this tutorial, we will make it multiplayer, by adding a second player. We will also add a battle game mode, where the two players must compete agains each other. The following topics will be covered:

  • Creating a Phaser plugin to receive user input
  • Making the game multiplayer
  • Creating a new game mode
  • Creating a title screen with the two game mode options

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.

Creating a Phaser plugin to receive user input

In Phaser, you can create plugins to add functionalities to your engine (you can check the documentation for more information). In this tutorial, we will create a Phaser plugin that will read a JSON file containing all the user input data. This JSON file must have the key for each action, and what action should be executed for each key.

The JSON file below shows the user input data I used for this tutorial. You can create your own by changing the key for each action as you prefer. In this file we define a list of inputs for each possible key event (keyDown, keyUp, keyPress). For each user input we define the key, the callback function and the arguments. For example, in the keyDown events, the key “UP” will call “player.move” with the arguments “[0, -1]” and “true” (we will define this callback function later).

{
    "keydown": {
        "UP": {
            "callback": "player.change_movement",
            "args": [0, -1, true]
        },
        "DOWN": {
            "callback": "player.change_movement",
            "args": [0, 1, true]
        },
        "LEFT": {
            "callback": "player.change_movement",
            "args": [-1, 0, true]
        },
        "RIGHT": {
            "callback": "player.change_movement",
            "args": [1, 0, true]
        },
        "SPACEBAR": {
            "callback": "player.try_dropping_bomb"
        }
    },
    "keyup": {
        "UP": {
            "callback": "player.change_movement",
            "args": [0, -1, false]
        },
        "DOWN": {
            "callback": "player.change_movement",
            "args": [0, 1, false]
        },
        "LEFT": {
            "callback": "player.change_movement",
            "args": [-1, 0, false]
        },
        "RIGHT": {
            "callback": "player.change_movement",
            "args": [1, 0, false]
        }
    }
}

Now, we have to write our plugin code. We start by creating a class that extends Phaser.Plugin. The Phaser.Plugin class has a method “init” which we must use as our constructor. This method will receive the user input data as parameter, and will create an object mapping, for each event type, each key to its callback function. The callback function must define the prefab and its method that will be called. Then, we add an event for each possible key event, calling the method “process_input”.

var Phaser = Phaser || {};
var Bomberman = Bomberman || {};

Bomberman.UserInput = function (game, parent, game_state, user_input_data) {
    "use strict";
    Phaser.Plugin.call(this, game, parent);
};

Bomberman.UserInput.prototype = Object.create(Phaser.Plugin.prototype);
Bomberman.UserInput.prototype.constructor = Bomberman.UserInput;

Bomberman.UserInput.prototype.init = function (game_state, user_input_data) {
    "use strict";
    var input_type, key, key_code;
    this.game_state = game_state;
    this.user_inputs = {"keydown": {}, "keyup": {}, "keypress": {}};
    
    // instantiate object with user input data provided    
    // each event can be keydown, keyup or keypress
    // separate events by key code
    for (input_type in user_input_data) {
        if (user_input_data.hasOwnProperty(input_type)) {
            for (key in user_input_data[input_type]) {
                if (user_input_data[input_type].hasOwnProperty(key)) {
                    key_code = Phaser.Keyboard[key];
                    this.user_inputs[input_type][key_code] = user_input_data[input_type][key];
                }
            }
        }
    }
    
    // add callback for all three events
    this.game.input.keyboard.addCallbacks(this, this.process_input, this.process_input, this.process_input);
};

Bomberman.UserInput.prototype.process_input = function (event) {
    "use strict";
    var user_input, callback_data, prefab;
    if (this.user_inputs[event.type] && this.user_inputs[event.type][event.keyCode]) {
        user_input = this.user_inputs[event.type][event.keyCode];
        if (user_input) {
            callback_data = user_input.callback.split(".");
            // identify prefab
            prefab = this.game_state.prefabs[callback_data[0]];
            // call correct method
            prefab[callback_data[1]].apply(prefab, user_input.args);
        }
    }
};

The method “process_input” identifies the event type and key and gets the callback information. From the callback, it uses Javascript split function to identify the prefab and the method and call it passing the arguments defined in the user input file.

Now, we have to define the “move” and “try_dropping_bomb” methods in the Player prefab. The first method will receive the direction and if the player should start or stop moving. By doing so, we can use the same method to move or stop the player in each direction, as you can see in the user input file. The player prefab sets an object move according to the given direction. Then, in the Player prefab update method, instead of checking for pressed keys, we only check if the move object is true for each direction. For the “try_dropping_bomb” method we just moved the piece of code responsible for dropping bombs from the “update” method. The code below shows the necessary modifications 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.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;
    
    this.movement = {left: false, right: false, up: false, down: false};
};

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

Bomberman.Player.prototype.update = function () {
    "use strict";
    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.movement.left && 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.movement.right && 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.movement.up && 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.movement.down && 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];
    }
};

Bomberman.Player.prototype.change_movement = function (direction_x, direction_y, move) {
    "use strict";
    if (direction_x < 0) {
        this.movement.left = move;
    } else if (direction_x > 0) {
        this.movement.right = move;
    }
    
    if (direction_y < 0) {
        this.movement.up = move;
    } else if (direction_y > 0) {
        this.movement.down = move;
    }
};

Bomberman.Player.prototype.try_dropping_bomb = function () {
    "use strict";
    var colliding_bombs;
    // if it is possible to drop another bomb, try dropping it
    if (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();
        }
    }
};

Finally, we have to add our plugin in the game state. First, we have to change the JSON level file to tell the LoadingState which file contains the user input, as shown below.

{
    "assets": {
        "map_tiles": {"type": "image", "source": "assets/images/bomberman_spritesheet.png"},
        "player_spritesheet": {"type": "spritesheet", "source": "assets/images/player_spritesheet.png", "frame_width": 16, "frame_height": 16},
        "bomb_spritesheet": {"type": "spritesheet", "source": "assets/images/bomb_spritesheet.png", "frame_width": 16, "frame_height": 16},
        "enemy_spritesheet": {"type": "spritesheet", "source": "assets/images/enemy_spritesheet.png", "frame_width": 16, "frame_height": 16},
        "explosion_image": {"type": "image", "source": "assets/images/explosion.png"},
        "heart_image": {"type": "image", "source": "assets/images/heart.png"},
        "target_image": {"type": "image", "source": "assets/images/target.png"},
        "goal_image": {"type": "image", "source": "assets/images/portal.png"},
        "life_item_image": {"type": "image", "source": "assets/images/life_item.png"},
        "bomb_item_image": {"type": "image", "source": "assets/images/bomb_item.png"},
        
        "level_tilemap": {"type": "tilemap", "source": "assets/maps/level1_map.json"}
    },
    "groups": [
        "targets",
        "items",
        "explosions",
        "bombs",
        "goals",
        "enemies",
        "players",
        "hud"
    ],
    "map": {
        "key": "level_tilemap",
        "tilesets": ["map_tiles"]
    },
    "user_input": "assets/levels/user_input.json",
    "next_level": "assets/levels/level2.json",
    "first_level": true
}

Now, we have to change the LoadingState to load the user input file and add the plugin in the “create” method of TiledState. The code below show the modifications in both classes.

Bomberman.LoadingState.prototype.preload = function () {
    "use strict";
    var assets, asset_loader, asset_key, asset;
    assets = this.level_data.assets;
    for (asset_key in assets) { // load assets according to asset key
        if (assets.hasOwnProperty(asset_key)) {
            asset = assets[asset_key];
            switch (asset.type) {
            case "image":
                this.load.image(asset_key, asset.source);
                break;
            case "spritesheet":
                this.load.spritesheet(asset_key, asset.source, asset.frame_width, asset.frame_height, asset.frames, asset.margin, asset.spacing);
                break;
            case "tilemap":
                this.load.tilemap(asset_key, asset.source, null, Phaser.Tilemap.TILED_JSON);
                break;
            }
        }
    }
    // load user input file
    if (this.level_data.user_input) {
        this.load.text("user_input", this.level_data.user_input);
    }
};
Bomberman.TiledState.prototype.create = function () {
    "use strict";
    var group_name, object_layer, collision_tiles;
    
    // create map layers
    this.layers = {};
    this.map.layers.forEach(function (layer) {
        this.layers[layer.name] = this.map.createLayer(layer.name);
        if (layer.properties.collision) { // collision layer
            collision_tiles = [];
            layer.data.forEach(function (data_row) { // find tiles used in the layer
                data_row.forEach(function (tile) {
                    // check if it's a valid tile index and isn't already in the list
                    if (tile.index > 0 && collision_tiles.indexOf(tile.index) === -1) {
                        collision_tiles.push(tile.index);
                    }
                }, this);
            }, this);
            this.map.setCollision(collision_tiles, true, layer.name);
        }
    }, this);
    // resize the world to be the size of the current layer
    this.layers[this.map.layer.name].resizeWorld();
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    this.prefabs = {};
    
    for (object_layer in this.map.objects) {
        if (this.map.objects.hasOwnProperty(object_layer)) {
            // create layer objects
            this.map.objects[object_layer].forEach(this.create_object, this);
        }
    }
    
    this.game.user_input = this.game.plugins.add(Bomberman.UserInput, this, JSON.parse(this.game.cache.getText("user_input")));
    
    this.init_hud();
};

You can already try playing your game with the user input file. Nothing should change in the gameplay, the only difference is in the code.

Adding a new player

Now that we’re handling all the user input with our plugin, it becomes easier to add another player. We just have to add another Player prefab in our game and add its correspondent user input in the user input file. The new user input file is shown below. Notice that the prefab “player” becomes “player1” and we add the user input for “player2” as well.

{
    "keydown": {
        "W": {
            "callback": "player1.change_movement",
            "args": [0, -1, true]
        },
        "S": {
            "callback": "player1.change_movement",
            "args": [0, 1, true]
        },
        "A": {
            "callback": "player1.change_movement",
            "args": [-1, 0, true]
        },
        "D": {
            "callback": "player1.change_movement",
            "args": [1, 0, true]
        },
        "Z": {
            "callback": "player1.try_dropping_bomb"
        },
        "U": {
            "callback": "player2.change_movement",
            "args": [0, -1, true]
        },
        "J": {
            "callback": "player2.change_movement",
            "args": [0, 1, true]
        },
        "H": {
            "callback": "player2.change_movement",
            "args": [-1, 0, true]
        },
        "K": {
            "callback": "player2.change_movement",
            "args": [1, 0, true]
        },
        "N": {
            "callback": "player2.try_dropping_bomb"
        }
    },
    "keyup": {
        "W": {
            "callback": "player1.change_movement",
            "args": [0, -1, false]
        },
        "S": {
            "callback": "player1.change_movement",
            "args": [0, 1, false]
        },
        "A": {
            "callback": "player1.change_movement",
            "args": [-1, 0, false]
        },
        "D": {
            "callback": "player1.change_movement",
            "args": [1, 0, false]
        },
        "U": {
            "callback": "player2.change_movement",
            "args": [0, -1, false]
        },
        "J": {
            "callback": "player2.change_movement",
            "args": [0, 1, false]
        },
        "H": {
            "callback": "player2.change_movement",
            "args": [-1, 0, false]
        },
        "K": {
            "callback": "player2.change_movement",
            "args": [1, 0, false]
        }
    }
}

We also need to update the maps to add the new player. Those are the maps I created. You can use them, which are available in the source code, or create your own.

map1 map2

There are some other things we must change so our game still works. First, we must change our Lives prefab so it knows which player lives it should show. We do this by adding a property with the player prefab name, so in the “update” method it can get the correct Player prefab and update the number of lives correctly. We also change the “init_hud” method in TiledState to show the number of lives of each player. The codes below show the modifications in the Lives prefab and TiledState.

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.player = properties.player;
    
    this.fixedToCamera = true;
    
    this.anchor.setTo(0.5);
    this.scale.setTo(0.6);
    
    // 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: "10px 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[this.player].number_of_lives;
};
Bomberman.TiledState.prototype.init_hud = function () {
    "use strict";
    var player1_lives_position, player1_lives_properties, player1_lives, player2_lives_position, player2_lives_properties, player2_lives;
    
    // create the lives prefab for player1
    player1_lives_position = new Phaser.Point(0.1 * this.game.world.width, 0.07 * this.game.world.height);
    player1_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player1"};
    player1_lives = new Bomberman.Lives(this, "lives", player1_lives_position, player1_lives_properties);
    
    // create the lives prefab for player2
    player2_lives_position = new Phaser.Point(0.9 * this.game.world.width, 0.07 * this.game.world.height);
    player2_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player2"};
    player2_lives = new Bomberman.Lives(this, "lives", player2_lives_position, player2_lives_properties);
};

Also, when a bomb explodes, it decreases the current bomb index of its owner. Since now we have two possible owners, the bomb must know which one it should decrease the current bomb index. We can easily do that by adding a property with the bomb owner, as the code below shows. Then, when a player creates a bomb, it passes its object as a parameter to the bomb. This modifications are also shown below.

var Bomberman = Bomberman || {};

Bomberman.Bomb = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.bomb_radius = +properties.bomb_radius;
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.immovable = true;
    
    this.exploding_animation = this.animations.add("exploding", [0, 2, 4], 1, false);
    this.exploding_animation.onComplete.add(this.explode, this);
    this.animations.play("exploding");
    
    this.owner = properties.owner;
};

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

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.owner.current_bomb_index -= 1;
};
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, owner: this};
    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;
};

Now, you can already try playing with two players, and see if it is working.

multiplayer

Adding the battle mode

We will add a battle mode where each player should explode the other to win. To do that, we must create a new game state, called BattleState. However, this state will be very similar to the TiledState we already have. So, we will change the TiledState to have only the common code between the BattleState and our current game mode. Then, we will create the new states, called ClassicState and BattleState, which will extend TiledState, only adding the differences.

The code for TiledState, ClassicState and BattleState is shown below. Notice that the only differences are the “game_over” method, which will show the winner in the BattleState, and the “next_level” method, which exists only for the ClassicState. Remember that we must change all references to “TiledState” in our code to be either ClassicState or BattleState.

var Bomberman = Bomberman || {};

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"}}
    };
};

Bomberman.TiledState.prototype = Object.create(Phaser.State.prototype);
Bomberman.TiledState.prototype.constructor = Bomberman.TiledState;

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.create = function () {
    "use strict";
    var group_name, object_layer, collision_tiles;
    
    // create map layers
    this.layers = {};
    this.map.layers.forEach(function (layer) {
        this.layers[layer.name] = this.map.createLayer(layer.name);
        if (layer.properties.collision) { // collision layer
            collision_tiles = [];
            layer.data.forEach(function (data_row) { // find tiles used in the layer
                data_row.forEach(function (tile) {
                    // check if it's a valid tile index and isn't already in the list
                    if (tile.index > 0 && collision_tiles.indexOf(tile.index) === -1) {
                        collision_tiles.push(tile.index);
                    }
                }, this);
            }, this);
            this.map.setCollision(collision_tiles, true, layer.name);
        }
    }, this);
    // resize the world to be the size of the current layer
    this.layers[this.map.layer.name].resizeWorld();
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    this.prefabs = {};
    
    for (object_layer in this.map.objects) {
        if (this.map.objects.hasOwnProperty(object_layer)) {
            // create layer objects
            this.map.objects[object_layer].forEach(this.create_object, this);
        }
    }
    
    this.game.user_input = this.game.plugins.add(Bomberman.UserInput, this, JSON.parse(this.game.cache.getText("user_input")));
    
    this.init_hud();
};

Bomberman.TiledState.prototype.create_object = function (object) {
    "use strict";
    var object_y, position, prefab;
    // tiled coordinates starts in the bottom left corner
    object_y = (object.gid) ? object.y - (this.map.tileHeight / 2) : object.y + (object.height / 2);
    position = {"x": object.x + (this.map.tileHeight / 2), "y": object_y};
    // create object according to its type
    if (this.prefab_classes.hasOwnProperty(object.type)) {
        prefab = new this.prefab_classes[object.type](this, object.name, position, object.properties);
    }
    this.prefabs[object.name] = prefab;
};

Bomberman.TiledState.prototype.init_hud = function () {
    "use strict";
    var player1_lives_position, player1_lives_properties, player1_lives, player2_lives_position, player2_lives_properties, player2_lives;
    
    // create the lives prefab for player1
    player1_lives_position = new Phaser.Point(0.1 * this.game.world.width, 0.07 * this.game.world.height);
    player1_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player1"};
    player1_lives = new Bomberman.Lives(this, "lives", player1_lives_position, player1_lives_properties);
    
    // create the lives prefab for player2
    player2_lives_position = new Phaser.Point(0.9 * this.game.world.width, 0.07 * this.game.world.height);
    player2_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player2"};
    player2_lives = new Bomberman.Lives(this, "lives", player2_lives_position, player2_lives_properties);
};

Bomberman.TiledState.prototype.show_game_over = function () {
    "use strict";
    var game_over_panel, game_over_position, game_over_bitmap, panel_text_style;
    // create a bitmap do show the game over panel
    game_over_position = new Phaser.Point(0, this.game.world.height);
    game_over_bitmap = this.add.bitmapData(this.game.world.width, this.game.world.height);
    game_over_bitmap.ctx.fillStyle = "#000";
    game_over_bitmap.ctx.fillRect(0, 0, this.game.world.width, this.game.world.height);
    panel_text_style = {game_over: {font: "32px Arial", fill: "#FFF"},
                       winner: {font: "20px Arial", fill: "#FFF"}};
    // create the game over panel
    game_over_panel = this.create_game_over_panel(game_over_position, game_over_bitmap, panel_text_style);
    this.groups.hud.add(game_over_panel);
};

Bomberman.TiledState.prototype.create_game_over_panel = function (position, texture, text_style) {
    "use strict";
    var game_over_panel_properties, game_over_panel;
    game_over_panel_properties = {texture: texture, group: "hud", text_style: text_style, animation_time: 500};
    game_over_panel = new Bomberman.GameOverPanel(this, "game_over_panel", position, game_over_panel_properties);
    return game_over_panel;
};
var Bomberman = Bomberman || {};

Bomberman.ClassicState = function () {
    "use strict";
    Bomberman.TiledState.call(this);
};

Bomberman.ClassicState.prototype = Object.create(Bomberman.TiledState.prototype);
Bomberman.ClassicState.prototype.constructor = Bomberman.ClassicState;

Bomberman.ClassicState.prototype.init_hud = function () {
    "use strict";
    var player1_lives_position, player1_lives_properties, player1_lives, player2_lives_position, player2_lives_properties, player2_lives;
    
    // create the lives prefab for player1
    player1_lives_position = new Phaser.Point(0.1 * this.game.world.width, 0.07 * this.game.world.height);
    player1_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player1"};
    player1_lives = new Bomberman.Lives(this, "lives", player1_lives_position, player1_lives_properties);
    
    // create the lives prefab for player2
    player2_lives_position = new Phaser.Point(0.9 * this.game.world.width, 0.07 * this.game.world.height);
    player2_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player2"};
    player2_lives = new Bomberman.Lives(this, "lives", player2_lives_position, player2_lives_properties);
};

Bomberman.ClassicState.prototype.game_over = function () {
    "use strict";
    this.game.state.start("BootState", true, false, "assets/levels/level1.json", "ClassicState");
};

Bomberman.ClassicState.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, "ClassicState");
};
var Bomberman = Bomberman || {};

Bomberman.BattleState = function () {
    "use strict";
    Bomberman.TiledState.call(this);
};

Bomberman.BattleState.prototype = Object.create(Bomberman.TiledState.prototype);
Bomberman.BattleState.prototype.constructor = Bomberman.BattleState;

Bomberman.BattleState.prototype.init_hud = function () {
    "use strict";
    var player1_lives_position, player1_lives_properties, player1_lives, player2_lives_position, player2_lives_properties, player2_lives;
    
    // create the lives prefab for player1
    player1_lives_position = new Phaser.Point(0.1 * this.game.world.width, 0.07 * this.game.world.height);
    player1_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player1"};
    player1_lives = new Bomberman.Lives(this, "lives", player1_lives_position, player1_lives_properties);
    
    // create the lives prefab for player2
    player2_lives_position = new Phaser.Point(0.9 * this.game.world.width, 0.07 * this.game.world.height);
    player2_lives_properties = {group: "hud", texture: "heart_image", number_of_lives: 3, player: "player2"};
    player2_lives = new Bomberman.Lives(this, "lives", player2_lives_position, player2_lives_properties);
};

Bomberman.BattleState.prototype.show_game_over = function () {
    "use strict";
    if (this.prefabs.player1.alive) {
        this.winner = this.prefabs.player1.name;
    } else {
        this.winner = this.prefabs.player2.name;
    }
    Bomberman.TiledState.prototype.show_game_over.call(this);
};

Bomberman.BattleState.prototype.create_game_over_panel = function (position, texture, text_style) {
    "use strict";
    var game_over_panel_properties, game_over_panel;
    game_over_panel_properties = {texture: texture, group: "hud", text_style: text_style, animation_time: 500, winner: this.winner};
    game_over_panel = new Bomberman.BattleGameOverPanel(this, "game_over_panel", position, game_over_panel_properties);
    return game_over_panel;
};

Bomberman.BattleState.prototype.game_over = function () {
    "use strict";
    this.game.state.restart(true, false, this.level_data);
};

Before finishing, we will add a game over message, which will be different for each game mode. In the classic mode, it will only show the Game Over message, while in the battle mode it will also show the winner player. To do that, we will create a GameOverPanel prefab, as shown below. This prefab will have a bitmap texture, and will start with an animation to appear on the screen. Once the animation finishes, it shows the game over message. We create the GameOverPanel in the TiledState, as shown before.

var Bomberman = Bomberman || {};

Bomberman.GameOverPanel = function (game_state, name, position, properties) {
    "use strict";
    var movement_animation;
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.text_style = properties.text_style;
    
    this.alpha = 0.5;
    // create a tween animation to show the game over panel
    movement_animation = this.game_state.game.add.tween(this);
    movement_animation.to({y: 0}, properties.animation_time);
    movement_animation.onComplete.add(this.show_game_over, this);
    movement_animation.start();
};

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

Bomberman.GameOverPanel.prototype.show_game_over = function () {
    "use strict";
    var game_over_text;
    // add game over text
    game_over_text = this.game_state.game.add.text(this.game_state.game.world.width / 2, this.game_state.game.world.height * 0.4, "Game Over", this.text_style.game_over);
    game_over_text.anchor.setTo(0.5);
    this.game_state.groups.hud.add(game_over_text);
    
    // add event to restart level
    this.inputEnabled = true;
    this.events.onInputDown.add(this.game_state.game_over, this.game_state);
};

Finally, to show a different message in the BattleState, we create a BatleGameOverPanel, which extends GameOverPanel but includes the message showing the winner. To allow this, we also change the “create_game_over_panel” method from BattleState to create this prefab accordingly.

var Bomberman = Bomberman || {};

Bomberman.BattleGameOverPanel = function (game_state, name, position, properties) {
    "use strict";
    var movement_animation;
    Bomberman.GameOverPanel.call(this, game_state, name, position, properties);
    
    this.winner = properties.winner;
};

Bomberman.BattleGameOverPanel.prototype = Object.create(Bomberman.GameOverPanel.prototype);
Bomberman.BattleGameOverPanel.prototype.constructor = Bomberman.BattleGameOverPanel;

Bomberman.BattleGameOverPanel.prototype.show_game_over = function () {
    "use strict";
    var winner_text;
    Bomberman.GameOverPanel.prototype.show_game_over.call(this);
    
    // show the winner if it's in battle mode
    winner_text = this.game_state.game.add.text(this.game_state.world.width / 2, this.game_state.game.world.height * 0.6, "Winner: " + this.winner, this.text_style.winner);
    winner_text.anchor.setTo(0.5);
    this.game_state.groups.hud.add(winner_text);
};

You can already try playing the battle mode to see if it is working as expected. Try winning with each player to check if the game over message is correct.

battle_mode

Adding a title screen with the menu

To create our menu, we will create a Menu and a MenuItem prefabs. The Menu prefab will have a list of menu items, and allows navigating through them using the arrow keys. When the user press the UP or DOWN arrow key, it changes the current menu item. When the SPACEBAR is pressed, it selects the current item. The MenuItem prefab will play an animation when it is the current item and has a “select” method to start the game. In this method it will call the BootState to start the game with the ClassicState or the BattleState. In our title screen we will have two menu items, one for each game mode. The code for both prefabs is shown below.

var Bomberman = Bomberman || {};

Bomberman.Menu = function (game_state, name, position, properties) {
    "use strict";
    var live_index, life;
    Bomberman.Prefab.call(this, game_state, name, position, properties);
    
    this.visible = false;
    
    this.menu_items = properties.menu_items;
    this.current_item_index = 0;
    this.menu_items[0].selection_over();
    
    this.cursor_keys = this.game_state.game.input.keyboard.createCursorKeys();
};

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

Bomberman.Menu.prototype.update = function () {
    "use strict";
    if (this.cursor_keys.up.isDown && this.current_item_index > 0) {
        // navigate to previous item
        this.menu_items[this.current_item_index].selection_out();
        this.current_item_index -= 1;
        this.menu_items[this.current_item_index].selection_over();
    } else if (this.cursor_keys.down.isDown && this.current_item_index < this.menu_items.length - 1) {
        // navigate to next item
        this.menu_items[this.current_item_index].selection_out();
        this.current_item_index += 1;
        this.menu_items[this.current_item_index].selection_over();
    }
    
    if (this.game_state.game.input.keyboard.isDown(Phaser.Keyboard.SPACEBAR)) {
        this.menu_items[this.current_item_index].select();
    }
};
var Bomberman = Bomberman || {};

Bomberman.MenuItem = function (game_state, name, position, properties) {
    "use strict";
    Bomberman.TextPrefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.on_selection_animation = this.game_state.game.add.tween(this.scale);
    this.on_selection_animation.to({x: 1.5 * this.scale.x, y: 1.5 * this.scale.y}, 500);
    this.on_selection_animation.to({x: this.scale.x, y: this.scale.y}, 500);
    this.on_selection_animation.repeatAll(-1);
    
    this.level_file = properties.level_file;
    this.state_name = properties.state_name;
};

Bomberman.MenuItem.prototype = Object.create(Bomberman.TextPrefab.prototype);
Bomberman.MenuItem.prototype.constructor = Bomberman.MenuItem;

Bomberman.MenuItem.prototype.selection_over = function () {
    "use strict";
    if (this.on_selection_animation.isPaused) {
        this.on_selection_animation.resume();
    } else {
        this.on_selection_animation.start();
    }
};

Bomberman.MenuItem.prototype.selection_out = function () {
    "use strict";
    this.on_selection_animation.pause();
};

Bomberman.MenuItem.prototype.select = function () {
    "use strict";
    // starts game state
    this.game_state.state.start("BootState", true, false, this.level_file, this.state_name);
};

To show the title screen, we will create a TitleState. In this state, we only have to change the “create” method to create the menu. To simplify the code, I put the menu items data in a JSON file, shown below. The TitleState starts by showing the game title and then reads the menu items data from this file, storing all menu items in an array. This array of menu items is then used to create the menu. The code for TitleState is shown below.

{
    "menu_items": {
        "classic_mode": {
            "position": {"x": 120, "y": 144},
            "properties": {
                "text": "Classic mode",
                "style": {"font": "16px Arial", "fill": "#FFF"},
                "group": "menu_items",
                "level_file": "assets/levels/level1.json",
                "state_name": "ClassicState"
            }
        },
        "battle_mode": {
            "position": {"x": 120, "y": 192},
            "properties": {
                "text": "Battle mode",
                "style": {"font": "16px Arial", "fill": "#FFF"},
                "group": "menu_items",
                "level_file": "assets/levels/battle_level.json",
                "state_name": "BattleState"
            }
        }
    },
    "groups": [
        "background",
        "menu_items",
        "hud"
    ]
}
var Bomberman = Bomberman || {};

Bomberman.TitleState = function () {
    "use strict";
    Phaser.State.call(this);
};

Bomberman.TitleState.prototype = Object.create(Phaser.State.prototype);
Bomberman.TitleState.prototype.constructor = Bomberman.TitleState;

Bomberman.TitleState.prototype.init = function (level_data) {
    "use strict";
    this.level_data = level_data;
    
    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.scale.pageAlignHorizontally = true;
    this.scale.pageAlignVertically = true;
};

Bomberman.TitleState.prototype.create = function () {
    "use strict";
    var title_position, title_style, title, menu_position, menu_items, menu_properties, menu_item_name, menu_item, menu;
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    this.prefabs = {};
    
    // adding title
    title_position = new Phaser.Point(0.5 * this.game.world.width, 0.3 * this.game.world.height);
    title_style = {font: "36px Arial", fill: "#FFF"};
    title = new Bomberman.TextPrefab(this, "title", title_position, {text: "Bomberman", style: title_style, group: "hud"});
    title.anchor.setTo(0.5);
    
    // adding menu
    menu_position = new Phaser.Point(0, 0);
    menu_items = [];
    for (menu_item_name in this.level_data.menu_items) {
        if (this.level_data.menu_items.hasOwnProperty(menu_item_name)) {
            menu_item = this.level_data.menu_items[menu_item_name];
            menu_items.push(new Bomberman.MenuItem(this, menu_item_name, menu_item.position, menu_item.properties));
        }
    }
    menu_properties = {texture: "", group: "background", menu_items: menu_items};
    menu = new Bomberman.Menu(this, "menu", menu_position, menu_properties);
};

Now you can change main.js to start with TitleState and see if everything is working. Try playing both game modes to check if the menu is working correctly.

menu

Finishing the game

And now we finished our Bomberman game! To learn more about game development Phaser, please support our work by checking out our Phaser Mini-Degree.