Weekly Update – April 2, 2021

My plan for this week was, among other things, to fix ranged combat. It became apparent that the fix required enemies to have their own inventories (so that ranged attacks could be based on ranged weapons that the enemies carried). This led me down the familiar refactoring path, but it was time well spent. Achievements this week:

  • Separate inventories for each actor. Each actor can now have its own inventory simply by adding a component. This enables the AI to determine possible actions from the items the actor has. It also allows for future features such as pickpocketing, running out of consumables, and dropping items on death.
  • To enable enemies to have their own inventories, I had to decouple inventory from UI. I was most of the way there already, so it wasn’t too much additional work. There’s still room for improvement – equipped items and carried items are separate classes and there’s some redundancy. Separating inventories and decoupling inventory from UI also makes it possible to implement a feature I’ve been thinking about adding – the ability to possess an enemy and view its belongings in the same inventory view used by the player.
  • Refactored effects/conditions. Effects (e.g. poison, paralysis, invisibility) were moved from an inheritance structure to a composition structure. This allowed me to merge tile effects and actor effects into a single set of effects that can be applied to anything.
  • Swapped out some multi-parameter method signatures with a single predicate parameter. In a few situations, there were multiple methods that applied different criteria to return a subset of values from a list. Using predicates instead of multiple parameters provides a lot more flexibility and is cleaner.
  • Item randomization class. There was enough randomization code for items specifically to warrant a randomization class solely for items. This class is primarily used to determine initial item placement. The main method uses a predicate to only certain items such as potions, or items that can be sold in shops.
  • Item database class clean up. There’s still some murkiness around prefabs, GameObject instances, plain old objects, and instances vs types for actors, items, and tiles. The item database class, which is responsible for loading and storing item types, received a good scrubbing.
  • Durability refactoring. Item durability code existed in a few places and had some unnecessary coupling. I again applied a composition model and created a class specifically to store item condition and process events that degrade an item’s condition. Since the class inherits from MonoBehavior and can be added as a component, it can be used for objects other than items, such as a wooden door that can be chopped down with an axe. Of course, this could be accomplished with the health attribute as well, and I may in the future eliminate durability altogether and just use health to track an item’s wear.
  • Dictionary class extension methods. I added some convenience extension methods to the Dictionary class: an IsEmpty method and a couple of methods to display the contents of entries and the entire dictionary in a readable format.

Next week, I’ll continue fixing ranged combat and some other combat-related bugs. 

Weekly Update – March 12, 2021

I made a lot of “under the hood” improvements this week, including:

  • Expanding testing configuration settings. In my ongoing effort to improve productivity, I realized that I needed a way to quickly reproduce and test specific features and objects. For example, there was a bug that allowed the player to take items in shops when the table the item was placed on was burned. I was wasting a lot of time reloading the map until a nearby shop appeared, then I had to find a fire potion that I could use to burn a table in the shop, and then walk back to the shop to reproduce the issue and confirm it was fixed. For this scenario, I added test configuration settings to increase the probability of shops and preload the player’s inventory.
  • Centralizing testing configuration settings in a single GameObject. Different settings, such as fog of war, spawning enemies, and visual map generation, were spread across different scripts and GameObjects. I put all of these into a single GameObject so that I can quickly see what’s enabled and disabled and reconfigure test settings.
  • Reusing random seeds for debugging. The game has the ability to reuse a random seed to generate the same map across multiple sessions, but I wasn’t using that feature much for fixing bugs. Capturing the random seed in my bug reports and regenerating the associated map has accelerated bug fixing.
  • Automatic class and method logging. All game log writes are performed by a custom class. Using .NET’s handy reflection capabilities, I was able to automatically include the class and method that called the log write method. This has been very handy for tracing through sequences of events.
  • Switch from binary to JSON serialization for saving/loading. I was using the .NET BinaryFormatter for serialization and didn’t learn until this week that the serialization method is now obsolete due to a security issue. Switching to JSON serialization was a quick and easy change.
  • Overhauled script and Unity resource folder structure. There are currently 145 scripts and 175 resources. Over time, the original folder structure got messier and it became more difficult to find things. I was starting to guess in some cases where a particular script was located. Even a sub-second delay was a concern because that added up over time, in terms of time and mental energy. I created a new folder structure that is much better fit for the current file set.
  • Many bug fixes, some recent and some long-standing. The bug backlog is finally shrinking, and after all the refactoring, there are far fewer occasions where I have to think hard about a solution; there’s now usually an obvious place for a fix to go and the fix is simple.

Next week, I’m adding enemies back to the map (they’ve been disabled for a while as I was focusing on other game aspects) to wrap up combat refactoring and close out enemy/combat-related bugs.

