I have been reading through The Pragmatic Programmer for a couple of weeks now. The more I read, the more I realized I myself have some small side notes to add. Although it's not a prerequisite to understand the arguments below, I highly recommend the book, as it contains applicable advice regarding every face of software engineering.
The numbers correspond to the chapter's number in the book.
It's actually good to hear from elsewhere sometimes, that we 're in charge of what's happening to us. It's also realistic in the sense that as software developers, we're in a privileged position and actually are able to take charge of our professional happiness. We should have the confidence to make things better in our companies, for ourselves, because if it doesn't work out, we always can find other places to jump to. We have that privilege. We get away with a lot less complaining, compared to other work fields. In leet speak: less QQ more pew-pew
If there is one takeaway from that book it's this point: We have agency.
This is something I've gone through actually. So often I was pointed out what to do and it felt so obvious. Then it came to a point that I was gonna ask somebody some question, and I ran the conversation through my mind, and I could guess what other options they would provide. A lot of conversations didn't happen due to this.
This is something I've heard as boy scouting: When little boy scouts camp somewhere in the nature, if they see trash around, they collect them to clean the place up. Translation to software being, whenever you see a little issue (like formatting, compiler suggestions) that could be fixed with an IDE shortcut or under 1 minute of work, you just do that. On top of reducing the entropy, that way you become more comfortable doing edits throughout the project code and abandon the mentality of "I shouldn't be touching this part, it's not mine, whoever is in charge should fix it"
As far as my experience goes, broken windows are usually in the form of tech debt. It's a todo somewhere, hack in this other place, etc. I don't believe not breaking a single window is something realistic under the deadline pressure sometimes, but what has to be done is documenting and saving them somewhere, and actively trying to schedule a time to tackle them. One thing I would attempt is to add a mandatory 'tech debt' task to every sprint. This way we would have to have an organized and sorted list of broken windows, and get them fixed in a regular basis. Organizing them is another crucial thing; maybe it's not as important of a task as we thought in the beginning, or maybe we uncover even more implications when we think about it so it gains priority.
As gamedevs, we know better than most that we never ship perfect products. But there's a limit to this, you don't want to ship buggy software if you don't want to hear complaints. Sometimes, I would rather the developers of the tools I use to stop feature development and tackle bugs for some time.
This is pretty relevant in gamedev I think. There's a period for experimentation, i.e. prototyping, where we accept the fact that all the concrete work we do is throwaway and we're conducting experiments to see if the mechanics are fun to interact with. After this period, we move into the phase we know what we want to do and build the actual thing. Since the goal in gamedev is the experience, not the code, we want to see something on screen and interact with it at all times. That's what matters in the end. Pretty much for every feature, we use this tracer bullet approach to get results as fast as possible, so that we can check if the experience is moving in the direction we planned.
It's indeed correct to say: "I need a couple of hours to estimate". I'm lucky in the sense that I've worked in teams where the work of estimation itself was scheduled. The time I took to examine the existing code not only contributed to the accuracy of my estimation, but also gave me a more holistic view of the current state of the code in general, which in turn, increased the accuracy of future estimations as well.
One habit I'm trying to get into is commenting on estimation inaccuracies when it's too much. It's like doing a mini-retrospective. Writing it down helps me solidifying the thoughts in my mind, so that it'll be easier to remember when I'm estimating something the next time.
It's true that one way or another, we have to rely on command line tools at some point. This is less frequent with gamedev, however. It's perfectly possible to be in the games industry for several years and not touch the command line more than a handful of times. Working with a game engine (Unity, Unreal etc.) requires us to interact with some GUI and use an IDE for more than 99.9% of the work we do.
This is about lifting the barrier between your mind and the text editor; how thoughts translate into code. In a sense, it's similar to learning a new language. When you want to speak a language that you're a beginner of, you first translate the thought to words in your mother tongue and then to the target language. As time passes, this intermediate step vanishes, which is the point of becoming fluent at the language.
Throughout my career, I kept looking for keyboard hotkeys whenever I had to do something with the mouse (alt+T+O in Visual Studio for example). The interrupt of moving my hand felt annoying ever so slightly every time I did it. I was aware of vim/emacs style of doing things back in the day; I kept switching to vim plugins of the IDE's I use. Every single time though, I switched back to default controls because I couldn't handle the productivity hit. At one random point during coding, I felt a sharp pain in my wrist when I was reaching for the mouse. I immediately turned on the vim plugin. That became a point of no return. In the end, my transition to (mostly-)keyboard-only workflows began due to ergonomic reasons.
It's a pity that I have to work with the mouse most of the time; it's pretty unpractical to try to navigate through Unity/Unreal editors keyboard-only (I assume the same thing would go for webdev as well, with the browser). If my job allowed me to do everything from a shell (and be productive at the same time; you don't want to develop Unity games that way), I would probably do it.
There are a few things I've taught myself about debugging.
The first is having a cold and calculating mindset. Debugging has some human element to it; someone introduced the bug and we're fixing it. The current focus is getting rid of the problem, not discussing why there's a bug or why we're doing someone else's job or something. The other part of the mindset is to actually focus on the problem. It's not always easy to pay the correct amount of attention to the correct spot, especially when the deadline is a short time away (I remember debugging the software literally seconds before a customer presentation; they were directly at the door of the room and I was fixing the configuration file, while talking to the relevant developer on the phone (only I was present in the room as tech). My colleague actually had to stall the customers with lorem-ipsum-talking to grant me the precious moments I desperately needed). Isolating oneself from the world around and focusing 100% on the problem is necessary sometimes. We should be able to do this.
The second is, it's mandatory for us to reproduce the issue. Sometimes we read the bug report, we jump to the problematic code, fix it, then run the software to make sure the issue doesn't occur, then submit the fix. This doesn't verify that our fix actually fixed the issue. We have to see the issue occurring before our fix. Depending on the environment, it might not be possible to reproduce it on our development machine. In that case, we should try to emulate the issue's environment on our machine; we should be as close as possible to that.
The third is about the outcome of the debugging. We think it's the fix itself, but that's not always the first thing we chase after. For example if the issue is blocking someone on the team from working, or worse, affecting people on the live environment, the first thing to focus on is to prevent people from experiencing the issue. If you think about it, this isn't necessarily the proper fix itself. In fact, these two might be completely different pieces of work. Depending on the software, this might be done in a thousand different ways, and it's a valuable skill to provide multiple alternatives to the proper fix (I remember intentionally feeding in bad data to a live game as a critical fix, so that the code would throw an exception at a very particular point, and wouldn't execute the actual problematic part below that point). Get your priorities correct, do the damage control first.
The fourth is something I'm sort of ashamed of myself (and felt actually good to read that on the book; that means other people are doing it as well) and that is: reading the error message. More times than I'd like to admit, I've heard from people: "but it mentions what you're asking for here in the error message". Yeah, sometimes C++ error messages aren't meant for humans to read, but that's the minority. If you receive an error message, read it!
One day, as I came back to the office from my vacation (May 2nd 2019), I saw a big pile of things to catch up with, and it felt impossible to remember them all. So I wrote them down in the morning. At the end of the day, I wasn't able to go through them all so I put a "tomorrow" section for the ones I'd tackle the next day. That's how my daily notes are born.
Every morning I open a text file for that day, with the same format (for example the day of writing this: 230321.txt, so that it will be easier to process if I want to change the folder structure in the future), I copy the "tomorrow" section from the previous day's entry, and start the work from there. There's no warming up the engine; I instantly recall my train of thought from the previous day and go on from where it left off (I used to do this with leaving comments in the code, then remove the // so that it wouldn't compile).
During the day this list grows larger. Whenever something comes up, practically anything that has a possibility of occupying space in my mind, goes into the list. Because I know myself enough not to trust; if something occupies space, it has the possibility of going away without notice (It's kind of embarrassing to admit that this works for me. More times I can count, I saw items in the list and went like "gosh darn, I totally forgot about it", but a bit more vulgar). I'm not running out of text space, so why not use it, right?
Not only the work items go in there; this text file is my rubber-duck from time to time. At times, the task at hand isn't quite clear in my mind. When it happens, I immediately start writing whatever comes to mind about the task. This abstract-thought to solid-text conversion clears up the way, probably 100% of the time. Sometimes I use an empty text buffer for this though, to reassure myself I'm doing mental prototyping and nothing I write needs to have any sort of structure. After the brain-dump, I delete the buffer. I edit this text file with vim in a Windows Terminal, not gVim, because I use WT's "quake" console feature, which enables me to switch to it from anywhere, and make it go away when I'm done. The list is always one shortcut away, so I can start pouring the contents of my mind in half a second, at any time.
At the end of the day, there's a small ceremony of thinking about the next day. I leave notes to my future-self, in a way that could be parsed and understood in a before-morning-coffee mindset (which is the reason why getting into the rhythm is easy in the morning). This involves a tiny bit of planning, so that the next day already has a shape and more predictable, which makes me feel good a little. I used to keep these files just in a directory. After I needed to work with another PC, I pushed the files to a private git repository, which introduced one more step to the daily closing ceremony: committing and pushing the file (One thing to be careful about though, in case of any leak of this data, it shouldn't break the confidentiality of the projects that I work on. So I don't use any person or actual project names anywhere. In fact, I write down everything with a mindset of "would this harm me or the company if it was public")
I can definitely say I took some inspiration from Carmack's .plan files (you might want to know that Tim Sweeny also has a similar habit), however this process I'm conducting doesn't involve any retrospective value in the intention; I don't go back in time to check how a certain thing was the way it was, or tick a work item as done. The primary goal of what I've described is to keep me focused during the day. The reason why I keep separate text files (instead of one todo file) is to treat the day as a single unit, from a psychological perspective; it makes it clear what is done (and to be done) in that particular day (besides, I don't want to stare at the same file for years).
Beyond it's technical side, this contract helps verification with QA department as well. I try to maintain the habit of listing down what QA should see when they test the feature I worked on. The concept of "post-conditions" granted me a slightly new perspective. In addition to what the feature should do, now I also think of "what must be visibly true/false", as if I'm writing a unit test and I would check for that particular thing in the assertions section. Through that, I'm able to think of the cases which aren't directly related to the feature, but the state of the world during/after the usage of the feature.
During coding, sometimes I end up making assumptions of a return value of a function or variable to be in certain ranges, or pointers to be valid. These assumptions could theoretically be wrong, but at the time of writing the code I'm kind of sure that they'd be correct. As we gain experience as developers, we witness that they do turn out to be wrong, and another voice is telling us we're introducing a potential failure point. This is the defensive mindset, which makes us put an if-check here. But that would introduce an error-case handling path in the code, and carries across the same idea to the reader: "here the code branches off, so there are two cases to handle", whereas this is not our intention. In this sort of cases, an assertion would look a lot less complicated, and reflect the intention clearer: "I didn't think this condition would be false at the time of writing, so if you see it happening, then I was wrong and this needs to be fixed".
On the other hand, there are cases where you can't afford not to be defensive: in a live game's code for example. If you don't have the resources to test and verify the changes thoroughly, you might prefer the code to fail silently in the such a case, while making sure the failure doesn't cause any serious harm.
The book mentions leaving assertions on in the live environment, so that the errors gets fixed in a shorter timeframe and the product is more stable. This might work when your customers are more inclined to create error reports. When it comes to gamedev, however, I'm not entirely sure if this reflects the reality (there are exceptions). The people who pay for the game want a flawless experience, and any interruption to this would harm the game's credibility (we all heard about "gamers"), which would cost money. So it's a business decision.
I think the reason of existence of this phenomena is described accurately by the name. Up to a certain point, we had a plan to execute, towards a predefined goal. After that, we might have almost no notion of what's still left to do (which is the lack of a goal, really). That's the key here: not knowing what's missing.
I remember the C language class from my university years. In this class we had lab-exams, in which we're supposed to solve a question under a time limit. The grader program checked our code with a certain number of inputs, if the output of our code was correct, it granted us some points. During one of the exams, I kept getting 60 out of 100. I was appalled because I thought I handled everything. Probably in an act of desperation, I asked the teaching assistant about it. The answer he gave is ringing in my ears: "think of the ways it can stomp on your code"
There are a couple of takeaways here. There was some objective mechanism telling me that I was not done when I thought I was. This is a precise definition of 'done'. When we reach a point during a project and say "we're done" and "don't know, there might be something missing" at the same time, it indicates a problem in that definition. We should be able to call it done (or not-done) without hesitation. If the resulting product is incorrect in some way, then we should revise the definition to install the necessary checks in there. In all honesty, this is a bit different in gamedev, since the missing ingredient is about the "game feel", which is difficult to quantify, if not impossible. Since gamedev is closer to art, we might even say that it's never finished, but abandoned.
The other takeaway is that the concept of agency in "stomping on the code". There's someone doing that. The reason of error isn't (or let's say, might not be) a huge coincidence by which the data happened to be in such a way that the program broke. Another human being predicted the possible solution and devised the cases which might be easy to overlook when writing the solution the first time round. Therefore there are such cases and covering those is also doable by our side. In a similar vein, competitive programming website topcoder's events include a 'challenge' phase: after writing and submitting the solution to the problem, we can take a look at other contestants' solutions and try our own custom inputs on them. We get points if we can find a valid input that 'explodes' the solution, and we lose points if the solution happens to tackle our custom input. So aside from the skill of reading someone else's code, this phase requires us to look at our own code from an attacker's perspective and cover up the holes, otherwise we fall behind in the scoreboard.
There's a common pattern in these examples: the 'completeness' is rewarded quantitively. We immediately get the feedback, whether it's scoring points or ticking a checkbox.
I've heard this from a lead programmer of mine: "Naming is hard". This brought validation to my efforts to come up with accurate names of things when I thought I was going a bit overboard with the effort. "It's only a name, just keep writing the code" I used to think. However, after reading other people's code a fair bit throughout the years, I see where the wisdom comes from. Minutes spent in discussions about "naming" classes and modules is the time perfectly well-spent.
The tech lead of the company I used to work for once told me in my first ever annual-talk of my career: "Customers don't know what they want, and we don't expect them to. As software engineers, it's our job to go into the customer's mind, and carefully extract their expectations from there and place it before their eyes". Back then, this sounded a little excessive, to a fresh-graduate student who got most of his programming experience from assignments with clear outputs. "We're supposed to do that as well?", I remember thinking, "Programming is hard enough, I can't be bothered with that, just let them tell me what to do". Well, you can probably conclude how wrong I was.
The Agile Manifesto sounds like it's exceptionally applicable to gamedev. In fact, pretty much all gamedev is done in an agile fashion, by this definition. Software in general solves real world problems, whereas game development is chasing after an ephemeral experience. It's impossible to plan for a game to be fun from day-one, go in the very same direction, and have it actually fun on shipping. There must be course-correction on the way, sanity checks to see if the game is delivering the intended experience. In this part of the software industry, agility is a must.
It sounds silly indeed, personally I find it unnecessary at best. It makes me feel like we rely on these kindergarten games to build up the synergy between the team members. If nobody takes this seriously, then it's superfluous. If half the team takes it seriously then it feels forced to ones who don't. If everybody takes it seriously then it indicates another problem: people want to group together against something, which is nothing but the rest of the company. In such an environment, I can certainly expect some sort of toxicity at some level, some time in the future.