Weekly Update – January 20, 2023

  • Map generation optimization. I knew as I wrote Map Generator 2.0 that some of the code was horribly slow and wasteful and would need to be optimized later. Map generation had ballooned to 10-15 seconds and .5 GB of memory. It’s now down to 3-5 seconds and 200 MB of memory, and there’s much more room for improvement. The optimization techniques were converting LINQ statements to for loops and reducing use of temporary lists. One optimization example involved how connections between rooms are stored. I started with one list that stored all original connections between rooms. Then I created a new list for connections that constructed loops and another new list for connections that joined sections. I used separate lists instead of the original list because I needed to do different things with the items in these lists, and it was more expedient to create new lists (though a little voice inside my head was telling me to slow down and do it the right way). I added a fourth list when I realized I needed to track each connection in each room that used the connection (as opposed to only the room that originated the connection). Because it was sometimes necessary to get all of the connections, I created a property that combined all four lists into one new list. Yikes. The allocations… The solution was to combine the lists into one and add an attribute indicating the type of connection. This caused way more rework, and troubleshooting issues caused by the rework, than I anticipated. At least the rework made the code simpler and easier to understand, which is always beneficial.
  • Movement optimization. Enabling actor actions to be displayed simultaneously exposed a problem: the movement code took a long time to run, causing actors to instantly move to the next cell rather than moving incrementally over multiple frames. Linear interpolation is used to calculate how far an actor moves each frame, with the actor’s movement speed and elapsed time since the last update as inputs. I ran the Unity profiler and identified the main causes: dynamic lighting and excessive Unity log calls. The log calls are easy enough to deal with; they won’t be in production releases. Dynamic lighting, which uses the Smart Lighting 2D asset, is a dilemma. I want to keep it in the game but I’m not sure how much I can optimize it. Temporarily disabling the lighting and logging fixed movement between two cells, but there was still an issue when moving across multiple cells. Actors momentarily stopped at each new cell before moving again. I had seen this before and knew the source: the state logic in the Update method caused some frames to skip movement. For example, an Update call would determine that all actions had finished in the previous Update and it would update the turn state. Movement wouldn’t resume until the next Update. With nested state logic (there are turn phases, actions phases, and action step phases), several frames passed before movement resumed. This was resolved by modifying the state logic to process state changes in the same update when applicable. For example, when an action step finishes, the logic will start the next action step in the same update.
  • Displaying actor actions simultaneously. I reverted the changes I made last week to present actor actions simultaneously. It became clear that an enormous amount of rework was needed to separate action logic and presentation. Fortunately, a much simpler solution had been right in front of me the whole time: asynchronous actions. Instead of waiting for each action to finish being presented, I’d simply start off each action at the same time. I didn’t consider this initially because one actor’s actions can affect another; I believed that all actors’ actions had to be resolved before any of them could be presented. For example, if the player hits an enemy, and that enemy dies, the enemy shouldn’t be able to move. I still had to make some modifications to get this working, such as checking that an actor is still alive, and tracking cells that actors are moving to before they reach the cell (so that other actors don’t attempt to move into the same cell).
  • Pathfinding improvement. Over time, I’ve changed my mind on how actors interact with other actors and objects that are diagonally adjacent. I may change my mind again, but what’s certain is that there needs to be a way to allow interactions with adjacent objects in ordinal or cardinal directions, depending on the object. Currently, a melee attack can be performed from an adjacent diagonal cell, but opening a door cannot. Until this week, the latter was not possible because of a limitation in the pathfinding code – since actors can move diagonally, and the pathfinding code finds the shortest route, paths end at a cell diagonal to the cell containing the object being interacted with. The fix for this was to change the path destination based on the interaction range of the object. An object with a range of 1 can only be interacted with if the actor is adjacent to the object in a cardinal direction. 
  • Better debugging statements. It just occurred to me that I’ve written a lot of bad debugging statements. I typically add debugging statements while troubleshooting a particular issue. They make sense when working within the context of an issue, but not on their own. Without context, they do more harm than good because they increase cognitive load, which is already high from being in troubleshooting mode. I improved these statements by adding more relevant state information to them. I also rearranged the statement in some cases so that the subject of the statement (actor, item, etc.) was at the beginning of the statement. This made it easier to skim through the debug log.
  • Inspector improvements for ScriptableObjects using Odin. To reap the full benefit of Odin, I added Odin attributes to all classes inheriting from ScriptableObject. These objects are now easier to view and edit in the Unity Inspector.
  • Duplicate door bug fix. Doors recently stopped opening when they were clicked. Actually, most doors didn’t open but a few did. I reviewed the pertinent code but couldn’t find a problem. I started a game and right-clicked on a door to open the Inspect Panel, which shows everything in the cell. Nothing appeared to be out of the ordinary, and the door opened when I clicked it. Then I clicked another door. This one didn’t open. I opened the Inspect Panel and found the problem: there were two doors on the cell. It turns out that the recent change to track connections between rooms in both rooms caused most doors to be added twice. The fix was trivial; I just had to exclude door creation on the duplicate connections.

