Upgrading to auto tiles

By this point we have a bare minimum level generator that uses one boxy tile type.

In this lesson we’ll use the auto tile feature to make our level more appealing as you can see here.

We’ll also allow for other types of assets such as chests.

Adding more assets gives us more flexibility when building rooms because we can include sprites or other interactive objects and stack them on top of the TileMap.

Updating the tile set

We have prepared a tile set which has ground and vegetation auto tiles along with some single tiles we can use to fill out the rooms.

Update the Tile Set property in the Inspector for all TileMaps from both RandomWalker and Rooms scenes to use the RandomWalker/Levels/tileset.tres instead.

Feel free to explore this tile set in Godot’s TileSet docker before moving on but be careful not to redraw any tile regions.

Update each room to use the new tile set like in the example below.

Be sure to add a few Maybe* tiles. These tiles have a chance at being placed to add variety to the rooms.

Updating placement functions

Since our auto tiles use a 2×2 bitmask let’s first widen the level border walls by 2 in _place_walls() function from RandomWalker.gd.

func _place_walls(type: int = 0) -> void:
    var cell_grid_size := _grid_to_map(grid_size)

    for x in [-2, -1, cell_grid_size.x, cell_grid_size.x + 1]:
        for y in range(-2, cell_grid_size.y + 2):
            level.set_cell(x, y, type)

    for x in range(-1, cell_grid_size.x + 2):
        for y in [-2, -1, cell_grid_size.y, cell_grid_size.y + 1]:
            level.set_cell(x, y, type)

If Godot can’t find matching auto tiles based on the bitmask, it will default to using the tile set as the icon in the TileSet docker.

Increasing the thickness of the outer walls fixes the possible problem shown in the next picture.

We also need to call level.update_bitmask_region() at the end of _place_side_rooms() to refresh the TileMap so that Godot properly render the auto tiles.

func _place_side_rooms() -> void:
    yield(self, "path_completed")
    var rooms_max_index: int = _rooms.Type.size() - 1
    for key in _state.empty_cells:
        var type := _rng.randi_range(0, rooms_max_index)
        _copy_room(key, type)
    level.update_bitmask_region()

Updating the Rooms script

Before updating _copy_room() let’s update _get_room_data() from Rooms.gd since _copy_room() will depend on it.

Open Rooms.gd in the script editor and define the following enumeration below enum Type:

enum Cell { GROUND, VEGETATION, SPIKES, MAYBE_GROUND, MAYBE_BUSH, MAYBE_TREE, MAYBE_SPIKES }

These are the tile types corresponding to the indices in tileset.tres.

For example Cell.GROUND is 0 and corresponds to the Ground auto tile while Cell.MAYBE_TREE corresponds to the MaybeTree single tile. These will be used to map static & probabilistic tiles. To do that, define the following constant:

const CELL_MAP := {
    Cell.GROUND: {"chance": 1.0, "cell": [[Cell.GROUND]], "size": Vector2.ONE},
    Cell.VEGETATION: {"chance": 1.0, "cell": [[Cell.VEGETATION]], "size": Vector2.ONE},
    Cell.SPIKES: {"chance": 1.0, "cell": [[Cell.SPIKES]], "size": Vector2.ONE},
    Cell.MAYBE_GROUND: {"chance": 0.7, "cell": [[Cell.GROUND]], "size": Vector2.ONE},
    Cell.MAYBE_BUSH: {"chance": 0.3, "cell": [[Cell.VEGETATION]], "size": Vector2.ONE},
    Cell.MAYBE_TREE:
    {
        "chance": 0.8,
        "cell": [[Cell.VEGETATION, Cell.VEGETATION], [Cell.VEGETATION, Cell.VEGETATION]],
        "size": 2 * Vector2.ONE
    },
    Cell.MAYBE_SPIKES: {"chance": 0.5, "cell": [[Cell.SPIKES]], "size": Vector2.ONE}
}

Static tiles have the chance key set to 1.0 while probabilistic tiles have chance set to something other than 1.0.

Let’s take Cell.MAYBE_TREE as an example.

Cell.MAYBE_TREE:
{
    "chance": 0.8,
    "cell": [[Cell.VEGETATION, Cell.VEGETATION], [Cell.VEGETATION, Cell.VEGETATION]],
    "size": 2 * Vector2.ONE
},

Cell.MAYBE_TREE corresponds to MaybeTree single tile. This is mapped to Cell.VEGETATION which, in turn, corresponds to the Vegetation auto tile.

We previously set up the 2×2 bitmask on the Vegetation auto tile such that when four tiles of this type are touching in a square, it results in a tree. You can see the red bitmask which shows this in the picture below.

To capture this outcome, we use the size key which defines the size on X and Y for the array stored in the cell key.

Let’s update _get_room_data() to allow for the probabilistic tiles.

func get_room_data(type: int) -> Dictionary:
    var group: Node2D = get_child(type)
    var index := _rng.randi_range(0, group.get_child_count() - 1)
    var room: TileMap = group.get_child(index)

    var data := {"objects": [], "tilemap": []}
    for object in room.get_children():
        data.objects.push_back(object)

    for v in room.get_used_cells():
        var mapping: Dictionary = CELL_MAP[room.get_cellv(v)]
        if _rng.randf() > mapping.chance:
            continue

        for x in range(mapping.size.x):
            for y in range(mapping.size.y):
                data.tilemap.push_back({"offset": v + Vector2(x, y), "cell": mapping.cell[x][y]})
    return data

The first part of the function remains the same, but we want to differentiate between other interactive objects and TileMap cells so we return a dictionary instead of an array. For this we define var data := {"objects": [], "tilemap": []}.

We first iterate over any child nodes in this room and add them to the data.objects array in our data dictionary.

for object in room.get_children():
    data.objects.push_back(object)

We then iterate over the used cells in this room’s TileMap. For each of these, we generate a random number and if it’s greater than the chance for this cell type, we skip it.

for v in room.get_used_cells():
    var mapping: Dictionary = CELL_MAP[room.get_cellv(v)]
    if _rng.randf() > mapping.chance:
        continue

For example if _rng.randf() > CELL_MAP[Cell.MAYBE_TREE].chance is true, the random number is greater than 0.8 so we skip this tile and don’t include the tree Vegetation auto tiles in this data set.

If the check fails, we include the mapped cells in the data.tilemap based on it’s size key.

Fixing the copy room function

We have one final step to finish the project. We need to fix _copy_room() in RandomWalker.gd to take into account the new data dictionary from _get_room_data(). We now have to duplicate the interactive objects as well as copy the TileMap data.

func _copy_room(offset: Vector2, type: int) -> void:
    var world_offset := _grid_to_world(offset)
    var map_offset := _grid_to_map(offset)
    var data: Dictionary = _rooms.get_room_data(type)

    for object in data.objects:
        var new_object: Node2D = object.duplicate()
        new_object.position += world_offset
        level.add_child(new_object)

    for d in data.tilemap:
        level.set_cellv(map_offset + d.offset, d.cell)

We do this in the first loop by going over the elements in data.objects. We duplicate each object and place it at the proper coordinates, in pixels.