Want to make a procedurally generated level for your game that includes caves or forests? We did too, and here's how we learned to do it using Godot!



First we looked to see what info was already available; and while there is a lot of great resources like RogueBasin, few of them covered Godot or GDscript.



Fortunately, GDscript is very similar to Python, and we found a nice script on github where many procgen methods were created in Python. So here we will show how we re-created the cellular automata one in Godot. How's it look?



sample cave generation using cellular automata

Getting Started

To follow along and see how this works you can download the project files from my github repo. The project is for Godot 3.1, and is just 1 Caves.tscn scene and about 200 lines of code.

To begin you want to create a scene in Godot and add a TileMap node to it, and then create a few tiles (you need ones for the ground and roof at least). Next you want to attach a script to the TileMap node.

As a first step, lets add some variables that will let us tweak the procedural generation. Paste the following into your script you added to the TileMap:

extends TileMap export ( int ) var map_w = 80 export ( int ) var map_h = 50 export ( int ) var iterations = 20000 export ( int ) var neighbors = 4 export ( int ) var ground_chance = 48 export ( int ) var min_cave_size = 80 enum Tiles { GROUND , TREE , WATER , ROOF } var caves = [ ]

Once you save your script, if you select your TileMap node in the editor, you will now see some Script variables show up:



These variables show up in the editor because we used the export keyword, and will allow us to tweak how our caves are generated. So let's move on to the generation code.

Generation Code



func generate ( ) : clear ( ) fill_roof ( ) random_ground ( ) dig_caves ( ) get_caves ( ) connect_caves ( )

To generate our caves we first want to make a function describing all the tasks we need to do to create our caves. The first task, clear(), is a method of the TileMap node and simply clears it of any existing tiles. The rest of the tasks we need to create.



func fill_roof ( ) : for x in range ( 0 , map_w ) : for y in range ( 0 , map_h ) : set_cell ( x , y , Tiles . ROOF )

First, we loop through all the cells we will use in our TileMap, defined by the map_w and map_h variables, and set them to be roof tiles.

func random_ground ( ) : for x in range ( 1 , map_w -1 ) : for y in range ( 1 , map_h -1 ) : if Util . chance ( ground_chance ) : set_cell ( x , y , Tiles . GROUND )

Then, we want to loop through our TileMap again, and this time randomly change some of the roof tiles to ground tiles. The chance this happens is defined by our ground_chance variable. To make this work, we also have a Util.chance() method that we need to create.

Create a new script and call it Util.gd, and paste in the following code:

extends Node func chance ( num ) : randomize ( ) if randi ( ) % 100 <= num : return true else : return false func choose ( choices ) : randomize ( ) var rand_index = randi ( ) % choices . size ( ) return choices [ rand_index ]

Now we want to make this Util.rb class available to all of our scenes, so we can use it in our Caves.tscn scene. To do that we need to make it AutoLoad, by going to the Project menu -> Project Settings -> AutoLoad and adding Util.gd there.

Next up, let's create our actual caves:

func dig_caves ( ) : randomize ( ) for i in range ( iterations ) : var x = floor ( rand_range ( 1 , map_w -1 ) ) var y = floor ( rand_range ( 1 , map_h -1 ) ) if check_nearby ( x , y ) > neighbors : set_cell ( x , y , Tiles . ROOF ) elif check_nearby ( x , y ) < neighbors : set_cell ( x , y , Tiles . GROUND ) func check_nearby ( x , y ) : var count = 0 if get_cell ( x , y -1 ) == Tiles . ROOF : count += 1 if get_cell ( x , y + 1 ) == Tiles . ROOF : count += 1 if get_cell ( x -1 , y ) == Tiles . ROOF : count += 1 if get_cell ( x + 1 , y ) == Tiles . ROOF : count += 1 if get_cell ( x + 1 , y + 1 ) == Tiles . ROOF : count += 1 if get_cell ( x + 1 , y -1 ) == Tiles . ROOF : count += 1 if get_cell ( x -1 , y + 1 ) == Tiles . ROOF : count += 1 if get_cell ( x -1 , y -1 ) == Tiles . ROOF : count += 1 return count

Here we are choosing a random cell in our TileMap, and then checking the surrounding cells to see if they are ground or roof tiles, then updating it to also be a ground or roof tile based off how many neighbors. We do this how ever many times you specify in the iterations variable. When done many times (20,000 in this demo), it carves out our caves for us.



Connecting Caves

With the above code in place, you have actual caves you can generate. But there is a problem, the caves are not connected, which makes for a rather unplayable map.