Next week, I’ll further optimize map generation. Possibly, I’ll start coding the procedural history generation, which I’ve been slowly designing over the past month.

Weekly Update – January 6, 2023

  • Node pattern-based stocking. This identifies the best locations on the map to place specific types of content using node patterns on the map graph. To my pleasant surprise, this was easy to implement. The difficult pieces – constructing the map graph and identifying various patterns – were already in place. I just need to tie it all together and make it configurable through the Unity inspector. All four of the Map Generation 2.0 objectives are now complete. A stretch goal, story-driven map content placement, is still out there. 
  • Symmetry and alignment in room / corridor generation. A preference for symmetry and alignment is now built into the generation, resulting in maps that look more human-made.  
  • Priority randomizer. This is a new randomizer that randomly selects items from a prioritized list. In practice, the priorities are used as categories rather than a stat-ranking (the latter wouldn’t be random). This randomizer is used to randomly select a list of items in order of priority, forcing preferred options to be selected first.
  • 9-slice sprite support. Unity has 9-slice support built in, but it didn’t apply to my use case. Now 9-slice objects such as rugs and pools can be created in the editor.
  • Map Generation 2.0 code clean-up. Some of the cell locator classes, which identify specific cells to place content, were combined, reducing the number of classes from seven to three. Two different sets of classes had emerged for specifying Map Element placement criteria. One set of these classes was removed. 

Next week’s goals are:

  1. Multi-area Map Elements. This allows Map Elements involving multiple map areas, such as a locked door and key, to be added.
  2. Simultaneous actor animations. Each turn, all actors’ actions should be shown at the same time, instead of sequentially.

Weekly Update – December 30, 2022

At the close of 2022, three of the four Map Generation 2.0 objectives have been completed:

  1. Structuring – layout of walls and floors in rooms, corridors, caverns, and other shapes (done)
  2. Sections – map partitioned into separate areas with discrete structures, themes, and content (done)
  3. Data-driven stocking – replace the existing hardcoded dungeon stocking with a data-driven implementation (done)
  4. Node pattern-based stocking – identify the best locations on the map to place specific types of content using node patterns on the map graph (in progress)

There’s also an aspirational fifth objective, which is to use generated background stories for each map to select, place, and customize content.

I’ve been working on map generation exclusively for the past two months. It’s been fun and challenging but I’m starting to feel burned out on it. I need to complete objective 4 and switch over to something else.

Accomplishments this week:

  • New Room Type Map Elements: Barracks, Bedchamber, Bone Pile, Corpse
  • New Objects: Beds (Plain, Dirty, Fancy), Prison Door, Blood Fountain
  • Data-driven Map Elements. Map Elements, the procedural generation objects used to stock the dungeon, are now data-driven. Previously, a class was created for each Map Element. For the most part, Map Elements define what objects go in a room, and where those objects are placed. So, a separate class was needed for each room type. Now, new room types can be created from the Unity Inspector. With this new capability, I was able to quickly recreate the class-based Map Elements and add some new ones. Odin has been an incredible tool for this.
  • Expanded object placement capabilities. Objects can now be placed in grids with random element sizes and rows and columns. Objects can now also be placed in clusters, which is useful for objects that typically appear next to each other such as barrels. Parameters have been added to some existing placement patterns for more flexibility. For example, corners can now be offset relative to the edge or the center of the room. Placed objects can now be grouped, with conditions for placing groups (such as minimum room size). Groups can be configured to place all of the objects they contain or a single, randomly selected object. These improvements provide many more ways to populate rooms in the dungeon.
  • Section-based structure. Map sections can now have distinct structures. For example, catacombs sections have longer corridors and smaller rooms.   
  • Map generation performance improvement. The recent additions and changes to map generation really slowed it down. A quick and impactful fix was adding some caching. There’s more work to be done here, but the current performance is tolerable again.

