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.
Most of the improved AI and game event response was built out this week.
- Field of view for game events. Returns a list of the cells that can see an event. For each cell, the distance from the cell to the event is also calculated so that actor sight distance is taken into consideration.
- Game event notification optimization. Previously, each actor received a notification for each event, and then determined whether the event was in the actor’s field of view. Now, only the actors in the game event field of view are notified, and only a quick sight distance check needs to be done.
- Door opening and closing game event reactions. Actors now respond to doors that they can see open and close. They will walk to any door that opens or closes (other behaviors will be added in the future). This allows for player tactics like luring an enemy into a vulnerable location, or away from a guarded location.
- Preoccupied and Observing awareness levels. Actors in the former state must wait one turn before reacting to a game event. Actors in the latter state can react in the same turn. So, when the player enters a room with guards who are actively observing, the guards can respond immediately.
- Refactoring. Consolidated duplicate code and extracted some classes in the actor action code. I’ve been reading Clean Code and getting some good refactoring ideas.
- Optimizations and bug fixes. The recent changes introduced slowness and some undesirable behavior (enemies attacking each other, enemies attacking doors, enemies attacking dead enemies).
11/30 is the target date for Release 3. The goals of Release 3 have largely changed since it was originally scoped six months ago. The last milestone is completing the AI game event response system. I should be able to complete this in the coming week.
Release 3 is scheduled to be completed at the end of this month. I started on this phase five months ago. I’ve completed around 30% of what I planned for this release, due to shifting priorities rather than underestimation. Much of the Release 3 scope consisted of adding – new monsters, new objects, new abilities, new items. Yet, the game wasn’t truly playable; it was buggy, there was no progression, and the main game loop was boring. In response, I stopped adding content and started concentrating on the fundamental experience. I improved game feel. I made combat more tactical. I refined procedural generation to produce more cohesively populated maps. I continuously asked myself what needed to be done to put this game in front of people. The game is now in better shape than it would have been had I stuck to the original Release 3 scope. Key achievements this week:
- Fixed projectile raycasting. I originally implemented Bresenham’s line algorithm to determine the path of a projectile. This algorithm generally worked, but sometimes projectiles would pass right through obstacles, typically corner walls. On multiple occasions I stood waiting for an archer to come to me, thinking I was protected by the wall in between us, only to be shot by an arrow that passed through the wall. It turned out that Bresenham’s line algorithm wasn’t suited for identifying all cells on a grid between two points. This post illustrates the issue. I wrote a new cell traversal algorithm from scratch (I didn’t use the answer from the post) that determines every cell between two cells. The new algorithm fixed the issue.
- Overhauled CellGetter. CellGetter is a utility class for getting cells in a specified pattern, such as adjacent cells, room perimeter cells, room corner cells, etc. It’s used extensively in procedural generation to place Map Elements in appropriate locations. It had become a 1,000 line class because a method was created for every variation of a pattern that was needed. For instance, there were separate methods for filled and unfilled rectangles and room perimeters and borders. I refactored the class by moving the variation to public, read-only C# Funcs that could be chained together as needed (e.g. all cells in a filled rectangle that can have objects placed on them). Then, I consolidated most of the methods into a handful of methods that have Func parameters. This will make it easier to add new Map Elements that place objects in patterns.
I’m still firming up plans for next week. I’ll likely focus on bug fixes and some minor user experience improvements. I also need to continue expanding the Map Elements.
I’ve found that an effective tool for avoiding burnout is to diversify work. After a stretch of mentally demanding and tedious work, I’ll switch over to something simpler and more visual. I spent much of the past two months reworking some fundamental systems and finding and fixing everything that broke in the process. Interspersing some lighter work, such as the Select Class screen and sound effects, has helped me stay motivated.
- Completed a Release 3 feature, Saving and Loading. The bulk of this was completed a month ago (saving/loading the map) but there were some odds and ends I needed to add: deleting saved games after loading and dying, and saving/loading status effects, which are stored in a singleton, the Status Effect Manager. This manager stores the active status effects for each entity (actors and items). Each entity can only have one active status effect per status effect type. The original data structure for storing status effects was a nested dictionary (<Entity<Dictionary<Type, StatusEffect>>>). This worked during the game, but didn’t work well for saving and loading. Because Entity was an instance of an entity and Type was an object type, I was going to write a custom serialization/deserialization converter. When problems like this arise, I’m now trying to apply a “less is more”, i.e., instead of adding something new, can I simplify or remove something that already exists? In this case, I changed the status effect dictionary to a dictionary where each key is the entity’s unique id and each value is a list of status effects (<Guid, List<StatusEffect>). That structure can be serialized/deserialized without any custom code. After deserialization, objects are instantiated or referenced based on the guids and some UI and visuals are updated as needed (e.g., if the player was previously translucent from an invisibility potion, the translucency has to be reapplied).
- Added a unique identifier for each entity. It’s common to have a unique id for each entity, but up until recently I didn’t need them. Saving/loading necessitated this; I needed to serialize references to entities rather that serializing the entire entity each time it was referenced. The solution is simple – a guid is generated each time an entity is created.
- Started on sound effects. I was holding off on this to focus on the core mechanics, but this video (the part about adding “pizzazz” early on) convinced me to start now. I purchased a sound effect bundle from the Unity asset store that will at least provide placeholder effects. The sound effects may be too realistic match the 16-bit retro visual style (didn’t occur to me until after I purchased). I dropped a few sound effects into the game to learn how audio works in Unity, but I need to put more thought into the design. Since a sound effect can depend on more than one factor in some cases (hitting a wooden object with a wooden item vs a metal item, for instance), it’s not as simple as adding an audio clip to each GameObject.
- Still working on hotbar design. I think I’ve got it, but need to test it out with some games that use a similar hotbar design.
Next week, the plan is to work out the details of the sound effect logic and start implementing the hotbar.
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.