Room Changing With Phaser 3 and Tiled
I decided to dive into Phaser 3 and Tiled with my next game project. I was looking to make a room change similar to Link to the Past (the SNES Zelda). The implementation is simple and the result was pleasing so I wanted to share.
I’ve used Tiled a little in the past but this is the first excuse I’ve really had to dive in to it. If you’re not familiar with Tiled, there are a million ways to learn more.
Tiled
First, we make up a level with some rooms. The image below shows a simple level layout. It has four layers; floor, walls, above player, and an object layer. The floor is for all the tiles on the floor (I am super clever at naming things). The walls you can guess what that is, but it has the added bonus of being used for collisions. You will need to set the collide property for the tiles you don’t want the player walking through in the TileSet. The above player layer is just for doorways in this example so that the player appears to pass through the door as they move from room to room.
The object layer is where each room’s limits are defined as a rectangle. I started by simply clicking and dragging a rectangle object around each room then used the object’s properties to make it a pixel-perfect match. Also in properties, you’ll want to give this room a name and give it the type “Room”.
You may also notice there is an object for the player spawn point and stairs. I’m not going to go into a lot of detail about those, but they’re not too hard to figure out once the room change concept is understood.
If you’re curious about those sprites, credit goes to Corey Archer. Thanks for the sprites Corey. The player sprite (not pictured) is by Aarmm1988, both featured on OpenGameArt.org.
Once that part is over, export it as a JSON map file. This exported file is what gets loaded in Phaser to define the level. It also contains the information we added in the object layer.
Phaser
This post only contains code snippets, to see the whole project (it’s small) check it out on GitHub. The Tiled map is also there.
These first two lines found in the Level class file’s preload method (game/Level.js) will bring in the tile image and object data.
// Level tiles and data.
this.load.image("tiles", "game/assets/dungeon_tiles_2_extruded.png");
this.load.tilemapTiledJSON("level-1", "game/assets/level-1.json");
After preload, the create method is called. This will set up the tilemap and the objects so we can use them throughout the rest of the game code. These first lines set up the map, tileset, and layers. The layers should look familiar from Tiled also it’s important to match the name set for each layer in Tiled… that’s how it’s found.
// Make map of level 1.
this.map = this.make.tilemap({key: "level-1"});
// Define tiles used in map.
const tileset = this.map.addTilesetImage("dungeon_tiles_2", "tiles", 16, 16,);
// The map layers.
this.floorLayer = this.map.createStaticLayer("floor", tileset);
this.wallsLayer = this.map.createStaticLayer("walls", tileset);
this.aboveLayer = this.map.createStaticLayer("above_player", tileset);
The next bit of code allows the player to collide with the walls. Physics is enabled for the world to make collisions handling easier. For the walls layer we set collisions by property, which is set up in Tiled. It’s literally just a checkbox for each tile the player should not be able to walk through. I didn’t go too deep into how to do these in Tiled because there are plenty of tutorials for this. The last line of this section is what puts the above layer… above.
// Set physics boundaries from map width and height.
this.physics.world.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
// Collisions based on layer.
this.wallsLayer.setCollisionByProperty({collides: true});
// Set the above player layer higher than everything else.
this.aboveLayer.setDepth(10);
Finally getting to something interesting. Here is where we loop through all the objects in the room and do some setup based on what the objects are. In Tiled we setup names and types for each object. This loop has an example of how to pull objects out based on types or names. The rooms are what we’re interested in, but it’s easy to add enemy spawn points in the tiled map similar to how we added the player. I also threw in some stairs.
// Loop through all the objects.
this.map.findObject('Objects', function(object) {
// rooms
if (object.type === 'Room') {
this.rooms.push(object);
}
// stairs
if (object.name === 'Stairs') {
this.stairs.add(new Phaser.GameObjects.Sprite(this, object.x, object.y));
}
// spawn points
if (object.type === 'Spawn') {
if (object.name === 'Player') {
this.player = new Player(this, object.x, object.y);
}
}
}, this);
There is another method in the Level class, but we’ll get back to that. Next, we need to look in the Player class (game/Player.js). The player was instantiated in the previous loop once the object with the player’s spawn location was found. Looking through the Player class you’ll see a lot of set up required to make the player work; keys, animations, physics, etc. roomCheck is a method called every frame, it checks to see what room the player is currently in. If the player changes rooms, it will set a roomChange flag.
Back in the Level class, the update method I said we’d get back to later (it didn’t take long) does the magic. On a roomChange flag being true it updates the camera boundaries to the room object location’s and size’s we set up in the Tiled object layer.
// Change camera boundaries when player moves to new room.
if (this.player.roomChange) {
this.cameras.main.setBounds(this.rooms[this.player.currentRoom].x,
this.rooms[this.player.currentRoom].y,
this.rooms[this.player.currentRoom].width,
this.rooms[this.player.currentRoom].height,
true);
}
There is plenty of room for improvement too. For example, the transition is instant but it could be a nice scrolling effect. A very noticeable problem is if you have a room smaller than the camera boundaries you can see into adjacent rooms. There is an example of this in the demo, the room with the checkered floor is small enough to show it. A potential fix for this would be to use a dynamic layer (opposed to the static layers we used in the Level class) to hide tiles not in the current room.
The effect is simple and neat, this example is playable on the GitHub page, and the code is there too.