Chapter 10: Pointers, the Standard Template Library, and Texture Management

We will learn a lot as well as get plenty done in terms of the game in this chapter. We will first learn about the fundamental C++ topic of pointers. Pointers are variables that hold a memory address. Typically, a pointer will hold the memory address of another variable. This sounds a bit like a reference, but we will see how they are much more powerful and use a pointer to handle an ever-expanding horde of zombies.

We will also learn about the Standard Template Library (STL), which is a collection of classes that allow us to quickly and easily implement common data management techniques.

Once we understand the basics of the STL, we will be able to use that new knowledge to manage all the textures from the game because, if we have 1,000 zombies, we don't really want to load a copy of a zombie graphic into the GPU for each and every one.

We will also dig a little deeper into OOP and use a static function, which is a function of a class that can be called without an instance of the class. At the same time, we will see how we can design a class to ensure that only one instance can ever exist. This is ideal when we need to guarantee that different parts of our code will use the same data.

In this chapter, we will cover the following topics:

  • Learning about pointers
  • Learning about the STL
  • Implementing the TextureHolder class using static functions and a singleton class
  • Implementing a pointer to a horde of zombies
  • Editing some existing code to use the TextureHolder class for the player and background

Learning about Pointers

Pointers can be the cause of frustration while learning to code C++. However, the concept is simple.

Important note

A pointer is a variable that holds a memory address.

That's it! There's nothing to be concerned about. What probably causes the frustration to beginners is the syntax—the code we use to handle pointers. We will step through each part of the code for using pointers. You can then begin the ongoing process of mastering them.

Tip

In this section, we will actually learn more about pointers than we need to for this project. In the next project, we will make greater use of pointers. Despite this, we will only scratch the surface of this topic. Further study is definitely recommended, and we will talk more about that in the final chapter.

Rarely do I suggest that memorizing facts, figures, or syntax is the best way to learn. However, memorizing the brief but crucial syntax related to pointers might be worthwhile. This will ensure that the information sinks so deep into our brains that we can never forget it. We can then talk about why we would need pointers at all and examine their relationship to references. A pointer analogy might help:

Tip

If a variable is a house and its contents are the value it holds, then a pointer is the address of the house.

In the previous chapter, while discussing references, we learned that when we pass values to, or return values from, a function, we are actually making a completely new house, but it's exactly the same as the previous one. We are making a copy of the value that's passed to or from a function.

At this point, pointers are probably starting to sound a bit like references. That's because they are a bit like references. Pointers, however, are much more flexible, powerful, and have their own special and unique uses. These special and unique uses require a special and unique syntax.

Pointer syntax

There are two main operators associated with pointers. The first is the address of operator:

&

The second is the dereference operator:

*

We will now look at the different ways in which we can use these operators with pointers.

The first thing you will notice is that the address of the operator is the same as the reference operator. To add to the woes of an aspiring C++ game programmer, the operators do different things in different contexts. Knowing this from the outset is valuable. If you are staring at some code involving pointers and it seems like you are going mad, know this:

Tip

You are perfectly sane! You just need to look at the detail of the context.

Now, you know that if something isn't clear and immediately obvious, it is not your fault. Pointers are not clear and immediately obvious but looking carefully at the context will reveal what is going on.

Armed with the knowledge that you need to pay more attention to pointers than to previous syntax, as well as what the two operators are (address of and dereference), we can now start to look at some real pointer code.

Tip

Make sure you have memorized the two operators before proceeding.

Declaring a pointer

To declare a new pointer, we use the dereference operator, along with the type of variable the pointer will be holding the address of. Take a look at the following code before we talk about pointers some more:

// Declare a pointer to hold

// the address of a variable of type int

int* pHealth;

The preceding code declares a new pointer called pHealth that can hold the address of a variable of the int type. Notice I said can hold a variable of the int type. Like other variables, a pointer also needs to be initialized with a value to make proper use of it.

The name pHealth, just like other variables, is arbitrary.

Tip

It is common practice to prefix the names of variables that are pointers with a p. It is then much easier to remember when we are dealing with a pointer and can then distinguish them from regular variables.

The white space that's used around the dereference operator is optional because C++ rarely cares about spaces in syntax. However, it's recommended because it aids readability. Look at the following three lines of code that all do the same thing.

We have just seen the following format in the previous example, with the dereference operator next to the type:

int* pHealth;

The following code shows white space either side of the dereference operator:

int * pHealth;

The following code shows the dereference operator next to the name of the pointer:

int *pHealth;

It is worth being aware of these possibilities so that when you read code, perhaps on the web, you will understand they are all the same. In this book, we will always use the first option with the dereference operator next to the type.

Just like a regular variable can only successfully contain data of the appropriate type, a pointer should only hold the address of a variable of the appropriate type.

A pointer to the int type should not hold the address of a String, Zombie, Player, Sprite, float, or any other type, except int.

Let's see how we can initialize our pointers.

Initializing a pointer

Next, we will see how we can get the address of a variable into a pointer. Take a look at the following code:

// A regular int variable called health

int health = 5;

// Declare a pointer to hold the address of a variable of type int

int* pHealth;

// Initialize pHealth to hold the address of health,

// using the "address of" operator

pHealth = &health;

In the previous code, we declare an int variable called health and initialize it to 5. It makes sense, although we have never discussed it before, that this variable must be somewhere in our computer's memory. It must have a memory address.

We can access this address using the address of operator. Look closely at the last line of the previous code. We initialize pHealth with the address of health, like this:

  pHealth = &health;

Our pHealth pointer now holds the address of the regular int, health.

Tip

In C++ terminology, we say that pHealth points to health.

We can use pHealth by passing it to a function so that the function can work on health, just like we did with references.

There would be no reason for pointers if that was all we were going to do with them, so let's take a look at reinitializing them.

Reinitializing pointers

A pointer, unlike a reference, can be reinitialized to point to a different address. Look at this following code:

// A regular int variable called health

int health = 5;

int score = 0;

// Declare a pointer to hold the address of a variable of type int

int* pHealth;

// Initialize pHealth to hold the address of health

pHealth = &health;

// Re-initialize pHealth to hold the address of score

pHealth = &score;

Now, pHealth points to the int variable, score.

Of course, the name of our pointer, pHealth, is now ambiguous and should perhaps have been called pIntPointer. The key thing to understand here is that we can do this reassignment.

At this stage, we haven't actually used a pointer for anything other than simply pointing (holding a memory address). Let's see how we can access the value stored at the address that's pointed to by a pointer. This will make them genuinely useful.

Dereferencing a pointer

We know that a pointer holds an address in memory. If we were to output this address in our game, perhaps in our HUD, after it has been declared and initialized, it might look something like this: 9876.