Next week, I’ll start on the node pattern-based dungeon stocking. I believe the design work is done; I’ve cataloged many patterns using the graphs generated during testing. Some pattern recognition already exists, as it was implemented before I had the big picture. For instance, sequences of rooms and required rooms are identified during generation.

Weekly Update – December 17, 2022

  • Map generation refinements. Map sections now connect to each other (most of the time; smarter connecting corridors are still needed). The starting section locations are better spread out across the map. This helps balance the section sizes, but doesn’t eliminate a frequent issue I’m seeing in which a particular section grows disproportionately and blocks other sections from growing. The solution I’m considering is to check the size of each section as the map is being generated and dynamically adjust the growth of sections that are too large or small relative to the other sections.
  • Catacombs and Crypt themes. Map sections can now be themed as catacombs or crypts, in addition to the existing bastion and cavern themes. It’s just a tile swap at the moment, but future updates will alter structure and content.
  • Refactoring. Some enums have been converted to classes that inherit from ScriptableObject so that they can be referenced in the Unity inspector. MapElements, which are responsible for stocking the dungeon, are also in the process of being converted to ScriptableObjects. This will accelerate creating new MapElements, which will soon be needed because the upcoming section-based stocking requires a wide variety of room types.
  • Started using Odin Inspector. On multiple occasions I’ve run into the limitations of the out-of-the-box functionality of the Unity inspector. While it is possible to customize the inspector, a relatively high level of effort is required to do so (coding). I originally heard about Odin from a Jason Weimann video. In the video, Jason said that if you can only get one Unity asset, make it Odin. That set my expectations pretty high, but so far Odin is living up to those expectations. It’s extremely easy to use and is very well documented. In a nutshell, I’m using it to make the Unity inspector more useful, refining the controls and presentation.

I’m on vacation for the rest of December and looking forward to making huge progress with all that time. Map generation refinement will be the main focus. If time allows I will also tackle speeding up game turns by enabling all actors to move at the same time.

Weekly Update – September 23, 2022

I threw away a big chunk of the code that I just wrote last week for Stat Modifiers. I made this decision when it became evident that maintaining a running sum of all Stat Modifiers for each Stat Type wasn’t worth the complexity. I originally chose this approach for efficiency; since the modifiers didn’t change that often, it would be more efficient to precalculate and store the values. In practice, however, the values are used infrequently enough that recalculating them every time doesn’t impact performance. The added complexity, primarily due to having to synchronize the running sums with the individual modifiers, increased the implementation effort and defect risk. In general, it’s preferable to store information in one place only.

  • Improved Inventory Panel. The number of inventory slots has expanded from 24 to 32, the panel has widened, and combat stats are now displayed.
Inventory with stats
  • Tooltips for stat bars and status effects. Hovering over the Health, Stamina, or Magic bars, or a status effect icon, now displays a tooltip with additional info.
  • Actor Debugging Panel. A new toggleable panel displays detailed info about active actors, including location, vitals, status effects, and AI state. It’s been very useful for squashing AI bugs. This is a developer-only feature; it won’t be available to players.
Actor debugger
  • Bug fixes.

Next week starts with closing out AI bugs, followed by validating combat calculations (now that Stat Modifiers are in place) and continuing to add sound effects. Also, I’m going to start on interactions between items and objects, such as scooping water into an empty vial from a fountain. 

Weekly Update – September 16, 2022

