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.
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.
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.
This week, there were lots of miscellaneous improvements that stemmed from play testing. The summoned swarm of giant rats continued to be a source of both AI improvements and entertainment. I took great sadistic pleasure in hitting one of the rats to turn it against me and then watching its brethren surround it and rip it apart in my defense. The Conjurer class is going to be fun…
Summoned actors now turn hostile if attacked by the summoner.
Actors will now attack the closest target rather than an arbitrary target.
Actor items now transfer to the actor’s corpse. Previously, when an actor died, all of the actor’s items were added to the cell that the actor died on. Now, the items are contained in the actor’s corpse and can be taken by inspecting the corpse.
Random actor items can now be limited to specific item types. This enables actors to carry items that are more applicable to their descriptions. For example, a bandit may carry some gold and throwing knives, while a wizard may carry some scrolls and potions.
Actor actions. That actions that can be performed on an object or item are defined by that object or item. Previously, any actor could perform these actions. This didn’t make sense in some cases. For example, a blue slime isn’t intelligent enough to open a door (and doesn’t have hands of course!). Actions that an actor is capable of performing can now be defined.
Extended Tier system to actors. Each item has a Tier attribute that indicates the item’s power relative to other items. Tier is used during map generation to ensure that the items placed on the map are appropriate for the dungeon level. I extended Tiers to actors for the same reason. Plus, for actors that have randomly generated items, the possible items can now be limited to the actor’s Tier.
Gold is represented as an inventory item for non-player actors. The player’s gold is tracked and displayed in a counter rather than a physical item in the player’s inventory. Now that other actors can carry gold, gold needs to appear as an item that can be taken in the actor’s inventory. When gold is dragged into the player’s inventory, it is added to the player’s gold counter and removed from inventory.
Actor sprite support for map themes. Tiles, such as floors and walls, have had theming (caves, dungeons, etc.) from early on. Changing stairs from tiles to actors necessitated adding theme support for actors too. This was trickier than I anticipated. The implementation for tile themes wouldn’t work for actors. The solution was to set the sprite during actor instantiation, based on the theme of the cell where the actor was located. But, that didn’t work when I tested it. It took me a while to realize what should have been an obvious problem – actors were instantiated before their locations were set, so the instantiation method didn’t know which theme to apply. I added an optional parameter to the instantiation method for the theme. It’s a bad hack, but it works and its scope is limited (there are very few actors that will have a different sprite for each theme).
Tracked actors aren’t re-evaluated when an actor’s attitude toward the player changes
The camera doesn’t follow the player when the player was temporarily controlled by AI (such as under the effect of a Fear spell)
Rats can be summoned in walls
Rats are able to take items
Dragging from the hotbar causes the screen to scroll
Clicking the hotbar after scrolling the screen causes the player to move to the cell under the clicked hotbar slot
The default player click action for a dry fountain is attack
Mass Fear stopped working on the player
Next week, there will be more miscellaneous improvements as I work toward making Legend fully playable. I need to stop putting off the decision of how to limit spell and ability use. This decision is required to balance gameplay. Currently, spells and abilities can be used without limits, making the game far too easy.
Adding a new ability to summon a swarm of giant rats nearly derailed my plans for this week (and potentially the next few months) when I tried out the ability and, to my surprise, the rats started attacking each other. I realized that there was no way of indicating that the rats were both allies of the player and each other; I needed to be able to specify relationships between actors. Factions were the first thing that came to my mind. They’re overkill for this single use case, but I’ve always intended to have factions in the game and reasoned that I could use them to solve the rat problem. As I’ve done with other aspects of the game design, I start with a flurry of research and brainstorming. I read up on factions in game design and roguelikes specifically, including a FAQ Friday on the topic, and wrote down all of the ideas and details I could think of. And, as I always do in this situation, I got lost in the subject and created a conceptual design that far exceeded what I likely needed and what I had time to build. That’s not a bad thing, as long as you can quickly pull yourself back into reality and reduce a grandiose design to something practical. I was able to do that in this case. My solution was to add an actor attribute indicating whether the actor is friendly, neutral, or hostile to the player. It’s a compromise, and there’s a good chance it will be thrown away in the future when I add a true faction system, but it’s the right solution right now, because it was extremely simple to implement and I don’t have a clear vision for how factions will work.
New Ability: Summon Giant Rat Swarm. Summons a swarm of giant rats to fight on the player’s behalf.
New Item: Fire Sword. Sets enemies and objects it strikes on fire. It’s overpowered at the moment, but I haven’t decided how to nerf it.
Chests and corpse item access. The items contained in chests and corpses can now be viewed and taken from the Inspect Panel. A “Take All” button lets all items in the container be taken at once.
Inventory Panel is automatically displayed when inspecting a container. When viewing a container, such as a chest, the player inventory is now displayed automatically. This allows items to be dragged from the container into inventory and vice versa.
Weighted observation selection. One of the configurable AI components is the ObservationDecider. This component examines the actor’s observations since the last turn and chooses an observation to react to. There are two implementations of this component, one that selects the first observation involving a tracked actor, and another that randomly selects an observation (used by the Fear status effect). These implementations are primitive. They don’t work well when there’s more than one suitable observation to respond to. To address this, I modified the ObservationDeciders to weight each observation and select the highest weighted observation.
New automated tests: Eggs. I’ve repeatedly broken the functionality of eggs, so this was a great automated test to add. The unique behavior of eggs (turning into a cracked egg upon detecting the player, waiting 10 turns to hatch, hatching a creature at the end of waiting) tests a number of systems.
Installed History Inspector from the Unity Asset store. My project asset list has gotten enormous. ScriptableObjects are used extensively, and many of these ScriptableObjects reference other ScriptableObjects. I often have to follow a trail of ScriptableObject references to fully understand how something is working, or go back to the previous asset I was viewing. The time spent scrolling through the asset tree, and the effect of navigating the structure on cognitive load, is impacting my productivity. I looked for a built-in Inspector history viewer in Unity but couldn’t find one. So, I turned to the Unity Asset Store and found the History Inspector asset. It’s already become an indispensable tool that’s saving me a lot of time.
Next week, I’m play-testing combat and overall level difficulty. I’ll adjust and fix based on what I find. The overarching goal for the year remains getting to a version of the game that I can distribute to others.
Resurrection ability particle effects. I mentioned last week that implementing Resurrection was simple. However, this week I hit a snag when I tried adding particle effects. The particle effects were triggered to start at the same time that the Resurrection effect itself started. This didn’t look right because the corpse would change to a living actor just as the particle effects were starting. I added a new Action Step for particle effects so that the particle effects could run in parallel with the Effect Action Step. Then I added a delay parameter to the Effect Action Step so that it could start half a second after the particle effects started. Now the actor appears to come back to life half-way through the particle effect emission and looks much better.
AI fixes. I fixed some problems that were rooted in my losing track of what the ActorTracker class is supposed to do. Actors are designed to only act when they need to, which is typically when they see another actor that they’re interested in. This is done for efficiency; the game would be too slow if every actor in the level acted on every turn. The ActorTracker class was created to allow an actor to continue acting after it can no longer see any actors that it’s interested in, enabling the actor to pursue another actor temporarily. At some point, I started using this class to permanently track actors that an actor was interested in, and built logic on top of this new use case. When the player’s actor was instantiated, every enemy in the level added the player to its ActorTracker. That caused every enemy to check if it could see the player at the start of each turn. This is an expensive check when the player is in the enemy’s sight range, because line of sight is used to confirm that the enemy can see the player. Aside from the performance issues, the shift in how ActorTracker was used broke AI. And, ironically, pursuing actors that were no longer visible, which was the original purpose of ActorTracker, didn’t even work anymore. To solve these problems, I went to my best tools for walking through complicated flows: graph paper and a pen. I diagrammed the AI steps, located the logic flaws, and fixed them in the code. I scaled ActorTracker back to its original purpose and added comments to help me remember its scope in the future.
Chest and corpse user experience design. One of the oldest items on my todo list is getting chests fully working. This has been held up by incomplete design. Until this week, I hadn’t determined how the player would interact with chests, and how that would translate to the user interface. I’ve been putting off making that determination until I played some other roguelikes and CRPG’s to get a sense of how other games handled this. Downloading, installing, and playing multiple games until I found a chest seemed too time-consuming, so I resorted to watching a bunch of Youtube videos to find examples. The latter was much more efficient.
Next week, I’ll get chests fully working and fix bugs along the way.
Ring of Protection. Improves the wearer’s damage absorption.
Ring of Evasion. Improves the wearer’s chance of evasion.
Resurrection. Resurrects a targeted corpse. I was pleasantly surprised by how easy it was to implement this. It was 95% accomplished in the Unity Editor leveraging existing capabilities.
Fear now works on the player. The Fear ability, implemented several weeks ago, only worked on enemies. It needs to work on the player too because an enemy may have this ability. Also, I debated whether the caster should be affected if a Fear spell’s area of effect included the caster. I decided that gameplay would be more interesting if the caster was also affected. This forces the caster to be more careful about choosing the area of effect.
AI can now be applied to the player. This enables status effects that temporarily take control of the player, such as Fear. This required a lot of work to implement. I originally assumed the player wouldn’t need AI, and that assumption was reflected in multiple code locations that needed to be modified. This enables some interesting potential future capabilities. For instance, an AI could be written to simulate playing the game.
Fixed several bugs related to torches. Torches have been the source of many time-eating bugs. The underlying logic is undoubtedly overengineered. Torches are designed to be lit when placed in a Quick Switch Slot, and extinguished when placed back in inventory. When in a Quick Switch Slot, they generate a light tied to the carrier’s position. Lighting and extinguishing torches is handled by a Convert Item effect, which destroys the original item and replaces it with the new item. This effect is triggered by moving the torch in or out of the Quick Switch Slot. I’ve spent far too much time getting this all to work. Bug fixing has been arduous because it requires tracing through a sequence of triggered events rather than walking through code line-by-line. The implementation had to be simplified, even if that meant hard-coding. I settled on changing how Convert Item worked. Instead of replacing an item, Convert Item now keeps the original item and changes the item type and some other item attributes. It’s not as clean, but it gets rid of item destruction and creation (and the logic triggered by those events). It’s also preferable from a performance standpoint, but that was a minor concern.
Changing levels is fully working (again). I’ve been play-testing on a single level for months and broke most of the functionality around changing levels (example: going upstairs to the previous level placed the player on the upstairs cell rather than the downstairs cell). To support the new way that actions work, I had to add Ascend and Descend actions. I discovered some functionality that had never been implemented, for example preserving Quick Switch and Hotbar slots. This was a little tricky because the Quick Switch slot state is part of the standard actor inventory (as selected items, decoupled from the UI), while the Hotbar slot state is housed outside of actor inventory as it’s only needed by the player.
Automated tests added for potions, scrolls, and rings.
Next week, I’ll keep fixing the known bugs and play-testing to find more bugs. This will continue until the game is in a state where I can play it for an extended length of time and not encounter a single error in the log or exception. I’m getting there…
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.
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 configuredto 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.
Implementing the Vampire’s AI was a lot of work. The process revealed the need for additional capabilities to support more varied and advanced AI. With the new AI framework, creating AI for new enemies in the future will be faster. The Egg, also added this week, is evidence of that.
Vampire. The Vampire is a dangerous enemy that has the special ability to transform into a bat and flee when its health is low. It returns to Vampire form after its health regenerates.
Egg. When the Egg detects the player nearby, it starts to hatch. The Egg can be destroyed in one hit if the player gets to it in time. But, if the player doesn’t get to it in time, a creature hatches from it.
Coffin. A Coffin can be opened or closed and can contain a Vampire and/or items, or be empty.
Puddle. This is a single cell containing shallow water. It’s only found in caves. It’s just decorative at the moment. I plan to leverage the existing Physical Materials framework to add interactivity.
New Map Elements
Coffin Area. This map element places a single Coffin in a significant location in a room.
Egg Area. The Egg Area Map Element contains a cluster of Eggs.
Built new reusable capabilities. The above features required the implementation of new capabilities including a new action type to move an actor and trigger an effect at the same time (used when the Vampire flees to move away from the player and transform into a bat), a health regeneration status effect (used by the Vampire), the ability to prevent health bars from animating in specific situations (used when a Vampire transforms into a bat and health is transferred), generate particle effects when effect types are triggered (used to display a cloud of smoke when the Vampire transforms), and a new AI for actors that triggers an effect when the player is seen (used by the Egg to transform into a Cracked Egg), with an optional delay (used by the Cracked Egg to hatch after a number of turns).
`Next week will be a mix of new content and bug fixing.
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.
I started to add a new enemy this week, vampires. This revealed a problem with my AI framework. Vampires have the same move and attack behavior of a normal enemy, but they have some additional behaviors as well. For instance, they can change into a bat and will use that ability when their health is low to temporarily flee and regenerate health. Prior to vampires, the AI framework worked fine. Each time I needed to give an actor a different behavior, I’d simply add a new AI class. I had a single enemy AI class, a neutral NPC class, and a few classes for actors that do something but aren’t sentient, like fire and gas. This worked because the logic for each class was completely different. The vampire AI revealed a problem because it needed some of the standard enemy behaviors and its own unique behaviors. I spent a couple of days thinking about what to do about this. The solution came to me when I identified the pieces of the AI class that needed to change with each enemy: choosing which actors to track, choosing which observations to react to, and choosing an action from a list of potential actions. I defined interfaces for each of these and created standard enemy and vampire implementations. I extracted shared logic, such as determining all of the potential attacks an enemy has, into new classes so that the logic could be reused. I reduced the enemy AI class to the logic that was applicable to all enemies, which was mainly state management. I can now easily add new enemy behaviors without having to replicate code.
Rework can be a discouraging exercise, especially this far into a project. It doesn’t add anything to the game from a player standpoint. It doesn’t concretely move the project closer to the finish line, but there’s an expectation that it will save time in the long run. It can feed self-doubt (if I had written good code the first time around, I wouldn’t have needed to rework it). There’s a risk of over-engineering or building capabilities that you’ll never need. In this case, I almost scrapped my entire AI framework and considered implementing it using a Unity asset, Opsive Behavior Designer. I actually bought the asset and read the documentation. It seems like a great tool that provides a visual designer for AI behavior trees. It also supports utility AI within a behavior tree, which is essentially what my AI framework is doing in code currently. However, I decided to rework my existing code instead because it took less time to do.
With the AI rework filling up the week, the vampire wasn’t completed. I should be able to easily finish adding it next week. I’ll add one or two more enemies to test out the reworked framework. I will do the same with abilities, which went through a similar process recently of having to be reworked to support new types.
I missed the 11/30 target date for Release 3 but expect to complete the remaining work this weekend. As with the prior two releases, this release is internal (i.e. I’m the only person playing the game). Release 4 will be the first release available to others, though I’m not sure in what form yet. Certainly the game won’t be ready for Steam Early Access, but I need to start getting feedback from other players.
Completed AI 2.0. Actors now determine actions from their observations. Observations are generated from the game events that an actor perceives as well as significant actors/objects/items in an actor’s field of view. The AI determines which observations an actor cares about and the action to take in response to the observations. The game events actors respond to include actors moving, doors and chests opening/closing, and projectiles hitting targets or landing on a cell. A number of small improvements were made to optimize performance. The most impactful optimization has been to reduce the number of actors running the AI code each turn. Also, performance was improved by organizing the AI code so that the most expensive operations are performed last and preceded by inexpensive checks that allow methods to exit before running expensive code.
Fixed a lot of bugs. Half of the bugs were introduced by the AI changes and the other half were random bugs found during playtesting and fixed to tighten up Release 3.
Next week, I’ll create the Release 4 feature list. The focus will be refinement rather than new capabilities and content. New features and changes will be smaller; I don’t plan to implement any new systems or do any major refactorings. More time will be spent on bug fixing and testing than in prior releases. I also plan on finding an artist and replacing the stock images that are currently in the game.