It is just a value – a value that represents an address in memory. On different operating systems and hardware types, the range of these values will vary. In the context of this book, we never need to manipulate an address directly. We only care about what the value stored at the address that is pointed to is.

The actual addresses used by variables are determined when the game is executed (at runtime) and so there is no way of knowing the address of a variable and hence the value stored in a pointer while we are coding the game.

We can access the value stored at the address that's pointed to by a pointer by using the dereference operator:

*

The following code manipulates some variables directly and by using a pointer. Try and follow along and then we will go through it:

Tip

Warning! The code that follows is pointless (pun intended). It just demonstrates using pointers.

// Some regular int variables

int score = 0;

int hiScore = 10;

// Declare 2 pointers to hold the addresses of int

int* pIntPointer1;

int* pIntPointer2;

// Initialize pIntPointer1 to hold the address of score

pIntPointer1 = &score;

// Initialize pIntPointer2 to hold the address of hiScore

pIntPointer2 = &hiScore;

// Add 10 to score directly

score += 10;

// Score now equals 10

// Add 10 to score using pIntPointer1

*pIntPointer1 += 10;

// score now equals 20. A new high score

// Assign the new hi score to hiScore using only pointers

*pIntPointer2 = *pIntPointer1;

// hiScore and score both equal 20

In the previous code, we declare two int variables, score and hiScore. We then initialize them with the values 0 and 10, respectively. Next, we declare two pointers to int. These are pIntPointer1 and pIntPointer2. We initialize them in the same step as declaring them to hold the addresses of (point to) the score and hiScore variables, respectively.

Following on, we add 10 to score in the usual way, score += 10. Then, we can see that by using the dereference operator on a pointer, we can access the value stored at the address they point to. The following code changed the value stored by the variable that's pointed to by pIntPointer1:

// Add 10 to score using pIntPointer1

*pIntPointer1 += 10;

// score now equals 20, A new high score

The last part of the preceding code dereferences both pointers to assign the value that's pointed to by pIntPointer1 as the value that's pointed to by pIntPointer2:

// Assign the new hi-score to hiScore with only pointers

*pIntPointer2 = *pIntPointer1;

// hiScore and score both equal 20

Both score and hiScore are now equal to 20.

Pointers are versatile and powerful

We can do so much more with pointers. Here are just a few useful things we can do.

Dynamically allocated memory

All the pointers we have seen so far point to memory addresses that have a scope limited only to the function they are created in. So, if we declare and initialize a pointer to a local variable, when the function returns, the pointer, the local variable, and the memory address will be gone. They are out of scope.

Up until now, we have been using a fixed amount of memory that is decided in advance of the game being executed. Furthermore, the memory we have been using is controlled by the operating system, and variables are lost and created as we call and return from functions. What we need is a way to use memory that is always in scope until we are finished with it. We want to have access to memory we can call our own and take responsibility for.

When we declare variables (including pointers), they are in an area of memory known as the stack. There is another area of memory which, although allocated and controlled by the operating system, can be allocated at runtime. This other area of memory is called the free store, or sometimes, the heap.

Tip

Memory on the heap does not have scope to a specific function. Returning from a function does not delete the memory on the heap.

This gives us great power. With access to memory that is only limited by the resources of the computer our game is running on, we can plan games with huge amounts of objects. In our case, we want a vast horde of zombies. As Spiderman's uncle wouldn't hesitate to remind us, however, "with great power comes great responsibility."

Let's look at how we can use pointers to take advantage of the memory on the free store and how we can release that memory back to the operating system when we are finished with it.

To create a pointer that points to a value on the heap, we need a pointer:

int* pToInt = nullptr;

In the previous line of code, we declare a pointer in the same way we have seen before, but since we are not initializing it to point to a variable, we initialize it to nullptr. We do this because it is good practice. Consider dereferencing a pointer (changing a value at the address it points to) when you don't even know what it is pointing to. It would be the programming equivalent of going to the shooting range, blindfolding someone, spinning them around, and telling them to shoot. By pointing a pointer to nothing (nullptr), we can't do any harm with it.

When we are ready to request memory on the free store, we use the new keyword, as shown in the following line of code:

pToInt = new int;

pToInt now holds the memory address of space on the free store that is just the right size to hold an int value.

Tip

Any allocated memory is returned when the program ends. It is, however, important to realize that this memory will never be freed (within the execution of our game) unless we free it. If we continue to take memory from the free store without giving it back, eventually it will run out and the game will crash.

It is unlikely that we would ever run out of memory by occasionally taking int sized chunks of the free store. But if our program has a function or loop that requests memory and this function or loop is executed regularly throughout the game, eventually the game will slow and then crash. Furthermore, if we allocate lots of objects on the free store and don't manage them correctly, then this situation can happen quite quickly.

The following line of code hands back (deletes) the memory on the free store that was previously pointed to by pToInt:

delete pToInt;

Now, the memory that was previously pointed to by pToInt is no longer ours to do what we like with; we must take precautions. Although the memory has been handed back to the operating system, pToInt still holds the address of this memory, which no longer belongs to us.

The following line of code ensures that pToInt can't be used to attempt to manipulate or access this memory:

pToInt = nullptr;

Tip

If a pointer points to an address that is invalid, it is called a wild or dangling pointer. If you attempt to dereference a dangling pointer and if you are lucky, the game will crash, and you will get a memory access violation error. If you are unlucky, you will create a bug that will be incredibly difficult to find. Furthermore, if we use memory on the free store that will persist beyond the life of a function, we must make sure to keep a pointer to it or we will have leaked memory.

Now, we can declare pointers and point them to newly allocated memory on the free store. We can manipulate and access the memory they point to by dereferencing them. We can also return memory to the free store when we are done with it, and we know how to avoid having a dangling pointer.

Let's look at some more advantages of pointers.

Passing a pointer to a function

In order to pass a pointer to a function, we need to write a function that has a pointer in the prototype, like in the following code:

void myFunction(int *pInt)

{

   // Dereference and increment the value stored

   // at the address pointed to by the pointer

   *pInt ++

   return;

}

The preceding function simply dereferences the pointer and adds 1 to the value stored at the pointed to address.

Now, we can use that function and pass the address of a variable or another pointer to a variable explicitly:

int someInt = 10;

int* pToInt = &someInt;

myFunction(&someInt);

// someInt now equals 11

myFunction(pToInt);

// someInt now equals 12

As shown in the previous code, within the function, we are manipulating the variable from the calling code and can do so using the address of a variable or a pointer to that variable, since both actions amount to the same thing.

Pointers can also point to instances of a class.