The first half of the week was solid – new feature development for release readiness, lots of bug fixes, and no scope creep. The rest of the week was consumed by messy rework that slowed progress and, I fear, will produce many new bugs. Automated testing should catch most of the bugs, however.

  • Stat Modifiers. Stat modifications caused by equipment, status effects, and terrain are now defined in lists instead of individual variables. This greatly simplifies the logic for calculating net stat values and displaying modifiers in an itemized fashion (which the Character Panel now does). Stat modifiers are either added or subtracted; multiplication and division aren’t used. This allows modifiers to be applied in any order.
  • Separate visual effects for unavailable drag drop slots. The UI dims drag drop slots to inform users when slots are unavailable. This was initially used to indicate slots that couldn’t receive the dragged object, for example dragging leather armor into a hotbar slot. When ability resource consumption was recently added, the same dimming functionality was used to indicate abilities that couldn’t be used due to inadequate resources. This introduced some bugs because the dimming logic now needed to handle combination scenarios. For instance, if an ability was dimmed because of inadequate resources, it would undim after being dragged because the drag drop logic would reset it. Using the same dimming effect for two different reasons also made the effect ambiguous. I addressed these issues by adding a separate dimming effect for slots with abilities that couldn’t be used and separating the dimming logic for the two scenarios. One visual effect darkens the slot and the other displays the slot contents in greyscale.
  • Many bug fixes

Next week, I’ll complete the remaining clean up for stat modifiers and add them to the character panel. Then, I need to get back to sound effects. The sound effects implementation has to be reworked somewhat. The varying sounds for physical material and item combinations are cumbersome to configure. There’s too much repetition; centralization is needed. I also want to create an in-game tool for understanding what the AI is doing. Much of the AI info is already logged but it’s difficult to wade through it.

Weekly Update – July 15, 2022

The bugs keep crawling out. I’m fixing them and finding new ones at about the same pace. On the bright side, they’re getting easier to find. I usually know where to go or at least where to start now. I don’t know if that’s the result of the code improving or me starting to recognize where the common problem areas are. Bugs are also getting easier to fix. Often I simply have to change a configuration value or modify a single line of code rather than having to do major rework or implement a system I didn’t anticipate. However, I’m spending 25% of my dev time on bugs and that is too high. To lower this percentage, I’m allocating more time for automated test development. My strategy is to incrementally add tests from a master list of test cases, and add tests related to the bugs I find.

A major achievement this week was reworking interactions between entities in the same cell. This was originally handled in the following manner:

  1. When an entity enters a cell, it applies its interaction effects to all existing entities in the cell, and all existing entities in the cell apply their interaction effects to the entering entity.
  2. Each turn that entities co-occupy a cell, their AI reapplies interaction effects. For example, a fire will continue applying a burning status effect to each entity in the cell.
  3. When an entity exits a cell, the interaction effects it caused are removed from the remaining entities in the cell, and the interaction effects caused by the exiting enemy are removed from the remaining entities in the cell.

This implementation had a few limitations. It prematurely removed some status effects, such as poison, that have a multi-turn duration. It required effects to be applied by the AI, which never conceptually made sense. Because it relied on AI to apply effects each turn, it hurt performance because it increased the number of entities that acted in each turn. For example, in a room filled with poisonous gas, the gas in each cell acted in each turn. To address these limitations, I did the following:

  • Added a game service to manage per-turn interactions between entities occupying the same cell, allowing efficient interaction handling on each new turn.
  • Added a new effect trigger that only occurs when two entities interact for the first time. This is used when the player walks onto spikes, for example. The player is damaged when walking onto the spikes, but will not be further damaged while remaining in the cell containing the spikes.
  • Removed the logic that removes effects when an entity leaves a cell.
  • Removed the AI logic that applies effects each turn (this is now handled by the interaction manager).

I added a new Map Element, Darkness. When the player walks into a room with Darkness, visibility becomes limited to the adjacent cells. This is intended to be an unnatural or magically-induced darkness, as opposed to normal darkness. It’s not fully working yet. When the player opens the door into the room, all of the room cells the player can see from the doorway are visible. I need to fix this.

The most difficult bug I fixed this week was in the grid traversal raycast algorithm, which is used for line of sight and projectile calculations. Sometimes, arrows didn’t travel to the cell the player clicked. It wasn’t obvious why this was happening, and it wasn’t a major problem, so I didn’t do anything about it for a long time. Now that I’m fixing all of the known bugs, I decided to tackle it. I traced the issue to a method that calculates the final position of a ray (a projectile or light) given a start and end point. The algorithm used is similar to Bresenham’s line algorithm, but it includes every cell that the ray passes through. I can’t remember when I added this algorithm, but it’s been in use long enough for me to have forgotten when I started using it. So, I was surprised to find that there was a problem in it (I should have written unit tests for it). I spent a couple of hours adding debugging statements and tracing through the code. I could see that the algorithm was generating the wrong points, but I couldn’t figure out what I needed to do to fix it. I got out some graph paper and sketched an example. The visual representation was much more helpful than the code. There were actually two errors in the algorithm. The code fixes were trivial at that point. 

