Refactoring my own code: A lesson in good coding practices


Dolorem ipsum docet.

This image sums up how I feel spending hours refactoring my own bad code:


Few things will teach you a better lesson about how to write good code than having to fix and revise bad code. It's nothing to be ashamed of - no programmer starts out knowing the best way to do everything, and a lot of what the pros know didn't come from formal education, but instead from making those mistakes and drawing the right lessons from them. In my previous devlog, I wrote about my experiences creating a prototype of Battle Tower Royale back in 2018 and the lessons I had learned then. I still consider myself to be a beginner even now, but at least I've learned a few useful things along the way. The 2018 prototype was my first time coding anything for real, and boy did it show.

At the time, I possessed just enough programming knowledge to make a few small modifications to some code I found for the dialogue and inventory systems. They were inelegant and sloppily implemented, but at the time they got the job done. When I began Battle Tower Royale's development in October 2022, I knew a little more about programming and its foundational concepts, but I was still a novice in every respect. I relied heavily on the prototype's legacy code to get my game up and running as I had to re-teach myself how to use Gamemaker. It did the job, but that also meant that I was inheriting the problems the old code had as well.

------------

A lot of areas of the game have seen refactoring, but the three that really stand out are enemies, dialogue, and the inventory system. Enemies are nearly complete in how they are handled; learning how to use things like classes, structs, methods, and helper functions have made my life so much easier. It's made a world of difference in being able to add more spells, weapons, and better AI while having substantially less overhead and far fewer dependencies. Unlike the prototype, the new system can actually scale well. With the new system, I can actually implement the ideas that I have planned for the future.

The inventory/spell system is a work in progress. It's definitely a significant improvement over the old system. The code is (mostly) simpler, some of the dependencies have been taken out, and there's new functionality that did not exist in the prototype. The new system I wrote was also able to handle the creation of the feat system, which uses similar logic to the inventory system but in many new ways that I had no clue how to implement back in 2018. All that remains of the old system are a few basic helper functions. The one area that still needs an overhaul is how the text is drawn whenever things like quantities or enhanced stats are handled. The text for it still has a lot of tortured logic, and it may necessitate the creation of a new system to handle the text. I'm still deciding if this is needed, since I don't intend for that system to be used anywhere else.

The system that needs the most revision is the dialogue system. What makes revising the dialogue system so difficult is that it is closely intertwined with the cutscenes and animation changes for the sprites. There's a number of different ways that this can be changed, and being so uncertain about how to handle it and putting it off in favor of other things meant that the issue has continued to propagate. Sure, it gets the job done and there's no apparent glitches, but it scales poorly and makes implementing changes increasingly difficult over time. 

This came to a head when I re-factored my code to take out the hardcoded values for keys and replaced them with variables that allows keys to be reassigned. This was easy to do for some things, but for the dialogue system the hard-coded keys were so intertwined with the code that untangling this web of dependencies took FOREVER. Additional intertwining with dialogue menus proved to be extremely frustrating, and it took two days to unravel the dependency created by that. If I had written my code better the first time around and didn't hard code the keys, this could have been avoided entirely.

------------

So what are the changes I've made that have helped my code be more readable, efficient, and maintainable? One, writing helper functions. As I wrote in my previous post, all dialogue had to be pre-constructed with newlines strategically placed to keep the text within the dialogue box. This made adding a lot of dialogue a huge pain, and if I re-wrote anything I had to do it all again. Writing a simple helper function to parse text automatically made it possible to add and modify text so easily now. I also wrote scripts for saving and loading the game, various math functions, state checks, and more.

Player sprites and stats have been moved to arrays, where they can easily be swapped out and changed. This has made it far easier to add in new feats, races, abilities, modifiers, and so much more. The prototype had these values all hard coded, but the new system stores every relevant value in a few arrays for easy use by the scripts and objects that use them. Instead of hard-coding values for where to place a weapon when the player attacks, the needed values can be called based upon what race the player selected and automatically handled with no additional logic. (Note: selecting different races is an upcoming feature in the game!)

I have also made better use of structs and "classes". The class aspect is especially critical, since it drastically reduced how much duplicate code I needed to write! Structs also make it easy to pass information through functions, and there are several places where this could be useful (such as the dialogue system). I have also recently learned about the application of enums for state machines, which offers great potential for refactoring as I use state machines extensively throughout the game.

I have also found various ways to consolidate many different blocks of code where the only difference was a single variable or state. For example, if there is a variable called "selection" that executes different code depending on the state, there was an example where three identical blocks of code for "selection" values of 3, 4, and 5 were used. I noticed that the code was almost exactly the same, so I deleted two of the blocks and made the triggering condition (if selection > 2), and adjusted the value of selection in the code as needed to handle the times where the numerical value of the variable was needed.

-----------

These are pretty simple things in hindsight, but learning these things on your own means that you'll stumble quite a few times along the way. But these kinds of mistakes are what make you a better programmer, because you learned and corrected them yourself. It's tempting to want to have other people solve the problem for you, but while it's totally okay to ask for help you won't learn anything unless you go through the process yourself. As I have learned making this game, programming is not for the impatient.

This is the sort of post that I could write again in a year with so many new things to say. But I think this is a good time to reflect a bit and muse upon the mistakes I made earlier. Hopefully those of you reading this will learn from my errors and avoid making some of these same mistakes. As tempting as it is to just "get the job done", poorly-written code has a way of coming back and haunting you. Take the time to really think about what you're writing and how it will scale. If you want to make changes, how easy is it to do? Put the time in to do it right the first time, and future you will be very thankful.

Get Tower of the Immortals

Leave a comment

Log in with itch.io to leave a comment.