Declaring and using a pointer to an object

Pointers are not just for regular variables. We can also declare pointers to user-defined types such as our classes. This is how we would declare a pointer to an object of the Player type:

Player player;

Player* pPlayer = &Player;

We can even access the member functions of a Player object directly from the pointer, as shown in the following code:

// Call a member function of the player class

pPlayer->moveLeft()

Notice the subtle but vital difference: accessing a function with a pointer to an object rather than an object directly uses the -> operator. We won't need to use pointers to objects in this project, but we will explore them more carefully before we do, which will be in the next project.

Let's go over one more new pointer topic before we talk about something completely new.

Pointers and arrays

Arrays and pointers have something in common. An array's name is a memory address. More specifically, the name of an array is the memory address of the first element in that array. To put this yet another away, an array name points to the first element of an array. The best way to understand this is to read on and look at the following example.

We can create a pointer to the type that an array holds and then use the pointer in the same way using exactly the same syntax that we would use for the array:

// Declare an array of ints

int arrayOfInts[100];

// Declare a pointer to int and initialize it

// with the address of the first

// element of the array, arrayOfInts

int* pToIntArray = arrayOfInts;

// Use pToIntArray just as you would arrayOfInts

arrayOfInts[0] = 999;

// First element of arrayOfInts now equals 999

pToIntArray[0] = 0;

// First element of arrayOfInts now equals 0

This also means that a function that has a prototype that accepts a pointer also accepts arrays of the type the pointer is pointing to. We will use this fact when we build our ever-increasing horde of zombies.

Tip

Regarding the relationship between pointers and references, the compiler actually uses pointers when implementing our references. This means that references are just a handy tool (that uses pointers "under the hood"). You could think of a reference as an automatic gearbox that is fine and convenient for driving around town, whereas pointers are a manual gearbox – more complicated, but with the correct use, they can provide better results/performance/flexibility.

Summary of pointers

Pointers are a bit fiddly at times. In fact, our discussion of pointers was only an introduction to the subject. The only way to get comfortable with them is to use them as much as possible. All you need to understand about pointers in order to complete this project is the following:

  • Pointers are variables that store a memory address.
  • We can pass pointers to functions to directly manipulate values from the calling function's scope, within the called function.
  • Array names hold the memory address of the first element. We can pass this address as a pointer because that is exactly what it is.
  • We can use pointers to point to memory on the free store. This means we can dynamically allocate large amounts of memory while the game is running.

    Tip

    There are yet more ways to use pointers. We will learn about smart pointers in the final project, once we have got used to using regular pointers.

There is just one more topic to cover before we can start coding the Zombie Arena project again.

The Standard Template Library

The Standard Template Library (STL) is a collection of data containers and ways to manipulate the data we put in those containers. If we want to be more specific, it is a way to store and manipulate different types of C++ variables and classes.

We can think of the different containers as customized and more advanced arrays. The STL is part of C++. It is not an optional thing that needs to be set up like SFML.

The STL is part of C++ because its containers and the code that manipulates them are fundamental to many types of code that many apps will need to use.

In short, the STL implements code that we and just about every C++ programmer is almost bound to need, at least at some point, and probably quite regularly.

If we were to write our own code to contain and manage our data, then it is unlikely we would write it as efficiently as the people who wrote the STL.

So, by using the STL, we guarantee that we are using the best written code possible to manage our data. Even SFML uses the STL. For example, under the hood, the VertexArray class uses the STL.

All we need to do is choose the right type of container from those that are available. The types of container that are available through the STL include the following:

  • Vector: This is like an array with boosters. It handles dynamic resizing, sorting, and searching. This is probably the most useful container.
  • List: A container that allows for the ordering of the data.
  • Map: An associative container that allows the user to store data as key/value pairs. This is where one piece of data is the "key" to finding the other piece. A map can also grow and shrink, as well as be searched.
  • Set: A container that guarantees that every element is unique.

    Important note

    For a full list of STL container types, their different uses, and explanations, take a look at the following link: http://www.tutorialspoint.com/cplusplus/cpp_stl_tutorial.htm.

In the Zombie Arena game, we will use a map.

Tip

If you want a glimpse into the kind of complexity that the STL is sparing us, then take a look at this tutorial, which implements the kind of thing that a list would do. Note that the tutorial implements only the very simplest bare-bones implementation of a list: http://www.sanfoundry.com/cpp-program-implement-single-linked-list/.

We can easily see that we will save a lot of time and end up with a better game if we explore the STL. Let's take a closer look at how to use a Map instance, and then we will see how it will be useful to us in the Zombie Arena game.

What is a map?

A map is a container that is dynamically resizable. We can add and remove elements with ease. What makes the map class special compared to the other containers in the STL is the way that we access the data within it.

The data in a map instance is stored in pairs. Consider a situation where you log in to an account, perhaps with a username and password. A map would be perfect for looking up the username and then checking the value of the associated password.

A map would also be just right for things such as account names and numbers, or perhaps company names and share prices.

Note that when we use map from the STL, we decide the type of values that form the key-value pairs. The values could be string instances and int instances, such as account numbers; string instances and other string instances such as usernames and passwords; or user-defined types such as objects.

What follows is some real code to make us familiar with map.

Declaring a map

This is how we could declare a map:

map<string, int> accounts;

The previous line of code declares a new map called accounts that has a key of string objects, each of which will refer to a value that is an int.

We can now store key-value pairs of the string type that refer to values of the int type. We will see how we can do this next.

Adding data to a Map

Let's go ahead and add a key-value pair to accounts:

accounts["John"] = 1234567;

Now, there is an entry in the map that can be accessed using the key of John. The following code adds two more entries to the accounts map:

accounts["Smit"] = 7654321;

accounts["Larissa"] = 8866772;

Our map has three entries in it. Let's see how we can access the account numbers.

Finding data in a map

We would access the data in the same way that we added it: by using the key. As an example, we could assign the value stored by the Smit key to a new int, accountNumber, like this:

int accountNumber = accounts["Smit"];

The int variable, accountNumber, now stores the value 7654321. We can do anything to a value stored in a map instance that we can do to that type.

Removing data from a map

Taking values out of our map is also straightforward. The following line of code removes the key, John, and its associated value:

accounts.erase("John");

Let's look at a few more things we can do with a map.

Checking the size of a map

We might like to know how many key-value pairs we have in our map. The following line of code does just that:

int size = accounts.size();

The int variable, size, now holds the value of 2. This is because accounts holds values for Smit and Larissa, because we deleted John.

Checking for keys in a map

The most relevant feature of map is its ability to find a value using a key. We can test for the presence or otherwise of a specific key like this:

