Abstract
Problem: When and why should game developers refactor code, and what are the pitfalls?
Approach: Tim Cain draws on decades of game development experience (Fallout, Outer Worlds era) to break down the three reasons for refactoring, its benefits, and its significant downsides.
Findings: Refactoring should be driven by demonstrated need β efficiency, maintainability, or reusability β not by a programmer's desire to write "better" code. Premature and frequent refactoring introduces bugs, wastes time, and can make code harder to maintain.
Key insight: Just like premature optimization, premature refactoring is harmful. If you can't state a concrete reason to refactor, don't do it.
What Is Code Refactoring?
Restructuring existing, working code to make it "better" β cleaner, faster, more maintainable. The code is usually bug-free before you start; you're rewriting it because you see room for improvement. Though sometimes Tim has refactored code with a pernicious bug he couldn't find, and the refactor made it disappear.
Three Reasons to Refactor
Efficiency
Making code faster or use less memory. This is the most commonly stated reason. Tim recommends waiting to see how code is actually used before refactoring for efficiency, because usage patterns should guide the rewrite.
Example β array sorting: You have a sorted array where new items get appended to the end, then the whole thing gets re-sorted with quicksort. But since the array is almost-sorted, quicksort is a poor choice. The right refactor depends on usage context:
- If insertion needs to be near-instant, keep appending and sort during idle time (pause menus, UI screens where CPU cycles are free)
- If both operations need to be fast, do an insertion sort that places items in the correct position immediately
- If you have downtime available (game paused, UI open), use those spare CPU cycles for expensive operations
Practical story: In several games Tim worked on, they would schedule expensive operations (like Unity garbage collection) during moments the player wouldn't notice a stutter β bringing up the pause menu, inventory screen, or options. A 100ms garbage collection pause is devastating at 60fps gameplay but invisible when you're browsing a menu.
Maintainability
Keeping code clean enough that you (or a new programmer, or you six months later) can understand and modify it. This fights "spaghetti code" β methods that were supposed to do one thing now doing three, functions that were called twice now called every update, methods growing to 10-20 pages.
Why spaghetti code happens in games: Design changes. The design was flawed but you didn't realize it until implementation, or a better idea came along, or focus testing revealed usability problems. UI code is especially prone to this because it gets rewritten repeatedly to accommodate how players actually use it. Even simple design changes can require dramatically large code changes.
Example β fast travel: A game with fast travel where time passes during travel. Originally simple, but then:
- Status effects need to tick during travel (a damage-over-time effect should kill you mid-journey, not on arrival)
- Fast travel needs to continuously tick time quickly
- Pop-up UIs might need to interrupt fast travel (low health warning, poison notification)
- The fast travel UI wasn't built to handle pop-up interruptions
Each of these "small" design changes turns the fast travel code into a mess. After shipping, you want to go back and rewrite it with full knowledge of all the requirements.
Reusability
Making code portable across projects. You don't want to reinvent the wheel every game. Refactored code can be made more reusable by isolating game-specific parts into modules or libraries.
Example β GANOL and TIG: Tim isolated all DOS-specific calls in Fallout into a library called GANOL ("GAN's Not Windows"). When they needed to port to Windows, they only changed the library β the game code had no idea the underlying platform changed. Need mouse input? Call GANOL. Need a window? Call GANOL. How GANOL implemented it was invisible to game code.
This was so effective that after leaving Interplay, Tim wrote a successor library called TIG ("TIG Isn't GANOL") with even more advanced Windows features (since it no longer needed DOS support). This architecture made porting to Mac trivial β only the library calls needed to change.
Pros
Done right, refactoring gives you faster, better, cleaner code. But "done right" is doing a lot of heavy lifting.
Cons
Done Too Early
Programmers love to refactor immediately after writing something. But you don't know how it will be used, whether it has bugs, or if it's too slow. Just having a "better idea" is not a good reason to refactor. Wait to see if you even need to.
Done Too Frequently
Some programmers want to refactor at the end of every task, or refactor before bug-fixing thinking it will eliminate bugs. This leads to the opposite of spaghettification β code that's too clever, too rigid, and harder to modify. Over-refactored code with obscure algorithms becomes incomprehensible six months later, even to the author.
Introducing New Bugs
Every time code is rewritten, there's a good chance bugs get introduced. You might have perfectly working code, refactor it to be slightly faster (when it didn't need to be), and now there's a bug. Rewriting is writing, and writing means potential bugs.
Time and Money for No Immediate Benefit
The biggest con from a producer's perspective. If existing code is bug-free, fast enough, and memory-efficient, why spend time refactoring for some abstract future benefit you may never need? Producers are completely fair to push back on this. Programmers should be working on immediate, tangible benefits instead.
The Programmer Psychology Problem
Tim observes that programmers "love to feel one way but try to logic their way out of the explanation." They want to refactor but don't have a good reason, yet they'll argue for hours that they're being logical. Sometimes just wanting to isn't good enough.
Summary
Refactoring is just a tool. Like all tools, it helps enormously when used for the right reasons and causes harm when misused. Do it for efficiency, maintainability, or reusability β but only when there's a demonstrated need. If you can't state a good reason, don't refactor your code.
References
- Tim Cain. YouTube video. https://www.youtube.com/watch?v=SuMElKtydDQ