Building the LevelManager class

It will take several phases of coding to make our level designs work. The first thing we will do is code the LevelManager header file. This will allow us to look at and discuss the member variables and functions that will be in the LevelManger class.

Next, we will code the LevelManager.cpp file, which will have all the function definitions in it. As this is a long file, we will break it up into several sections, to code and discuss them.

Once the LevelManager class is complete, we will add an instance of it to the game engine (Engine class). We will also add a new function to the Engine class, loadLevel, which we can call from the update function whenever a new level is required. The loadLevel function will not only use the LevelManager instance to load the appropriate level but it will also take care of aspects such as spawning the player characters and preparing the clock.

As already mentioned, let's get an overview of LevelManager by coding the LevelManager.h file.

Coding LevelManager.h

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 LevelManager.h. Finally, click the Add button. We are now ready to code the header file for the LevelManager class.

Add the following include directives and private variables, and then we will discuss them:

#pragma once 
 
#include <SFML/Graphics.hpp> 
using namespace sf; 
using namespace std; 
 
 
class LevelManager 
{ 
private: 
   Vector2i m_LevelSize; 
   Vector2f m_StartPosition; 
   float m_TimeModifier = 1; 
   float m_BaseTimeLimit = 0; 
   int m_CurrentLevel = 0; 
   const int NUM_LEVELS = 4; 
 
// public declarations go here 

The code declares Vector2i m_LevelSize to hold two integer values that will hold the horizontal and vertical number of tiles that the current map contains. Vector2fm_StartPosition contains the coordinates in the world where Bob and Thomas should be spawned. Note that this is not a tile position relatable to m_LevelSize units, but a horizontal and vertical pixel position in the level.

The m_TimeModifier member variable is a float that will be used to multiply the time available in the current level. The reason we want to do this is so that by changing (decreasing) this value, we will shorten the time available each time the player attempts the same level. As an example, if the player gets 60 seconds for the first time they attempt level one then 60 multiplied by 1 is, of course, 60. When the player completes all the levels and comes back to level 1 for the second time, m_TimeModifier will have been reduced by 10 percent. Then, when the time available is multiplied by 0.9, the amount of time available to the player will be 54 seconds. This is 10 percent less than 60. The game will get steadily harder.

The float variable, m_BaseTimeLimit, holds the original, unmodified time limit we have just been discussing.

You can probably guess that m_CurrentLevel will hold the current level number that is being played.

The int NUM_LEVELS constant will be used to flag when it is appropriate to go back to level one again and reduce the value of m_TimeModifier.

Now add the following public variables and function declarations:

public: 
 
   const int TILE_SIZE = 50; 
   const int VERTS_IN_QUAD = 4; 
 
   float getTimeLimit(); 
 
   Vector2f getStartPosition(); 
 
   int** nextLevel(VertexArray& rVaLevel); 
 
   Vector2i getLevelSize(); 
 
   int getCurrentLevel(); 
 
}; 

In the previous code, there are two constant int members. TILE_SIZE is a useful constant to remind us that each tile in the sprite sheet is fifty pixels wide and fifty pixels high. VERTS_IN_QUAD is a useful constant to make our manipulation of a VertexArray less error-prone. There are, in fact, four vertices in a quad. Now we can't forget it.

The getTimeLimit, getStartPosition, getLevelSize, and getCurrentLevel functions are simple getter functions, that return the current value of the private member variables we declared in the previous block of code.

A function that deserves a closer look is nextLevel. This function receives a VertexArray reference, just like we used in the Zombie Arena game. The function can then work on the VertexArray, and all the changes will be present in the VertexArray from the calling code. The nextLevel function returns a pointer to a pointer, which means we can return an address that is the first element of a two-dimensional array of int values. We will be building a two-dimensional array of int values that will represent the layout of each level. Of course, these int values will be read from the level design text files.

Coding the LevelManager.cpp file

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 LevelManager.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the LevelManager class.

As this is quite a long class, we will break it up to discuss it in six chunks. The first five will cover the nextLevel function, and the sixth, all the rest.

Add the following include directives and the first (of five) part of the nextLevel function:

#include "stdafx.h" 
#include <SFML/Graphics.hpp> 
#include <SFML/Audio.hpp> 
#include "TextureHolder.h" 
#include <sstream> 
#include <fstream> 
#include "LevelManager.h" 
 
using namespace sf; 
using namespace std; 
 