if(accounts.find("John") != accounts.end())

{

    // This code won't run because John was erased

}

if(accounts.find("Smit") != accounts.end())

{

    // This code will run because Smit is in the map

}

In the previous code, the != accounts.end value is used to determine when a key does or doesn't exist. If the searched for key is not present in the map, then accounts.end will be the result of the if statement.

Let's see how we can test or use all the values in a map by looping through a map.

Looping/iterating through the key-value pairs of a map

We have seen how we can use a for loop to loop/iterate through all the values of an array. But, what if we want to do something like this to a map?  

The following code shows how we could loop through each key-value pair of the account's map and add one to each of the account numbers:

for (map<string,int>::iterator it = accounts.begin();

    it != accounts.end();  

    ++ it)

{

    it->second += 1;

}

The condition of the for loop is probably the most interesting part of the previous code. The first part of the condition is the longest part. map<string,int>::iterator it = accounts.begin() is more understandable if we break it down.

 map<string,int>::iterator is a type. We are declaring an iterator that's suitable for a map with key-value pairs of string and int. The iterator's name is it. We assign the value that's returned by accounts.begin() to it. The iterator, it, now holds the first key-value pair from the accounts map.

The rest of the condition of the for loop works as follows. it != accounts.end() means the loop will continue until the end of the map is reached, and ++it simply steps to the next key-value pair in the map, each pass through the loop.

Inside the for loop, it->second accesses the value of the key-value pair and += 1 adds one to the value. Note that we can access the key (which is the first part of the key-value pair) with it->first.

You might have noticed that the syntax for setting up a loop through a map is quite verbose. C++ has a way to cut down on this verbosity.

The auto keyword

The code in the condition of the for loop was quite verbose – especially in terms of map<string,int>::iterator. C++ supplies a neat way to reduce verbosity with the auto keyword. Using the auto keyword, we can improve the previous code:

for (auto it = accounts.begin(); it != accounts.end(); ++ it)

{

    it->second += 1;

}

The auto keyword instructs the compiler to automatically deduce the type for us. This will be especially useful with the next class that we write.

STL summary

As with almost every C++ concept that we have covered in this book, the STL is a massive topic. Whole books have been written covering just the STL. At this point, however, we know enough to build a class that uses the STL map to store SFML Texture objects. We can then have textures that can be retrieved/loaded by using the filename as the key of the key-value pair.

The reason why we would go to this extra level of complexity and not just carry on using the Texture class the same way as we have been so far will become apparent as we proceed.

The TextureHolder class

Thousands of zombies represent a new challenge. Not only would loading, storing, and manipulating thousands of copies of three different zombie textures take up a lot of memory, but also a lot of processing power. We will create a new type of class that overcomes this problem and allows us to store just one of each texture.

We will also code the class in such a way that there can only ever be one instance of it. This type of class is called a singleton.

Tip

A singleton is a design pattern. A design pattern is a way to structure our code that is proven to work.

Furthermore, we will also code the class so that it can be used anywhere in our game code directly through the class name, without access to an instance.

Coding the TextureHolder header file

Let's make a new header file. Right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h), and then in the Name field, type TextureHolder.h.

Add the code that follows into the TextureHolder.h file, and then we can discuss it:

#pragma once

#ifndef TEXTURE_HOLDER_H

#define TEXTURE_HOLDER_H

#include <SFML/Graphics.hpp>

#include <map>

using namespace sf;

using namespace std;

class TextureHolder

{

private:

    // A map container from the STL,

    // that holds related pairs of String and Texture

    map<    string, Texture> m_Textures;

    // A pointer of the same type as the class itself

    // the one and only instance

    static TextureHolder* m_s_Instance;

public:

    TextureHolder();

    static Texture& GetTexture(string const& filename);

};

#endif

In the previous code, notice that we have an include directive for map from the STL. We declare a map instance that holds the string type and the SFML Texture type, as well as the key-value pairs. The map is called m_Textures.

In the preceding code, this line follows on:

static TextureHolder* m_s_Instance;

The previous line of code is quite interesting. We are declaring a static pointer to an object of the TextureHolder type called m_s_Instance. This means that the TextureHolder class has an object that is the same type as itself. Not only that, but because it is static, it can be used through the class itself, without an instance of the class. When we code the related .cpp file, we will see how we can use this.

In the public part of the class, we have the prototype for the constructor function, TextureHolder. The constructor takes no arguments and, as usual, has no return type. This is the same as the default constructor. We are going to override the default constructor with a definition that makes our singleton work how we want it to.

We have another function called GetTexture. Let's look at the signature again and analyze exactly what is happening:

static Texture& GetTexture(string const& filename);

First, notice that the function returns a reference to a Texture. This means that GetTexture will return a reference, which is efficient because it avoids making a copy of what could be a large graphic. Also, notice that the function is declared as static. This means that the function can be used without an instance of the class. The function takes a string as a constant reference, as a parameter. The effect of this is two-fold. Firstly, the operation is efficient and secondly, because the reference is constant, it can't be changed.

Coding the TextureHolder function definitions

Now, we can create a new .cpp file that will contain the function definition. This will allow us to see the reasons behind our new types of functions and variables. Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp), and then in the Name field, type TextureHolder.cpp. Finally, click the Add button. We are now ready to code the class.

Add the following code, and then we can discuss it:

#include "TextureHolder.h"

// Include the "assert feature"

#include <assert.h>

TextureHolder* TextureHolder::m_s_Instance = nullptr;

TextureHolder::TextureHolder()

{

    assert(m_s_Instance == nullptr);

    m_s_Instance = this;

}

In the previous code, we initialize our pointer of the TextureHolder type to nullptr. In the constructor, assert(m_s_Instance == nullptr) ensures that m_s_Instance equals nullptr. If it doesn't the game will exit execution. Then, m_s_Instance = this assigns the pointer to this instance. Now, consider where this code is taking place. The code is in the constructor. The constructor is the way that we create instances of objects from classes. So, effectively, we now have a pointer to a TextureHolder that points to the one and only instance of itself.

Add the final part of the code to the TextureHolder.cpp file. There are more comments than code here. Examine the following code and read the comments as you add the code, and then we can go through it:

Texture& TextureHolder::GetTexture(string const& filename)

{

    // Get a reference to m_Textures using m_s_Instance

    auto& m = m_s_Instance->m_Textures;

    // auto is the equivalent of map<string, Texture>

    // Create an iterator to hold a key-value-pair (kvp)

    // and search for the required kvp

    // using the passed in file name

    auto keyValuePair = m.find(filename);

    // auto is equivalent of map<string, Texture>::iterator

    

        

    // Did we find a match?

    if (keyValuePair != m.end())

    {

        // Yes

        // Return the texture,

        // the second part of the kvp, the texture

        return keyValuePair->second;

    }

    else

    {

        // File name not found

        // Create a new key value pair using the filename

        auto& texture = m[filename];

        // Load the texture from file in the usual way

        texture.loadFromFile(filename);

        // Return the texture to the calling code

        return texture;

    }

}

