The Aware Game Engineer 1
What do game developers and Fortune Tellers have in common?
🗓️
In this article, the first part of The Aware Game Engineer series, I'm gonna share my thoughts on:
What does experience mean for a game developer?
What does the architectural decision-making process look like?
What key code qualities should we consider during the architectural decision-making process?
I will share the story of one of the cases of technological debt incurred in No Rest for the Wicked.
Throughout my career, I've seen many early prototype architectures being pushed far beyond their intended scale, leading to compounding technical debt and burning production budgets.
1. Tech Fortune Teller
The core skill that differentiates an experienced developer is the ability to predict.
As an architect, one of the key skills is predicting the future - a vision as sharp as a fortune teller's. It's about envisioning where your system will be, and how to implement it, long before it gets there.
When I am asked to implement a system for a large-scale game project, I always consider several qualities of a potential solution:
Scalability: How well can it handle many dependencies?
Extensibility: How easy is it to extend the functionality?
Performance Cost: How much performance budget can I spend on it?
Implementation Cost: How long and how many hands does it take to build?
Maintenance Cost / Technical Debt: How problematic will this be in the future?
Clearly, my goal is to keep costs as low as possible. Choosing the right solution often involves trade-offs. Sometimes I need to give up on some performance due to scalability, sometimes I decide to spend more money on Implementation Costs to avoid further technical debt. Another time, I decided to completely ignore scalability for cost reasons, since I had just found a good-enough solution. Making good micro-decisions can potentially save or kill our production.
That’s why I never refuse when someone asks me to read the future from the cards lol.
2. The story of some missed costly prophecy
It’s been the post-prototype stage of No Rest for the Wicked development. I had to deal with the game’s Item Framework that had around 3k unique item assets. Each item data type also had some injected system logic.
I started by fixing a small logic issue. I fixed the problem, and after a week, I got two new bugs, both related to a very simple change I made.

During the investigation, it was found that all these item types were implemented using a hyper-dense, multi-level inheritance hierarchy. The hierarchy was 8 levels deep with almost 50 classes, spanning approximately 8,000 lines of code. Maintaining such a multi-level tree of dependencies led to situations where even trivial logic adjustments caused significant trouble for other dependent nodes. That was a very tangible example of technical debt.
For example, let’s imagine a simplified 4-level version of the type dependency tree I found.

What’s the first red flag you notice in this four-level tree? Just by looking at the hyper-dense dependency graph, the technical debt is obvious. What happened here? This dependency tree is a great example of missed prophecy. My task was about changing some behavior of bows. Every time I touched the bow-related code, I had to modify the same copy-pasted code twice in BowItemData and EquippableBowItemData.
Then let's say I got a request to make some other item type equippable - what to do now? Copy-paste the code again and create another weird sub-tree of items?
I don’t need to mention bugs that once fixed spread across dependencies - so once we fixed something for EquippableItemData, it turned out some of its dependencies were broken because we unexpectedly changed children's behaviour.
What concrete problems were hidden behind the lack of scalability and extensibility?
Code duplication
Dependencies hell
Logic separation from Data Assets
Lack of editor time customizability.
3. Wander through solution space
Before I even started considering refactoring to make the system a better place to live, I needed to see what the future potential solution might look like. The first step is to understand the requirements the system must meet.
This is an inventory system in an action RPG game. It's a central gameplay system with numerous dependencies. Data-wise, I want to support as much customization as possible since this system will be heavily utilized by design. For example, when someone asks me later to make an item equippable, I should be able to do it easily.
There are a lot of paths to take. This is where the developer experience really shines. I won’t be able to predict the results of an architectural change if I didn’t have experience that previously led me to similar solutions.
What would I do?
3.1 How to achieve better customizability
Currently, I can’t simply make some other item types Equippable or not. Just by looking at the type dependency tree, I can tell someone tried to achieve that by copying and pasting one sub-branch. To fix that, I need an option to make an item always available on all types. I need to extract the EquippableItem data and logic into a separate entity. I could do the same thing with WeaponItemData.
3.2 Interface - abstraction
I could limit the type tree to a single level of inheritance. I’m gonna have the base ItemData type that’s gonna contain all items’ shared contents, and a specific implementation of it defined with the use of interfaces.

I got rid of a few levels of dependencies, but I still lack any customizability at editor time, plus I haven’t fixed the code duplication problem at all - I still have two instances of bow types. Every new permutation of used types is gonna result in another type on the pile. Aghh, that solution sucks.
3.3 Struct - composition
Another option is to pack everything into a struct, with flags that control the final type.