int** LevelManager::nextLevel(VertexArray& rVaLevel) 
{ 
   m_LevelSize.x = 0; 
   m_LevelSize.y = 0; 
 
   // Get the next level 
   m_CurrentLevel++; 
   if (m_CurrentLevel > NUM_LEVELS) 
   { 
      m_CurrentLevel = 1; 
      m_TimeModifier -= .1f; 
   } 
 
   // Load the appropriate level from a text file 
   string levelToLoad; 
   switch (m_CurrentLevel) 
   { 
 
   case 1: 
      levelToLoad = "levels/level1.txt"; 
      m_StartPosition.x = 100; 
      m_StartPosition.y = 100; 
      m_BaseTimeLimit = 30.0f; 
      break; 
 
   case 2: 
      levelToLoad = "levels/level2.txt"; 
      m_StartPosition.x = 100; 
      m_StartPosition.y = 3600; 
      m_BaseTimeLimit = 100.0f; 
      break; 
 
   case 3: 
      levelToLoad = "levels/level3.txt"; 
      m_StartPosition.x = 1250; 
      m_StartPosition.y = 0; 
      m_BaseTimeLimit = 30.0f; 
      break; 
 
   case 4: 
      levelToLoad = "levels/level4.txt"; 
      m_StartPosition.x = 50; 
      m_StartPosition.y = 200; 
      m_BaseTimeLimit = 50.0f; 
      break; 
 
   }// End switch 

After the include directives, the code initializes m_LevelSize.x and m_LevelSize.y to zero.

Next, m_CurrentLevel is incremented. The if statement that follows checks whether m_CurrentLevel is greater than NUM_LEVELS. If it is, m_CurrentLevel is set back to 1 and m_TimeModifier is reduced by .1f in order to shorten the time allowed for all levels.

The code then switches based on the value held by m_CurrentLevel. Each case statement initializes the name of the text file, which holds the level design and the starting position for Thomas and Bob, as well as m_BaseTimeLimit, which is the unmodified time limit for the level in question.

Tip

If you design your own levels, add a case statement and the appropriate values for it here. Also edit the NUM_LEVELS constant in the LevelManager.h file.

Now add the second part of the nextLevel function, as shown. Add the code immediately after the previous code. Study the code as you add it so we can discuss it:

   ifstream inputFile(levelToLoad); 
   string s; 
 
   // Count the number of rows in the file 
   while (getline(inputFile, s)) 
   { 
      ++m_LevelSize.y; 
   } 
 
   // Store the length of the rows 
   m_LevelSize.x = s.length(); 

In the previous (second part) we have just coded, we declare an ifstream object called inputFile, which opens a stream to the filename contained in levelToLoad.

The code loops through each line of the file using getline, but doesn't record any of its content. All it does is count the number of lines by incrementing m_LevelSize.y. After the for loop, the width of the level is saved in m_LevelSize.x using s.length. This implies that the length of all the lines must be the same or we would run in to trouble.

At this point, we know and have saved the length and width of the current level in m_LevelSize.

Now add the third part of the nextLevel function, as shown. Add the code immediately after the previous code. Study the code as you add it so we can discuss it:

   // Go back to the start of the file 
   inputFile.clear(); 
   inputFile.seekg(0, ios::beg); 
 
   // Prepare the 2d array to hold the int values from the file 
   int** arrayLevel = new int*[m_LevelSize.y]; 
   for (int i = 0; i < m_LevelSize.y; ++i) 
   { 
      // Add a new array into each array element 
      arrayLevel[i] = new int[m_LevelSize.x]; 
   } 

First, we clear inputFile using its clear function. The seekg function called with the 0, ios::beg parameters resets the stream back to before the first character.

Next, we declare a pointer to a pointer called arrayLevel. Note that this is done on the free store/heap using the new keyword. Once we have initialized this two-dimensional array, we will be able to return its address to the calling code and it will persist until we either delete it or the game is closed.

for loops from 0 to m_LevelSize.y -1. In each pass, it adds a new array of int values to the heap to match the value of m_LevelSize.x. We now have a perfectly configured (for the current level) two-dimensional array. The only problem is that there is nothing in it.

Now add the fourth part of the nextLevel function, as shown. Add the code immediately after the previous code. Study the code as you add it so we can discuss it:

    // Loop through the file and store all the values in the 2d array 
   string row; 
   int y = 0; 
   while (inputFile >> row) 
   { 
      for (int x = 0; x < row.length(); x++) { 
 
         const char val = row[x]; 
         arrayLevel[y][x] = atoi(&val); 
      } 
 
      y++; 
   } 
 
   // close the file 
   inputFile.close(); 

First, the code initializes a string, called row, which will hold one row of the level design at a time. We also declare and initialize an int called y that will help us count the rows.

The while loop executes repeatedly until inputFile gets past the last row. Inside the while loop there is a for loop, which goes through each character of the current row and stores it in the two-dimensional array, arrayLevel. Notice that we access exactly the right element of the two-dimensional array with arrayLevel[y][x] =. The atoi function converts char val to int. This is what is required, because we have a two-dimensional array for int, not char.

Now add the fifth part of the nextLevel function, as shown. Add the code immediately after the previous code. Study the code as you add it so we can discuss it:

   // What type of primitive are we using? 
   rVaLevel.setPrimitiveType(Quads); 
 
   // Set the size of the vertex array 
   rVaLevel.resize(m_LevelSize.x * m_LevelSize.y * VERTS_IN_QUAD); 
 
   // Start at the beginning of the vertex array 
   int currentVertex = 0; 
 
   for (int x = 0; x < m_LevelSize.x; x++) 
   { 
      for (int y = 0; y < m_LevelSize.y; y++) 
      { 
         // Position each vertex in the current quad 
         rVaLevel[currentVertex + 0].position =  
            Vector2f(x * TILE_SIZE,  
            y * TILE_SIZE); 
 
         rVaLevel[currentVertex + 1].position =  
            Vector2f((x * TILE_SIZE) + TILE_SIZE,  
            y * TILE_SIZE); 
 
         rVaLevel[currentVertex + 2].position =  
            Vector2f((x * TILE_SIZE) + TILE_SIZE,  
            (y * TILE_SIZE) + TILE_SIZE); 
 
         rVaLevel[currentVertex + 3].position =  
            Vector2f((x * TILE_SIZE),  
            (y * TILE_SIZE) + TILE_SIZE); 
 
         // Which tile from the sprite sheet should we use 
         int verticalOffset = arrayLevel[y][x] * TILE_SIZE; 
 
         rVaLevel[currentVertex + 0].texCoords =  
            Vector2f(0, 0 + verticalOffset); 
 
         rVaLevel[currentVertex + 1].texCoords =  
            Vector2f(TILE_SIZE, 0 + verticalOffset); 
 
         rVaLevel[currentVertex + 2].texCoords =  
            Vector2f(TILE_SIZE, TILE_SIZE + verticalOffset); 
 
         rVaLevel[currentVertex + 3].texCoords =  
            Vector2f(0, TILE_SIZE + verticalOffset); 
 
         // Position ready for the next four vertices 
         currentVertex = currentVertex + VERTS_IN_QUAD; 
      } 
   } 
 
   return arrayLevel; 
} // End of nextLevel function 