The first thing you will probably notice about the previous code is the auto keyword. The auto keyword was explained in the previous section.

Tip

If you want to know what the actual types that have been replaced by auto are, then look at the comments immediately after each use of auto in the previous code.

At the start of the code, we get a reference to m_textures. Then, we attempt to get an iterator to the key-value pair represented by the passed-in filename (filename). If we find a matching key, we return the texture with return keyValuePair->second. Otherwise, we add the texture to the map and then return it to the calling code.

Admittedly, the TextureHolder class introduced lots of new concepts (singletons, static functions, constant references, this, and the auto keyword,) and syntax. Add to this the fact that we have only just learned about pointers and the STL, and this section's code might have been a little daunting.

So, was it all worth it?

What have we achieved with TextureHolder?

The point is that now that we have this class, we can go wild using textures from wherever we like in our code and not worry about running out of memory or having access to any texture in a particular function or class. We will see how to use TextureHolder soon.

Building a horde of zombies

Now, we are armed with the TextureHolder class to make sure that our zombie textures are easily available as well as only loaded into the GPU once. Then, we can investigate creating a whole horde of them.

We will store zombies in an array. Since the process of building and spawning a horde of zombies involves quite a few lines of code, it is a good candidate for abstracting to a separate function. Soon, we will code the CreateHorde function but first, of course, we need a Zombie class.

Coding the Zombie.h file

The first step to building a class to represent a zombie is to code the member variables and function prototypes in a header file.

Right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h), and then in the Name field, type Zombie.h.

Add the following code to the Zombie.h file:

#pragma once

#include <SFML/Graphics.hpp>

using namespace sf;

class Zombie

{

private:

    // How fast is each zombie type?

    const float BLOATER_SPEED = 40;

    const float CHASER_SPEED = 80;

    const float CRAWLER_SPEED = 20;

    // How tough is each zombie type

    const float BLOATER_HEALTH = 5;

    const float CHASER_HEALTH = 1;

    const float CRAWLER_HEALTH = 3;

    // Make each zombie vary its speed slightly

    const int MAX_VARRIANCE = 30;

    const int OFFSET = 101 - MAX_VARRIANCE;

    // Where is this zombie?

    Vector2f m_Position;

    // A sprite for the zombie

    Sprite m_Sprite;

    // How fast can this one run/crawl?

    float m_Speed;

    // How much health has it got?

    float m_Health;

    // Is it still alive?

    bool m_Alive;

    

    // Public prototypes go here

};

The previous code declares all the private member variables of the Zombie class. At the top of the previous code, we have three constant variables to hold the speed of each type of zombie: a very slow Crawler, a slightly faster Bloater, and a somewhat speedy Chaser. We can experiment with the value of these three constants to help balance the difficulty level of the game. It's also worth mentioning here that these three values are only used as a starting value for the speed of each zombie type. As we will see later in this chapter, we will vary the speed of every zombie by a small percentage from these values. This stops zombies of the same type from bunching up together as they pursue the player.

The next three constants determine the health level for each zombie type. Note that Bloaters are the toughest, followed by Crawlers. As a matter of balance, the Chaser zombies will be the easiest to kill.

Next, we have two more constants, MAX_VARRIANCE and OFFSET. These will help us determine the individual speed of each zombie. We will see exactly how when we code the Zombie.cpp file.

After these constants, we declare a bunch of variables that should look familiar because we had very similar variables in our Player class. The m_Position, m_Sprite, m_Speed, and m_Health variables are for what their names imply: the position, sprite, speed, and health of the zombie object.

Finally, in the preceding code, we declare a Boolean called m_Alive, which will be true when the zombie is alive and hunting, but false when its health gets to zero and it is just a splurge of blood on our otherwise pretty background.

Now, we can complete the Zombie.h file. Add the function prototypes highlighted in the following code, and then we will talk about them:

    // Is it still alive?

    bool m_Alive;

    

    // Public prototypes go here    

public:

    

    // Handle when a bullet hits a zombie

    bool hit();

    // Find out if the zombie is alive

    bool isAlive();

    // Spawn a new zombie

    void spawn(float startX, float startY, int type, int seed);

    // Return a rectangle that is the position in the world

    FloatRect getPosition();

    // Get a copy of the sprite to draw

    Sprite getSprite();

    // Update the zombie each frame

    void update(float elapsedTime, Vector2f playerLocation);

};

In the previous code, there is a hit function, which we can call every time the zombie is hit by a bullet. The function can then take the necessary steps, such as taking health from the zombie (reducing the value of m_Health) or killing it dead (setting m_Alive to false).

The isAlive function returns a Boolean that lets the calling code know whether the zombie is alive or dead. We don't want to perform collision detection or remove health from the player for walking over a blood splat.

The spawn function takes a starting position, a type (Crawler, Bloater, or Chaser, represented by an int), as well as a seed to use in some random number generation that we will see in the next section.

Just like we have in the Player class, the Zombie class has getPosition and getSprite functions to get a rectangle that represents the space occupied by the zombie and the sprite that can be drawn each frame.

The last prototype in the previous code is the update function. We could have probably guessed that it would receive the elapsed time since the last frame, but also notice that it receives a Vector2f vector called playerLocation. This vector will indeed be the exact coordinates of the center of the player. We will soon see how we can use this vector to chase after the player.

Now, we can code the function definitions in the .cpp file.

Coding the Zombie.cpp file

Next, we will code the functionality of the Zombie class— the function definitions.

Create a new .cpp file that will contain the function definitions. Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp), and then in the Name field, type Zombie.cpp. Finally, click the Add button. We are now ready to code the class.

Add the following code to the Zombie.cpp file:

#include "zombie.h"

#include "TextureHolder.h"

#include <cstdlib>

#include <ctime>

using namespace std;

First, we add the necessary include directives and then using namespace std. You might remember a few instances when we prefixed our object declarations with std::. This using directive means we don't need to do that for the code in this file.

Now, add the following code, which is the definition of the spawn function. Study the code once you have added it, and then we will discuss it:

void Zombie::spawn(float startX, float startY, int type, int seed)