Using that solution, I can even define a bow that is not a weapon anymore, which might be interesting from our game’s point of view - that might be a lore-related bow weapon, who knows?
That looks promising, the only problem it has is asset size - imagine I’m gonna have like 50 subtypes and each of them defines its specific set of data. Each item data asset in our game is gonna have the combined size of all available sub-type sizes.
3.4 Component List - composition by abstraction
That’s the trial of optimizing struct-composition-approach size problems with abstraction, so I kinda try to use the best of both worlds.

Thanks to that, we can keep the asset's size low again. That feature is not free, though; as always in software development, I need to make a tradeoff. I pay the price of the performance budget. First, the cost is simply the time required to get access to the component. Right now, it’s going to be O(n) when n is the count of elements in the list. Second, the cost of data fragmentation (details depend on the serializer you use). Unity will serialize the Component instances right after the ItemData struct, and they won’t be in any specific order. I would expect some increase in cache misses when working with data from the item asset.
Even for an item constructed from all components, the cost of iterating over 50 elements is negligible on modern CPUs. The data fragmentation problem is something I can easily pay for from our performance budget.
Once we’ve walked through the solution space, we can evaluate the cost system.
4. System Cost Evaluation
The previous implementation cost has been paid, and I couldn’t do much about it. Now, once we know potential solutions, we can estimate the costs of refactoring.
The initial item hierarchy likely took its current form due to rapid iteration. The system has super high maintenance costs due to constantly popping up new system bugs. Any interaction with the system - trial of extending it or making it used by another system results in another list of problems.

Cost can be expressed with real currency values. I kept them hidden under some abstract scores, just for simplicity. I could calculate actual dollar budgets only if I wanted to be more convincing in talks with producers.
Refactor Cost = (Implementation Estimate + Testing Estimate) X (Regression Coefficient)
Maintenance Cost = (Extra Time Wasted) X (Frequency of Interactions) X (Project Lifespan)
True tech fortune teller relies on intuition, and intuition works well enough with abstract measures, though, so I won’t spend too much time calculating some only-slightly-less-abstract dollar values :D
I try to grade each aspect of refactoring separately, since some parts might turn out to be crazy expensive and don’t significantly reduce tech debt.
In that case, maintenance costs are high across the board. As are the repair costs. What should we do in such a case? That question doesn’t have an easy answer. I would answer with the golden “It depends”. Depends on some more factors like:
What stage of development is it?
How likely is gonna touch or use the data?
Do we have the resources even to consider repair?
Can we release with this level of tech debt?
... and some more related to your specific production situation. For example, if that were the very end of the development, I wouldn’t touch it, regardless of how bad the code, design, or whatever it is. It works, we ship. That’s it.
On the other hand, if that’s the middle/early stage of production, the technical debt behaves like a normal bank loan. I need to pay interest on it, and it compounds, so the longer I keep it, the more I'll owe during the development period.
If the maintenance cost is lower than the refactor cost, I don’t touch any code. Period.
5. Final paths taken
No Rest was in the post-prototype stage of development back then, so it was early. We were sure we can’t release the game with the debt, assuming this is gonna be a constantly growing system. It became clear that we needed to refactor to unblock further production.
Given how complex the item system has become over time (and it keeps growing with every update), that was a critical step to protect our production timeline.
We haven’t managed to fully rework the system, though; we killed its most obscure parts. Most of the code duplication was fixed. A large portion of the subtypes were converted to ItemData-type substructures, so we went with a pure composition-based solution (3.3). We sacrificed asset size, but it didn’t come back to bite us.
We just stopped the refactor when the dark powers of tech debt were significantly reduced. In the end, that’s the sweet spot when we wanna finish our work - a spot where the further refactor cost is higher than the maintenance cost of what we have. If we don’t stop, we can stomp upon another trap - overengineering - a mysterious guest whom we will host soon.
Some new tech debt was introduced after all, but I won't write much about it now :D
Most of the industry stays in architectural solutions that “somehow work”, which is not bad; the final product works, the game is fun - that’s what it is about.
6. Key takeaways
We learned why experience and the ability to predict the future are among the most valuable developer features.
We walked through an example decision-making process on the item framework refactor.
We saw how to decide whether to invest resources in potential reworks.
We know how to calculate and compare the costs of fixing vs. maintenance.
We learned it’s worth stopping any refactors only when further upgrades are more expensive than the profit we will get from eliminating the remaining technical debt.

