My main goal this week was to finalize features for Release 3. That was completed with the exception of magic; I’m still determining how magic will work in the game.
The other major focus was reimplementing object interaction. I made good progress but a lot of time was required to think through the problem and solution. I realized that I was mixing two things: player actions and interactions between objects. For instance, when a player clicks on a pile of bones that is a few cells away, I want the player to move to the cell adjacent to the bones and then attack the bones. In this case, the player’s intent was to attack the bones. However, if there’s a pile of bones along the player’s path, that isn’t in the destination cell, the player needs to move over the bones instead of attacking. In this case, the player has a different intent and opts not to act when it comes upon the bones. The solution was to specify the default player action for each object and use that to determine what the player will do if that object is in the cell the player clicked. Each player action is a Monobehaviour that implements the IPlayerAction interface. This allows player actions to be added to objects and easily identified by calling GetComponents<IPlayerAction>(). I think there is a more elegant solution out there, but this gets the job done.
That’s pretty much it for this week. My time was more limited than usual. I recently discovered the Game Maker’s Toolkit Youtube Channel and have been getting a lot of great insights from it. It’s very well done; I highly recommend it.
Next week, I need to sort out how magic will work. And, I need to get some more Release 3 features done. I’m almost one month into a six month schedule and already behind.
This week went in some unexpected directions. I still don’t have the Release 3 feature set finalized; it got put on the backburner for some other items that I probably spent too much time on.
Learning material science. I spent way too much time reading about material science and researching different materials and properties of those materials. I was going to use this information to build a model governing how different objects interacted in the game. I haven’t been able to distill the complexity into a simple model yet, and will likely end up explicitly defining the material-to-material interactions. This was actually something I was working on in my previous game project, and it became a deep rabbit hole and one of the reasons why I abandoned that game.
Material-based logic. Actors and items can now have immunity to status effects based on their material. This fixed some bugs, like wooden doors getting poisoned.
Modified the UI to work on PC screens. Originally, Legend was going to be released on mobile devices first and PC/Mac second. I’ve since decided to reverse that sequence. As such, I had to resize and reposition the UI to work on a higher resolution, landscape-oriented screen.
Message log improvements. The message log now writes to a file in addition to the screen, enabling me to see the complete history of messages. I also cleaned up the code.
Debug log improvements. The debug log now writes to a file as well. It’s in a comma-delimited format so that it can be imported into a spreadsheet app or log viewer. Each debug log entry contains a timestamp, Unity log message type (Error/Warning/Info), category, the class and method that generated the message (using Reflection), and the message itself.
Using the unit tests I wrote last week. The new unit tests are already paying for the time it took to write them. They caught a few bugs introduced by refactoring this week.
Next week I need to finalize the Release 3 scope, but I’m feeling compelled to keep working on a solution for object interactions. The current solution involves an ugly switch statement (if object is X, execute code block Y) that needs to go away. I want these mappings to be done in the Unity Inspector, so I will probably refactor this into a solution using UnityEvents. More to come on that next week. I also need to change the ratio of time spent on new features and refactoring. I’m spending a bit too much time on the latter, and there are a lot of new features to add for Release 3 over the next 6 months.
I’m still focused on testing and bug fixing as I try to wrap up the second release by end of May.
Switched from Visual Studio for Mac OS X to JetBrains Rider. I’m forcing myself to give Rider another try after quickly dismissing it the first time I installed it. Rider is highly recommended by some well-respected Unity developers. I didn’t like it at first because I felt that it excessively decorated and annotated the code displayed in the editor. And, some key features I constantly used in VS were missing or worked differently (keyboard mappings, search box, finding usages). But, Rider’s growing on me. Debugging is much better; being able to see the current values of variables inline with the code is awesome. Expensive method calls and heap allocations are highlighted. It’s better integrated with Unity.
Optimization. There was a brief but noticeable stutter each time the player moved onto a new cell. I fired up the Unity Profiler and found the cause: on the start of each new turn, every actor checked which other actors it could see. The check was expensive; it involved getting all the cells between the two actors (using the Bresenham line algorithm) and iterating through the cells until reaching the other actor or hitting a cell that blocked line of sight. The performance was terrible because the line method was allocating a new list every time, and the check was being performed many times (O(n2)). Initially I tried using the ArrayPool class to avoid the new list allocations, but it didn’t work well for a few reasons I won’t go into. Then I realized a list wasn’t needed at all; I just needed the last point. I wrote a modified version of the Bresenham line algorithm that started from the origin and stopped when the current cell matched the specified predicate and returned the current cell. I reran the Profiler and the performance was much better, but could still be improved. Having each actor call the “CanSee” method for each other actor was simple to code but very wasteful; most of those checks weren’t needed. I changed the code from checking each actor in the list to checking for actors only in the other cells the actor could see. This made performance worse, because the checks against all the cells were costly, and the map is relatively sparse in terms of actors. I removed that change, and went back to checking the list of actors, but added a new check to determine if an actor is in the viewing range of the other actor before running the line algorithm. This optimization significantly reduced the number of times that the line algorithm ran and greatly improved performance.
Created an instant status effect for adding/removing money. It seems a little strange to have an effect for money, but it basically does the same thing the instant healing effect does in that it’s changing an attribute by some amount. This allowed a few hard-coded references to money item types (like gold coins) to be removed.
Added the ability for items to cause status effects as soon as they are picked up. This was needed for the money status effect and future instant effects.
Fixed the health bar display logic. I kept having issues with when health bars were displayed and hidden. I’d fix one issue and create a new one. After sketching out the logic on paper the solution was obvious. The basic rule is: health bars are only visible when an actor/object is damaged and visible. However, when an actor/object is first damaged, the health bar needs to appear at 100% and animate down to the % of health after being damaged, so the basic rule doesn’t apply. And, when an actor/object dies, the health bar shouldn’t be removed (even though health is at 0%) until the animation finishes and the health bar shows 0%.
Peeled some more code off of the GameManager class.
Fixed a lot of bugs found during playtesting.
Next week my goal is to complete testing on Release 2. I’ve found and fixed 100+ issues over the past few weeks, but it’s still going to be tight.
Revamped level initialization. At some point, the ability to start over after dying stopped working. I ignored it because I was working on other things. Now, as I’m getting ready for the second developer release (Release 2), I needed to fix this bug. Understanding how levels were being initialized was difficult; code was running across multiple scripts on multiple Unity GameObjects, triggered by different events at different times. Some of the game state was stored in objects that persisted across scenes, so the solution wasn’t as simple as reloading the scene. The solution I came up with was largely based on event handling. The various game managers (game turn manager, actor manager, etc.) now listen for significant events, such as setting the map size (required to set the tilemap sizes), generating the map, and creating the player, and do the appropriate initialization at the point that the required objects become available. The procedural initialization logic that was spread out across multiple scripts has been removed. I also made some changes to the game managers to reduce or eliminate their reliance on other managers. It’s not perfect but it’s a lot cleaner and easier to follow.
Remember the last known position for tracked actors. Actors now remember the last known position of the actors they are tracking and have previously seen. This is useful for when the player can no longer be seen by the actor, for example if the player moves into another room or drinks an invisibility potion. The actor will now move to the player’s last known position and determine what to do next.
Bug fixes and code clean up.
Next week, more play testing and bug fixes to close out Release 2.
Good momentum again this week. Things are easier to add and fix after the recent refactoring.
Replicate actor action. This is a new action that allows actors to replicate themselves. The actor’s AI controller provides the replication parameters. This action was created primarily to enable effects to spread. For example, a fire will spread to adjacent cells that contain a flammable object.
Converted cell effects to actors. I originally created a class called CellEffect for objects that could cause status effects on actors and move independently. I realized that these objects were better implemented as actors so that they could take turns like other active actors and their unique behaviors could be defined in AI controllers. I call these Effect Actors. There are currently two Effect Actors: Fire and Poisonous Gas.
Items can now have status effects and be damaged. This allows items on the map and items in an actor’s inventory to be affected by status effects and damaged. For example, if an actor becomes engulfed in flames, the actor’s flammable items may be damaged or destroyed. This was previously implemented, but in a kludgy, hard-coded way.
Fire Arrow. It’s been months since any new items have been added to the game. The Fire Arrow starts a fire where it lands.
Arrows can create actors upon hitting a target. This was required to enable the Fire Arrow to create a fire when it strikes something flammable. This functionality can be used in many other ways, for instance a wand that summons a pet.
Replaced some enums with ScriptableObjects. I’ve used a lot of enums in because they are an easy way to define a range of values and they work in the Unity inspector (just don’t rearrange them because the inspector stores enum values by index). But, they have limitations. You can’t store any additional info on an enum. Also, new values have to be coded; they can’t be added in the editor. ScriptableObjects are a great alternative to enums because you can define additional attributes for each value and you can add new values in the editor. I’m in the process of moving the majority of enums to ScriptableObjects.
Materials. Objects now have a primary material, such as wood, stone, paper. These are defined as ScriptableObjects and have physical properties like hardness and flammability.
Basic material / element interactions. Implemented basic rules for interactions of different objects based on material. Examples: poisonous gas doesn’t affect a table, fire doesn’t affect a stone wall.
Message log improvements. Added messages for status effects and grammar fixes.
Next week, lots of miscellaneous improvements and fixes pertaining to combat animation, message log, fog of war, player detection, enum to ScriptableObject conversion, and object interactions. Roadmap-wise, I’m closing out open issues in the scope of Release 2.
Added health bars over enemies. I based the solution on Jason Weimann’s excellent tutorial (part 1) (part 2). I don’t have time today to post an example – will do next week.
Consolidated enemy and NPC AI code into AI Controllers. The AI controllers can be created for a single enemy/NPC type or a group of enemy/NPC types. They inherit from MonoBehaviour and implement an AIController interface. This cleaned up a lot of code, especially for the “Giant Spider” boss, which had code across half a dozen classes.
Improved player detection by enemies. Previously, enemies began to act when they were seen by the player. This had some drawbacks: it didn’t allow enemies to have different viewing distances, continue acting after the player moved out of sight, or track enemies other than the player. I created an ActorTracker class to maintain a list of actors the enemy is tracking. The ActorTracker inherits from MonoBehavior and gets added as a component to actors at design time. In most cases, only the player is being tracked, but this can also support situations where enemies attack each other or coordinate attacks. It’s also much cleaner.
Shrinking the bug list. The above improvements closed out some old open bugs.
Next week, I’m fixing ranged combat, which was disabled during the recent refactoring (lots of commented out code to remove or refactor), refactoring effects/conditions, and adding some visibility-based map features (mist, darkness traps).
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.
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.
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:
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.
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:
Making greater use of Unity composition to handle actor/item/object interactions in a more maintainable way.
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.
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.