Platformer Tutorial with Phaser and Tiled – Part 3

Until my last platformer tutorial, we created a simple platformer game with some nice content, however there are still some things to add before making it playable. In this tutorial, we will add the following content:

  • Player lives, so now you can actually lose.
  • Items that will increase the player lives or give an attack.
  • A level boss.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Source code files

You can download the tutorial source code files here.

Game states

In this tutorial, we will use the same states from the last one:

  • Boot State: loads a json file with the level information and starts the Loading State.
  • Loading Sate: loads all the game assets, and starts the Level State.
  • Tiled State: creates the map and all game objects.

The code for all states are almost the same, except for two methods added in TiledState: one to do the game over and other to initialize the hud. So, I will omit them for now and only show the changes when they’re necessary.

Player lives

Our player will start with a given number of lives, and it will lose one every time it is killed by an enemy. To do that, we will change the Player prefab to have a lives property and the “die” method to decrease the number of lives if the player was killed, as follows:

<a class="wpil_keyword_link" href="https://gamedevacademy.org/best-platformer-tutorials/" target="_blank" rel="noopener" title="Platformer" data-wpil-keyword-link="linked">Platformer</a>.Player.prototype.die = function () {
    "use strict";
    this.lives -= 1;
    this.shooting = false;
    if (this.lives > 0) {
        this.game_state.restart_level();
    } else {
        this.game_state.game_over();
    }
};

Also, if the player number of lives becomes zero, we will call the “game_over” method in TiledState, instead of “restart_level”:

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

Besides having the player lives, we have to show how many lives the player still have. So, we will create a Lives prefab that belongs to the hud group and show the current player lives. Our Lives prefab will have the player live asset, but it will be invisible, since we will use the asset only to create new sprites showing the player lives.

var Platformer = Platformer || {};

Platformer.Lives = function (game_state, position, properties) {
    "use strict";
    Platformer.Prefab.call(this, game_state, position, properties);
    
    this.frame = +properties.frame;
    this.visible = false;
    
    this.spacing = +properties.spacing;
    
    this.fixedToCamera = true;
    // saving initial position if it gets changed by window scaling
    this.initial_position = new <a class="wpil_keyword_link" href="https://gamedevacademy.org/what-is-phaser/" target="_blank" rel="noopener" title="Phaser" data-wpil-keyword-link="linked">Phaser</a>.Point(this.x, this.y);
    
    this.lives = [];
    this.dead_life = null;
    this.create_lives();
};

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

Platformer.Lives.prototype.update = function () {
    "use strict";
    // update to show current number of lives
    if (this.game_state.prefabs.player.lives !== this.lives.length) {
        this.update_lives();
    }
};

Platformer.Lives.prototype.create_lives = function () {
    "use strict";
    var life_index, life_position, life;
    // create a sprite for each one of the player lives
    for (life_index = 0; life_index < this.game_state.prefabs.player.lives; life_index += 1) {
        life_position = new Phaser.Point(this.initial_position.x + (life_index * (this.width + this.spacing)), this.initial_position.y);
        life = new Phaser.Sprite(this.game_state.game, life_position.x, life_position.y, this.texture, this.frame);
        life.fixedToCamera = true;
        this.game_state.groups.hud.add(life);
        this.lives.push(life);
    }
};

Platformer.Lives.prototype.update_lives = function () {
    "use strict";
    var life, life_position;
    life = this.lives[this.lives.length - 1];
    if (this.game_state.prefabs.player.lives < this.lives.length) {
        // the player died, so we have to kill the last life
        life.kill();
        this.dead_life = life;
        this.lives.pop();
    } else {
        // the player received another life
        if (!this.dead_life) {
            // if there is no dead life we can reuse, we create a new one
            life_position = new Phaser.Point(this.initial_position.x + (this.lives.length * (this.width + this.spacing)), this.initial_position.y);
            life = new Phaser.Sprite(this.game_state.game, life_position.x, life_position.y, this.texture, this.frame);
            life.fixedToCamera = true;
            this.game_state.groups.hud.add(life);
        } else {
            // if there is a dead life, we just reset it
            life = this.dead_life;
            life_position = new Phaser.Point(this.initial_position.x + ((this.lives.length - 1) * (this.width + this.spacing)), this.initial_position.y);
            life.reset(life_position.x, life_position.y);
            this.dead_life = null;
        }
        this.lives.push(life);
    }
};

