Games programming has a bit of a special snowflake problem. I’ve lost count of the amount of times I’ve explained a practice to people in the games industry that is commonplace in non-games programming, and the games people have nodded and gotten excited about the benefits and then said “But it will never work for games”.
This happens most often with writing tests; programmers will see the value reducing bugs, getting faster feedback, and being pushed towards better designs, but seem unable to imagine how this would translate to their day to day lives.
And it’s true that it can take a while to learn the skills you need to write good tests that don’t become a maintenance nightmare, add to this the relative scarcity of game programming specific information, and it becomes more understandable that people are weary of making the transition.
So I’ve decided to do my bit to help by making a game-specific TDD resource, in this series I’ll make a simple platforming game using TDD (and various supporting techniques) and explain what I’m doing and why.
The Project
I’m going to make the game with the Unity engine, which has some benefits and some drawbacks. On the plus side it allows me to use C#, which is a language I know well and one with mature testing frameworks (although I have applied all of the techniques I will show on C++ codebases too). Unity also has a testing framework which integrates well into the engine, this saves me spending the first however many posts making a good test framework. But it’s not all upside; Unity suffers from some of the same issues that plague other game engines, for example the main interaction with the Unity Engine is done through classes derived from the MonoBehaviour class, which cannot be unit tested (creating one without the engine running is tricky and error-prone) which pushes you to write larger scoped tests, which can create maintenance issues. This gives me a good opportunity to explain some techniques which help you create a testable codebase that still maintains a fairly tight coupling to the engine (because abstracting it too much would cause it’s own issues).
I chose to make a platform game because I think it will make a good real-life case to work against, there is enough complexity to make testing interesting, but not so much that I’ll get bogged down implementing game mechanics (the production of the game is secondary after-all), also, I wanted to make a game that would be quite tightly bound to things that Unity makes more difficult to test (in this case physics) rather than something that is easier to abstract (for example a strategy game, where the engine is mostly used for rendering)
The Techniques
Throughout this series I’ll be demonstrating a number of techniques, but most will be being used to support the main goal of these tutorials: Demonstrating TDD
Test Driven Development
TDD, or Test Driven Development is a practice where you turn the usual process of writing code on it’s head by writing test first, then write the code that makes the tests pass, this has a number of benefits, including:
- Your code has to be designed in a testable way, otherwise you can’t actually write it
- Code becomes much less risky to change, because you will catch any breaks quickly
- Get a high-level of code coverage ‘for free’ (100% of interesting code, usually about 90% of total code)
The first point is particularly important; well designed code tends to be modular, loosely coupled, only have a single responsibility, etc. And it so happens that code that is easy to test also tends to have these properties, code that is difficult to test also tends to be poorly designed, by writing the tests first, you’re putting pressure on the codebase to be well-designed. This is often described as getting design feedback from the tests.
The second point is also very important, I often hear gameplay programmers argue that games change too quickly for unit-tests to be effective, which to me seems strange because testing allows you to change code quicker and more safely, you don’t have to spend time thinking about all the things that could possibly break (and you’re very likely to miss some), you just make the change and the tests will make sure everything keeps working.
A high level of coverage is useful, but it’s important not to focus on this as a metric (after-all, it’s really easy to write massive tests that touch a large portion of the code and do not provide any real value). Mostly this just gives you the confidence that the tests will catch errors you would never think of (I’m often surprised by what mine catch)
To practice TDD, you need to repeatedly follow the following steps:
- Write a test that fails. This will usually be a small unit test for the next bit of behaviour you’re adding to the code. Making sure that the test fails will catch cases where you wrote too much code last iteration, and also make sure that the error message is clear, ideally, the name of the test and the contents of the error message should give you all the information you need to debug a unit test.
- Write the simplest bit of code you need to make the test pass. It’s hard to write only this minimum code, but after you’ve been doing it a while you find that often, you didn’t actually need all the code you initially thought you would, and you end up with something much more simple and concise.
- Refactor the code (refactor the tests too, but only change either the code or the tests at once). This is your chance to replace the simple and non-optimal version of the code you wrote last step with something a bit nicer. You already know the test works at this point, so you don’t have to be scared of breaking anything (and you won’t break any previous behaviour because you have tests for that too)
Each one of these steps should take at most a few minutes, and as you repeat this loop, you gradually build up behaviour that you know works, and you know that as long as you keep running the tests, it will continue to work. This means you don’t have to keep thinking about what a change might break, and your mind will be free to concentrate on the actual change you want to make.
Refactoring
Refactoring is so important that it gets a section all on it’s own. Throughout this project I will be aggressively looking for opportunities to refactor the code as I go, always trying to make it clearer and simpler. Thankfully, since I will be working in C# I have a really good tool (ReSharper) to help automate this.
Refactoring effectively is a difficult skill to learn (this book helps), but a really useful one. It allows you to delay decisions (and all other things being equal, a decision made later will be more likely to be correct as you’ll usually have more information), helps you to keep on top of changing requirements, and also helps keep the codebase clean of the messiness that tends to slows things down as a project goes on, this can also make estimates more accurate (most estimates are made by comparing the size of a task to the size of a task that has already been completed, this process is more accurate when there isn’t some parts of the codebase that are awkward or difficult to work with).
Great Names
“There are only two hard things in Computer Science: Cache invalidation, naming things, and off-by-one errors”.
Giving something the correct name is both difficult and powerful, once you have correctly named a concept it is easier to find places where that concept is being expressed (and remove the duplication), it also allows you to spot where functionality is the extension of a concept. ‘Great Names’ requires that every bit of code has a name that accurately and concisely says what it does, working within this limitation means you end up with a lot of really tiny functions, and once you get there, it becomes more difficult to write bugs (it’s harder to write a bug in a 5 line function than it is in a 50 line one).
When I first started following this I was worried about an explosion of small classes and functions, but I’ve found that it doesn’t actually cause me much of an issue, there does end up being a lot of small classes and functions, but because each one expresses a single concept, you’re only really thinking about a small subset of them at any given moment.
My aim here is to get to the point where each function is so simple, so astoundingly obvious, that I don’t ever have to think about what the code does (Alro Belshee refers to this as ‘tweetable code’, i.e. If you tweet the name of the function two programmers will write the same body).
For an example, say you have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | bool Update() { if((lastUpdateTime - currentTime) < UPDATE_INTERVAL) { return false; } var playerPos = getPlayerPos(); var input = (playerPos - getPosition()).Normalised; if(isWalking) { input *= WALKING_SPEED; } else { input *= RUN_SPEED; } setPosition(getPosition() + input); return true; } |
This function is fairly short, but already long enough that I would actually have to think about the code (not for long, but long enough). I’d probably change it to something more like:
1 2 3 4 5 6 7 8 9 10 11 12 | bool UpdateMovement() { if(!IsTimeToUpdate()) { return false; } var movement = getDirToPlayer() * getMoveSpeed(); var newPosition = getPosition() + movement; setPosition(newPosition); return true; } |
This gives me a load of 1 or 2 line functions, but makes the intent of the code much clearer, and breaks out some of the concepts into separate functions (for example, when the movement speed gets more complex I just have to change the ‘getMoveSpeed’ function which only has that one job)
Whole Value
The Whole Value pattern is something that is often used to fix the Primitive Obsession code smell. Primitive Obsession occurs when a programmer uses a primative type (say a string, or an integer) to mean something else (like a name, or a phone number). There’s many issues with this but they mainly revolve around duplication and not having a place to put a concept.
Let’s take, for example, the concept of a path to a file, often these are expressed as strings. But since the set of all valid file paths is smaller than the set of all valid arbitrary strings, you end up with a duplication of validation code, and often a duplication of parsing code too. This happens because the data isn’t really a string, it’s a special string, with limitations, and certain operations that only make sense for this special kind of string.
You can solve primitive obsession by creating a type for the special string (by all means, make it so you can construct one from a string) and passing that around. Now all the code that deals with it just has to work with the FilePath class, validation is handled at the creation of the FilePath, so all the code that uses it already knows that the path is valid (for whatever you decide ‘valid’ means).
Writing tests for all your code really helps you to detect this, you might not notice code in a few places doing the same sort of operations (getting the name of a file from a file path, for example) but you really start to notice when you have to write tests to cover all of those places (it’s something about the extra thought that you have to put in to writing the tests that makes it easier to spot duplication)
Summary
Ok so I’ve talked about the project and gone in to detail about some of the techniques I hope to demonstrate, I’m going to wrap this up now because it’s already a small essay. I hope that this series will be useful or at-least interesting. If you want to discuss any of this stuff, there is ways for this to happen