As of writing this, I’ve been working as a software engineer in the game development industry for about 15 years. The game industry provides a unique challenge by combining many areas of computer science and software development. Here are 10 adages or rules of thumb that I have learned over that time. If you have any questions, comments or want more detail about the subject matter in this document, please visit http://danhamel.com and let me know!
1 – Favor Simplicity
One of my favorite tenants of Computer Science is K.I.S.S. When I was taught it in school it stood for Keep It Simple Stupid. I hear people say a more politically correct version these days, Keep It Super Simple. Albert Einstein said “Everything should be made as simple as possible, but no simpler.”. When you are weighing the tradeoffs between two approaches, don’t underestimate the negative impact of complexity. As the code base grows it becomes increasingly difficult to keep things simple, but doing so tends to pay dividends in the long run.
2 – Be Meticulous With Dependencies
In order to create software that is capable of scaling, you must pay careful attention to dependencies. Keep modules decoupled if possible. Don’t be lazy with includes. Continuously factor common functionality into shared modules. Keep well defined interfaces between modules. For game development a common approach is to organize the modules into a few layers like core, engine and game. Modules are able to depend on lower level and sibling modules, but never on high level modules. For instance a module in engine could use and depend on a module in core or another module in engine, but never a module in game. Ask yourself the question “If I want to lift this code and send it to a friend to use, what is the minimum amount of code I need to send to allow them to build it?”. If you end up dragging the entire code base along for the ride when sharing a low level module, it’s a sign the dependencies are not clean.
3 – Refactor Changing Code Often
Modern software development and game development in particular tends to be very feature driven. We want to continually push the product forward. New code committed to a project can often times be transient, and serves only to push the product forward in the right direction. IMHO this is the best way to write software. Take a direct path to create working software with a minimal implementation and iterate. Quickly failing forward to arrive at the best outcome. When developing software this way, you must continuously refactor. Rewrite systems when the requirements change. Be diligent about removing dead code. Pull redundant code into shared modules when that makes sense. As the product matures it will become clear what code is going to be around long term, focus on improving the quality of that code.
4 – Abstraction Layers and Interfaces
It’s usually useful to hide how a module works (its private implementation details) behind a public interface. The case study all beginning computer science students learn as motivation for this, is to be able to change how a module is implemented without having to change other code that happens to use the module. The module could also be swapped out with a different one that implements the same public interface. The calling code can use a well encapsulated module without being concerned about any of the gory implementation details. Here are some other examples where clearly defined interfaces are extremely valuable:
Application Logic and Display or User Interface
Cross Platform and Platform Specific Code
Interfaces for Automated Testing
Releasing a Library or SDK for other Engineers
5 – Automated Testing
In the days of boxed software, it was easier to muscle your way through a project, burning the midnight oil and crunching for a big release. These days software is often continuously delivered to the customers. We need automation to do the bulk of testing and validation to ensure the software is in a state that is good enough to deploy out to our users. Here is an example of what that might look like:
Local Testing: Before an engineer exposes her changes to others, unit tests and integration tests should be run on their local desktop machine. Unit tests are code written against the interfaces of application code to verify system behave as expected. In a perfect world this is possible, and tests are not restricted to being ran on a build farm only. Failing tests should be resolved before moving on.
Pre Flight Testing: The next phase is to build and run unit/integration tests on code that is proposed for commit, but has not actually been committed to source control yet. This can be done as a Pull Request in Git or a Shelf in Perforce. The build farm should pick up the changes, build and test them as if it was a committed change and report the results back to the author. It’s good policy not to merge any changes that do not pass this phase.
Build Farm Creates Release Artifacts: Once a change is merged or committed to source control, the build farm takes over, building and unit testing the change. If the code build and unit tests pass, an artifact is created and stored. An artifact is a built and packaged version of the software that can be retrieved, unpacked and executed.
Integration Testing: When new artifacts are posted, the build farm performs automated integration testing. That is, tests are run on the entire application, emulating the way a user would interact with the software. If all of these tests pass, the artifact can be tagged as passing integration testing.
Manual Testing: At this stage, a human can retrieve the latest artifacts that have been tagged as passing automated testing and begin manual testing. If manual testing looks good the artifacts can be tagged as passing manual tests, and are safe to deploy to users.
6 – Do a Deep Dive on Existing Code
Typical day to day life for a software engineer often times involves modifying an existing code base. Problems that seem really strange and difficult to track down can to seem really trivial once you understand how the code you are touching works. I’ve been a contractor for the last 6 years so I’m continuously learning new code bases. It is tempting to just take a stab in the dark with a quick code change in attempt to close the assigned task. And sometimes you get lucky and that works out, but I’ve found it’s almost always worth the investment to take the time to learn as much as you can about the code you need to modify and the surrounding systems. Sometimes this means setting breakpoints and stepping through the code, adding lots of temporary logging or just sitting down and actually reading through the code. If there any other engineers around that are familiar with the code base, try to get an hour of their time to give you a high level overview of the code. For me personally all this discovery work can be pretty painful. My ego wants to just dive in and start trying to fix the problem. But making myself do research until I have a deep understanding of the code in question pays dividends quickly.
7 – Measure First
This is one of the interview questions I used to ask to gauge how much experience a potential hire has. “Say you are tasked with improving the performance of a game or application. What would you do?”. Junior engineers would always just jump right into telling me what they would optimize. For instance, “I would use hash tables instead of linear searches”. The correct answer here is to measure first. Invest the time to gather as much performance data as you can. Write your own timing systems and/or research and use a profiler. The more metrics you gather the more accurately you will be able to identify the problem areas of the code. Once you know where the problems are, fixing them often becomes easy.
8 – Keep Poking
There are times when things just aren’t clicking. Maybe I have been trying to fix a bug for a couple of days, and nothing seems to work. It can be really frustrating and programming can feel like cruel and unusual punishment. I think this feeling is why most people quit coding. They hit this wall in school or on the job and think, “computer science isn’t for me”. We all run into this from time to time. When I find myself here, there are a few strategies I use.
Sleep On It: The brain works best in the morning after it’s well rested. Sometimes staying up late and pounding your head against the wall just doesn’t work. Take your mind off the problem, quiet you thoughts, get some rest and try again in the morning.
Shelve It: If schedule allows, time box how long you are willing to spin your wheels. Move onto a different problem and come back to it. Sometimes a fresh run at it will help you look at the problem in a new light.
Try Different Approaches: If you are stumped, try writing code to gather as much data related to the problem as possible. Write unit test code, write logging code, document the existing code or write a new application that tries to reproduce the problem with a more narrow scope.
Find a Second Pair of Eyes: If possible drag a second person in and explain everything you know about the problem to them. Sometimes just going through this exercise of talking out loud helps solve the problem. Or perhaps the second engineer can give some insight that leads to the solution. Don’t be afraid to ask for help. Even the most rock star engineers I know will do this instead of spinning their wheels for days at a time.
All of these tactics are meant to keep you moving, something will eventually click and you will have that ah ha moment.
9 – Use Internet Resources
I’m not ashamed to admit it, I rely pretty heavily on things like StackOverflow.com these days. In fact I will sometimes go as far as to let it influence decisions on what technologies to use. Google’s search is so good, you can find answers to nuts and bolts sort of questions really effectively. Break the problem down into small chunks, and use internet resource to help solve those problems one at a time. How to: GET a json payload from a REST endpoint in Python, Sort a STL vector, read a file in java for Android … just do a search in Google! Solve the small problems one at a time to keep making progress.
10 Have a Strict Code Style Guide
I’m usually lax about how another engineer chooses to implement something. There are many ways to solve a given problem. A lot of the time it’s not worth being a perfectionist about the nitty gritty details. It’s far better to be pragmatic and focus on finding solutions to problems actually reported by customers. All that being said, I have found it to be very beneficial to have a fairly strict coding style guide, and stick to it. If everyone on the team can converge on a specific style guide it removes resistance to reading, writing and comprehending the code. That leads to fewer bugs. Having a strict style guide also makes it easier to grep/search through the code and that will prove very useful.