Next week will be 40% bug fixing, 40% automated testing, and 20% feature development. The feature development will involve expanding the types of interactions between entities. There’s already a framework for this based on the primary physical material that an entity is made of, but more interactions between materials need to be defined (for example, when fire hits a cell containing a puddle).

Weekly Update – July 8, 2022

The two days I had off from work this week doubled my available dev time. Note those weren’t full days; they were just two extra 3-4 hour blocks of time similar to weekend work. A wide assortment of to-do’s got done, including minor tweaks and enhancements, bug fixes, refactoring, and some miscellaneous items. The general theme was getting the game to a stable, bug-free experience.

  • New Abilities
    • Turn Undead. Classic cleric ability that causes nearby undead to flee.
    • Heal. Heals the caster.
  • Self-Targeted Abilities. Abilities, such as Heal, can now be configured to target the user by default.
  • Entity collection-specific Abilities. Abilities can now target specific entity or entity collection types. This capability was used to implement the Turn Undead Ability (it’s basically the Fear status effect, limited to undead actors). 
  • Interesting bug fixes
    • Some enemies were fleeing from the player unexpectedly. Upon further inspection I determined that this only occurred when multiple enemies were present. Even stranger, when a new enemy spawned, an enemy that previously attacked the player started fleeing. It turned out that enemies were reacting to the movement events of other enemies. This shouldn’t happen because enemy AI contains an actor tracker component that limits the actors that the enemy will react to (typically, enemies only react to player actions). The actor tracker wasn’t being used by the enemy’s movement generator.
    • I wanted to display bones on some of the spikes so I configured the map generator to randomly add a bone pile actor on top of some of the spikes. When I tested this, ghosts appeared where bones should have been. I knew instantly what had happened. The bones are configured to spawn ghosts when they are destroyed. The spike damage destroyed the bones in the first game turn, causing the ghosts to appear. This was an easy fix. I changed the effect trigger for spikes from Touch, which is applied each turn, to DidEnterCell, which is only applied when an actor enters a new cell.
  • Other bug fixes
    • Tooltips remain on screen when the Inventory and Ability panels are closed.
    • Exception thrown when displaying the Abilities panel.
    • The default player action for Rugs and Summoning Circles was attack.
    • Cracked Eggs stopped hatching.
    • Eggs weren’t changing into Cracked Eggs.
    • Exception thrown when removing the Ring of Invisibility.
    • Heal Ability wasn’t working.
    • Closing the Select Cell prompt ended the player’s turn.
    • Poisonous gas, once spawned, would spread throughout the entire dungeon instead of a limited area.
    • Run Ability wasn’t working.
    • Exception thrown when Fires burned out.
  • Minor tweaks and enhancements
    • Eggs now have 1 HP so they are killed in one hit.
    • Destroyed eggs now  leave behind the corpse of the creature inside the egg.
    • Added descriptions for Cauldrons and Crystals.
    • Grass is no longer damaged by arrows or other piercing weapons.
    • Bones are now randomly added to some spike cells. 
    • When the game crashes before play starts, unusable saved game files are created. These are now automatically removed.
    • The default action type for an Open Door was changed from Close to Move. Having Open Doors close when clicked was annoying because 99% of the time the desired action is to move into the doorway rather than close the door. Closing a door can still be done through the Inspect Panel. 
  • Refactored / cleaned up several classes, including the Cell class, which had gotten up to 1,000 lines. There was a lot of code that didn’t belong in Cell – handling projectiles landing on the Cell and handling actors taking items in the Cell, for example. I pulled this code into new static handler classes. This is a poor long-term solution, but it’s an effective way of quickly extracting code that belongs elsewhere.
  • New automated tests: Ring of Invisibility. I haven’t added any new automated tests in a while. The Ring of Invisibility provides a good set of tests not just for the ring itself but for the Invisibility status effect as well. I intend to write automated tests for all Abilities.    
  • Improved ergonomics (for me). I’ve been having some pain in my right arm lately. I’m not an ergonomics expert, but after identifying the positions that caused my arm to hurt, I concluded that I needed to reduce the rotation of both my forearm and wrist. My mouse was too far off to the side, and I was holding it with a pronated grip (the normal mouse grip). I bought a smaller keyboard (the Logitech MX Keys Mini) to move my mouse closer to me. I bought a vertical mouse (the Logitech MX Vertical Advanced Ergonomic Mouse) to maintain a more natural, neutral grip when using my mouse. I’m very happy with the result. In the first week of use, the pain has mostly subsided.