{

    

    switch (type)

    {

    case 0:

        // Bloater

        m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/bloater.png"));

        m_Speed = BLOATER_SPEED;

        m_Health = BLOATER_HEALTH;

        break;

    case 1:

        // Chaser

        m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/chaser.png"));

        m_Speed = CHASER_SPEED;

        m_Health = CHASER_HEALTH;

        break;

    case 2:

        // Crawler

        m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/crawler.png"));

        m_Speed = CRAWLER_SPEED;

        m_Health = CRAWLER_HEALTH;

        break;

    }

    // Modify the speed to make the zombie unique

    // Every zombie is unique. Create a speed modifier

    srand((int)time(0) * seed);

    // Somewhere between 80 and 100

    float modifier = (rand() % MAX_VARRIANCE) + OFFSET;

    // Express this as a fraction of 1

    modifier /= 100; // Now equals between .7 and 1

    m_Speed *= modifier;

    

    // Initialize its location

    m_Position.x = startX;

    m_Position.y = startY;

    // Set its origin to its center

    m_Sprite.setOrigin(25, 25);

    // Set its position

    m_Sprite.setPosition(m_Position);

}

The first thing the function does is switch paths of execution based on the int value, which is passed in as a parameter. Within the switch block, there is a case for each type of zombie. Depending on the type of zombie, the appropriate texture, speed, and health is initialized to the relevant member variables.

Tip

We could have used an enumeration for the different types of zombie. Feel free to upgrade your code when the project is finished.

Of interest here is that we use the static TextureHolder::GetTexture function to assign the texture. This means that no matter how many zombies we spawn, there will be a maximum of three textures in the memory of the GPU.

The next three lines of code (excluding comments) do the following:

  • Seed the random number generator with the seed variable that was passed in as a parameter.
  • Declare and initialize the modifier variable using the rand function and the MAX_VARRIANCE and OFFSET constants. The result is a fraction between zero and one, which can be used to make each zombie's speed unique. The reason we want to do this is so that the zombies don't bunch up together on top of each other too much.
  • We can now multiply m_Speed by modifier and we will have a zombie whose speed is within the MAX_VARRIANCE percent of the constant defined for this type of zombie's speed.

After we have resolved the speed, we assign the passed-in position held in startX and startY to m_Position.x and m_Position.y, respectively.

The last two lines of code in the previous listing set the origin of the sprite to the center and use the m_Position vector to set the position of the sprite.

Now, add the following code for the hit function to the Zombie.cpp file:

bool Zombie::hit()

{

    m_Health--;

    if (m_Health < 0)

    {

        // dead

        m_Alive = false;

        m_Sprite.setTexture(TextureHolder::GetTexture(

            "graphics/blood.png"));

        return true;

    }

    // injured but not dead yet

    return false;

}

The hit function is nice and simple: reduce m_Health by one and then check whether m_Health is below zero.

If it is below zero, then it sets m_Alive to false, swaps the zombie's texture for a blood splat, and returns true to the calling code so that it knows the zombie is now dead. If the zombie has survived, the hit returns false.

Add the following three getter functions, which just return a value to the calling code:

bool Zombie::isAlive()

{

    return m_Alive;

}

FloatRect Zombie::getPosition()

{

    return m_Sprite.getGlobalBounds();

}

Sprite Zombie::getSprite()

{

    return m_Sprite;

}

The previous three functions are quite self-explanatory, perhaps with the exception of the getPosition function, which uses the m_Sprite.getLocalBounds function to get the FloatRect instance, which is then returned to the calling code.

Finally, for the Zombie class, we need to add the code for the update function. Look closely at the following code, and then we will go through it:

void Zombie::update(float elapsedTime,

    Vector2f playerLocation)

{

    float playerX = playerLocation.x;

    float playerY = playerLocation.y;

    // Update the zombie position variables

    if (playerX > m_Position.x)

    {

        m_Position.x = m_Position.x +

            m_Speed * elapsedTime;

    }

    if (playerY > m_Position.y)

    {

        m_Position.y = m_Position.y +

            m_Speed * elapsedTime;

    }

        

    if (playerX < m_Position.x)

    {

        m_Position.x = m_Position.x -

            m_Speed * elapsedTime;

    }

    if (playerY < m_Position.y)

    {

        m_Position.y = m_Position.y -

            m_Speed * elapsedTime;

    }

    // Move the sprite

    m_Sprite.setPosition(m_Position);

    // Face the sprite in the correct direction

    float angle = (atan2(playerY - m_Position.y,

        playerX - m_Position.x)

        * 180) / 3.141;

    m_Sprite.setRotation(angle);

}

In the preceding code, we copy playerLocation.x and playerLocation.y into the local variables called playerX and playerY.

Next, there are four if statements. They test to see whether the zombie is to the left, right, above, or below the current player's position. These four if statements, when they evaluate to true, adjust the zombie's m_Position.x and m_Position.y values appropriately using the usual formula, that is, speed multiplied by time since last frame. More specifically, the code is m_Speed * elapsedTime.

After the four if statements, m_Sprite is moved to its new location.

We then use the same calculation we previously used with the player and the mouse pointer, but this time, we do so for the zombie and the player. This calculation finds the angle that's needed to face the zombie toward the player.

Finally, for this function and the class, we call m_Sprite.setRotation to actually rotate the zombie sprite. Remember that this function will be called for every zombie (that is alive) on every frame of the game.

But, we want a whole horde of zombies.

Using the Zombie class to create a horde

Now that we have a class to create a living, attacking, and killable zombie, we want to spawn a whole horde of them.

To achieve this, we will write a separate function and we will use a pointer so that we can refer to our horde that will be declared in main but configured in a different scope.

Open the ZombieArena.h file in Visual Studio and add the following highlighted lines of code:

#pragma once

#include "Zombie.h"

using namespace sf;

int createBackground(VertexArray& rVA, IntRect arena);

Zombie* createHorde(int numZombies, IntRect arena);

Now that we have a prototype, we can code the function definition.

Create a new .cpp file that will contain the function definition. Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp), and then in the Name field, type CreateHorde.cpp. Finally, click the Add button.

Add in the following code to the CreateHorde.cpp file and study it. Afterward, we will break it down into chunks and discuss it:

#include "ZombieArena.h"

#include "Zombie.h"

Zombie* createHorde(int numZombies, IntRect arena)

