Soulstone's procedural generation wasn't planned initially, and instead it was proposed very soon after we began development. After looking at multiple different games for inspiration, Spelunky bubbled to the top. Its system was the simplest to both understand and recreate using online tutorials. From this point, having Spelunky as a base I read through the code and begin stripping it of initial functionality to get solely to rooms generating in directions. Using that base I then kept expanding and improving the code, allowing for points of generation, direction of generation, dynamic room sizes for space validation, re-evaluation of pathing, and path termination.
The system took a surprisingly small amount of time to create, I was allotted somewhere around 3 months to create the system and had it in a functioning state within 5 weeks, however it was far from optimal and still isn't even close to perfect. Given sufficient time and incentive i'm certain that I could make a superior system in almost every way with what i've learned from this experience.
This video consolidates the information provided with the images below but with a bit more of a personal touch as I explain some of my decisions and how they impacted the generator and project as a whole.
Starting from Spelunky's design of a grid of rooms within a confined space, and being offset by a set amount of space instead of taking odd room sizes into account it made me begin to think of what would be necessary to accomplish what I was aiming for, which caused me to come up with the spawnPoints shown as sdPoints in the earlier iterations. These points serve the purpose of giving an offset for the new room to spawn based on, as well as a direction to determine if there are any rooms in the array with the opposite direction to allow spawning. The points were additionally given the functionality of being able to be marked InitSpawn for the generator's states to know whether there are nodes in the current path remaining or if there are only nodes for new paths remaining.
The first successful generation of this new system was greatly uneventful and there existed a few bugs from the very beginning but it provided a proof of concept at the very least. The nodes somewhat did their job and assured that I could at least validate the direction of the rooms being spawned, and on the final generation it also properly returned the room offset and created exactly the effect I was looking for. At this point however, I wasn't quite staying in line with the teachings of modularity and my script was beginning to inflate in bad ways.
The manager script up to this point had a core coroutine which held all of the real logic, drawing information from each of the room arrays to know where their nodes are, what direction the nodes are facing, and how many nodes exist on the room alongside any additional information the room itself may need to provide. The generator is driven by states to determine if it needs to complete the current path or if it can move onto the next path, and per each state it has a very unique purpose, with the InitPath state generating a number of rooms equivalent to GenLevelInit before ending the state and moving back out to the CritGen state to check if all InitPaths have been completed and if they have not, the CritGen State will restart the InitPath state on a remaining InitPoint node.
The few days of development past the previous update had massive leaps forward with the points being fixed, prefabs being checked for inconsistencies in their design, and logic being improved to work properly while going much faster with total generation time improving from a second or two per room to the speed of an update cycle.
Reaching this point gave the generator a real chance of being viable in the game with the node system working perfectly with our level design, as well as the system generating everything at massive speed and with us having complete control over the number of rooms generated.
This next update to the generator came with multiple additional features as well as a huge overhaul to the code itself, re-aligning the system with the concepts of modularity, allowing iteration much quicker and easier by moving the directional arrays of rooms to a singular room pool with the logic determining which rooms are possible or not given the initial node, and finally the generator's directional biases were reduced so that generation in more directions per path are possible.
The code overhaul was necessary both in the interest of other designers and artists being able to iterate the system without reading a manual prior, and to reduce the overly convoluted nested ifs and whiles down to a more concise modular system.
The spawn nodes were updated with the capability to allow for different sized connection points now, meaning if we have a 4x4 opening it will only ever connect to a 4x4 opening but you can also have a 4x2 or 8x4 opening and they will always only connect with like openings. This system is denoted by the raised boxes along the paths, where the raised box is a larger opening and the smooth paths are smaller openings.
Finally the code was updated to allow for paths to split and generate in multiple directions per path simultaneously, allowing for the generator to create much more complex levels with more unique pathing to avoid fatigue and improve level diversity.
This iteration of the generator was now capable of validating the space that a room would occupy to avoid path collisions, if a collision would happen the previous room generated would be destroyed and regenerated to terminate the path, and variable path duration was added to account for the premature path terminations and path splitting.
The primary difficulty in this update was properly getting space validation to work prior to generating the room due to a lack of understanding with raycasting, boxcasting and unity's Physics.Overlapbox. After some hair pulling it worked out fantastically and came to do almost exactly what I wanted.
This was the final time I would work in my test environment and the next update was slated to be in our game build, which would eliminate certain bugs unique to this environment, but also introduce new bugs in the game build.
The generator was finally at the point that it could be added to our game build since it had touched on all of the requirements of the game and I had tested them to almost completion in my personal environment.
The generator came into the project with less bugs than expected and improved our levels massively with new rooms and unique experiences per play through. However, bugs inevitably brought me to resume the task of working on the generator with some rooms being incapable of generating and the room regeneration system not working at all.
After much head scratching and re-evaluating the code it became necessary for yet another minor overhaul and refactor. Up to this point there hadn't been many logical errors in my order of operations per state and it took a while to find those discrepancies due simply to how long I had been working on this one system and without another's eyes on the system as well. Having finally found the solution and restructured my code yet again, I came upon what I expected to be the final iteration of this system.
Reaching the penultimate iteration of the procedural generator, I had at this point removed all but the most rare of bugs that occurred only once in every 100+ generations and added random door spawns at doorways. The system at this point was considered finished for quite some time to be resumed after a few months.
Following the procedural generator being completed, I was tasked to create a new system that can fill the empty space of levels and occlude vision of the excess level geometry. This fill system ran along a grid based on the maximum and minimum room positions on the X and Z axes. The system created rows of planes that then expanded outward and filled the remaining space, getting progressively smaller and smaller to fill every place that the player could possibly see the scene background. If it weren't for the procedural generator and our differing theme per level, we could have simply placed this fill ourselves, but with the procedural generation we weren't afforded that convenience and I was required to create a system that can accomplish that goal. This system being based on a grid and moving down in scale as it fills the crevasses remaining, it could easily be given a tiling texture and fill that space very well with objects that are consistent with our level art.
And here is the final addition to the procedural system, which is a teleporter and key spawn at the end of the longest path, allowing the player to be rewarded for their trek in the randomly generated areas and preventing the need for backtracking and the frustration therein.
Unfortunately the system was never fully integrated into the game due to not getting an art pass and was cut from the final game build. It is an unfortunate outcome but that is sometimes the case with game development and I am grateful for the experience and joy that I was granted by the task of creating this system and plan to build a new and better system for a different game at some point in the future.