Every now and then, in a game’s development lifecycle, you get the chance to pause and reflect. With the recent conclusion of our Jetsam Beta Test, we’ve been given such an opportunity.
While we’ve used the feedback to recalibrate our approach to level design, it’s also made a great stopping point to reflect on the bane of all developers’ existence: tech debt.
Jetsam has been in off-and-on development since 2015, when I was in grad school and working on it as an independent study. My programming prowess has improved noticeably since then – so looking through the codebase shocks me at times.
This week, I identified a number of anti-patterns in the code, and I wanted to walk through each of them to hopefully help newer developers recognize the traps they can lead to and how to avoid them. But first, I’d like to answer a question about the whole process.
Why refactor your code in the first place?
Refactoring can be really controversial among developers, especially in the game development community. It can be said that shipping something, even something that isn’t perfect, is more important than worrying about code quality – and rightly so! If no one actually gets a chance to play your game, it doesn’t matter how pretty your code is.
You’re not going to win awards for writing beautiful code in your game. You’re not going to be showered with acclaim for making a key algorithm perform slightly more efficiently. In fact – it’s pretty likely none of your players are ever going to see your game’s source code. Ever.
Then why refactor at all?
I think refactoring is important as a matter of hygiene, not as a goal in and of itself. I posit that refactoring is worth it if and only if it will make feature development faster in the future, and you have more features to develop. Refactoring greases the wheels to make development go faster, and it can do so in all kinds of situations:
- Working in a team?
- Refactoring can make it easier for other developers to understand what everyone else has built, meaning that people can “learn” other areas of the codebase faster and thus get work done quicker.
- Working alone?
- Refactoring can help you quickly re-learn areas of the code that you’ve long forgotten about, and increase your ramp-up speed the next time you need to come back to that part of the codebase.
- Do you want to add new features to your game, ever?
- Refactoring can make connecting new components to old code much easier.
After our beta test of Jetsam, it was clear that a few new features would really improve the game’s design. Those new features will change the way Jetsam moves – and the movement code in the game was a tangled hornet’s nest of repetition, magic numbers, and closed design. So it became a prime target for refactoring.
Don’t Repeat Yourself
This one’s a classic – the DRY (Don’t Repeat Yourself) principle. In Jetsam’s movement code, I had four separate methods bound to swipe event handlers – movePlayerLeft, movePlayerRight, movePlayerUp, and movePlayerDown. Each method was itself a rough copy-paste job of the others, with only X and Y values changed to reflect different iterator patterns for each of the cardinal directions. Most of the code in these methods was identical!
The solution: creating a movePlayer method that has an enum of the four cardinal directions as a parameter. That way, other code can call movePlayer(MoveDirection.kUp) to create an Upward motion; or movePlayer(MoveDirection.kLeft) to create a Leftward motion. The X and Y iterators are set in a switch statement at the beginning of the method, so that the looping logic works identically for all four directions.
As a result, Jetsam’s movement code went down in lines of code by a factor of four.
Minimize Magic Numbers
Numbers are extremely important as configuration parameters – especially in games. That shrinking animation won’t look quite right at a scale of 0.25x or 0.5x, but 0.378x seems just right, you know?
All too often though, these “magic numbers” get into our code in raw number form – we create a label with font size 30, or instantiate a sprite with dimensions 64px by 32px directly.
When I went to tweak the default menu button size in the game, I found, to my horror, that every single menu button throughout the entire game had its size set directly. Rather than changing the value in 39 separate locations, I declared a static variable on a Utilities class called defaultMenuButtonSize, and changed all 39 separate instantiations of menu buttons to utilize Utilities.defaultMenuButtonSize for their size.
As a result, I was able to tweak and re-tweak the button size until I got it juuuust right. And I’ll be able to easily tweak it again in the future if I change something about the UI’s layout.
This one is a little more hand-wavey than the rest, but one of the joys of game development is that things always change. If you’re just starting to develop a game, it’s pretty likely that it will go through several revisions of the core concept – whether they be tweaks, full-on redos, etc.
With that in mind, it is extremely important to write your code in a way that makes individual components easy to tweak, revise, and redo – without breaking the rest of your game in the process.
When I initially built Jetsam’s level editor, one of the core ideas was that levels themselves could be completely represented with ASCII strings. For example, ))/”()%%”!&”&#&$&%&&$&%&& or )) “(%((“!!!”!(!)”!”E”)$B%B&B(!(E())!)”)()). This more or less required that each bit in each character be assigned a meaning – and that any changes to the meanings of each bit would require all of the previous levels to be re-encoded.
So, when building the encoding scheme, I intentionally left some “spare bits” in these ASCII strings that I defaulted to zeroes. I didn’t have any plans to use them at the time, but I figured that in the future I might come up with a reason to use them, and I didn’t want to have to re-encode all the levels if I ever wanted to change something. And lo and behold, thanks to a beta tester’s great feedback, an entirely new way to play the puzzles was invented – and I had the spare bits to encode its activation into the level strings, without needing to change any of the existing 50+ levels.
Refactoring was the right call for Jetsam at this point – we significantly reduced the complexity of the movement code, making way for some exciting new features. We made our UI much easier to tweak with some magic number removal, and we also benefited from earlier open-ended thinking in that we were able to quickly add hooks in for a new level style to be announced in the near future.
While we don’t have anything tangible to show for it (other than some gory git diffs), we’ll definitely have lots to show for it in the near future as we build exciting new things.
Do you have a refactoring story to share? Post it in the comments!
If you have an iOS device and want to play Jetsam’s Beta, sign up here.
If you liked this blog post, show us some love by following us on Twitter. We’ll happily follow back.