The Lives prefab will have a “create_lives” method that fills an array with a sprite for each one of the player lives. Since the player live asset is already loaded we can use its width and the Lives prefab position to find the position for each live and draw them on the screen. Finally, in the update method we have to check if the player number of lives has changed and, if so, we call an “update_lives” method.

In the “update_lives” method we assume the number of lives can increase or decrease by only one between two updates. This makes sense because the update method will be called many times per second, and the player can’t die or get lives faster than that. So, in the “update_lives” method we only check if the number of lives has decreased or increased. In the first case, we have to kill the last live in the array. In the second case, we’ll do some checking to avoid creating too many life objects. First, we check if there is a dead life we can reuse and, if so, we just reset it. Otherwise, we create a new life object.

There are two last things we have to do regarding the player lives and the player score that we didn’t do in the last tutorial. First, you’ll notice I added an “init_hud” method in TiledState (shown below) that create the hud objects in fixed positions instead of loading it from the Tiled map. I did this because sometimes the Phaser world scaling could mess with the hud objects positions when reloading the screen or updating the lives. I also save the lives prefab initial position for the same reason.

Platformer.TiledState.prototype.init_hud = function () {
    "use strict";
    var score_position, score, lives_position, lives;
    score_position = new Phaser.Point(20, 20);
    score = new Platformer.Score(this, score_position, {"text": "Score: 0", "group": "hud"});
    this.prefabs["score"] = score;
    
    lives_position = new Phaser.Point(this.game.world.width * 0.65, 20);
    lives = new Platformer.Lives(this, lives_position, {"texture": "player_spritesheet", "group": "hud", "frame": 3, "spacing": 16});
    this.prefabs["lives"] = lives;
};

Second, we have to save the player lives and score before loading a new level, otherwise it will be restarted. For this, we will save this information in the browser localStorage when reaching the goal. Also, in the Player constructor, we first check if there’s already a previous score or lives saved in the localStorage and if so, load it. Finally, in TiledState “game_over” method we clear the localStorage.

Platformer.Goal.prototype.reach_goal = function () {
    "use strict";
    // start the next level
    localStorage.player_lives = this.game_state.prefabs.player.lives;
    localStorage.player_score = this.game_state.prefabs.player.score;
    this.game_state.game.state.start("BootState", true, false, this.next_level);
};

We can add the Lives prefab to our current levels and see how it’s working already:

lives

Items

We will create two different items:

  1. A LifeItem, that will increase the player number of lives.
  2. A ShootingItem, that will give the player the ability to attack.

First, we will create a generic Item prefab to reunite the common code between both items. All items will have an immovable physics body initialized in the constructor. Also, in the update method it will check for overlap with the player and call a “collect_item” method if that happens. By default, the “collect_item” method will only kill the Item, but we will overwrite it in our new items prefabs to do what we want.

var Platformer = Platformer || {};

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

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

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

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

Now, our LifeItem will be really simple. We only need to overwrite the “collect_item” method to increase the player number of lives after calling the original “collect_item” method, as below:

var Platformer = Platformer || {};

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

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

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

We can already see our game working with the LifeItem:

life_item

The ShootingItem is also simple, we only need to set a shooting variable in the Player prefab to true. However, now we have to add the shooting logic in the Player prefab.

var Platformer = Platformer || {};

Platformer.FireballItem = function (game_state, position, properties) {
    "use strict";
    Platformer.Item.call(this, game_state, position, properties);
};

Platformer.FireballItem.prototype = Object.create(Platformer.Item.prototype);
Platformer.FireballItem.prototype.constructor = Platformer.FireballItem;

Platformer.FireballItem.prototype.collect_item = function (item, player) {
    "use strict";
    Platformer.Item.prototype.collect_item.call(this);
    player.shooting = true;
};

To give the player the ability to shoot fireballs, we will first create a Fireball prefab. The Fireball prefab will create a physical body with a constant velocity given by its direction. Also, we will check for collisions, and when they happen, kill it.

var Platformer = Platformer || {};

