how to design a game with c++
About this book
C++ is one of the preferred languages for game development as it supports a variety of coding styles that provides low-level access to the system. C++ is still used as a preferred game programming language by many as it gives game programmers control of the entire architecture, including memory patterns and usage. However, there is little information available on how to harness the advanced features of C++ to build robust games.
This book will teach you techniques to develop logic and game code using C++. The primary goal of this book is to teach you to create high-quality games using C++ game programming scripts and techniques, regardless of the library or game engine you use. It will show you how to make use of the object-oriented capabilities of C++ so you can write well-structured and powerful games of any genre. The book also explores important areas such as physics programming and audio programming, and gives you other useful tips and tricks to improve your code.
By the end of this book, you will be competent in game programming using C++, and will be able to develop your own games in C++.
- Publication date:
- May 2016
- Publisher
- Packt
- Pages
- 346
- ISBN
- 9781785882722
Chapter 1. Game Development Basics
In this chapter, the following recipes will be covered:
-
Installing an IDE on Windows
-
Choosing the right source control tool
-
Using call stacks for memory storage
-
Using recursions cautiously
-
Using pointers to store memory addresses
-
Casting between various datatypes
-
Managing memory more effectively using dynamic allocation
-
Using bitwise operations for advanced checks and optimization
Introduction
In this chapter, we will cover the basic concepts that you need to know to kick-start your career in game development.
The first step before a person starts coding is to install an integrated development environment ( IDE ). Nowadays, there are a few online IDEs that are available, but we are going to use an offline standalone IDE, Visual Studio . The next most important thing that many programmers do not start using at an early stage is revision control software .
Revision control software helps to back up the code in one central location; it has a historical overview of the changes that are made, which you can access and revert to if needed, and it also helps to resolve conflicts between files that have been worked on by different programmers at the same time.
The most useful feature of C++, in my opinion, is memory handling . It gives the developers a lot of control over how memory must be assigned depending on the current usage and needs of the program. As a result of this, we can allocate memory when there is a need and deallocate it accordingly.
If we do not de-allocate memory, we might run out of memory very soon, especially if we are using recursion. Sometimes there is a need to convert from one datatype to another to prevent loss of data, to pass the correct datatype in a function, and so on. C++ provides us a few ways by which we can do those castings.
The recipes in this chapter will primarily focus on these topics and deal with practical ways to implement them.
Installing an IDE on Windows
In this recipe, we will find out how easy it is to install Visual Studio on your Windows machine.
Getting ready
To go through this recipe, you will need a machine running Windows. No other prerequisites are required.
How to do it…
Visual Studio is a powerful IDE in which most professional software is written. It has loads of features and plugins to help us write better code:
-
Go to https://www.visualstudio.com.
-
Click on Download Visual Studio Community .
-
This should download an
.exefile. -
After it downloads, double-click on the setup file to start the installation.
-
Make sure you have all the updates necessary on your Windows machine.
-
You can also download any version of Visual Studio or Visual C++ Express.
-
If the application asks for starting environment settings, select C++ from the available options.
Note
A few things to note are listed here:
-
You need a Microsoft account to install it.
-
There are other free IDEs for C++, such as NetBeans , Eclipse , and Code::Blocks .
-
While Visual Studio works only for Windows, Code::Blocks and other such IDEs are cross-platform and can work on Mac and Linux as well.
For the remainder of this chapter, all code examples and snippets will be provided using Visual Studio.
How it works…
An IDE is a programming environment. An IDE consists of various functionalities that can vary from one IDE to another. However, the most basic functionalities that are present in all IDEs are a code editor, a compiler, a debugger, a linker, and a GUI builder.
A code editor, or a source code editor as they are otherwise known, is useful for editing code written by programmers. They provide features such as auto-correct, syntax highlighting, bracket completion and indentation, and so on. An example snapshot of the Visual Studio code editor is shown here:
A compiler is a computer program that converts your C++ code to object code. This is necessary in order to create an executable. If you have a file called main.cpp, it will generate an object code called main.o.
A linker is a computer program that converts the object code generated by the compiler to an executable or a library file:
A debugger is a computer program that helps to test and debug computer programs.
A GUI builder helps the designer and programmer to create GUI content or widgets easily. It uses a drag and drop WYSIWYG tool editor.
Choosing the right source control tool
In this recipe, we will see how easy it is to take a backup of our code using the right version control. The advantages of having a backup to a central server is that you will never lose work, can download the code on any machine, and can also go back to any of your changes from the past. Imagine it is like a checkpoint that we have in games, and you can go back to that checkpoint if you face problems.
Getting ready
To go through this recipe, you will need a machine running Windows. No other prerequisites are required.
How to do it…
Choosing a correct version tool is very important as it will save a lot of time organizing data. There are a few versioning tools that are available, so it is very important that we should be informed about all of them so that we can use the correct one based on our needs.
First analyze the choices that are available to you. The choices primarily include Concurrent Versions System ( CVS ), Apache Subversion ( SVN ), Mercurial , and GIT .
How it works…
CVS has been around for a long time, so there is tons of documentation and help available. However, a lack of atomic operations often leads to source corruption and it is not well cut out for long-term branching operations.
SVN was made as an improvement to CVS and it does fix many of its issues relating to atomic operations and source corruption. It is free and open source. It has lots of plugins for different IDEs. However, one of the major drawbacks of this tool is that it is comparatively very slow in its operations.
GIT was made primarily for Linux but it improves operation speed a lot. It works on UNIX systems as well. It has cheap branch operations but it is not totally optimized for a single developer and its Windows support is limited compared to Linux. However, GIT is extremely popular and many prefer GIT to SVN or CVS.
Mercurial came into existence shortly after GIT. It has node-based operations but does not allow the merging of two parent branches.
So to sum up, use SVN if you want a central repository that others can push and pull. Although it has its limitations, it's easy to learn. Use Mercurial or GIT if you want a distributed model. In this case, there is a repository on every computer, and generally, one of them is regarded as the official one. Mercurial is often preferred if it is a relatively small team, and it's easier to learn than GIT.
We will look into these in more detail in a separate chapter.
Using call stacks for memory storage
The main reason why C++ is still the preferred language for most game developers is that you handle memory yourself and control the allocation and de-allocation of memory to a great extent. For that reason, we need to understand the different memory spaces that are provided to us. When data is "pushed" onto the stack, the stack grows. As data is "popped" off the stack, the stack shrinks. It is not possible to pop a particular piece of data off the stack without first popping off all data placed on top of it. Think of this as a series of compartments aligned top to bottom. The top of the stack is whatever compartment the stack pointer happens to point to (this is a register).
Each compartment has a sequential address. One of those addresses is kept in the stack pointer. Everything below that magic address, known as the top of the stack, is considered to be on the stack. Everything above the top of the stack is considered to be off the stack. When data is pushed onto the stack, it is placed into a compartment above the stack pointer, and then the stack pointer is moved to the new data. When data is popped off the stack, the address of the stack pointer is changed by moving it down the stack.
Getting ready
You need to have a working copy of Visual Studio installed on your Windows machine.
How to do it...
C++ is probably one of the best programming languages out there and one of the main reasons for that is that it is also a low level language, because we can manipulate memory. To understand memory handling, it is very important to understand how memory stacks work:
-
Open Visual Studio.
-
Create a new C++ project.
-
Select Win32 Console Application .
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> using namespace std; int countTotalBullets(int iGun1Ammo, int iGun2Ammo) { return iGun1Ammo + iGun2Ammo; } int main() { int iGun1Ammo = 3; int iGun2Ammo = 2; int iTotalAmmo = CountTotalBullets(iGun1Ammo, iGun2Ammo); cout << "Total ammunition currently with you is"<<iTotalAmmo; _getch(); }
How it works…
When you call the function CountTotalBullets, the code branches to the called function. The parameters are passed in and the body of the function is executed. When the function completes, a value is returned and the control returns to the calling function.
But how does it really work from a compiler's point of view? When you begin your program, the compiler creates a stack. The stack is a special area of memory allocated for your program in order to hold the data for each function in your program. A stack is a Last In First Out ( LIFO ) data structure. Imagine a deck of cards; the last card put on the pile will be the first card taken out.
When your program calls CountTotalBullets, a stack frame is established. A stack frame is an area of the stack set aside to manage that function. This is very complex and different on different platforms, but these are the essential steps:
-
The return address of
CountTotalBulletsis put on the stack. When the function returns, it will resume executing at this address. -
Room is made on the stack for the return type you have declared.
-
All arguments to the function are placed on the stack.
-
The program branches to your function.
-
Local variables are pushed onto the stack as they are defined.
Using recursions cautiously
Recursions are a form of programming design in which the function calls itself multiple times to solve a problem by breaking down a large solutions set into multiple small solution sets. The code size definitely shortens. However, if not used properly, recursions can fill up the call stack really fast and you can run out of memory.
Getting ready
To get started with this recipe, you should have some prior knowledge of call stacks and how memory is assigned during a function call. You need a Windows machine with a working copy of Visual Studio.
How to do it…
In this recipe, you will see how easy it is to use recursions. Recursions are very smart to code but also can lead to some serious problems:
-
Open Visual Studio.
-
Create a new C++ project.
-
Select Win32 Console Application .
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> using namespace std; int RecursiveFactorial(int number); int Factorial(int number); int main() { long iNumber; cout << "Enter the number whose factorial you want to find"; cin >> iNumber; cout << RecursiveFactorial(iNumber) << endl; cout << Factorial(iNumber); _getch(); return 0; } int Factorial(int number) { int iCounter = 1; if (number < 2) { return 1; } else { while (number>0) { iCounter = iCounter*number; number -= 1; } } return iCounter; } int RecursiveFactorial(int number) { if (number < 2) { return 1; } else { while (number>0) { return number*Factorial(number - 1); } } }
How it works…
As you can see from the preceding code, both the functions find the factorial of a number. However, when using recursion, the stack size will grow immensely with each function call; the stack pointer has to be updated every call and data pushed onto the stack. With recursion, as the function calls itself, every time a function is called from within itself the stack size will keep on rising until it runs out of memory and creates a deadlock or crashes.
Imagine finding the factorial of 1000. The function will be called within itself a very large number of times. This is a recipe for certain disaster and we should avoid such coding practices to a great extent.
There's more…
You can use a larger datatype than int if you are finding the factorial of a number greater than 15, as the resulting factorial will be too large to be stored in int.
Using pointers to store memory addresses
In the previous two recipes, we have seen how not having sufficient memory can be a problem to us. However, until now, we have had no control over how much memory is assigned and what is assigned to each memory address. Using pointers, we can address this issue. In my opinion, pointers are the single most important topic in C++. If your concept of C++ has to be clear, and if you are to become a good developer in C++, you must be good with pointers. Pointers can seem very daunting at first, but once you get the hang of it, pointers are pretty easy to use.
Getting ready
For this recipe, you will need a Windows machine with a working copy of Visual Studio.
How to do it…
In this recipe, we will see how easy it is to work with pointers. Once you are comfortable using pointers, we can manipulate memory and store references in memory quite easily:
-
Open Visual Studio.
-
Create a new C++ project.
-
Select Win32 Console Application .
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> using namespace std; int main() { float fCurrentHealth = 10.0f; cout << "Address where the float value is stored: " << &fCurrentHealth << endl; cout << "Value at that address: " << *(&fCurrentHealth) << endl; float* pfLocalCurrentHealth = &fCurrentHealth; cout << "Value at Local pointer variable: "<<pfLocalCurrentHealth << endl; cout << "Address of the Local pointer variable: "<<&pfLocalCurrentHealth << endl; cout << "Value at the address of the Local pointer variable: "<<*pfLocalCurrentHealth << endl; _getch(); return 0; }
How it works…
One of the most powerful tools of a C++ programmer is to manipulate computer memory directly. A pointer is a variable that holds a memory address. Each variable and object used in a C++ program is stored in a specific place in memory. Each memory location has a unique address. Memory addresses will vary depending on the operating system used. The amount of bytes taken up depends on the variable type: float = 4 bytes , short = 2 bytes :
Each location in the memory is 1 byte. The pointer pfLocalCurrentHealth holds the address of the memory location that has stored fCurrentHealth. Hence, when we display the contents of the pointer, we get the same address as that of the address containing the fCurrentHealth variable. We use the & operator to get the address of the pfLocalCurrentHealth variable. When we reference the pointer using the * operator, we get the value stored at the address. Since the stored address is same as the address storing fCurrentHealth, we get the value 10.
There's more…
Let us consider the following declarations:
-
const float* pfNumber1 -
float* const pfNumber2 -
const float* const pfNumber3
All of these declarations are valid. But what do they mean? The first declaration states that pfNumber1 is a pointer to a constant float. The second declaration states that pfNumber2 is a constant pointer to a float. The third declaration states that pfNumber3 is a constant pointer to a constant integer. The key differences between references and these three types of const pointers are listed here:
-
constpointers can be NULL -
A reference does not have its own address, whereas a pointer does
The address of a reference is the actual object's address
-
A pointer has its own address and it holds as its value the address of the value it points to
Casting between different datatypes
Casting is a conversion process of changing some data into a different type of data. We can convert between built-in types or our own datatypes. Some of the conversions are done automatically by the compiler, and the programmer does not have to intervene. Such conversions are called implicit conversions . Other conversions, which have to be directly specified by the programmer, are called explicit conversion. Sometimes we may get warnings about loss of data . We should pay heed to these warnings and think about how this might adversely affect our code. Casting is commonly used when the interface expects a particular type, but we want to feed it data of a different type. With C, we can cast anything to everything. However, C++ provides us with finer controls.
Getting ready
For this recipe, you will need a Windows machine with a working copy of Visual Studio.
How to do it…
In this recipe, we will see how we can easily cast or convert between various datatypes. Usually, a programmer uses C-style casting even in C++, but this is not recommended. C++ provides us with its own style of casting for different situations which we should use:
-
Open Visual Studio.
-
Create a new C++ project.
-
Select Win32 Console Application .
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> using namespace std; int main() { int iNumber = 5; int iOurNumber; float fNumber; //No casting. C++ implicitly converts the result into an int and saves //into a float fNumber = iNumber/2; cout << "Number is " << fNumber<<endl; //C-style casting. Not recommended as this is not type safe fNumber = (float)iNumber / 2; cout << "Number is " << fNumber<<endl; //C++ style casting. This has valid constructors to make the casting a safe one iOurNumber = static_cast<int>(fNumber); cout << "Number is " << iOurNumber << endl; _getch(); return 0; }
How it works…
There are four types of casting operators in C++, depending on what we are casting: static_cast, const_cast, reinterpret_cast, and dynamic_cast. Now, we are going to look at static_cast. We will look at the remaining three casting technique after we discuss dynamic memory and classes. Converting from a smaller datatype to a larger type is called promotion and is guaranteed to have no data loss. However, conversion from a larger datatype to a smaller one is called demotion and may lead to data loss. Compilers will generally give you a warning when this happens, and you should pay heed to this.
Let us look at the previous example. We have initialized an integer with the value 5. Next, we have initialized a floating point variable and stored the result of 5 divided by 2, which is 2.5. However, when we display the variable fNumber, we see that the displayed value is 2. The reason is the C++ compiler implicitly casts the result of 5/2 and stores it as an integer. So it is evaluating something similar to int (5/2) which is int (2.5), evaluating to 2. So to achieve our desired result, we have two options. The first method is a C-style explicit cast, which is not recommended at all because it does not have a type safe check. The format for the C-style cast is (resultant_data_type) (expression), which in this case is something like float (5/2). We are explicitly telling the compiler to store the result of the expression as a floating point number. The second method, and a more C++ style way of doing the cast, is by using the static_cast operation. This has suitable constructors to dictate that the conversion is type safe. The format for a static_cast operation is static_cast<resultant_data_type> (expression). The compiler checks if the casting conversion is safe and then executes the type casting operation.
Managing memory more effectively using dynamic allocation
Programmers generally deal with five areas of memory: global namespace , registers , code space , stack , and the free store . When an array is initialized, the number of elements has to be defined. This leads to lots of memory problems. Most of the time, not all elements that we allocated are used, and sometimes we need more elements. To help overcome this problem, C++ facilitates memory allocation while an .exe file is running by using the free store.
The free store is a large area of memory that can be used to store data, and is sometimes referred to as the heap . We can request some space on the free store, and it will give us an address that we can use to store data. We need to keep that address in a pointer. The free store is not cleaned up until your program ends. It is the programmer's responsibility to free any free store memory used by their program.
The advantage of the free store is that there is no need to preallocate all variables. We can decide at runtime when more memory is needed. The memory is reserved and remains available until it is explicitly freed. If memory is reserved while in a function, it is still available when control returns from that function. This is a much better way of coding than global variables. Only functions that have access to the pointer can access the data stored in memory, and it provides a tightly controlled interface to that data.
Getting ready
For this recipe, you will need a Windows machine with a working copy of Visual Studio.
How to do it…
In this recipe, we will see how easy it is to use dynamic allocation. In games, most of the memory is allocated dynamically at runtime as we are never sure how much memory we should assign. Assigning an arbitrary amount of memory may result in less memory or memory wastage:
-
Open Visual Studio.
-
Create a new C++ project.
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> #include <string> using namespace std; int main() { int iNumberofGuns, iCounter; string * sNameOfGuns; cout << "How many guns would you like to purchase? "; cin >> iNumberofGuns; sNameOfGuns = new string[iNumberofGuns]; if (sNameOfGuns == nullptr) cout << "Error: memory could not be allocated"; else { for (iCounter = 0; iCounter<iNumberofGuns; iCounter++) { cout << "Enter name of the gun: "; cin >> sNameOfGuns[iCounter]; } cout << "You have purchased: "; for (iCounter = 0; iCounter<iNumberofGuns; iCounter++) cout << sNameOfGuns[iCounter] << ", "; delete[] sNameOfGuns; } _getch(); return 0; }
How it works…
You can allocate memory to the free store using the new keyword; new is followed by the type of the variable you want to allocate. This allows the compiler to know how much memory will need to be allocated. In our example, we have used string. The new keyword returns a memory address. This memory address is assigned to a pointer, sNameOfGuns. We must assign the address to a pointer, otherwise the address will be lost. The format for using the new operator is datatype * pointer = new datatype. So in our example, we have used sNameOfGuns = new string[iNumberofGuns]. If the new allocation fails, it will return a null pointer. We should always check whether the pointer allocation has been successful; otherwise we will try to access a part of the memory that has not been allocated and we may get an error from the compiler, as shown in the following screenshot, and your application will crash:
When you are finished with the memory, you must call delete on the pointer. Delete returns the memory to the free store. Remember that the pointer is a local variable. Where the function that the pointer is declared in goes out of scope, the memory on the free store is not automatically deallocated. The main difference between static and dynamic memory is that the creation/deletion of static memory is handled automatically, whereas dynamic memory must be created and destroyed by the programmer.
The delete[] operator signals to the compiler that it needs to free an array. If you leave the brackets off, only the first element in the array will be deleted. This will create a memory leak. Memory leaks are really bad as it means there are memory spaces that have not been deallocated. Remember, memory is a finite space, so eventually you are going to run into trouble.
When we use delete[], how does the compiler know that it has to free n number of strings from the memory? The runtime system stores the number of items somewhere it can be retrieved only if you know the pointer sNameOfGuns. There are two popular techniques that do this. Both of these are used by commercial compilers, both have tradeoffs, and neither are perfect:
-
Technique 1:
Over-allocate the array and put the number of items just to the left of the first element. This is the faster of the two techniques, but is more sensitive to the problem of the programmer saying
delete sNameOfGuns, instead ofdelete[] sNameOfGuns. -
Technique 2:
Use an associative array with the pointer as a key and the number of items as the value. This is the slower of the two techniques, but is less sensitive to the problem of the programmer saying
delete sNameOfGuns, instead ofdelete[] sNameOfGuns.
There's more…
We can also use a tool called VLD to check for memory leaks.
After the setup has downloaded, install VLD on your system. This may or may not set up the VC++ directories correctly. If it doesn't, do it manually by right-clicking on the project page and adding the directory of VLD to the field called Include Directories , as shown in the following figure:
After setting up the directories, add the header file <vld.h> in your source file. After you execute your application and exit it, your output window will now show whether there are any memory leaks in your application.
Understanding the error messages
When using the debug build, you may notice the following values in memory during debugging:
-
0xCCCCCCCC: This refers to values being allocated on the stack, but not yet initialized. -
0xCDCDCDCD: This means memory has been allocated in the heap, but it is not yet initialized (clean memory). -
0xDDDDDDDD: This means memory has been released from the heap (dead memory). -
0xFEEEFEEE: This refers to values being deallocated from the free store. -
0xFDFDFDFD: "No man's land" fences, which are placed at the boundary of heap memory in debug mode. These should never be overwritten, and if they are, it probably means the program is trying to access memory at an index outside of an array's max size.
Using bitwise operations for advanced checks and optimization
In most cases, a programmer will not need to worry too much about bits unless there is a need to write some compression algorithms, and when we are making a game, we never know when a situation such as that arises. In order to encode and decode files compressed in this manner, you need to actually extract data at the bit level. Finally, you can use bit operations to speed up your program or perform neat tricks. However, this is not always recommended.
Getting ready
For this recipe, you will need a Windows machine with a working copy of Visual Studio.
How to do it…
In this recipe, we will see how easy it is to use bitwise operations to perform operations by manipulating memory. Bitwise operations are also a great way to optimize code by directly interacting with memory:
-
Open Visual Studio.
-
Create a new C++ project.
-
Add a source file called
main.cppor anything that you want to name the source file. -
Add the following lines of code:
#include <iostream> #include <conio.h> using namespace std; void Multi_By_Power_2(int iNumber, int iPower); void BitwiseAnd(int iNumber, int iNumber2); void BitwiseOr(int iNumber, int iNumber2); void Complement(int iNumber4); void BitwiseXOR(int iNumber,int iNumber2); int main() { int iNumber = 4, iNumber2 = 3; int iPower = 2; unsigned int iNumber4 = 8; Multi_By_Power_2(iNumber, iPower); BitwiseAnd(iNumber,iNumber2); BitwiseOr(iNumber, iNumber2); BitwiseXOR(iNumber,iNumber2); Complement(iNumber4); _getch(); return 0; } void Multi_By_Power_2(int iNumber, int iPower) { cout << "Result is :" << (iNumber << iPower)<<endl; } void BitwiseAnd(int iNumber, int iNumber2) { cout << "Result is :" << (iNumber & iNumber2) << endl; } void BitwiseOr(int iNumber, int iNumber2) { cout << "Result is :" << (iNumber | iNumber2) << endl; } void Complement(int iNumber4) { cout << "Result is :" << ~iNumber4 << endl; } void BitwiseXOR(int iNumber,int iNumber2) { cout << "Result is :" << (iNumber^iNumber2) << endl; }
How it works…
The left shift operator is the equivalent of moving all the bits of a number a specified number of places to the left. In our example, the numbers we are sending to the function Multi_By_Power_2 is 4 and 3. The binary representation of 4 is 100, so if we shift the most significant bit, which is 1, three places to the left, we get 10000, which is the binary of 16. Hence, left shift is equivalent to integer division by 2^shift_arg, that is, 4*2^3, which is again 16. Similarly, the right shift operation is equivalent to integer division by 2^shift_arg.
Now let us consider we want to pack data so that the data is compressed. Consider the following example:
int totalammo,type,rounds;
We are storing the total bullets in a gun; the type of gun, but it can only be a rifle or pistol; and the total bullets per round it can fire. Currently we are using three integer values to store the data. However, we can compress all the preceding data into one single integer and hence compress the data:
int packaged_data; packaged_data = (totalammo << 8) | (type << 7) | rounds;
If we assume the following notations:
-
TotalAmmon:
A -
Type:
T -
Rounds:
R
The final representation in the data would be something like this:
Just skimmed it so far, but it is very informative.
Solid book, good examples, helpful
great examples and explanations but there were some areas where I was lost and the codes provided did not work.
how to design a game with c++
Source: https://www.packtpub.com/product/c-game-development-cookbook/9781785882722
Posted by: larkinswerat1964.blogspot.com

0 Response to "how to design a game with c++"
Post a Comment