{

    Zombie* zombies = new Zombie[numZombies];

    int maxY = arena.height - 20;

    int minY = arena.top + 20;

    int maxX = arena.width - 20;

    int minX = arena.left + 20;

    for (int i = 0; i < numZombies; i++)

    {

        

        // Which side should the zombie spawn

        srand((int)time(0) * i);

        int side = (rand() % 4);

        float x, y;

        switch (side)

        {

        case 0:

            // left

            x = minX;

            y = (rand() % maxY) + minY;

            break;

        case 1:

            // right

            x = maxX;

            y = (rand() % maxY) + minY;

            break;

        case 2:

            // top

            x = (rand() % maxX) + minX;

            y = minY;

            break;

        case 3:

            // bottom

            x = (rand() % maxX) + minX;

            y = maxY;

            break;

        }

        // Bloater, crawler or runner

        srand((int)time(0) * i * 2);

        int type = (rand() % 3);

        // Spawn the new zombie into the array

        zombies[i].spawn(x, y, type, i);

                

    }

    return zombies;

}

Let's look at all the previous code again, in bite-size pieces. First, we added the now familiar include directives:

#include "ZombieArena.h"

#include "Zombie.h"

Next comes the function signature. Notice that the function must return a pointer to a Zombie object. We will be creating an array of Zombie objects. Once we are done creating the horde, we will return the array. When we return the array, we are actually returning the address of the first element of the array. This, as we learned in the section on pointers earlier in this chapter, is the same thing as a pointer. The signature also shows that we have two parameters. The first, numZombies, will be the number of zombies this current horde requires and the second, arena, is an IntRect that holds the size of the current arena in which to create this horde.

After the function signature, we declare a pointer to the Zombie type called zombies and initialize it with the memory address of the first element of an array, which we dynamically allocate on the heap:

Zombie* createHorde(int numZombies, IntRect arena)

{

    Zombie* zombies = new Zombie[numZombies];

The next part of the code simply copies the extremities of the arena into maxY, minY, maxX, and minX. We subtract twenty pixels from the right and bottom while adding twenty pixels to the top and left. We use these four local variables to help position each of the zombies. We made the twenty-pixel adjustments to stop the zombies appearing on top of the walls:

int maxY = arena.height - 20;

int minY = arena.top + 20;

int maxX = arena.width - 20;

int minX = arena.left + 20;

Now, we enter a for loop that will loop through each of the Zombie objects in the zombies array from zero through to numZombies:

for (int i = 0; i < numZombies; i++)

Inside the for loop, the first thing the code does is seed the random number generator and then generate a random number between zero and three. This number is stored in the side variable. We will use the side variable to decide whether the zombie spawns at the left, top, right, or bottom of the arena. We also declare two int variables, x and y. These two variables will temporarily hold the actual horizontal and vertical coordinates of the current zombie:

// Which side should the zombie spawn

srand((int)time(0) * i);

int side = (rand() % 4);

float x, y;

Still inside the for loop, we have a switch block with four case statements. Note that the case statements are for 0, 1, 2, and 3, and that the argument in the switch statement is side. Inside each of the case blocks, we initialize x and y with one predetermined value, either minX, maxX, minY, or maxY, and one randomly generated value. Look closely at the combinations of each predetermined and random value. You will see that they are appropriate for positioning the current zombie randomly across either the left side, top side, right side, or bottom side. The effect of this will be that each zombie can spawn randomly, anywhere on the outside edge of the arena:

switch (side)

{

    case 0:

        // left

        x = minX;

        y = (rand() % maxY) + minY;

        break;

    case 1:

        // right

        x = maxX;

        y = (rand() % maxY) + minY;

        break;

    case 2:

        // top

        x = (rand() % maxX) + minX;

        y = minY;

        break;

    case 3:

        // bottom

        x = (rand() % maxX) + minX;

        y = maxY;

        break;        

}

Still inside the for loop, we seed the random number generator again and generate a random number between 0 and 2. We store this number in the type variable. The type variable will determine whether the current zombie will be a Chaser, Bloater, or Crawler.

After the type is determined, we call the spawn function on the current Zombie object in the zombies array. As a reminder, the arguments that are sent into the spawn function determine the starting location of the zombie and the type of zombie it will be. The apparently arbitrary i is passed in as it is used as a unique seed that randomly varies the speed of a zombie within an appropriate range. This stops our zombies "bunching up" and becoming a blob rather than a horde:

// Bloater, crawler or runner

srand((int)time(0) * i * 2);

int type = (rand() % 3);

// Spawn the new zombie into the array

zombies[i].spawn(x, y, type, i);

The for loop repeats itself once for each zombie, controlled by the value contained in numZombies, and then we return the array. The array, as another reminder, is simply an address of the first element of itself. The array is dynamically allocated on the heap, so it persists after the function returns:

return zombies;

Now, we can bring our zombies to life.

Bringing the horde to life (back to life)

We have a Zombie class and a function to make a randomly spawning horde of them. We have the TextureHolder singleton as a neat way to hold just three textures that can be used for dozens or even thousands of zombies. Now, we can add the horde to our game engine in main.

Add the following highlighted code to include the TextureHolder class. Then, just inside main, we will initialize the one and only instance of TextureHolder, which can be used from anywhere within our game:

#include <SFML/Graphics.hpp>

#include "ZombieArena.h"

#include "Player.h"

#include "TextureHolder.h"

using namespace sf;

int main()

{

    // Here is the instance of TextureHolder

    TextureHolder holder;

    // The game will always be in one of four states

    enum class State { PAUSED, LEVELING_UP, GAME_OVER, PLAYING };

    // Start with the GAME_OVER state

    State state = State::GAME_OVER;

The following few lines of highlighted code declare some control variables for the number of zombies at the start of the wave, the number of zombies still to be killed, and, of course, a pointer to Zombie called zombies that we initialize to nullptr:

// Create the background

VertexArray background;

// Load the texture for our background vertex array

Texture textureBackground;

textureBackground.loadFromFile("graphics/background_sheet.png");

// Prepare for a horde of zombies

int numZombies;

int numZombiesAlive;

Zombie* zombies = nullptr;

// The main game loop

while (window.isOpen())

Next, in the PLAYING section, nested inside the LEVELING_UP section, we add code that does the following:

  • Initializes numZombies to 10. As the project progresses, this will eventually be dynamic and based on the current wave number.
  • Delete any preexisting allocated memory. Otherwise, each new call to createHorde would take up progressively more memory but without freeing up the previous horde's memory.
  • Then, we call createHorde and assign the returned memory address to zombies.
  • We also initialize zombiesAlive with numZombies because we haven't killed any at this point.

Add the following highlighted code, which we have just discussed:

if (state == State::PLAYING)

{

    // Prepare the level

    // We will modify the next two lines later

    arena.width = 500;

    arena.height = 500;

    arena.left = 0;

    arena.top = 0;

    // Pass the vertex array by reference

    // to the createBackground function

    int tileSize = createBackground(background, arena);

    // Spawn the player in the middle of the arena

    player.spawn(arena, resolution, tileSize);

    // Create a horde of zombies

    numZombies = 10;

    // Delete the previously allocated memory (if it exists)

    delete[] zombies;

    zombies = createHorde(numZombies, arena);

    numZombiesAlive = numZombies;

    // Reset the clock so there isn't a frame jump

    clock.restart();

}

Now, add the following highlighted code to the ZombieArena.cpp file:

/*

 ****************

 UPDATE THE FRAME

 ****************

 */

if (state == State::PLAYING)

{

    // Update the delta time

    Time dt = clock.restart();

    // Update the total game time

    gameTimeTotal += dt;

    // Make a decimal fraction of 1 from the delta time

    float dtAsSeconds = dt.asSeconds();

    // Where is the mouse pointer

    mouseScreenPosition = Mouse::getPosition();

    // Convert mouse position to world coordinates of mainView

    mouseWorldPosition = window.mapPixelToCoords(

        Mouse::getPosition(), mainView);

    // Update the player

    player.update(dtAsSeconds, Mouse::getPosition());

    // Make a note of the players new position

    Vector2f playerPosition(player.getCenter());

    // Make the view centre around the player                

    mainView.setCenter(player.getCenter());

    // Loop through each Zombie and update them

    for (int i = 0; i < numZombies; i++)

    {

        if (zombies[i].isAlive())

        {

            zombies[i].update(dt.asSeconds(), playerPosition);

        }

    }

}// End updating the scene

All the new preceding code does is loop through the array of zombies, check whether the current zombie is alive and, if it is, calls its update function with the necessary arguments.

Add the following code to draw all the zombies:

/*

 **************

 Draw the scene

 **************

 */

if (state == State::PLAYING)

{

    window.clear();

    // set the mainView to be displayed in the window

    // And draw everything related to it

    window.setView(mainView);

    // Draw the background

    window.draw(background, &textureBackground);

    // Draw the zombies

    for (int i = 0; i < numZombies; i++)

    {

        window.draw(zombies[i].getSprite());

    }

    // Draw the player

    window.draw(player.getSprite());

}

The preceding code loops through all the zombies and calls the getSprite function to allow the draw function to do its work. We don't check whether the zombie is alive because even if the zombie is dead, we want to draw the blood splatter.

At the end of the main function, we need to make sure to delete our pointer because it is a good practice as well as often being essential. However, technically, this isn't essential because the game is about to exit, and the operating system will reclaim all the memory that's used after the return 0 statement:

    }// End of main game loop

     // Delete the previously allocated memory (if it exists)

    delete[] zombies;

    return 0;

}

