Map generation version 2 continued to evolve this week, primarily through incorporation of features from the original map generator. These features include caverns, circular rooms, theme correction, and dungeon stocking.
Generator in action:
Looping didn’t make it in yet. I got hung up on partitioning the map into sections, which ties into how the looping will work.
The above examples use the version 1 stocking logic, which randomly populates each room using a weighted randomizer based on room type rarity. The need for more intelligent dungeon stocking is what triggered creating a second version of the map generator. As with looping, sections are key. The plan is to partition the map into several sections and apply content based on the section type.
Also, map graphs got some minor improvements – corridors have been de-emphasized by removing the node labels and making the nodes smaller. This makes it easier to understand the map structure. Here’s an example:
Next week, I’ll figure out map sectioning and looping.
The few hours available this week went to improving map generation. Adding dead-end removal and some rules for connecting corridors to other rooms and corridors has immensely improved map layouts. Here are a couple of examples:
Next week, I’ll add looping to reduce linearity and backtracking, and make some minor refinements such as removing redundant doors.
The algorithm for determining available space is fully working. I really struggled with it. I expected needing a few days to complete it but those days turned into weeks. The algorithm design itself was sound; it was the calculations that drove me nuts. Ample debug statements and code comments helped get the job done. Another useful technique for troubleshooting procedural generation issues is to temporarily increase simplicity, predictability, and repeatability. To debug the algorithm, I disabled the random starting point and instead set the starting point to 10, 10 and the initial feature size to 5×5, simplifying the math. By using fixed values, I knew what the expected values were (I didn’t have to calculate these in my head every time because the starting point and feature size changed each run). Using the same fixed values for every run made it easy to tweak the algorithm until the desired result was achieved.
Here are a few examples employing the new available space algorithm. Note that these aren’t playable maps; they simply demonstrate the algorithm at work.
Other accomplishments this week:
Room and corridor fine-tuning. The ratio of rooms to corridors was adjusted, along with the sizes. In both cases, I compiled data from multiple hand-drawn dungeon maps and calculated min, max, median, and mean values. The median and mean values were similar when I removed extraordinarily large rooms from the data set.
Smarter corridors. Corridors now only connect to other corridors and rooms from specific points, based on the orientation and direction of the corridor. This helps corridors connect to other features in a more logical fashion.
Level graphs working again and improved. Level graphs required a few modifications to function with the new map partitioner. While I was in the code, I made some improvements. The new partitioner treats both rooms and corridors as graph nodes (previously only rooms were nodes), so I gave room and corridor nodes different shapes and sizes to better distinguish them.
New automated tests. I rarely use test-driven development, but there were some situations this week where it made sense. The calculation-heavy utility methods for determining available space were easier to develop and test independently rather than when integrated into the full map generator.
Next week I’ll continue refining the new dungeon layout. The goal of putting out a demo by year-end is almost certainly unattainable at this point due to the unplanned procedural generation work. But, I am taking a lot of time off in December and may be able to pull it off.
I continued working on the new dungeon layout algorithm. I grossly underestimated how long it would take to finish. It’s making me miss BSP.
The grid logic eats massive amounts of time. It’s just little things like mapping local coordinates to global, making sure I’m adding and subtracting 1’s as needed when working with rectangles, remembering that the y value decreases when going north and increases when going south in the coordinate system but the order is reversed when displayed on screen. My graph paper tablet is indispensable for this type of work. I have ten full pages of sketches of rooms and corridors that I used to work out algorithms, calculations, and measurements. Debugging is time-intensive too. Tracing through the code line-by-line to check values, in conjunction with running the map generation visualizer one step at a time, has been effective.
The maps are better than last week, but still basic. The generation is much more robust than last week. Except in some rare cases, there’s no more problematic behavior like orphaned connection points and overlapping rooms. I haven’t re-enabled the processors that run after the basic level structure is created, so the map is unpopulated. There aren’t even any doors. Here are some examples of where the level generation stands:
Next week is all about fine-tuning the generator to improve the map layout. I expect this to be time-consuming because it’s a trial-and-error process.
The entire week was spent on a new dungeon layout algorithm. Binary space partitioning (BSP) has been replaced by what I’m calling dynamic space partitioning. Dungeon generation now occurs by adjoining new rooms to existing rooms. It’s similar, at a high level, to the Dungeon-Building Algorithm from Mike Anderson. A key difference is that before generating and placing a new room, the maximum amount of free space available from a connection point on the edge of an existing room is calculated, thus creating a new partition. When the new room is placed, the partition is resized to match the size of the new room, releasing the unused space from the original partition. This approach yields a more rational dungeon layout (if a dungeon layout can ever be considered rational), prevents overlapping rooms, and is more efficient than similar approaches that randomly generate new rooms until finding a room that fits.
I had hoped to have some amazing example maps to share by the end of the week. The core algorithm has been implemented, but it produces terrible maps in its current state. The sizes of rooms and corridors are completely random (within their bounding boxes), as is distribution and number of connection points per feature. More logic is required to generate playable, sensible maps, but I’m not certain of how much more yet. My plan is to identify common patterns in the many hand-drawn dungeon maps I’ve collected and generate rules from those patterns. I do miss some of the benefits of BSP – filling the entire map space is guaranteed, creating corridors between sibling nodes is simple and clean, and using different themes for different sections of the dungeon was easy.
Next week, I’ll continue to focus on the new dungeon layout generator. My plan for a playable demo by the end of 2022 is still possible (I’m not replacing the entire map generator, just the first step that places rooms and corridors), but less likely with the increased scope.
As with the previous week, I have some real life issues impacting my availability. It doesn’t take long for the codebase to begin feeling unfamiliar after a stretch of not working with it. The issue has been exacerbated by spending most of the time I’ve had outside of the code editor, studying hand-drawn dungeon maps. Maps in Legend are currently a random, incohesive collection of rooms. This is common in roguelikes, but one of my main goals for the game is maps that look and play like they were created by a person. It’s an ambitious goal, but one that is very important to me, so important that it needs to be in the initial release. Last week I designed a solution for matching structural patterns in the map to content. This week I designed a new solution for arranging map areas such as rooms and corridors. This solution will replace the current BSP-based map area generator. Next week, I will implement the new map area generator.
It was a highly productive week. 100% of my time went into features and fixes that are required for the initial release (not always the case).
Status effect icons. Enemy status effect icons now appear above the enemy. Player status effects are now represented as icons as well, located next to the health bar.
Loading screen. New levels now display a loading screen, complete with a progress bar and text describing what is currently happening. At first, the progress bar was based on AsyncOperation.progress, which tracks the progress of an asynchronous scene load in Unity. The bar quickly got to 90% and then hung for around ten seconds. This delay was, of course, the procedural generation. Luckily, when I made the map generation visualizer last year, I had to reduce map generation into hierarchical, independent units of work – maps are generated by a series of processors, processors are composed of stages, stages are composed of substages, and substages are composed of tasks. The tasks are pre-generated and placed in a queue. This enables the map generation visualizer to display the result of one task at a time and interactively fast forward through substages, stages, and processors. It also makes it easy to calculate the progress percentage. I only had to make a slight modification to make it work with the loading screen progress bar, which was to wrap it in a coroutine and have it yield after each substage so that the UI could update the status.
Cell selection improvements. The area of effect is now highlighted and the cell indicator (not just the mouse cursor) is now shown.
15 sound effectsadded.
Next week I’ll take a break from new features to shorten the bug list. Saving and loading is broken again.
It was a solid week with lots of smaller, but important, improvements and fixes. I added a few new map features and need to make sure that I do this every week, because map content variety is one of the key aims of the game.
Fibonacci weighted randomization. This is an extension of the existing weighted randomizer that adds the capability to automatically set weights based on fibonacci numbers. I created this because I kept needing a random number generator where each outcome was less common than the outcome before it, and the number of possible outcomes was variable. Fibonacci numbers naturally worked for this. I start at the second 1, so the outcome weights are 1, 2, 3, 5, 8, 13 and so on, and the weights are assigned in reverse order so that the most common outcome has the largest weight. I use this primarily for rarity-based random selection.
New sound effects. I added sounds for hitting webs, stalagmites, and bones and an ambient looping sound for braziers. The physical material-based sound effect configuration, in which the sound played is based on a combination of the weapon used and the physical material of the target, is becoming unwieldy. I will probably remove weapon-specific sound configuration and create a global configuration based on combination of weapon damage type (slashing, chopping, bludgeoning, piercing), weapon physical material, and target physical material.
New Map Elements. Added statues, grass, pots, and rugs.
Removed redundant Map Elements. Some common patterns emerged as I continued to create new Map Elements and it became evident that I could combine some of the elements. For example, there were dedicated elements to place a single actor such as a fountain or a shrine. The only difference between the elements was the actor. So, I removed these variations and created a single Actor Map Element that takes an actor type as a parameter.
Rubble tile variations. I got really tired of seeing the same rubble tile everywhere. I bought the Oryx 16-bit Sci-Fi sprite set primarily because it has two more rubble variations (that’s how much I like making my own art). The variation helps, but some of the rocks are too big. I’ll live with it because all the artwork is going to be replaced anyway.
Player corpse. When the player dies, there’s no visible indicator beyond the “Game Over” message. So, I made a corpse sprite (for the Knight; I still need to do the other player classes). As with the enemy corpse sprites I made, I used a simple process of rotating the sprite 90 or 180 degrees, slightly shearing it, and dropping the blood sprites above or below. The results are ugly but serviceable. Now when the player dies, a corpse will be displayed.
Animated Bones improvements. Animated Bones, which are piles of bones that come to life when the player approaches them, were improved. Now they animate when the player is diagonally adjacent to them and when the player attacks them from a distance with a ranged weapon.
Equipment hotbar design. Over the past few weeks, I’ve been thinking a lot about how melee weapon switching and ranged weapons should work from a UI standpoint. This week I finally figured out how this should be designed. I still need to implement the design to validate it.
Added unit tests for select utility classes. I practiced some test-driven development this week, which is a rarity. I found some issues with the Fibonacci randomizer right out of the gate, so the new unit tests already paid for their effort.
Bug fixes. Fixed ~10 issues that directly affected gameplay, which felt very satisfying.
Next week, I expect to mostly work on UI, specifically the melee and ranged weapon hotbars.
Legend has been in development for 2.3 years. It’s hard to believe that that much time has passed since I started working on the game. I don’t know if anyone else has experienced this, but how I felt at the two-year mark was in stark contrast to my feelings at the one-year mark. After the first year of development, I was thrilled with how much I had accomplished and excited for the future. At two years, panic set in. How could I possibly finish at the rate I was going? Was I wasting my time? Was this game even any good?
Ultimately, the mid-gamedev crisis was a good thing. This was my brain telling me to reassess and correct course. That’s exactly what I did in the latter part of the year. The reality was that, at the rate Legend was progressing, it would need at least several more years to release. I don’t want to wait that long (I have more games to do!). I cut a large chunk of planned features while preserving the original vision. I forced myself to make decisions. I have a tendency to postpone decisions as long as possible to avoid limiting possibilities. It’s been a sure-fire way of keeping completion in the distant future.
Development thus far has consisted primarily of building the game system framework, and rebuilding many facets of the framework as my Unity knowledge increased and my ideas crystalized. Heading into 2022, the framework is done and development shifts to using the framework to flesh out the actual game.
What I said I was going to do in 2021:
Replacing the stock art
I’m still using Oryx. I still have some uncertainty about the exact 2D perspective that will be used. I also feel that the art doesn’t need to be replaced until the game is ready for a public release.
A handful of new enemies, items, and objects were added. Every piece of existing content was reworked in some way, for example extracting a parent class for actors and items, and moving from room-based to element-based map generation.
At the beginning of 2021, I never would have expected to accomplish as much as I did in this area. I considered polish something you do at the end of development. That largely is the case, but I’m using “polish” loosely here to refer to any visual and audio effects beyond the bare minimum, and refinement of any sort, such as fine tuning procedural generation and balancing combat. The game looked bad and felt dull whenever I did testing. Even though I knew I’d improve the look and feel before launch, I was still getting discouraged. For my own psychological benefit more than anything else, I added some game juice, including:
This did the trick; I could finally envision other people playing the game.
I posted a weekly dev update on the website and Sharing Saturday on r/roguelikedev. I posted the link to each Sharing Saturday update on Twitter. I occasionally posted videos on Youtube. I have tiny followings on those channels. I continued to spend a minimal amount of time on community in favor of game development.
The new map generator, started in 2020, was finished in January. I built some dev tools for procedural generation analysis, tuning, and troubleshooting: an interactive map generation visualizer and a map graph visualizer. These proved to be very handy.
I made another major change to map generation later in the year with the addition of Map Elements. These are the basic building blocks for populating a map with content after the map structure’s been created. They’re hierarchical and modular, enabling rooms to be constructed from layers of interchangeable components and reuse common components. For example, many room types can include an Abandoned Map Element that adds cobwebs and debris to give the appearance that the room is abandoned.
Maps became both more varied and more playable. Map configuration parameters, which dictate a map’s structure (number of rooms, room sizes, room distances, room themes, etc.) are now randomized. This additional layer of procedural generation increased the variety of maps while maintaining consistency within a map. Map configuration parameter ranges were fine-tuned to avoid problematic maps. Room type probability changed from a linear distribution to a weighted distribution based on rarity to give maps a more logical composition of rooms.
Significant UI work was done. New screens were added, a hotbar was added, and the main game UI was refined.
AI was expanded. Now, actors can have their own AI controllers and behave differently than other actors. AI controllers can be reused across multiple actor types. Actors can now track any number of other actors, enabling them to do much more than charging the player. They can attack, defend, and interact in other ways with tracked actors as dictated by their AI controller. Actors now gain awareness of events based on their vision and hearing ranges and act based on the type of event occurring. Each actor has its own inventory and the ability to pick up items and use them. When an actor dies, other actors can acquire the items it was carrying.
In light of my time constraints and the mountain of work remaining, I made a deliberate effort throughout the year to increase my productivity. The ways in which I did this include:
Reducing and simplifying code.
Moving logic from code to configuration (physical material interactions, game events).
Adding unit testing.
Adding an in-game console for spawning objects to accelerate playtesting.
Adding more granular logging so that I have more information to troubleshoot.
Consolidating test settings into a single design-time editable object.
Finally, code rework occurred throughout the year. Most of it was necessary to keep the code maintainable, but I also know that I am overly eager to rework code and sometimes create more problems for myself than I solve. As the year progressed, the rework did slow down. There are two reasons for this: 1) the framework reached maturity and 2) I became more selective with when I reworked code. Before I make changes or add new features, I now consider what compromises I can make and what I can do within the existing framework to avoid rework.
This year’s goals are essentially the same as last year’s. However, the priorities have changed. An early access release is now the primary goal. Original art, a requirement for the release, is also a top priority. Effort will be concentrated on what is absolutely necessary for public release, including tightening the game loop, balancing, and clearing the bug list.
The first week of the year is always busy work-wise and this year was no exception. Hence, progress was limited.
Figuring out lighting. I started to get the hang of the Smart Lighting 2D Unity asset. I set up a light source from the player and tilemap lighting so that light collides with walls.
Item rarity. Items already had a rarity attribute, but it wasn’t used. Item rarity values have been populated and rarity is now considered when placing random items.
Next week, I’ll keep working on the lighting and some miscellaneous gameplay improvements/features like reducing the player input required to shoot arrows and making the weapon hotbar functional.