Randomly Generated Dungeons in Godot Part 3 – Finishing Touches

You can click here to read part 1
You can click here to read part 3

Last time we worked on this tutorial we had the isometric world being created, the player being moved to the center of the map and collisions with the tilemap working properly. What we did not have working properly was sprite depth, resulting in the player always rendering above the walls. So in this final part of the tutorial we are going to be adding the finishing touches to this little project.

Before we get working on the sprite depth issue, I want to first address an issue that will help clean up the rooms that the algorithm generates. Currently the random walk can sometimes leave a single wall tile surrounded by floor tiles. You may have seen them yourself if you followed the last tutorial.

There are examples in this screenshot

Fixing this little issue doesn’t take a whole lot of extra work. Inside of our _generate_map() function, right after the nested loop that is used to flag walls (right before we move the player at the end), we can make another nested for loop that will check for walls, and if the wall has a floor on all 4 sides, we just turn it into a floor.

for x in room_size.x:
		for y in room_size.y:
			if get_cell(x, y) == WALL:
				if get_cell(x-1, y) == GROUND && get_cell(x+1, y) == GROUND && get_cell(x, y-1) == GROUND && get_cell(x, y+1) == GROUND:
					set_cell(x, y, GROUND);

With that additional little bit of code, the maps that this algorithm generates will look a lot cleaner, and will not have random walls sticking out like a sore thumb in our rooms. Now we can focus on getting the player to render behind the walls.

Thankfully Godot 3.2.1 has some functionality built into the tilemap node that will help us out with this. Inside of the tilemap properties we can enable Y Sort, and change the Tile Origin from Top Left to Center.

If it was this easy, why didn’t you just do it last time?

This is only the first part of the solution. I am not sure if you ever tried to manually place tiles on the tilemap, but if you did you might have noticed something funny. Because my sprites are positioned at the bottom on the PNG file, the tiles are drawn below their coordinates on the grid. To fix this, we need to go inside of the tileset data and offset the texture and shape.

Where you can find the Texture and Shape offset for the Tiles

With my sprites I found that an offiset of -64 to the texture and collision shape puts it just about where it needs to be. If you are using your own sprites, your adjustments will probably be different than mine. You will need to apply the offset to the wall and floor tiles and even the void tile (because it has a collision shape). With all of this work complete, when we run our game we get something that looks like this.

Not quite the solution we were looking for.

The player is rendering behind the walls, just like we want, but he is also rendering behind the floor when he moves through it. The reason this is happening is because of the isometric perspective the player will register as above / below the floor at the half way point. This works perfectly for walls but not so great for the floor.

The easiest solution that was proposed to me was to split the floor and walls into separate tilemap nodes. You have the wall tilemap node as a child of the floor and only turn ysort on for the wall tilemap node. Any object that we want to render behind the walls (like the player or enemies) needs to be a child of the wall tilemap node.

Make sure your Wall Tilemap has the same settings as the floor. Also don’t forget to load the tileset

This change will break the game if you try to run it, we need to modify our script so that the game will run. At the start of the script we can make an onready variable to reference the new tilemap and the player scene.

onready var Walls = $WallTiles;
onready var Player = $WallTiles/KinematicBody2D;

After these references have been made we go to where we were adding the walls and insert our new variable before the function calls (do the same thing when we move the player). To remove the single walls we now add a floor tile to the floor tilemap and set the wall tile to be the default value of -1. You can edit your code to look like mine.

#This is inside of our _generate_map() function
for x in room_size.x:
	for y in room_size.y:
		if get_cell(x, y) == GROUND:
			if get_cell(x, y-1) == VOID:
				Walls.set_cell(x, y-1, WALL);
			if get_cell(x-1, y) == VOID:
				Walls.set_cell(x-1, y, WALL);
					
for x in room_size.x:
	for y in room_size.y:
		if Walls.get_cell(x, y) == WALL:
			if get_cell(x-1, y) == GROUND && get_cell(x+1, y) == GROUND && get_cell(x, y-1) == GROUND && get_cell(x, y+1) == GROUND:
				Walls.set_cell(x, y, -1);
				set_cell(x, y, GROUND);
	
Player.position = map_to_world(room_size / 2);

Now the player will always render above the floor, and will render behind the walls, like we want. There is one last thing that frustrates me with this solution. Currently the walls will always render in front of the floor when I had intended the floors to render in front of the walls. If you look carefully at the screenshot, you can see that the walls will cut off the floor, and I don’t really like the way that it looks.

My beautiful floors, what have you done?

The solution that I came up with is to lift up the walls so that the bottom of the wall lines up with the top of the floor. I adjusted the offsets to the walls from -64 to -69 and it seemed to work. But now that the walls have been lifted up, there will be a little gap under the walls that I don’t think looks very good. To fix that, I add another floor beneath every wall.

for x in room_size.x:
	for y in room_size.y:
		if get_cell(x, y) == GROUND:
			if get_cell(x, y-1) == VOID:
				Walls.set_cell(x, y-1, WALL);
				set_cell(x, y-1, GROUND);
			if get_cell(x-1, y) == VOID:
				Walls.set_cell(x-1, y, WALL);
				set_cell(x-1, y, GROUND);
Yay, its working properly.

With that, we should have the players rendering behind the walls when he is above them and in front of them when below. The walls should have a floor underneath them so that there isn’t a gap below the walls. If you notice that the player is still clipping behind the wall at the corners, you just need to make the hitbox for the wall or the player slightly bigger and it should fix the problem.

