The magic behind id Software
March 26, 2017
I recently read this blog post which distilled information from John Romero about what development was like at id Software. I watched the talk itself as well, but I liked that the blog post extracted the interesting bits for those on a time budget. In this post I wanted to take a few of the concepts that were talked about and go a little deeper on them than the original blog post. I especially wanted to talk about how some of these concepts can apply to writing software that isn’t a video game.
“No prototypes. Just make the game. Polish as you go. Don’t depend on polish happening later. Always maintain constantly shippable code.”
When trying to start your own business, there’s probably three bits of advice that always seem to come up. The first one being that you should start with a very small idea. The second one is that marketing is more important than anything. The third big idea is that you need to start with a MVP, a minimum viable product. In other words, the first thing you’re supposed to do is to prototype. Early advice to a lot of indie game developers is to be fast about prototyping your idea and getting people to try it early. You don’t want to spend a lot of time working on an idea or game mechanic that nobody is going to use. Because of all that, I found that this principle seems a little deceiving. However, the true nature of the advice comes out later in the quote. You should always maintain constantly shippable code. The code that you write at any given time should be good enough for you to feel like it could go out to customers. There’s still a problem with that sentiment though. Too many people would read this tip and immediately think about the Donald Knuth quote about premature optimization.
First, let’s look at the quote that everyone remembers:
“Premature optimization is the root of all evil”
Let’s take a look at the full original quote found here:
“Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”
I bring this up because many people will read the part about always having shippable code and then get the immediate urge to always write optimized code. I bring up the full quote almost as a history lesson because people like taking it out of context and not consider the last part where Knuth talks about critical code. Anyways, Romero is not saying to have optimized code. Shippable code does not have to be optimized. Shippable code does not mean optimized. What this does mean is that simple code is always better. Polish is something that could never end, so polish code with the boy scout rule. Always leave code cleaner than how you found it.
“It’s incredibly important that your game can always be run by your team. Bulletproof your engine by providing defaults upon load failure.”
This tip follows the previous one really well. Your code should always be runnable. Never check in code that breaks the compilation, interpretation, etc of the product. If you can’t get a clean run out of the code, don’t check it in to source control, or whatever system your team is using to work collaboratively. The second sentence in this tip holds weight as well. In December I wrote a post about how I had designed some code where the default value was the opposite behaviour from what I wanted the code to do. This was a problem for parts of the code that I had missed to update, and in fact it meant that there were more places that I had to write or edit code, as opposed to a smarter design where the default value was the value we’d expect in 99% of cases. Mike Acton has a great presentation where he talks about junior game engine developers and how they don’t have reasonable defaults. Although, he uses the term in a slightly different way, he’s saying that programs should continue to run even with a failure. I agree with some of the sentiment but I have problems with others. However, my difference in opinion is context specific and it’s important for me to point that out. When designing software, a common mentality to have is to fail-fast. The idea behind fail fast is that you want to bomb out as soon as possible to find a problem as soon as possible. If you handle a failure at some point by using a default value, you might run into the situation where you bomb out later in the program due to some compounding problems. There’s a happy medium here that we need to achieve, because as part of the user experience, you don’t want your software to bomb out on a user, but at the same time, it’s possible that you get them in a state where the application isn’t working as expected and that’s even more frustrating for the user because it’s unclear what might be happening to them.
“Keep your code absolutely simple. Keep looking at your functions and figure out how you simplify further.”
In this talk by Jonathan Blow, one of the things he talks about is a game by id Software, Doom. The whole talk is phenomenal and I believe you should watch the entire thing, but he specifically tells a story about the asset loading for Doom. The asset loader was done with a linear search, which would drive an academic programmer crazy. It turns out that the asset loading time was more than fast enough and it didn’t actually matter that they wrote the code in that way. The code is simple and easy to understand. There are very few moving parts and the algorithm is trivial to work with or modify if it needs to be. Simple code is just flat out better. In 97% of the code, the simple solution is not only the fastest solution both to write and to execute, but it’s also the best solution because of how robust it is.
“Great tools help make great games. Spend as much time on tools as possible.”
This tip feels like it would be true only for games. Creating infrastructure tools or domain specific programming languages feels like a very game-centric thing, but I’d argue that this isn’t the case. I choose to believe that almost everything in this world follows Pareto’s principle which is really just saying that large parts of the product are caused or affected by a small amount of the code. If there’s something that can be done more quickly thanks to an internal tool, and that something has a large impact on the product, make an internal tool. This might be difficult due to management or the higher ups telling you not to waste the time, and unfortunately it might be hard to convince them to think otherwise. My suggestion would be that if you’re serious about this tool saving you time, work on it on your own free time. If you developed it entirely outside of work, you might even be able to sell your software to your employer.
“We are our own best testing team and should never allow anyone else to experience bugs or see the game crash. Don’t waste others’ time. Test thoroughly before checking in your code.”
I like that this tip lends itself to being fulfilled by writing automated tests. Software is all about data in and data out. If your code is designed well, it should be easy to test at a unit level because you can write small repeatable automated tests that handle all kinds of inputs and outputs. Integration tests might be a different story but if you’re thorough about your unit tests then you should be covering a large amount of cases. Your tests should ensure that with expected input your code works correctly, then you want to make sure you try code with unexpected, null, or ill formatted inputs.
“As soon as you see a bug, you fix it. Do not continue on. If you don’t fix your bugs your new code will be built on a buggy codebase and ensure an unstable foundation.”
This tip really speaks to the heart as an engineer in a startup. For startups, they always have to optimize time to market because of the competitive spaces they tend to be in. Unfortunately, this means that an 80% solution can slip by more than once. The more you build on top of this buggy code base, the more technical debt you’re accruing which makes it harder and harder to go back and fix things because the more technical debt you accrue the harder it becomes to pay it off. I like that this tip is very aggresive about not letting anything linger. Unfortunately, this is another tip that relies on the management team to cooperate with you. This is much more difficult than it should be.
As Greg Young sarcastically put it, “…business people understand technical debt…“
It’s hard to get time to fix problems. You just kind of have to do it incrementally. Hopefully, you have a technical lead with an engineering background that can factor in bug fixing time.
“Use a superior development system than your target.”
I strongly disagree with this tip. At least, unless there’s some corollary. The problem I have with this tip is that it means that you’re not experiencing the same product that your customers might have. For example, the product that I work on right now is all designed to be real time. However, we had an issue where someone with a slow internet connection would have an experience so poor that the product was useless. As a home automation product, the point is to make your life more convenient. A slow product is going to make that impossible. So, I’d say this tip should really add on the detail of using a superior development system so that you can produce software more quickly, but to make it explicit that you need to test your software in least the worst context you expect to see/support.
“Write your code for this game only - not for a future game. You’re going to be writing new code later because you’ll be smarter.”
Let me start by saying that I really like this tip. Reusing code is a broad concept that people love to throw around. People don’t tend to specify if they want to reuse code across projects, or just reuse code within the same file, etc. I would take this principle and make the assumption that each version of the idTech engine was completely re-written from scratch. You should be constantly improving while you write software, which means code you did before is probably outdated. You might know more about the problem domain, or you just know a better way to solve the same problem, so you should write your code with your new knowledge instead of relying on a prior, less experienced you, to have written an adaptable solution.