Weekly Update – March 5, 2021

I made a lot of progress on the big refactoring project this week. The work is gradually shifting from refactoring to fixing bugs in the backlog. Each bug that is easy to find and easy to fix is a sign that the refactoring is making a difference.

I struggled a bit with player input handling. The original solution was not great because player input handling was both event-driven (button OnClick events) and time-driven (checking for input in Update calls). This didn’t work with Unity’s event sequence, in which mouse input events are handled before Update calls. What would happen is that a button click would be handled in the OnClick event, and then the mouse click would be processed again in the Update method in the same frame. I ended up removing the OnClick events and only checking for input in the Update method.

I rewrote the player input handling twice. Not sure why exactly, but it was difficult to implement it in a way that was simple and followed best practices like single responsibility. The first rewrite was overly complicated because of excessive abstraction. Player inputs triggered input events, input events triggered input commands, input commands triggered either UI or player actor actions. I started to get a handle on things when I sketched out the state diagram for inputs. It was also helpful to list out the resulting actions for each input and separate these into UI actions, like displaying a prompt, and player actor actions, like moving to a new location. The solution I ended up with was to check for all input in the Update method of the main UI controller for the scene, create classes for each player action, and map inputs to player actions. 

With UI and player input in good shape, I’m working 50/50 on refactoring and bug fixes. The GameManager is down to 1,000 lines, half of its original size. I need to find a home for most of the remaining methods in that class still. This is the goal for next week, along with clearing the bug backlog.

Weekly Update – February 27, 2021

I had limited time available this week, but made some progress on the multi-system refactoring work I started two weeks ago.  

I completed refactoring UI panels (inventory, item/actor/object detail) and prompts (select cell for targeted actions such as shooting and throwing). These are now maintained in a stack with input handling controlled by the UI panel/prompt at the top of the stack. The code is much cleaner now and the recurring bugs with user input, such as mouse clicks propagating to multiple controls in certain situations, are gone.

I also consolidated the methods that return a collection of cells into a single utility class. Many of these methods are used for procedural map generation to get cells in a specific pattern, such as adjacent cells, room perimeter cells, cells in a rectangle, etc.

Finally, I replaced multiple instances of mini state machines and wait logic with event handlers. The former pattern was messy and difficult to maintain. As a turn-based game, there still needs to be some state management and centralized control, but within that framework, there are many opportunities to use event handling to simplify logic.

Next week is when I originally planned to have the major refactoring done. The scope has expanded and that won’t be possible now, but I should have more time available to get more done than I did this week.

Weekly Update – February 20, 2021

Refactoring of the core systems continued this week. I thought I’d be done by now, but I keep finding new opportunities to better organize code. The biggest classes are shrinking, replaced by smaller, more focused classes and interfaces. The GameManager is down to 1,300 lines from 2,000. I’m still a bit hazy on what this class will ultimately do, but it’s clear that most of what it currently does needs to go somewhere else. Some examples of refactoring done this week:

  • Simplified modal window management. Originally, each UI modal window GameObject was statically added to the scene in the Unity Editor. Each time one of these windows needed to be displayed, its contents were updated and it was made visible. When the window was no longer needed, it would be hidden again. This necessitated some complex state management, complicated input handling, and had a limitation of only being able to use a window once within a sequence of UI interactions. I created a manager class for modal windows that stores the windows in a stack. The window GameObjects are now instantiated when shown and destroyed when hidden.
  • Reorganized player input handling. Player mouse and keyboard input handling was fragmented across the code. Input was handled differently based on the UI state. I had a lot of issues with mouse input in particular, for example clicks triggering events that were supposed to occur sequentially in different frames at the same time. I added an interface to the modal windows for input handling and implemented pertinent input handling in each window class.
  • Simplified UI state machine management. A finite state machine was woven into the GameManager class to manage player input and modal windows. This made troubleshooting UI and input issues difficult. Organizing player input handling by modal window and simplifying UI window management eliminated the need for the state machine and greatly simplified the logic.
  • Manager classes for enemies and cell effects. All objects in the game that act on their own, such as enemies and cell effects, need to be tracked separately from the rest of the game objects so that they can act in each turn. These were originally stored as lists in the GameManager, with methods in the GameManager class to manage them. I created a manager class for enemies and another manager class for cell effects, and moved the lists and methods into those classes.

The level of refactoring I’m currently doing would have been a lot more difficult to do had I focused on content creation instead of systems and mechanics. I’ve made a lot of changes to systems that have required changes to each object using that system. If I had had 100 objects instead of 5, this work would have been much more time-consuming. Adding content is tempting to do; it’s fun making new things. But, if you focus on content creation too early on, you may, at best, limit your ability to refactor efficiently or, at worst, end up with a game that is difficult to fix and enhance. For beginners, as I was with Unity, I strongly recommend building a “vertical slice” of the game before adding a lot of content and a lot of object-specific code. 

