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.
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. Vector2f
, m_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.
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.
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.
18.226.34.25