You can run the game and see the zombies spawn around the edge of the arena. They will immediately head straight toward the player at their various speeds. Just for fun, I increased the size of the arena and increased the number of zombies to 1,000 as you can see in the following screenshot:

 This is going to end badly!

Note that you can also pause and resume the onslaught of the horde using the Enter key because of the code we wrote in Chapter 8, SFML Views – Starting the Zombie Shooter Game.

Let's fix the fact that some classes still use a Texture instance directly and modify it to use the new TextureHolder class.

Using the TextureHolder class for all textures

Since we have our TextureHolder class, we might as well be consistent and use it to load all our textures. Let's make some very small alterations to the existing code that loads textures for the background sprite sheet and the player.

Changing the way the background gets its textures

In the ZombieArena.cpp file, find the following code:

// Load the texture for our background vertex array

Texture textureBackground;

textureBackground.loadFromFile("graphics/background_sheet.png");

Delete the code highlighted previously and replace it with the following highlighted code, which uses our new TextureHolder class:

// Load the texture for our background vertex array

Texture textureBackground = TextureHolder::GetTexture(

    "graphics/background_sheet.png");

Let's update the way the Player class gets a texture.

Changing the way the Player gets its texture

In the Player.cpp file, inside the constructor, find this code:

#include "player.h"

Player::Player()

{

    m_Speed = START_SPEED;

    m_Health = START_HEALTH;

    m_MaxHealth = START_HEALTH;

    // Associate a texture with the sprite

    // !!Watch this space!!

    m_Texture.loadFromFile("graphics/player.png");

    m_Sprite.setTexture(m_Texture);

    // Set the origin of the sprite to the centre,

    // for smooth rotation

    m_Sprite.setOrigin(25, 25);

}

Delete the code highlighted previously and replace it with the following highlighted code, which uses our new TextureHolder class. In addition, add the include directive to add the TextureHolder header to the file. The new code is shown highlighted, in context, as follows:

#include "player.h"

#include "TextureHolder.h"

Player::Player()

{

    m_Speed = START_SPEED;

    m_Health = START_HEALTH;

    m_MaxHealth = START_HEALTH;

    // Associate a texture with the sprite

    // !!Watch this space!!

    m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/player.png"));

    // Set the origin of the sprite to the centre,

    // for smooth rotation

    m_Sprite.setOrigin(25, 25);

}

Tip

From now on, we will use the TextureHolder class for loading all textures.

Summary

In this chapter, we have covered pointers and discussed that they are variables that hold a memory address to a specific type of object. The full significance of this will begin to reveal itself as this book progresses and the power of pointers is revealed. We also used pointers in order to create a huge horde of zombies that can be accessed using a pointer, which it turns out is also the same thing as the first element of an array.

We learned about the STL, and in particular the map class. We implemented a class that will store all our textures, as well as provide access to them.

You might have noticed that the zombies don't appear to be very dangerous. They just drift through the player without leaving a scratch. Currently, this is a good thing because the player has no way to defend themselves.

In the next chapter, we will make two more classes: one for ammo and health pickups and one for bullets that the player can shoot. After we have done that, we will learn how to detect collisions so that the bullets and zombies do some damage and the pickups can be collected by the player.

FAQ

Here are some questions that might be on your mind:

Q) What's the difference between pointers and references?

A) Pointers are like references with boosters. Pointers can be changed to point to different variables (memory addresses), as well as point to dynamically allocated memory on the free store.

Q) What's the deal with arrays and pointers?

A) Arrays are really constant pointers to their first element.

Q) Can you remind me about the new keyword and memory leaks?

A) When we use memory on the free store using the new keyword, it persists even when the function it was created in has returned and all the local variables are gone. When we are done with using memory on the free store, we must release it. So, if we use memory on the free store that we want to persist beyond the life of a function, we must make sure to keep a pointer to it or we will have leaked memory. It would be like putting all our belongings in our house and then forgetting where we live! When we return the zombies array from createHorde, it is like passing the relay baton (memory address) from createHorde to main. It's like saying, OK, here is your horde of zombies— they are your responsibility now. And, we wouldn't want any leaked zombies running around in our RAM! So, we must remember to call delete on pointers to dynamically allocated memory.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.204