Next week, and for the near future, stability and bug removal will remain the focus.

Weekly Update – July 1, 2022

It was another light week due to being on vacation for the first half of the week. More bugs were squashed and some new abilities were added. Abilities continue to require a surprising amount of work, including rework and expansion of the supporting systems.

  • New AI Type: Fear. This AI Type causes the actor to select a fleeing action each turn, even when cornered.
  • New Status Effect Type: Changed AI. This Status Effect temporarily changes the target actor’s AI Type. It’s used by the Fear spell to change the target’s AI from attacking to fleeing. When the effect ends, the target’s original AI is restored.
  • New Abilities: Fear and Mass Fear. The former causes Fear in a single target. The latter causes Fear on all actors in the area of effect.
  • Refactoring: Action consolidation. The recent rework on Actions, in which Actions were reduced to a series of Action Steps, made a number of Actions identical. For example, the Eating, Drinking, and Reading Actions consist of a single Action Step that causes the effect of the selected item. Since Actions are uniquely defined by their Action Step sequence, the number of Actions can be reduced to the number of unique Action Step sequences.
  • Bug fixes.

Next week will be more of the same, but I’ll have more time available. There are still many known issues to fix, and many more, I’m sure, that are still unknown. After a few weeks of mainly bug fixing my motivation is waning. It’s been fun creating and trying out new Abilities and I may spend more time on that next week than anything else.

Weekly Update – June 3, 2022

In my relentless pursuit of increasing software development productivity, I started the week off pondering what is slowing me down the most. I kept coming back to aspects of object-oriented programming – encapsulation, abstraction, inheritance/composition, polymorphism. OOP has always been a double-edged sword for me, providing both solutions and problems. Certainly some of my issues are the result of my shortcomings as a developer, but I believe there are inherent shortcomings in OOP as well. A frequent challenge is determining where things belong, and a frequent source of bugs is putting things in the wrong place. I began questioning whether data and functionality belonged together in the same class (I was quite deep into the rabbit hole at this point) and if I could reduce complexity by separating the two. I also considered making data and functionality, once separated, completely public (I know, OOP heresy) and using either immutable or versioned data. I googled these ideas to see what already existed and found something very close: Data-Oriented Programming (DOP). Now, it would be impractical to go back and rewrite 2+ years of code using a DOP paradigm. But, I’m going to experiment with it for some of the new code I’m writing (see the AI example below). 

  • AI Overhaul part 2. I thought I was done with AI rework after last week, but I put even more time into it this week. To make the new composition-based AI configurable in the Unity editor, I added AIType classes (implementing the Type Object pattern). inheriting from ScriptableObject, I also made the pluggable components of AIType, such as the observation and action deciders, ScriptableObjects. The legacy AI classes were gutted and consolidated. AI state data was moved into a separate generic data structure (see below) and AI functionality was moved into the AIType classes. I added general AI behaviors such as offense and flee, and mapped actions to the behaviors. This simplifies the action decider code because only the behavior has to be specified; the behavior class will return all of the applicable actions to the action decider. With these improvements, I can assemble AI’s in the Unity editor, provided that the pluggable components have been written. I may need to move to data-driven behavior trees if the AI logic becomes too complicated, but for now I’ll stick with conditional statements.
  • Generic Data Structure. To support my data-oriented programming experiment, I created a class to act as a general-purpose data container. It’s essentially a map data structure, but contains three dictionaries to store values of different types (bools, ints, and objects). It’s not sophisticated but it works. I’m now using this to store AI state data, which varies by AI type. The syntax for accessing data within the structure is more cumbersome than individually defined variables, but that drawback is outweighed by flexibility and ease of serialization/deserialization. I also like that the syntax makes it obvious which variables are part of the state.

Next week’s goals are the same as last week’s goals: add the vampire and 1-2 more enemies to test the new AI, and add a few new abilities.