Next week, it’s more of the same. I think I’m two weeks out from completing the major refactoring. Once this work is done, I’ll close out the remaining bugs and get back to the ever-growing feature backlog.

Weekly Update – February 13, 2021

Last week I started a major refactoring to make it easier to add new features, reduce the number of bugs introduced from poorly architected code, and decrease average troubleshooting time. I made massive progress on this effort this week thanks to having three days off from work.

When I started developing Legend over a year ago, I used some source code from a Unity tutorial, most of which has since been culled or replaced. The source code included a GameManager class. I wasn’t sure what the appropriate use of this class was in Unity, and over time it grew into a 2000-line monstrosity and a textbook example of how to misuse singletons. It contains game turn management, multiple finite state machines, movement and combat logic, public references to other classes such as the map class (making the classes globally accessible), and miscellaneous utility methods. I began dismantling the GameManager class this week. It’s been a painful process but well worth the effort because it’s greatly reducing cognitive load.

I added one new feature this week: when the map is fully revealed by a “reveal map” scroll, the revealed map locations are shown with a blue tint to distinguish where the player has and hasn’t been. Here’s an example:

Revealing a level map

I moved the legendrl.com website to a new host, SiteGround, because the website was extremely slow on the former host. The site is much faster now!

Next week I’ll complete the major refactoring and squeeze in a new feature or two.

Weekly Update – February 6, 2021

The list of open bugs and enhancements has been accumulating for some time now and I haven’t been able to keep up. I need to increase my productivity by reducing the number of bugs created and making it easier to implement new features. Refactoring is the key to accomplishing this. Specifically, I’m doing the following:

  1. Making greater use of Unity composition to handle actor/item/object interactions in a more maintainable way.
  2. Consolidating methods that do similar things. For example, I implemented a method for adding items to the map early in development, and a different one later on in a different class. They don’t do exactly the same thing, and this anti-pattern has been a major source of bugs, unsurprisingly. I need to more consistently follow the single-responsibility principle; the challenge has been that, as the game has evolved, the appropriate source of responsibility in some cases has changed.
  3. Reducing instances of functional coupling. For example, there are separate methods to create items and instantiate Unity GameObjects. These methods are often called together, and problems arise when I forget to call both. Except in a couple of edge cases, these two methods can be combined into one.

To that end, I spent most of the week on the first and second items listed above and made enormous progress. I’m always amazed at how much easier software development is when solid architecture and code design are in place; recreating existing objects into the new architectural framework has been a breeze compared to creating the objects in the previous framework.

Next week, I should be able to wrap up the refactoring and then revisit the bug list. Hopefully a lot of the bugs will have been indirectly addressed through the refactoring.

Weekly Update – January 23, 2021

I spent a lot of time on pathfinding this week. That wasn’t the plan, but as I was testing combat and thinking of ways to make it more interesting, I ran into a problem. When the player detects a trap, all enemies are made aware of the trap too because every entity is sharing the same pathfinding map. This prevents the player from luring an enemy into a detected trap. The initial solution I came up with was two pathfinding maps – one for the player and another for all enemies. Then I recalled the hack I implemented for the giant spider boss. The giant spider, unlike the player, is able to move across webs. The hack was to 1) swap out the main pathfinding map with a copy that treated the cells with webs as walkable 2) move the giant spider 3) restore the main pathfinding map. So, another pathfinding map was needed for enemies that can walk on webs. Then I thought about future enemies that I wanted to add, such as a ghost that can move through walls, and a bat that can fly over ground obstacles. Those would require their own pathfinding maps. I ended up building a new class to maintain multiple pathfinding maps, and added a pathfinding map attribute to each entity. Now, entities share pathfinding maps based on common movement constraints. And, if I need to add pathfinding maps for specific entities, that’s now possible.

I added player health regeneration because I wasn’t happy with items as the sole way to restore health. I’m not sure if I’ll keep this feature, but I’m trying it out as I balance combat. I don’t quite understand why, but playing with regeneration enabled feels better. Maybe it’s just because I’m not dying as often…

I reduced the size of a class from 900 to 300 lines using the Command design pattern. The class contained a dozenish actions. I pulled those actions into separate classes and the original class is now much easier to maintain. Large class size is one of the best clues that refactoring is needed.

Next week I’ll keep working on making combat more interesting. Also, I need to get the [2021 in RoguelikeDev] post out before January’s over.