Until Next Time
– Steven

Randomly Generated Dungeons in Godot Part 2 – Isometric

You can click here to read part 1

Last time I touched this project (almost a year ago, ouch) we got the drunken walk algorithm working inside of the Godot game engine. Now we are going to continue with the project and spawn in walls and a player.

Because it had been so long I went and tried to follow my own tutorial, to get back up to speed, and found it hard to follow. So I have gone back and added some details that I felt that it was lacking. It should be easier to follow now.

I have been on a bit of an Isometric kick so I thought it would be cool to get this random map generation to work in an isometric perspective. Like last time, the sprites that I used can be downloaded here

Just like with the isometric dungeons in Gamemaker Studio, we don’t need walls on all 4 sides of the floor, only two of them. So at the end of our _generate_map() function we can run a nested for loop, checking for ground tiles and flag the tiles above and to the left as walls.

for x in room_size.x:
	for y in room_size.y:
		if get_cell(x, y) == GROUND:
			if get_cell(x, y-1) == VOID:
				set_cell(x, y-1, WALL);
			if get_cell(x-1, y) == VOID:
				set_cell(x-1, y, WALL);

This step alone will not flag the walls because the tilemap is full of null values and not VOID so these if statements will not resolve to true. At the beginning of _generate_map() we need another for loop that sets all of the tiles as void.

for x in room_size.x:
	for y in room_size.y:
		set_cell(x, y, VOID);

Now when we run the scene, it should flag all of the walls. What you may notice is that floor tiles right at the edge of the scene will not have any walls, and to fix that you will just need to update the clamps from 0 and room_size.x/y -1 to 1 and room_size.x/y -2. This creates a 1 tile border around the edge of the map that can be dedicated to spawning in walls.

2D Random Walk with walls spawning in

Next we need to go to the editor and change the tilemap mode from square to isometric and update the sprites that we are using. You can either add my sprites or make your own.

If you do decide to make your own sprites the floor tile needs to be wider than it is tall. A good ratio is 64 pixels wide and 32 pixels tall. If You can make your floor 34 pixels tall to give it a little bit of thickness at the edges (You will see what I mean when we are done).

Clicking on the TileMap Node will display its inspector information on the right hand side of the window. Using the Mode drop down, select isometric instead of square, update the cell size from 32 by 32 to 64 by 32 and using the Tile Set drop down select New Tile Set.

When we click on the New Tile Set, the window at the bottom of the screen will update. Drag your new tile set sprite into the filesystem window and then add your new sprite to the tile set selector. My isometric sprites have a little bit of white space in between the tiles so we need to modify the step to be 64 by 97 and add a separation of 2 between the tiles and an offset of 1 like this.

A better quality video can be located here, Set the snap offset to 1 instead of 0

We need to add collision to the void and wall tiles that matches the shape of the floor. You will probably need to play with the snap options to move the grid lines so that it lines up properly.

I set the snap step to 32 by 16 and the offset at 5 to set the collision for the wall, and 1 to set the collision for the void space. You don’t need to worry about the little bit of the wall that is below the collision, the floor is going to draw over that.

If you have done everything properly when you execute the scene, it should be drawing the map in an isometric perspective, but some of your map may be off screen.

With the map rendering in an isometric perspective we are ready to add a player character and a camera so that we can explore the map. You will need to change the game window from 1024 by 1024 to something more appropriate. I picked 960 by 540.

To make our player we need to make a new scene. In the new scene menu, select other node and add a KinematicBody2D as the root node, add the player character sprite to your project and then drag the player sprite into the scene, and add a Camera2D and CollisionPolygon2D as child nodes to the Kinematic Body. The feet of the player sprite should be lined up with the horizontal pink line in the scene, consider that the floor.

The collision box on the CollisionPolygon should be in an isometric shape just like the tiles. It only needs to have 4 points. You can set them at the values in the screenshot below, and you need to check the “Current” flag on the camera2d node so that the camera will follow the player.

The KinematicBody2d node is going to need a script attached so we can listen for input and move the player. We assign a speed value to the player, and inside of _physics_process(delta): we listen for keystrokes and move the object based off of our speed value. The finished script looks something like this

extends KinematicBody2D

# Declare member variables here. Examples:
export (float) var spd = 120;

# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta):
	var motion = Vector2.ZERO;
	
	if (Input.is_action_pressed("ui_up")):
		motion += Vector2.UP;
	if (Input.is_action_pressed("ui_down")):
		motion += Vector2.DOWN;
	if (Input.is_action_pressed("ui_right")):
		motion += Vector2.RIGHT;
	if (Input.is_action_pressed("ui_left")):
		motion += Vector2.LEFT;
		
	motion = motion.normalized();
	motion *= spd;
	move_and_slide(motion);

Finally you can add the player scene as a child of the tilemap simply by dragging the player scene on top of the tilemap in the editor.

If you were to run the game at this point, you would notice that the player does not spawn inside of the map, and you are unable to move. We need to add 1 more line of code to the _generate_map() function of the tilemap script which moves the player inside of the tilemap.

$KinematicBody2D.position = map_to_world(room_size / 2);

With that one line of code added, when you execute the game, the player should be moved onto the very first tile that was flagged as a floor, and you should be able to move around the map, and collide with the walls.

Depth doesn’t work yet, so the player will always render above the walls, but as you can see, the player is able to move around the map, collide with the walls and the void space. We have set up the beginnings of a 2d Isometric game in the GODOT engine.

You can click here to read part 3

Until Next Time
– Steven