Platformer.Fireball = function (game_state, position, properties) {
    "use strict";
    Platformer.Prefab.call(this, game_state, position, properties);
    
    this.direction = properties.direction;
    this.speed = +properties.speed;
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.allowGravity = false;
    // velocity is constant, but defined by direction
    if (this.direction == "LEFT") {
        this.body.velocity.x = -this.speed;
    } else {
        this.body.velocity.x = this.speed;
    }
    
    this.anchor.setTo(0.5);
    // Fireball uses the same asset as FireballItem, so we make it a little smaller
    this.scale.setTo(0.75);
};

Platformer.Fireball.prototype = Object.create(Platformer.Prefab.prototype);
Platformer.Fireball.prototype.constructor = Platformer.Fireball;

Platformer.Fireball.prototype.update = function () {
    "use strict";
    // the fireball is destroyed if in contact with anything else
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision, this.kill, null, this);
    this.game_state.game.physics.arcade.overlap(this, this.game_state.layers.invincible_enemies, this.kill, null, this);
};

With the Fireball prefab created, we have to add the ability to shoot them in the Player prefab. For this, we start checking in the update method if the player is able to shoot and if the shooting key was pressed (SPACEBAR). If so, we start a timer (created in the constructor) which will call the shoot method in a loop.

Platformer.Player.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision);
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.enemies, this.hit_enemy, null, this);
    // the player automatically dies if in contact with invincible enemies or enemy fireballs
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.invincible_enemies, this.die, null, this);
    this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.enemy_fireballs, this.die, null, this);
    
    if (this.cursors.right.isDown && this.body.velocity.x >= 0) {
        // move right
        this.body.velocity.x = this.walking_speed;
        this.direction = "RIGHT";
        this.animations.play("walking");
        this.scale.setTo(-1, 1);
    } else if (this.cursors.left.isDown && this.body.velocity.x <= 0) {
        // move left
        this.body.velocity.x = -this.walking_speed;
        this.direction = "LEFT";
        this.animations.play("walking");
        this.scale.setTo(1, 1);
    } else {
        // stop
        this.body.velocity.x = 0;
        this.animations.stop();
        this.frame = 3;
    }
    
    // jump only if touching a tile
    if (this.cursors.up.isDown && this.body.blocked.down) {
        this.body.velocity.y = -this.jumping_speed;
    }
    
    // dies if touches the end of the screen
    if (this.bottom >= this.game_state.game.world.height) {
        this.die();
    }
    
    // if the player is able to shoot and the shooting button is pressed, start shooting
    if (this.shooting && this.game_state.input.keyboard.isDown(Phaser.Keyboard.SPACEBAR)) {
        if (!this.shoot_timer.running) {
            this.shoot();
            this.shoot_timer.start();   
        }
    } else {
        this.shoot_timer.stop(false);
    }
};

In the shoot method we will use a concept called pool of objects. To avoid create and deleting a lot of objects, which could negatively impact our game performance, we will keep a group of Fireballs (called a pool) and everytime we have to create another Fireball we check if there isn’t one dead Fireball in the pool. If so, we just reset it in the new position. Otherwise, we create a new one. This is similar to what we have done in the Lives prefab, and it is expected that as the game continues, we will have created all the necessary fireballs, and will start to reuse old Fireballs instead of creating new ones.

Platformer.Player.prototype.shoot = function () {
    "use strict";
    var fireball, fireball_position, fireball_properties;
    // get the first dead fireball from the pool
    fireball = this.game_state.groups.fireballs.getFirstDead();
    fireball_position = new Phaser.Point(this.x, this.y);
    if (!fireball) {
        // if there is no dead fireball, create a new one
        fireball_properties = {"texture": "fireball_image", "group": "fireballs", "direction": this.direction, "speed": this.attack_speed};
        fireball = new Platformer.Fireball(this.game_state, fireball_position, fireball_properties);
    } else {
        // if there is a dead fireball, reset it in the new position
        fireball.reset(fireball_position.x, fireball_position.y);
        fireball.body.velocity.x = (this.direction == "LEFT") ? -this.attack_speed : this.attack_speed;
    }
};

Now, we can see our game working with fireballs:

fireball

Boss level

For the boss level I added a new enemy and the boss, which are both invincible, so if the player touches them, he will always die. This can be done by adding a new group (called “invincible_enemies”, which was actually present in some of the already shown code) and, if the player overlaps with it, he automatically dies.

Stone enemy

The stone enemy will be stopped in the ceiling until the player goes below it. When that happens, the enemy will fall over the player. To do that, in the update method we will check if the player is below the enemy, and if so, the enemy start falling.

