Topdown - Layers, Moving and Collision

Last modified: 04 Feb 2018

Summary

We’re going to render a tilemap exported from Tiled with multiple layers and have a player walking around with collision in place.

Source code

The different layers

In order to render our tilemap properly with correct depth and collision, our tilemap will consist of multiple layers:

  1. Base - this will be floors, walls and everything else that will be behind any sprites.

  2. Collision - this layer will mark which tiles on the base layer are impassable.

  3. Foreground - everything else that needs to be infront of all the sprites will sit on this layer. Top portion of the trees, bottom walls etc., are good candidates for the foreground layer.

  4. Objects - I’m using the Object layer in Tiled to add meta information for the current tilemap. From the following screenshot, the tilemap identifies where the entrance is and the exit. As you will see later, once the map loaded our player’s sprite will have a short cut-scene that will move the player from the entrance to it’s starting position which is identified by the green tile.

And this is how it looks like when we turn on all the layers:

Creation

After we preload our tilemap JSON file exported from Tiled and the corresponding images into Phaser’s cache system:

1
2
3
game.load.tilemap('tavern', '/files/phaser/tilemaps/tavern.json', null, Phaser.Tilemap.TILED_JSON);
game.load.image('tiles', '/files/phaser/tilemaps/tilesetHouse.png');
game.load.image('hints', '/files/phaser/tilemaps/Hints.png');

We proceed to creating the tilemap and the necessary layers. The way Phaser’s depth sorting works is it follows the order you added the display objects into the world.

This meean that our player’s sprite and all other object sprites are created between the base layer and the foreground layer. This way we can make sure that the sprites are always in front of the base tiles and behind the foreground tiles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
initTilemap() {
  //  The 'tavern' key here is the Loader key given in game.load.tilemap
  let map = game.add.tilemap('tavern');
  // store a reference so we can access it elsewhere on this class
  this.map = map;

  // The first parameter is the tileset name, as specified in the Tiled map editor (and in the tilemap json file)
  // The second parameter maps this name to the Phaser.Cache key 'tiles'
  map.addTilesetImage('tilesetHouse', 'tiles');
  map.addTilesetImage('Hints', 'hints');

  // create the base layer, these are the floors, walls
  // and anything else we want behind any sprites
  map.createLayer('Base');

  // next create the collision layer
  let collisionLayer = map.createLayer('Collision');
  this.collisionLayer = collisionLayer;

  // we don't want the collision layer to be visible
  collisionLayer.visible = false;

  // inform phaser that our collision layer is our collision tiles
  // in our case, since we separated out the collision tiles into its own layer
  // we pass an empty array and passing in true to enable collision
  map.setCollisionByExclusion([], true, this.collisionLayer);

  //  This resizes the game world to match the layer dimensions
  collisionLayer.resizeWorld();

  // we will have to initialize our player here
  // so it's sprite will show between the base and foreground tiles
  this.initPlayer();

  // creating the foreground layer last after all moving sprites
  // ensures that this layer will stay above during depth sorting
  map.createLayer('Foreground');

  // pull the exit area from the object layer
  // we will be using this one during update to check if our player has moved into the exit area
  let exit = this.map.objects.Meta.find( o => o.name == 'exit');
  this.exitRect = new Phaser.Rectangle(exit.x, exit.y, exit.width, exit.height);
}

