Topdown - Layers, Moving and Collision
09 Jan 2017Summary
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
- gameState.js is common to all my Phaser demos
- demo.js is code specific to this demo
The different layers
In order to render our tilemap properly with correct depth and collision, our tilemap will consist of multiple layers:
-
Base - this will be floors, walls and everything else that will be behind any sprites.
-
Collision - this layer will mark which tiles on the base layer are impassable.
-
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.
-
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
- Military Sprite from Gamefroot, the platform for kids to make games
- Tileset - this probably won’t exists without the creation of Manuel Riecke 1GAM entry game Blackguard. I have searched far and wide looking for a suitable tilemap with high walls and ready made TMX files.
- Player’s sprite - another important art asset to piece all of these together. Couldn’t find anything else suitable to use which have perfect animation and does actually look nice.
- Virtual gamepad plugin
- The free general purpose Tiled map editor
- WASD keys from lewster32. I love how he came up with the way to handle the WASD keys to behave like the cursor keys.
- and of course the awesome HTML5 Phaser game framework by Richard Davey and his website
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);
}
}
}