On October 24, 1960, a massive explosion shook the ground at Baikonur Cosmodrome in the Soviet Union [1]. During preparation for a test flight, the rocket accidentally exploded, causing a fire and massive destruction. It was a very sad day as more than 70 people lost their lives. The USSR strategic rocket program suffered a big step back. The catastrophe was named after Mitrofan Nedelin, the commanding officer of the project, who died in the explosion.
Let us retrace the steps that led to the catastrophe [1-4]:
Engineers were working on a very tight schedule (the rocket had to be launched by November 7, the anniversary of the Bolshevik Revolution).
Because of the lack of time, many safety procedures were abandoned or not followed correctly.
Many incidents and defects that occurred during launch preparation were ignored or not carefully followed up to analyze the consequences.
On the day of the explosion, to speed the launching process, multiple tests were conducted at the same time in the wrong order.
Due to delays with rocket start-up, the engineers continued to work directly at the launch pad. Many people were allowed to be there without any justification.
It is believed that someone in the control room saw that the button for the second stage engines was in the wrong position and, without any warning, moved it back to the initial position.
Other components (like the battery connection) were also incorrectly positioned, leading the system to execute a “start” order and ignite the second stage engines, causing the unprepared rocket to explode.
Figure 1. The Nedelin catastrophe at Baikonur Cosmodrome [4].
The account of the events leading to the catastrophe inspired me to write this article.
Comparison with game development
Video game programming is not safety-critical and doesn’t pose a threat to human life. However, it still raises a number of challenges regarding the reliability of our solutions. No players like corrupted save data, broken gameplay features or a crash in the middle of a winning match.
In this article, I will present a list of common problems in game programming to help you recognize risky patterns and localize potential problems in your game code. Specifically, I will describe:
Initialization, update and deinitialization patterns
Bad input data
Too much control exposed in data
Dereferencing null pointer
Big classes
Division
Vector normalization
Also, I will discuss how we can improve our code and prevent common errors. I will be using examples written in C++.
Defensive programming and Design by Contract™
I will start with two special programming techniques. The first is “defensive programming” and the second is “design by contract.”
A program written using defensive programming should properly recover even in unexpected situations [5, Chapter 8][6]. It should not trust the input data, always validate arguments and provide default values in the case of broken input. Here is an example of a function written with defensive programming:
float GetSquareRoot(float Argument) { if (Argument <= 0.0f) { return 0.0f; } return sqrt(Argument); }
Even if the client provides wrong input, the function returns a default result of zero.
The second technique, design by contract, sets up a relationship between the code and its clients [7][8]. The code needs to specify preconditions, postconditions and invariants. Preconditions are obligations for the clients to be true before the code starts. Postconditions are obligations for the code, which it promises to be true at the end. Invariants are conditions that should not change during execution of the code. Conditions can be expressed using:
Assertions [9][10][11]
Comments
Any other appropriate way (for example, a set of tests)
Typically, assertions are disabled during compilation of the final customer build. An example of the same function using design by contract:
// Argument needs to be equal to or greater than zero. float GetSquareRoot(float Argument) { assert(Argument >= 0.0f ); return sqrt(Argument); }
The contract is specified here using assertion and commentary.
Both techniques, defensive programming and design by contract, help us to write better software. However, I believe that game programming deserves special treatment due to its unique circumstances:
1. Maximum execution speed is essential for games.
Defensive programming cannot be used everywhere in the code, because it would result in bloated code if every possible input was validated, and this additional code would likely introduce its own issues. Our game would suffer severely in terms of execution speed. Also, too much defensive code can hide important issues if errors are bypassed silently.
2. Programming is done in a rapidly changing environment where programmers need to quickly take new ideas and run with them.
Game programming involves rapid development cycles where ideas need to be proven fast. But creating code full of unsafe solutions without any defensive approach is a recipe for serious problems in the future.
3. The code we write is also used as a product during development (not only after publishing).
When we work on a new gameplay feature, a new tool as part of an editor or improvements for an engine pipeline, our first users are other team members, designers and artists. These people also create the game and use our software as a tool. This means that our code lives in two realities while it is being worked on: the development world and the customer world, even before it is released to players. Using assertions and forcing the game (or editor) to exit when a condition is false might be good for a programmer, but it is the last thing a designer wants when creating new content for a game.
Additionally, your perception of the problem will depend on the specific needs of your game, team and type of work. Rendering programmers might favor speed at all costs, while AI programmers want more stable code with fail-safe solutions.
We will start our review of typical problems with one of the most common base designs for game programming classes.
Initialization, update and deinitialization patterns
Let us consider an example of implementation of the RAII (Resource Acquisition Is Initialization) concept [12, Item 13]:
class MyClass { public: MyClass() { m_Data1 = new DataClass1; m_Data2 = new DataClass2; } ~MyClass() { delete m_Data1; delete m_Data2; } void Update(float FrameTime) { m_Data1->Update(FrameTime); m_Data2->Update(FrameTime); } private: DataClass1* m_Data1; DataClass2* m_Data2; };
We create two objects in the constructor and delete them in the destructor. Also, this class contains a very typical method called Update to perform certain operations on the data in every frame.
Now, let us remove memory management from the constructor and destructor and create specific methods to handle these data resources. This often happens when we want to avoid executing too many operations in one place. Usually, resource management is a costly operation, so we would like to have better control of when it happens in the game. We will create two new methods: Initialize and Deinitialize to handle memory management.