Before we can connect the caves, we need to get the caves first and store them in a list. To do this we use a flood fill. This works like the paint bucket tool in drawing apps - you pick a ground cell and remove it (changing it temporarily to a roof), then check its surrounding cells; if they are also ground remove them, and check their surrounding cells, until no ground cells are found - then store all those cells as a cave, and start again until you have mapped all caves.



func get_caves ( ) : caves = [ ] for x in range ( 0 , map_w ) : for y in range ( 0 , map_h ) : if get_cell ( x , y ) == Tiles . GROUND : flood_fill ( x , y ) for cave in caves : for tile in cave : set_cellv ( tile , Tiles . GROUND ) func flood_fill ( tilex , tiley ) : var cave = [ ] var to_fill = [ Vector2 ( tilex , tiley ) ] while to_fill : var tile = to_fill . pop_back ( ) if !cave . has ( tile ) : cave . append ( tile ) set_cellv ( tile , Tiles . ROOF ) var north = Vector2 ( tile . x , tile . y -1 ) var south = Vector2 ( tile . x , tile . y + 1 ) var east = Vector2 ( tile . x + 1 , tile . y ) var west = Vector2 ( tile . x -1 , tile . y ) for dir in [ north , south , east , west ] : if get_cellv ( dir ) == Tiles . GROUND : if !to_fill . has ( dir ) and !cave . has ( dir ) : to_fill . append ( dir ) if cave . size ( ) >= min_cave_size : caves . append ( cave )

With our caves stored, we only have one step left, and that is to connect them. To do this we want to loop through our caves, picking a point in each, and then walk a path between them. To make the path feel more natural then a straight line, we will do a random/drunken walk, weighing the directions to guide us.



func connect_caves ( ) : var prev_cave = null var tunnel_caves = caves . duplicate ( ) for cave in tunnel_caves : if prev_cave : var new_point = Util . choose ( cave ) var prev_point = Util . choose ( prev_cave ) if new_point != prev_point : create_tunnel ( new_point , prev_point , cave ) prev_cave = cave func create_tunnel ( point1 , point2 , cave ) : randomize ( ) var max_steps = 500 var steps = 0 var drunk_x = point2 [ 0 ] var drunk_y = point2 [ 1 ] while steps < max_steps and !cave . has ( Vector2 ( drunk_x , drunk_y ) ) : steps += 1 var n = 1.0 var s = 1.0 var e = 1.0 var w = 1.0 var weight = 1 if drunk_x < point1 . x : e += weight elif drunk_x > point1 . x : w += weight if drunk_y < point1 . y : s += weight elif drunk_y > point1 . y : n += weight var total = n + s + e + w n /= total s /= total e /= total w /= total var dx var dy var choice = randf ( ) if 0 <= choice and choice < n : dx = 0 dy = - 1 elif n <= choice and choice < ( n + s ) : dx = 0 dy = 1 elif ( n + s ) <= choice and choice < ( n + s + e ) : dx = 1 dy = 0 else : dx = - 1 dy = 0 if ( 2 < drunk_x + dx and drunk_x + dx < map_w -2 ) and \ ( 2 < drunk_y + dy and drunk_y + dy < map_h -2 ) : drunk_x += dx drunk_y += dy if get_cell ( drunk_x , drunk_y ) == Tiles . ROOF : set_cell ( drunk_x , drunk_y , Tiles . GROUND ) set_cell ( drunk_x + 1 , drunk_y , Tiles . GROUND ) set_cell ( drunk_x + 1 , drunk_y + 1 , Tiles . GROUND )

And that's it! You now have procedural caves generated in Godot that are connected to each other.

Bonus: Godot Tools

One really cool feature of Godot is that its easy to make a script run inside the editor. To do this, we add the tool keyword to the beginning of the script. In our case, we would want to add it to both the TileMap script, and to the Util.gd script.



tool extends TileMap export ( bool ) var redraw setget redraw func redraw ( value = null ) : if ! Engine . is_editor_hint ( ) : return generate ( )

With tool added, we can make a new checkbox exported variable called Redraw, and when we click it, it will call the generate() function to create our caves. Now we can tweak our procedural variables right inside the editor, and click redraw to see the results.



A cool use for this is it turns Godot into a level editor for you, allowing you to procedurally generate levels, save them as scenes, and then further customize them in the editor, tweaking the tiles, adding enemies and objects, etc.



That's a Wrap!

We hope you enjoyed our first tutorial on Godot. If you want to get notified of our new posts or simply chat with us follow @abitawake on Twitter =)

