One Release 3 feature completed this week: the Title Screen! It will likely completely change after I bring an artist onboard, but it gets the job done.
Finished Actor Refactoring
I dug myself into a deep hole last week by refactoring actors. Every unit test failed and there were a couple hundred compiler issues to fix. I’ve mostly climbed my way out of the hole since, and I think the actor architecture is solid enough now to get to the finish line. I now have:
The main actor class is a plain C# class for all actors. It contains all actor state and is therefore the only class involved in actor saving and loading.
A GameObject prefab is defined for each Actor Type. These prefabs are loaded into memory when the game starts. They use composition in a limited manner, typically having only Transform, SpriteRenderer, and Animator components. When a new actor is created, the corresponding Actor Type GameObject is instantiated and associated with the actor.
A ScriptableObject prefab is defined for each actor type’s definition data. Composition is employed here as well, though it is not supported “out of the box” by Unity, at least not in the way I’m using it. The technique is to add a field to the ScriptableObject that is a parent class or interface, and create custom editors to enable an inherited class (or implementing class in the case of interfaces) to be selected from a dropdown. Reflection is used to get all of the subclasses and populate the dropdown. When an actor is created in the game, Activator.CreateInstance is used to instantiate the class. This allows me to define an actor’s AI and abilities, for example, in the editor instead of in code.
This isn’t an elegant solution, but it addresses the things that were bothering me about the previous architecture, namely redundant type data in each actor instance, having to use MonoBehaviours or ScriptableObjects for composition but not being able to easily save/load component state data, inadequate information hiding, circular dependencies, and unclear division of responsibilities between the different classes comprising actors. The drawbacks of this solution are having to maintain two prefabs for each actor type and not doing composition the “Unity way” with MonoBehaviours.
All Unit Tests Passing, More Unit Tests Added
I’m repeating myself from previous posts, but the unit tests have been well worth the investment in time.
Next week, the plan is to finish the class selection and load game screens. There are still some things that are broken from refactoring and I need to fix those too.
Feeling a bit overwhelmed this week… I had to do some major rework (again) instead of working on new features. I find myself battling with Unity again, specifically where to put things – prefabs, components/MonoBehaviours, ScriptableObjects, plain vanilla classes. When I first started using Unity a couple of years ago, I tended to write code for everything, because that’s what I was familiar with. As I gained familiarity with Unity, I pushed myself to embrace it and fully leverage the editor capabilities. However, that produced a lot of constraints, and now I’m back to relying on code more (though not as much as in the beginning). Anyway, a lot happened this week due having a couple of days off:
Finished the save system. I mentioned last week on reddit that I was struggling to determine the best way to code the save system in Unity. I ended up pulling all state data out of MonoBehaviours and into plain classes for each object type. Nested objects are supported as well (serialized attributes have to be explicitly declared). All objects that need to be saved are nested in the Map class, so saving the game is as simple as serializing this class. Loading is a tad more complicated because, after deserialization, game objects have to be instantiated.
A byproduct of the save system was changing actors and items to inherit from the same base class. There’s a lot of commonality between actors and items – they’re in-game objects, they can be damaged, have status effects, etc. I was handling this through composition. Attributes were spread across multiple MonoBehaviours. Because Monobehaviours can’t be serialized, capture/restore state code needed to be written for each component. It made more sense to move all state attributes into a single serializable class. This was a case where inheritance made more sense than composition.
Another byproduct was pulling health into the base class from a MonoBehaviour. Previously, the Damageable component made an actor or item damageable. This component tracked health. However, each actor and item ended up needing this component so it didn’t need to be optional. Also, this was done to address the issue in the previous bullet. This change broke a lot more than I expected, but fortunately the unit tests helped pinpoint the issues quickly.
Test map generator. The map generator can now generate a map with designated layout, actors, and items. This will allow a greater degree of automated testing.
New map generation capabilities. Map generation now fully supports different sets of parameters. These parameter sets can be statically predefined in the Unity editor as ScriptableObjects, or dynamically generated in the initial steps of the map generator, enabling additional layers of procedural generation.
Next week, the goal is to finish the load game and select class screens, and start on the hotbar time permitting.
Dev time was extremely limited this week. After reprioritizing Release 3 features, my plan for the week changed and I ended up working on the save system. This was functioning in Release 1 but I haven’t updated it for all the features and refactoring done in Release 2. So much has changed that I’m basically starting over. I’m also having to rework some classes that contain a mix of data that should be saved and data that shouldn’t or can’t be saved (e.g., MonoBehaviours). I read a lot of articles and watched a lot of videos on save systems in Unity. Most of what I found was aimed at beginners and only covered the basics. This video was helpful, but I didn’t like having to create a new temporary object and save and load methods with explicit setters and getters; it’s slow performance-wise and difficult to maintain. I’d rather just isolate the data that needs to be saved and serialize it. Either I haven’t discovered the right pattern, or saving complex data in Unity requires a lot of effort and there’s no way around it. I could put all of an object’s state data into a single plain C# class, but then I couldn’t use composition to build game objects. I could isolate the state data on each game object component, but then I’d have to individually serialize the data in each component, or aggregate the stage data spread across the components first. Ideally, the save system would simply save and restore game objects marked for saving and restoring. This is technically possible in Unity, but from what I’ve gathered it’s not advisable. Anyway, after some analysis paralysis, I started implementing the new save system starting with the map. Once a monolithic class, the map now simply contains its dimensions, a cell collection, and half a dozen other collections that are only used for procedural generation, such as the room collection. I don’t believe I need to save the procedural generation data after the map is created, so I’m not going to worry about saving that data for now (ideally, this data would be in a class specifically for map generation that is destroyed after the map is created; will refactor in the future). So, the bulk of the data being saved is the cell collection. Each cell contains multiple attributes, including value and reference types. By default, the serializer I’m using (Json.NET) serializes objects by value. This is a problem for “type” objects. For example, each cell has a cell type, such as a wall or a floor. Each cell type has many attributes. By default, Json.NET serializes all of the attributes in the cell type for each cell that has that cell type. This increases the size of the save file significantly and loses the reference to the original cell type object. I solved this problem by adding an attribute for the cell type’s unique id to the cell class. The advantages of this solution are 1) less data written to file and 2) when loading data, the reference to the cell type object can be restored by retrieving the cell type from a dictionary using the unique id as the key. I applied this pattern to other attributes as well, such as actor types and item types. As of now, the map successfully saves to file, but loading hasn’t been implemented.
Other than that, I briefly descended into a rabbit hole on story-driven procedural map generation. This came out of the map generation work I did last week on room types. I want maps to have some coherence or purpose rather than just being a random assortment of rooms, and cohesion across levels that is driven by an overarching procedurally generated story. Ideally, the player’s actions could alter the story and influence future levels. I created a conceptual framework for doing this and implemented some of the building blocks. However, after reprioritizing Release 3 features, this is on the backburner for now.
Next week, the goal is to get saving and loading working again and start on the hotbar.
I stuck to the plan this week (for a change) and finalized the Release 3 features. I’ll publish a summary in next week’s post.
Added cell-based status effects. These are effects that are added when an actor moves onto a cell and removed when an actor leaves a cell. An example is a Darkness effect that limits the player’s visibility. This builds on the existing capability to add status effects by using an item or interacting with an object. This feature was unexpectedly difficult to add; a lot of the status effect code and Unity configuration needed to be reworked.
Room features. Each room has a room type that determines what’s in the room. For example, a Shop Room contains a shop and a Treasure Room contains treasure. I needed to be able to apply different features to a room to do things like placing a trap inside of a treasure room. I added a base RoomFeature class and some child classes like ShopRoomFeature and TrapRoomFeature. This makes it possible to mix and match different features can create a wider variety of rooms than was possible using room types. I still need to fully incorporate this into the map generator, specifically by replacing the existing room generation with feature-based generation.
Overhauled effect triggering. I was never comfortable with the way I originally implemented the configuration of status effects caused by an object or item. Effects inherited from MonoBehaviour so that they could be added to game objects in the Unity editor. It worked, and it was designer-friendly, but it was unintuitive (it wasn’t obvious the purpose of the component was to cause an effect) and too heavyweight (multiple effects required multiple components). I converted effect types to ScriptableObjects and created a new class containing a list of effect types. I called this class TriggeredEffectCollection and used it to replace the individual effect components. Adding effects is now easier (add/remove from a list editor in the Unity inspector) and the purpose of the component is more clear.
Removed the limit of one static object per cell. Now cells can have any number of static (not moving) and dynamic (moving) actors/objects. For example, a cell can now have a floor trap and a statue. This also simplified the code because now there’s just a list of actors/objects instead of a list plus the static object.
Extensive actor AI logging. Now the log very clearly shows why an actor chose a particular action and not others. Very handy for troubleshooting. The basic AI process is to 1) identify the potential actions and 2) select the best action. The logging explicitly indicates why potential actions were available or unavailable, and why potential actions were rejected or selected.
Added new effects: Cure Poison, Darkness, and Paralysis.
Added new items: Cure Poison Potion, Throwing Axe.
Damage type resistance. Physical materials now have explicit resistances to different damage types. This will be used to do things like making an axe more effective against a wooden door than a short sword.
The plan for next week is to replace Room Type-based room generation with Room Feature-based room generation and start on two new features: Enemy Alerts and Knowledge. The former provides visual indicators for whether enemies have seen the player and the former varies the descriptions of things based on the player’s knowledge.
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).