To check for the player, we just have to compare the player x position to the enemy left and right coordinates, and the player y position must be below the enemy. To make the enemy falls, we just have to change its allowGravity property to true, as follows:

var Platformer = Platformer || {};

Platformer.StoneEnemy = function (game_state, position, properties) {
    "use strict";
    Platformer.Prefab.call(this, game_state, position, properties);
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.allowGravity = false;
    
    this.anchor.setTo(0.5);
};

Platformer.StoneEnemy.prototype = Object.create(Platformer.Prefab.prototype);
Platformer.StoneEnemy.prototype.constructor = Platformer.StoneEnemy;

Platformer.StoneEnemy.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision);
    
    // if the player is below, the enemy will fall after some time
    if (this.check_player()) {
        this.fall();
    }
};

Platformer.StoneEnemy.prototype.check_player = function () {
    "use strict";
    var player;
    player = this.game_state.prefabs.player;
    // check if player is right below the enemy
    return (player.x > this.left && player.x < this.right && player.y > this.bottom);
};

Platformer.StoneEnemy.prototype.fall = function () {
    "use strict";
    // start falling
    this.body.allowGravity = true;
};

And here is our game with the StoneEnemy:

stone_enemies

Boss

Our boss will have a simple behavior, it will keep walking forward and backward and shooting fireballs. If the player touches the boss, he will always die. For the boss behavior we will repeat a lot of things we used in other prefabs, so feel free can change the code to avoid code repetition, but I will keep it repeated here to simplify the tutorial.

var Platformer = Platformer || {};

Platformer.Boss = function (game_state, position, properties) {
    "use strict";
    Platformer.Prefab.call(this, game_state, position, properties);
    
    this.attack_rate = +properties.attack_rate;
    this.attack_speed = +properties.attack_speed;
    this.walking_speed = +properties.walking_speed;
    this.walking_distance = +properties.walking_distance;
    
    // saving previous x to keep track of walked distance
    this.previous_x = this.x;
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.velocity.x = properties.direction * this.walking_speed;
    
    this.anchor.setTo(0.5);
    
    // boss will be always attacking
    this.attack_timer = this.game_state.game.time.create();
    this.attack_timer.loop(Phaser.Timer.SECOND / this.attack_rate, this.shoot, this);
    this.attack_timer.start();
};

Platformer.Boss.prototype = Object.create(Platformer.Prefab.prototype);
Platformer.Boss.prototype.constructor = Platformer.Boss;

Platformer.Boss.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision);
    
    // if walked the maximum distance, change the velocity, but not the scale
    if (Math.abs(this.x - this.previous_x) >= this.walking_distance) {
        this.body.velocity.x *= -1;
        this.previous_x = this.x;
    }
};

Platformer.Boss.prototype.shoot = function () {
    "use strict";
    // works the same way player shoot, but using a different pool group
    var fireball, fireball_position, fireball_properties;
    fireball = this.game_state.groups.enemy_fireballs.getFirstDead();
    fireball_position = new Phaser.Point(this.x, this.y);
    if (!fireball) {
        fireball_properties = {"texture": "fireball_image", "group": "enemy_fireballs", "direction": "LEFT", "speed": this.attack_speed};
        fireball = new Platformer.Fireball(this.game_state, fireball_position, fireball_properties);
    } else {
        fireball.reset(fireball_position.x, fireball_position.y);
        fireball.body.velocity.x = -this.attack_speed;
    }
};

To make the Boss movement, we will do it like the Enemy movement, the only difference is that we won’t change the boss sprite scale, this way it will always be looking in the same direction. To recall, we will save the Boss initial position before start moving and, when it has moved a maximum distance amount we invert its velocity and update the initial position.

The Boss attacks will be done like the player fireballs, except we will change the group to be “enemy_fireballs” and we don’t have to check for the boss direction, since it’s always the same. Notice that we use the same Fireball prefab and, since we’re changing the group, it won’t collide with other enemies, and we don’t have to create a new prefab for the them.

Finally, we can see our Boss in the game, and the boss level is complete:

boss

Finishing the game

Now, you can add the new content to the other levels as you wish, and finish your game. We added a lot of content to our game and I would like to know your opinion of the final result, and what kind of game you would like to see in the next tutorials.