Although this is the longest section of code from the five sections we divided nextLevel into, it is also the most straightforward. This is because we have seen very similar code in the Zombie Arena project.

What happens is that the nested for loop loops from zero through to the width and height of the level. For each position in the array, four vertices are put into VertexArray and four texture coordinates are assigned from the sprite sheet. The positions of the vertices and texture coordinates are calculated using the currentVertex variable, the TILE SIZE, and VERTS_IN_QUAD constants. At the end of each loop of the inner for loop, currentVertex is increased by VERTS_IN_QUAD, moving nicely on to the next tile.

The important thing to remember about this VertexArray is that it was passed into nextLevel by reference. Therefore, the VertexArray will be available in the calling code. We will call nextLevel from the code in the Engine class.

Once this function has been called, the Engine class will have a VertexArray to represent the level graphically, and a two-dimensional array of int values as a numerical representation of all the platforms and obstacles in the level.

The rest of the LevelManager functions are all simple getter functions, but do take the time to familiarize yourself with what private value is returned by which function. Add the remaining functions from the LevelManager class:

Vector2i LevelManager::getLevelSize() 
{ 
   return m_LevelSize; 
} 
 
int LevelManager::getCurrentLevel() 
{ 
   return m_CurrentLevel; 
} 
 
float LevelManager::getTimeLimit() 
{ 
   return m_BaseTimeLimit * m_TimeModifier; 
 
} 
Vector2f LevelManager::getStartPosition() 
{ 
   return m_StartPosition; 
} 

Now that the LevelManager class is complete, we can move on to using it. We will code another function in the Engine class to do so.

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

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