An alternative way to set collision tiles is using [setCollision](http://phaser.io/docs/2.6.1/Phaser.Tilemap.html#setCollision) and you can use the same base layer and pass in the collidable tiles id from Tiled. But I prefer having a completely separate collision layer which for me is more easy to manage and see on Tiled.

Player sprite

For this particular tilemap graphic I’m more comfortable using using an overhead rendered sprite, which for me provides a more seamless fluid directional movement.

Another option is to use the common 4 directional rendered sprite:

With an overhead rendered sprite, we only need one direction of the sprite and can have it face in any direction.

Virtual gamepad

I use this excellent Virtual Gamepad plugin, that is fixed on a position. Another alternative is one that appears only when the player starts touching the mobile screen. The touch control plugin specifically behaves that way so if you want that kind of style you can use that plugin instead.

Both behaves almost the same way, providing you a both x and y variables that automatically increases as the player moves the joystick far from the center.

1
2
3
4
5
6
7
8
9
10
// only if it's in use we go through all the logic below
if (this.joystick.properties.inUse) {
  // set the sprite's angle from the plugin
  player.angle = this.joystick.properties.angle;

  // the plugin has a max of 99
  // i'm just adding a bit more for faster movement
  player.body.velocity.x = this.joystick.properties.x * 1.5;
  player.body.velocity.y = this.joystick.properties.y * 1.5;
}

When we move our player’s sprite, we need to change it’s velocity instead of updating the sprite’s x and y position so Phaser’s collision system kicks in.

We then play the moving animation as long as the player is moving:

1
2
3
4
5
6
7
8
// check if player is moving
if(Math.abs(player.body.velocity.x) > 0 || Math.abs(player.body.velocity.y) > 0) {
  // play the animation, phaser just returns when it's currently animating
  // so it's fine to call it on every frame
  player.play('move');
} else {
  player.play('idle');
}

Collision

This one is easy - during update we just make a call to game.physics.arcade.collide(this.player, this.collisionLayer); passing the player and the collision layer we created earlier and Phaser takes care of everything for us. Phaser is just so fun!

Entrance and exit

I wanted to put in a sort of mini cutscene, where we can see our player enterting the level, so I put in an entrance and starting positions in the objects layer that we can extract from inside our code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resetPlayer() {
  // pull the entrace and start coordinates from the objects layer
  let entrance = this.map.objects.Meta.find( o => o.name == 'entrance');
  let start = this.map.objects.Meta.find( o => o.name == 'start');

  // flag so we can disable some parts of the game
  this.cutscene = true;

  // start position and angle of our player's sprite
  this.player.position.set(entrance.x, entrance.y);
  this.player.angle = 0;

  // start the cutscene
  let tween = game.add.tween(this.player).to({x: start.x, y: start.y}, 1500);
  tween.onComplete.add(()=> {
    // return control back to player
    this.cutscene = false;
  });
  tween.start();
}

And while the cutscene is playing out, we should ignore all updates:

1
2
3
4
5
6
7
update() {
  // disable any update, inputs etc., during cutscenes
  // we don't want anything intefering
  if(this.cutscene) return;

  // update code below
}

For the exit, because it is is an area, not just a point, I use Phaser.Rectangle to store the information:

1
2
let exit = this.map.objects.Meta.find( o => o.name == 'exit');
this.exitRect = new Phaser.Rectangle(exit.x, exit.y, exit.width, exit.height);

Then while our player is moving, we can check if the player hits our exit area:

1
2
3
4
5
// check if player has entered the exit area
if(Phaser.Rectangle.containsPoint(this.exitRect, player.position)) {
  // and we just reset it to it's starting position
  this.resetPlayer();
}

In my case, I just replay the cutscene.

This concludes part 1 of my topdown tutorial. I’ll have the 2nd part covering shooting and 3rd part we will have zombies walking around that we can actually shoot and kill. It won’t be anytime soon but I should have 2nd part publish atleast in the first quarter of this year.

My next Phaser tutorial would be about zooming. Pleasure writing this up for you. <3

Credits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
'use strict';

class GameState extends Phaser.State {
  preload() {
    this.load.onLoadStart.add(this.loadStart, this);
    this.load.onFileComplete.add(this.fileComplete, this);
    this.load.onLoadComplete.add(this.loadComplete, this);
  }

  loadStart() {
    this.loadingText = this.add.text(20, this.world.height - 32, 'Loading...', { font: '20px Arial', fill: '#ffffff' });
  }

  fileComplete(progress, cacheKey, success, totalLoaded, totalFiles) {
    this.loadingText.setText('File Complete: ' + progress + '% - ' + totalLoaded + ' out of ' + totalFiles);
  }

  loadComplete() {
    game.world.remove(this.loadingText);

    this.time.advancedTiming = true;
  }

  create() {
    this.showDebug = false;
    game.input.keyboard.addKey(Phaser.KeyCode.D).onDown.add(() => {
      this.showDebug = !this.showDebug;
    });

    game.camera.x = game.world.centerX - game.width / 2;
  }

  createKeyboardMovement() {
    this.keyboardCursors = game.input.keyboard.createCursorKeys();
    this.moveSpeed = { x: 0, y: 0 }

    this.wasd = {
      up: game.input.keyboard.addKey(Phaser.Keyboard.W),
      down: game.input.keyboard.addKey(Phaser.Keyboard.S),
      left: game.input.keyboard.addKey(Phaser.Keyboard.A),
      right: game.input.keyboard.addKey(Phaser.Keyboard.D),
    };
  }

  createVirtualGamepad() {
    // create virtual gamepad
    let gamepad = game.plugins.add(Phaser.Plugin.VirtualGamepad)
    this.joystick = gamepad.addJoystick(60, game.height - 60, 0.5, 'gamepad');

    // plugin wants the creation of a button
    // but there is no usage for it here so i'm just going to hide it
    this.gamepadbutton = gamepad.addButton(game.width - 60, game.height - 60, 0.5, 'gamepad');
    this.gamepadbutton.visible = false;
  }

  goingLeft() {
    return this.keyboardCursors.left.isDown || this.wasd.left.isDown || (this.joystick && this.joystick.properties.left);
  }

  goingRight() {
    return this.keyboardCursors.right.isDown || this.wasd.right.isDown || (this.joystick && this.joystick.properties.right);
  }

  update() {
  }

  render() {
    game.debug.text(game.time.fps, 5, 14, '#00ff00')
  }

  loadSpriter(key) {
    if(!this.spriterLoader) this.spriterLoader = new Spriter.Loader();

    let spriterFile = new Spriter.SpriterXml(game.cache.getXML(key + 'Animations'));

    // process loaded xml/json and create internal Spriter objects - these data can be used repeatly for many instances of the same animation
    let spriter = this.spriterLoader.load(spriterFile);

    return new Spriter.SpriterGroup(game, spriter, key, key);
  }

  drawIsoGrid() {
    let isoGrid = new IsoGrid(game);
    isoGrid.drawGrid();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
'use strict';

class DemoState extends GameState {

  preload() {
    super.preload();

    // Tiled exported tilemap
    game.load.tilemap('tavern', '/files/phaser/topdown/tavern.json', null, Phaser.Tilemap.TILED_JSON);
    // and it's corresponding tileset files
    game.load.image('tiles', '/files/phaser/topdown/tilesetHouse.png');
    game.load.image('hints', '/files/phaser/topdown/Hints.png');

    // our player sprite
    game.load.atlas('player', '/files/phaser/rifleguy/atlas.png', '/files/phaser/rifleguy/atlas.json');

    // the images for the virtual gamepad
    game.load.atlas('gamepad', '/files/phaser/gamepad/dark/atlas.png', '/files/phaser/gamepad/dark/atlas.json');
  }

  create() {
    super.create();

    // initialize our tilemap
    // the player initialization is inside the initTilemap
    this.initTilemap();

    // we provide both keyboard and a virtual gamepad for player movement
    this.initKeyboard();
    this.initVirtualGamepad();

    // cutscene
    this.resetPlayer();

    // show collision layer
    game.input.keyboard.addKey(Phaser.KeyCode.C).onDown.add(() => {
      this.collisionLayer.visible = !this.collisionLayer.visible;
    });
  }

  initTilemap() {
   //  The 'tavern' key here is the Loader key given in game.load.tilemap
    let map = game.add.tilemap('tavern');
    // store a reference so we can access it elsewhere on this class
    this.map = map;

    // The first parameter is the tileset name, as specified in the Tiled map editor (and in the tilemap json file)
    // The second parameter maps this name to the Phaser.Cache key 'tiles'
    map.addTilesetImage('tilesetHouse', 'tiles');
    map.addTilesetImage('Hints', 'hints');

    // create the base layer, these are the floors, walls
    // and anything else we want behind any sprites
    map.createLayer('Base');

    // next create the collision layer
    let collisionLayer = map.createLayer('Collision');
    this.collisionLayer = collisionLayer;

    // we don't want the collision layer to be visible
    collisionLayer.visible = false;

    // inform phaser that our collision layer is our collision tiles
    // in our case, since we separated out the collision tiles into its own layer
    // we pass an empty array and passing in true to enable collision
    map.setCollisionByExclusion([], true, this.collisionLayer);

    //  This resizes the game world to match the layer dimensions
    collisionLayer.resizeWorld();

    // we will have to initialize our player here
    // so it's sprite will show between the base and foreground tiles
    this.initPlayer();

    // creating the foreground layer last after all moving sprites
    // ensures that this layer will stay above during depth sorting
    map.createLayer('Foreground');

    // pull the exit area from the object layer
    // we will be using this one during update to check if our player has moved into the exit area
    let exit = this.map.objects.Meta.find( o => o.name == 'exit');
    this.exitRect = new Phaser.Rectangle(exit.x, exit.y, exit.width, exit.height);
  }

  initPlayer() {
    let player = game.add.sprite(0, 0, 'player');
    this.player = player;

    // basic stuff, the MOVE_SPEED is the same as the
    // max speed of the virtual game pad
    player.MOVE_SPEED = 150;
    player.anchor.set(0.5);
    player.scale.set(0.2);
    player.animations.add('idle', [0 ,1 ,2 ,3 ,4 ,5 ,6 ,7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 30, true);
    player.animations.add('move', [20 ,21 ,22 ,23 ,24 ,25 ,26 ,27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38], 40, true);
    player.play('move');

    // enable physics arcade
    // so phaser will take care of collision for us
    game.physics.arcade.enable(player);

    // set a custom smaller collision box for our player's sprite
    // so our player can fit into those area that have smaller walkable space
    player.body.setSize(100, 150,100, 50);

    // keep the camera following our player throughout
    game.camera.follow(player);
  }

  initKeyboard() {
    this.keyboardCursors = game.input.keyboard.createCursorKeys();
    this.moveSpeed = { x: 0, y: 0 }

    this.wasd = {
      up: game.input.keyboard.addKey(Phaser.Keyboard.W),
      down: game.input.keyboard.addKey(Phaser.Keyboard.S),
      left: game.input.keyboard.addKey(Phaser.Keyboard.A),
      right: game.input.keyboard.addKey(Phaser.Keyboard.D),
    };
  }

  initVirtualGamepad() {
    // create our virtual gamepad
    let gamepad = game.plugins.add(Phaser.Plugin.VirtualGamepad)
    this.joystick = gamepad.addJoystick(90, game.height - 90, 0.75, 'gamepad');

    // plugin wants the creation of a button
    // but there is no usage for it here so i'm just going to hide it
    let button = gamepad.addButton(game.width - 90, game.height - 90, 0.75, 'gamepad');
    button.visible = false;
  }

  resetPlayer() {
    // pull the entrace and start coordinates from the objects layer
    let entrance = this.map.objects.Meta.find( o => o.name == 'entrance');
    let start = this.map.objects.Meta.find( o => o.name == 'start');

    // flag so we can disable some parts of the game
    this.cutscene = true;

    // start position and angle of our player's sprite
    this.player.position.set(entrance.x, entrance.y);
    this.player.angle = 0;

    // start the cutscene
    let tween = game.add.tween(this.player).to({x: start.x, y: start.y}, 1500);
    tween.onComplete.add(()=> {
      // return control back to player
      this.cutscene = false;
    });
    tween.start();
  }

  update() {
    // disable any update, inputs etc., during cutscenes
    // we don't want anything intefering
    if(this.cutscene) return;

    this.updatePlayer();

    // let phaser handle our player collision with the collision layer
    game.physics.arcade.collide(this.player, this.collisionLayer);
  }

  updatePlayer() {
    // shorthand so i don't have to reference this all the time
    let keyboardCursors = this.keyboardCursors;
    let wasd = this.wasd;
    let player = this.player;
    let moveSpeed = this.moveSpeed;
    let joystick = this.joystick;

    // set our player's velocity to 0
    // so the sprite doesn't move when there is no input from our player
    player.body.velocity.x = 0;
    player.body.velocity.y = 0;

    // keyboard movement
    // left and right keyboard movement
    if (keyboardCursors.left.isDown || wasd.left.isDown) moveSpeed.x = -player.MOVE_SPEED;
    else if (keyboardCursors.right.isDown || wasd.right.isDown) moveSpeed.x = player.MOVE_SPEED;
    else moveSpeed.x = 0;

    // up and down keyboard movement
    if (keyboardCursors.up.isDown || wasd.up.isDown) moveSpeed.y = -player.MOVE_SPEED;
    else if (keyboardCursors.down.isDown || wasd.down.isDown) moveSpeed.y = player.MOVE_SPEED;
    else moveSpeed.y = 0;

    if(Math.abs(moveSpeed.x) > 0 || Math.abs(moveSpeed.y) > 0) {
      player.body.velocity.x = moveSpeed.x;
      player.body.velocity.y = moveSpeed.y;

      // set direction using Math.atan2
      let targetPos = { x: player.x + moveSpeed.x, y: player.y + moveSpeed.y };
      player.rotation = Math.atan2(targetPos.y - player.y, targetPos.x - player.x);
    }

    // virtual gamepad movement
    // check first if it's in use before we go through all the logic below
    if (joystick.properties.inUse) {
      // set the sprite's angle from the plugin
      player.angle = joystick.properties.angle;

      // the plugin has a max of 99
      // i'm just adding a bit more for faster movement
      player.body.velocity.x = joystick.properties.x * 1.5;
      player.body.velocity.y = joystick.properties.y * 1.5;

      // check if player has entered the exit area
      if(Phaser.Rectangle.containsPoint(this.exitRect, player.position)) {
        // and we just reset it to it's starting position
        this.resetPlayer();
      }
    }

    // check if player is moving
    if(Math.abs(player.body.velocity.x) > 0 || Math.abs(player.body.velocity.y) > 0) {
      // play the animation, phaser just returns when it's currently animating
      // so it's fine to call it on every frame
      player.play('move');
    } else {
      player.play('idle');
    }
  }

  render() {
    super.render();

    if(this.collisionLayer.visible) {
      game.debug.body(this.player);
    }
  }
}

Look! I have new stuff coming right up about... but really, I rarely send out emails! Promise.