Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
The article presents solutions on how to write more reliable game code and avoid common mistakes in video game programming.
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.
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++.
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.
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.
Here is the new version of the class:
class MyClass { public: MyClass() { } ~MyClass() { } void Initialize() { m_Data1 = new DataClass1; m_Data2 = new DataClass2; } void Deinitialize() { 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; };
This code still looks very simple, but it has several big problems:
Problem #1. There is no proper initialization for our member pointers in the constructor.
Problem #2. What if the client calls the Initialize method twice in a row? Unfortunately, we have a memory leak here.
Problem #3. There is a similar problem with the Deinitialize method. It works well when the client properly calls Initialize first and then Deinitialize. However, this method breaks down when the client calls Deinitialize twice in a row [13] – this creates a double delete problem.
Problem #4. Another problem is our Update method. What happens if the client calls this method without calling Initialize first? A crash will occur when dereferencing uninitialized or zero pointer m_Data1.
Problem #5. Copying the object of this class creates a possible double delete problem.
Note that we are not specifying any design by contract here. We simply want to consider possible usage patterns by clients of this class to get an idea of common problems.
To modify this code and make it correct, stable and fail-safe, we need to:
Add proper initialization for our member variables.
Add a Boolean member variable m_Initialized to indicate that the object is properly initialized and use this variable in our methods.
As a standard practice, we need to always clear the pointer after memory deallocation is executed [13].
Call the Deinitialize method from the destructor to be sure that memory is deallocated even if the client does not call Deinitialize.
Declare the copy constructor and copy operator private to simply prevent copying.
We will split the correct code into two parts. Here is the code for the header file:
class MyClass { public: MyClass(); ~MyClass(); void Initialize(); void Deinitialize(); void Update(float FrameTime); private: MyClass(const MyClass&); MyClass& operator=(const MyClass&); bool m_Initialized; DataClass1* m_Data1; DataClass2* m_Data2; };
Here is the code for correct implementation:
MyClass::MyClass() : m_Initialized(false) , m_Data1(nullptr) , m_Data2(nullptr) { } MyClass::~MyClass() { // Prevent a memory leak if the client does not call Deinitialize. Deinitialize(); } void MyClass::Initialize() { if (m_Initialized) return; m_Data1 = new DataClass1; m_Data2 = new DataClass2; m_Initialized = true; } void MyClass::Deinitialize() { if (!m_Initialized) return; delete m_Data1; m_Data1 = nullptr; delete m_Data2; m_Data2 = nullptr; m_Initialized = false; } void MyClass::Update(float FrameTime) { if (!m_Initialized) return; // Defensive approach to check the pointers separately. if (!m_Data1 || !m_Data2) return; m_Data1->Update(FrameTime); m_Data2->Update(FrameTime); }
Assumptions about the expected number and order of method calls by clients of the class are very common issues. This is an especially challenging problem for bigger classes with a significant number of methods.
We can find similar solutions in Unreal EngineTM, for example in the class UActorComponent [14]. The RegisterComponentWithWorld and UnregisterComponent methods use member variable bRegistered to indicate the state of the object. The UActorComponent class contains much more similar pairs of methods: BeginPlay and EndPlay, InitializeComponent and UninitializeComponent, etc.
Bad input data is a problem that has existed since the beginning of our industry [15]. Missing input files like meshes or unexpected values for parameters that cause a game or editor to crash are something that no developer likes.
In general, we as programmers should be especially sensitive to this problem. Our code needs to be written in such a way that we provide default values for missing input, like default textures or visual objects in place of a wrong file, as well as useful info to the user in the form of a warning message or log entry [16, Chapter 3.3.2.2, page 147]. No program should crash in such a case.
In most game engines, it is very easy to expose any variable as a data parameter in the editor. Let us consider the parameter to control the number of objects created in one command:
int NumberOfProjectilesToSpawn;
Probably we indirectly assume that this variable will be 0, 1 or maybe 5. But what will happen if the user either mistakenly or intentionally sets an extremely high value, like 1,000,000, or a negative number? Will our code react correctly?
Good solutions to consider here are the default value and limits in the range of possible values. Alternatively, if we want to allow any value without limiting the maximum number, we can consider warning the user that the input value may cause the program to become unstable.
In the defensive programming–based approach, we make a clear distinction between risky input and safe input. We can consider all external input as always untrusted, but some internal data can be trusted if previously validated [5, Chapter 8.5, Figure 8-2]. As such, we could mark as always untrusted all external files, all data exposed in the editor, all player input, all data from the network, etc. The validation and correction process needs to convert such risky data into trusted data:
void SetNumberOfProjectilesToSpawn(int NumberOfProjectilesToSpawn) { m_NumberOfProjectilesToSpawn = NumberOfProjectilesToSpawn; ValidateAndCorrectParameters(); } void ValidateAndCorrectParameters() { m_NumberOfProjectilesToSpawn = clamp(m_NumberOfProjectilesToSpawn, 0, 8); }
From the perspective of game programming, it would be best to validate input data as early as possible. For example, immediately after entering parameters in the editor, before saving data or just after loading data for the object. Marking data as trusted as early as possible makes it easier to avoid unnecessary checks in the runtime code critical for game performance.
Taking the problem of bad input data a step further, we come across whole systems that are data-driven. Such systems can be configured and controlled without the programmer, entirely through the set of input data.
It is useful to expose the full system control or game feature to every team member, especially non-programmers. But programmers need to ask themselves: do we support every possible combination of values for the given set of parameters? Or will the game crash when something unexpected is provided as input?
Creating a system that is fully data-driven is very hard and time-consuming [16, Chapter 14.3, page 857].
When we expose another variable as a new parameter, it becomes part of a bigger set of data-driven values. Automatically, we add a new layer of complexity and dependency between the code and the data.
Let us consider a simple structure to control enemy behavior, for example:
struct EnemyBehavior { float m_WalkSpeed; float m_TurningRate; bool m_JumpAllowed; float m_JumpSpeed; // ... };
Does the code support a very low value for m_WalkSpeed and a very high value for m_TurningRate at the same time? Is jumping still executed correctly when m_WalkSpeed is very high?
A safer solution might be to prepare predefined sets of parameters in the code that we know are supported in implementation. The user is given a set of presets defined in the code and exposed in the editor:
enum EnemyBehaviorPreset { EnemyBehaviorPreset_TightCorridors, EnemyBehaviorPreset_IndoorHalls, EnemyBehaviorPreset_OutdoorOpenSpace };
We provide the user with only one variable that can be changed in the editor:
EnemyBehaviorPreset m_EnemyBehavior;
This way we limit the user’s freedom in defining the behavior, but our solution is more stable and manageable by programmers.
Dereferencing null pointer is one of the main reasons for instability causing the game or editor to crash [17].
In Unreal Engine 2 and 3, one of the designing goals for the scripting language UnrealScript was to prevent this type of problem and create “a pointerless environment” [18]. Accessing object references in UnrealScript was always safe, even if they did not refer to any object (making the operation have no effect).
Because execution speed is so essential for games, we cannot add a conditional instruction in C++ before every possible pointer dereference. However, the policy on how to work with pointers may differ depending on the department. Rendering programmers can sacrifice defensive mechanisms to gain speed at all costs (adding only asserts to detect cases with null pointers), while gameplay or AI programmers might favor a more defensive approach because of frequent changes in gameplay mechanics and code evolution. Therefore, it is important to determine your policy when working with pointers.
First, we can avoid using pointers as function parameters and use references in C++ instead [17]. So, instead of writing the function as follows:
void MyFunction(ControlData* InputData) { if (InputData) { int parameter1 = InputData->GetParameter1(); // ... } }
We can rewrite it using a reference:
void MyFunction(ControlData& InputData) { int parameter1 = InputData.GetParameter1(); // ... }
This way, the caller of the function is responsible for providing the proper argument (and checking and dereferencing the pointer if needed).
In practice, specifying the contract for a non-null pointer as the argument in the public method tends to be one of the most commonly broken contracts.
If a method is exposed as part of the public interface, I recommend checking its pointer arguments against null:
void MyClass::AnalyzeInputData(Data* Input) { if (!Input) return; // ... }
If a method is part of a private implementation, I can consider removal of the early out code. In such a case, there needs to be a proper description in the comments or the method name should warn about no tests for zero pointers:
class MyClass { public: // Method always tests if the pointer argument is null. void AnalyzeInputData(Data* Input); private: void UpdateObject_NoTestForZeroPointer(ObjectClass* Object); // Method does not check validity of pointer arguments: void UpdateObject(ObjectClassA* Object1, ObjectClassB* Object2); };
Additionally, at the beginning of private methods UpdateObject and UpdateObject_NoTestForZeroPointer, we can validate pointers using assertions:
// Method does not check validity of pointer arguments: void MyClass::UpdateObject(ObjectClassA* Object1, ObjectClassB* Object2) { assert(Object1); assert(Object2); // ... }
However, this might not be the most practical approach if our assertion interrupts execution of the editor for other team members. What can greatly improve stability here is to create a “soft” assertion with a conditional defensive approach.
Soft assertions should allow the code to proceed (for example, for other team members) while still reporting the issue to the programmer:
void MyClass::UpdateObject_NoTestForZeroPointer(ObjectClass* Object) { #ifdef CONTRACT_SAFE_CHECK reportIfNull(Object); // Soft assert if (!Object) return; #endif // ... }
This way we inform the programmer about the abnormal situation (by sending the info to the database with errors) without interrupting execution. Using the #ifdef block we can control in which compilation configuration we want to include our defensive block. Development configurations should include the block while the profiling or final compilations can exclude it.
Preconditions expressed in such a way should be considered if continuing to run the editor or game does not create other issues.
Some classes tend to grow much faster than others. Some source files tend to be much bigger than others. If you check the current game you are working on, you will most likely find a couple files that are significantly bigger than the others. Such classes typically are responsible for characters, players or general game managers. This is not a surprise. When we add new features to the game, even in small steps, we add them to already existing classes. But such incremental growth is very problematic when nearing the critical point. It becomes very hard to maintain preconditions, postconditions and invariants. Maintaining big classes is difficult, and they make serious problems much more likely. Finding the issues described in the “Initialization, update and deinitialization patterns” section is easy. Most likely, they indicate that the class contains sub-elements that are not properly expressed as separate encapsulated functionalities. There are several possible ways to solve this problem. I would like to present one of them here.
Whenever I need toadd some new small element to an already existing class, I start by evaluating whether I can create a nested class within the parent class. Consider adding new member variables and methods to implement “Special Action 1” in the ObjectBehavior class:
class ObjectBehavior { public: // ... private: // ... // Begin Special Action 1. bool m_ShouldStartSpecialAction1; float m_TimeLeftToStartSpecialAction1; float m_SpecialAction1Parameter; void InitializeSpecialAction1(); void DeinitializeSpecialAction1(); void UpdateSpecialAction1(); // End Special Action 1. // ... };
Implementation using the nested class:
class ObjectBehavior { public: // ... private: // ... class SpecialAction1 { public: void Initialize(); void Deinitialize(); void Update(ObjectBehavior* ParentObject); private: bool m_ShouldStart; float m_TimeLeftToStart; float m_Parameter; }; SpecialAction1 m_SpecialAction1; // ... };
What is better here? We nicely encapsulate new variables in a separate class in a private section. Only the new SpecialAction1 nested class has access to them. This will also make it much easier to separate these classes further in the future if needed.
Video games use a lot of math operations on floating-point numbers. For example, we frequently perform division in our game code. This is a very basic math operation, yet still there is the edge case of division by zero. If we try to perform division by zero we end up with a floating-point exception or, if this exception is masked [19], the value for the resulting float variable will be “Infinity” (INF) or “Not a Number” (NaN).
Please also consider the following example, where both arguments are valid non-zero floating-point numbers, but the result of division is infinity (because it falls outside the representable range of a 32-bit float):
float f1 = 100000000000000000000.0f; // 1.0e20f float f2 = 0.0000000000000000001f; // 1.0e-19f float f3 = f1 / f2;
Variable f3 is equal to “Infinity.” Another example:
float f4 = 0.0f; float f5 = 0.0f; float f6 = f4 / f5;
Variable f6 is NaN – “Not a Number.”
We can continue execution of the program, considering “Infinity” as valid and performing further calculations using special arithmetic rules covering infinity [20, Chapter 1, page 18]. However, in most cases, we do not want to deal with such situations. Many libraries will break down on the assert if the input float variable is equal to any of these special values.
Unreal Engine 4 provides special functions to detect floats close to zero:
bool FMath::IsNearlyZero(float Value, float ErrorTolerance = SMALL_NUMBER);
In C# script in Unity EngineTM it can be written in this way:
bool IsNearlyZero(float Value) { return Mathf.Abs( Value ) <= 0.00000001f; }
We can use it before executing the division operation to check our denominator:
float denominator = b - a + c; if (!IsNearlyZero(denominator)) { result = d / denominator; }
To detect special values for the floating-point variable, we have:
float.IsInfinity(float) and float.IsNaN(float) in Unity Engine
FMath::IsFinite(float) in Unreal Engine 4
Also, we often forget about the edge case with respect to division in one common situation. Please consider the following update function:
void Object::UpdateLogic(float FrameTime) { Vector3 Velocity = (GetCurrentPosition() - m_PreviousPosition) / FrameTime; // ... }
It is easy to assume that FrameTime is always greater than zero. Of course, this code fails when it is not true and FrameTime is zero. This may happen, for example, in the game build when we want to freeze the time but continue to call the update logic. Also, in the editor, we normally do not update the logic for the world, but in some cases we may want to update the logic for the specific object to simulate gameplay behavior. This simple code would break down in such situations.
A similar edge case occurs with vector normalization. This is one of the most frequently used 3D math operations on vectors. Normalization is an operation used to compute a unit vector (the length equal to 1) by dividing the vector by its length. While this operation is very simple, we need to again remember the edge case with division: when the length of the input vector is zero or very close to zero.
In Unreal Engine 4 we have two methods:
FVector::GetUnsafeNormal() – it does not perform any check for vector length, but is faster.
FVector::GetSafeNormal() – it checks length if it is close to zero before performing division. If the length is smaller than a certain threshold, it returns the zero vector.
It is also interesting to note that a very direct naming convention explicitly informs the user of implementation.
Unity Engine offers only safe normalization in C# [21]:
Vector3.normalized
Vector3.Normalize()
So, it returns either a normalized vector or the zero vector.
I would also like to highlight various general solutions for improving the quality of game code:
Test your own code. You are the primary creator of your code. If you spend some time testing your code before submitting it, you can detect problems and fix them before many other members of the team are blocked by your unstable solution. This will take you a bit more time, but you will keep many others from wasting their time solving the same problem. So please always test your code before submitting it to the code repository.
Automatic tests. Preparing a set of test cases to validate every change will improve the quality of your code [22]. Automatic bots trying to behave like real players can also help detect bugs in the game [23, “Production” and “Testing” sections].
Visual debugging and text logging. It helps to add arrows, lines, spheres or similar elements to visualize key features in development configurations. Both ways to report problems, visually and using text, are complementary and very useful in finding problems in your system.
Use meaningful names. It is easy to use shortcuts as names for variables, methods or classes. Sometimes this is justified, like the highly popular “int i” to control a loop counter. However, who knows, for example, what this variable means:
bool UpdPrevOp;
When, actually, the author had in mind:
bool UpdatePreviousOperation;
I recommend using even longer names for variables or functions if they describe the role more specifically.
Code reviews. Ask your colleagues to review your code and participate in reviews of others. Code is written once but read a thousand times or more. If another programmer can read your code and understand it, it is more likely that other programmers will find it easy to read in the future. Also, code reviews not only detect problems with your code before it is submitted; they are also a good opportunity to learn and spread knowledge across your team. More generally, working as a pair with another programmer may help you find defects earlier.
Static code analysis tool. When humans can’t find errors during a code review, programs may find them when analyzing your source code [17][24].
Documentation. Once submitted, the code lives forever in your revision system. Take the time to properly describe what your change does and use fields in your revision system to write about it. Additionally, providing and maintaining external documentation will educate clients of your code on how to use it properly.
In this article, I presented common errors and typical sources of problems in game programming. As we saw at the beginning, providing safe and stable engineering solutions is not a new challenge. It is also not an easy task, especially in unique circumstances like game development. The main difficulty is knowing, predicting and deciding where defensive programming is necessary and how much of a defensive approach is enough.
It would be unwise to apply the same measures to video games as in other safety-critical industries, for example, space travel. However, it can be useful to know how people in such industries accomplished their goals.
The head of NASA’s Marshall Space Flight Center and the chief architect of the Saturn V rocket [25] favored engineering practices with bigger safety margins and solid “redundant structures” [26]. These practices were more time-consuming at the beginning, but ultimately led to the United States winning the grand prize with the Apollo 11 mission, which safely landed humans on the Moon and brought them back to Earth. The Saturn V rocket is the only launch vehicle to have carried humans beyond low Earth orbit.
It is fascinating to read Margaret H. Hamilton’s letter describing how software engineering helped achieve this moonshot [27, Section 2.2, “Lunar descent”]:
“Due to an error in the checklist manual, the rendezvous radar switch was placed in the wrong position. This caused it to send erroneous signals to the computer. The result was that the computer was being asked to perform all of its normal functions for landing while receiving an extra load of spurious data which used up 15% of its time. The computer (or rather the software in it) was smart enough to recognize that it was being asked to perform more tasks than it should be performing. It then sent out an alarm, which meant to the astronaut, I'm overloaded with more tasks than I should be doing at this time and I’m going to keep only the more important tasks; i.e., the ones needed for landing ... Actually, the computer was programmed to do more than recognize error conditions. A complete set of recovery programs was incorporated into the software. The software’s action, in this case, was to eliminate lower priority tasks and re-establish the more important ones ... If the computer hadn’t recognized this problem and taken recovery action, I doubt if Apollo 11 would have been the successful moon landing it was.”
Letter from Margaret H. Hamilton, Director of Apollo Flight Computer Programming, MIT Draper Laboratory, Cambridge, Massachusetts, titled “Computer Got Loaded,” published in Datamation, March 1, 1971.
Figure 2. Apollo 11 mission [27].
Special thanks to Maxime Bégin from Ubisoft Singapore and Marcin Undak from Ubisoft Toronto for helpful advice when I was writing this article.
[1] “Nedelin catastrophe,” https://en.wikipedia.org/wiki/Nedelin_catastrophe
[2] “Rockets: R-16 family: Nedelin disaster,” http://www.russianspaceweb.com/r16_disaster.html
[3] Boris Chertok, “Rockets and People, Volume 2: Creating a Rocket Industry,” 2006. Published by NASA. ISBN 0-16-076672-9. Online access: http://history.nasa.gov/SP-4110/vol2.pdf
[4] Joseph N. Yoon, “Nedelin Disaster,” http://www.aerospaceweb.org/question/spacecraft/q0179.shtml
[5] Steve McConnell, “Code Complete. A practical handbook of software construction. Second Edition,” Chapter 8: Defensive Programming, Microsoft Press, 2004.
[6] “Defensive programming,” https://en.wikipedia.org/wiki/Defensive_programming
[7] “Design by contract,” https://en.wikipedia.org/wiki/Design_by_contract
[8] John Lakos, “CppCon 2014: Defensive Programming Done Right. Part I and II,” https://www.youtube.com/watch?v=1QhtXRMp3Hg
https://www.youtube.com/watch?v=tz2khnjnUx8
[9] “assert Macro, _assert, _wassert,” MSDN, https://msdn.microsoft.com/en-us/library/9sb57dw4.aspx
[10] “Class Assert,” Unity Scripting Reference, https://docs.unity3d.com/ScriptReference/Assertions.Assert.html
[11] “Assertions,” Unreal Engine 4 Documentation, https://docs.unrealengine.com/latest/INT/Programming/Assertions
[12] Scott Meyers, “Effective C++. Third Edition. 55 Specific Ways To Improve Your Programs and Designs,” Addison-Wesley, 2012.
[13] ISO C++, https://isocpp.org/wiki/faq/freestore-mgmt#double-delete-disaster
[14] Unreal Engine 4, UActorComponent class, https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Runtime/Engine/Private/Components/ActorComponent.cpp
[15] John Romero, “id Software's Early Days,” keynotes at Digital Dragons 2016, East Coast Game Conference 2017.
[16] Jason Gregory, “Game Engine Architecture. Second Edition,” CRC Press, Taylor & Francis Group, 2014.
[17] John Carmack, “In-Depth: Static Code Analysis,” http://www.gamasutra.com/view/news/128836/InDepth_Static_Code_Analysis.php
[18] “Design goals of UnrealScript,” “Object and actor reference variables,” UnrealScript Language Reference, https://docs.unrealengine.com/udk/Two/UnrealScriptReference.html
[19] “_controlfp_s,” MSDN, https://msdn.microsoft.com/en-us/library/c9676k6h.aspx
[20] James M. Van Verth, Lars M. Bishop, “Essential Mathematics for Games and Interactive Applications. A Programmer’s Guide. Second Edition,” CRC Press, Taylor & Francis Group, 2008.
[21] “Vector3.normalized,” Unity Scripting Reference, https://docs.unity3d.com/ScriptReference/Vector3-normalized.html
[22] Fabian Röken, Dag Frommhold, “Automated Tests and Continuous Integration in Game Projects,” http://www.gamasutra.com/view/feature/130682/automated_tests_and_continuous_.php
[23] Peter Hall, “Crysis 2 Multiplayer: A Programmer’s Postmortem,” GDC Europe 2011, http://www.gdcvault.com/play/1014887/Crysis-2-Multiplayer-A-Programmer
[24] “Static program analysis,” https://en.wikipedia.org/wiki/Static_program_analysis
[25] “Saturn (rocket family),” https://en.wikipedia.org/wiki/Saturn_(rocket_family)
[26] Wernher von Braun, https://en.wikipedia.org/wiki/Wernher_von_Braun#Engineering_philosophy
[27] “Apollo 11,” https://en.wikipedia.org/wiki/Apollo_11#Lunar_descent
Read more about:
Featured BlogsYou May Also Like