What do you plan on the dungeon looking like?
Sort of like this bird drinking a soda.
So, you may have guessed that I seem to have gotten it.
I decided, after a brief discussion with my local search engine, that a walker generator was the one for me. Come, friends, and explore this marvel with me.
I watched a few videos on walker generation, but found
Heartbeasts' to be the best. You can find the repo for his stuff there - which I might need to dig into further to solve future problems.
So a walker is pretty simple - you start on a square, then move any of the cardinal directions. If you hit a wall, you turn. We'll also suggest it turn randomly and if it walks too far in one line. This is probably what dwarves with a fell mood do in DF when they're looking for someone to kill - though this time, we'll be storing the history of all those steps and painting a map with that history afterwards. Let's go through it...ahem...
step by step.Please, please, hold your applause.
So a walker essentially needs these three functions - the first one is the walk - where we walk a number of steps (roughly, map size). The second is the step function to check if we've walked outside the borders we want to go. Third is the direction changing algorithm, which will narrow the options down so you don't get stuck trying to walk into a corner for ages.
The top of the script looks like this:
This is the first time I've done a custom class in Godot (and honestly, one of the few times in my life). Basically, we're building a node like the many that Godot offers already. I should note that this is only a script in Godot, it is not attached to anything, and isn't autoloaded like a global - it's just a new type of node we're adding to the project: a Walker node (by class_name).
We set up the directions we can go, our starting position and starting direction (just right for no real reason), and tell Godot what to expect for "borders" - we'll get into that later.
Our step_history array is what we'll actually be sending back out from all the functions here. It'll be a collection of coordinates recalling where our "walker" went. steps_since_turn is renamed to straightsteps later and is just there to keep track of how far we walk. It'll be important to contrast hallwayness which will control how far is the upper limit before we force the walker to turn. I set it to 6 to begin with.
The _init() should be real familiar if you did anything in Python before. This is something like "onready", but also allows the use of arguments from other parts of the program. It's just going to run this stuff first. We're going to make sure we can start in the starting position that we feed the walker elsewhere in the code. We'll also record our starting position as the first step in our step history.
Turning is the easiest to understand, so let's start there.
If we turned, our straightsteps is going to be reset. We then duplicate the collection of possible directions, erase whichever one was previously tried (we'll only turn after taking at least one step, and if not, it'll just remove turning right from the rotation), shuffle the directions and then pop_front one of them. pop_front is something you're going to see me use again later when we need to give an NPC dialogue directing the player, but can make the rest of the dialogue random. It takes the first thing in an array, adds it to the variable, then removes it from the array.
that is:
if this array is [69, 420, blazeit, lol]
var mynumber = array.pop_front()
The end result will be
mynumber = 69
array = [420, blazeit, lol]
Again, we want to remove it from the array because if it doesn't work, there's no need to erase it again, we can just try again. That's what the while at the bottom does - if we hit the wall with that turn, we'll just turn again. A walker can walk over itself, so if it's a rectangle that we're working in, we'll eventually find a way to go.
Let's work on steps next.
Our next step is going to be where ever we are plus the direction we want to go (default is right, but if we turned, it'll be something else.) We check if there's space within the borders (that's what has_point means), and if so, we move that way, add one to our step counter, and add the new coordinates to our step_history. We also return true, so the rest of the program knows we were able to take a step. If we couldn't (say, if the step was out of bounds), then we'll return false to let the program know it was impossible to go that way.
So let's put it all together in the walk program itself.
Walk takes an argument of "steps" which will be how long we want the walker to keep walking. This is roughly how big the level will be, but can also relate to openness - since the walker is pretty likely to walk over itself a couple times as it approaches a corner. We iterate over those steps, and see if we should turn (a random chance, and also a hard maximum with hallwayness), then we return the step_history which should contain everything we've done.
(eagle-eyed readers will notice the fatal flaws I made here)With the walker done, let's make a quick scene of a Node2D and set it up!
I got the borders here in units of 24x24 - so at 1032x600, our window size is 43x25 units. I've put a 1 block border around the outside, leaving us with 41,23 for the interior rectangle. This could really be anything, since the camera follows the player - so the size is really just how much space do we want the walker to work with.
We add a new Walker to the scene! Since we defined it by class_name, the engine treats it like any other node, and we can call the functions in it immediately. Remember, it's waiting for a starting position and border size.
We set the starting position to roughly the center of the screen, but I might experiment with putting that start somewhere in a corner. We then give it that rectangle to walk around it. We call for 500 steps which will return that step_history to our new variable: map. we then get rid of the walker, since we don't need it. We then iterate over the map (which is just a bunch of coordinates like (24,13), (25,13), (25,12)) and paint a floor texture there from our tilemap.
I also set it so pressing Enter will regen a map. I did this in a separate project than Sewerkings proper, so it's only doing this map-gen. Oh baby, we're ready to go!
Well that's not very good.
So clearly something was broken. What's the best way to fix things? Print, baby!
And run it one more time!
Okay, so it's not turning. Well...why would that be...
It took me a bit to realize what I'd done. In the original walk function, you can see I put the return in the for loop:
And since it queue_frees as soon as it gets that variable assignment, it was killing itself after literally one step. I added an additional conditional as a safety net as well:
Now the return is in the right place, and it'll turn if all else fails (for whatever reason).
Now behold! Glorious sewer!
I futzed with the settings and feel pretty good about 20% chance to randomly turn and hallwayness at 3 - so it pretty regularly goes 3 steps, but not always.
Starting tomorrow, I'll need to put together how to add players and end-goals to this map we've made, but that's a problem for tomorrow delphonso.