In this chapter we are going to build the Level class. The Level class will
handle loading and rendering of the level, as well as keeping track of the details
for each brick.
Topics Covered in this chapter:
Storing A Level in Text
Creating the Level Class
The responsibilities of the Level class are to load the level file, render
the level and keep track of the bricks. Before we can load a level file, we
need to create one, and before we can create one we need decide how we want
to store the level data. An easy method of storing level information is in a
text file. A breakout level is essentially a gird of different colored bricks.
I have decided to store this information as lines of numbers separated by spaces
in a text file. Each number will represent a different colored brick, with 0
representing no brick. Each line will represent a row of bricks. The image below
is an example of how the mapping will work.
Once loaded, the level data will be stored as a vector of integer arrays. Each
array representing a line of bricks. To render the level we will cycle through
each array in the vector and each brick number in the array, and render the
appropriate colored brick. Removing a brick from the level will be as easy as
setting the brick number to 0 at the appropriate location. Take a look at the
Level class in the part 2, chapter 4 code and see how this is accomplished.
The Level Class:
At the top of the Level class I have defined a number of constants for use
by the level. When reading from the text file, CHARS_TO_READ will be how many
characters we read per line. Our level data will be quite small so 128 characters
is more than enough. The LEVEL_LENGTH, and LEVEL_HEIGHT are somewhat arbitrary,
I chose them based on how many bricks looked good on the screen. You could change
these constants to make a bigger level, however you would also have to back
the camera off so you could see it all. The BRICK_LENGTH and BRICK_HEIGHT are
based off the brick model and is the distance apart the bricks are rendered.
The STARTX and STARTY are the top left corner of the level. The Camera is focused
on the origin, i.e. 0,0,0 in world space coordinates, so the left of the screen
is negative x, and the right is positive. The top of the screen is positive
y, and the bottom negative. Our Camera is sitting at -40 in the z, so the screen
is approximately 40x across running from -20x to +20x and the screen is 32y
from top to bottom running from +16y to -16y. This is dependent entirely on
the position of the camera, so moving the camera would change everything.
//charactors to read from the level text file
const CHARS_TO_READ = 128;
//length of the level in bricks
const LEVEL_LENGTH = 16;
//height of the level in bricks
const LEVEL_HEIGHT = 8;
//size of the bricks in units
const float BRICK_LENGTH = 2.5f;
const float BRICK_HEIGHT = 1.25f;
const float STARTX = -19.0f;
const float STARTY = 12.0f;
The init function for the level is fairly basic,
it take a filename for the level data, and the device so it can load the bricks.
bool Level::init(std::string filename, LPDIRECT3DDEVICE9
device){
//Load the bricks
redBrick = new Model;
redBrick->loadModel(device, "redbrick.x");
redBrick->setScale(D3DXVECTOR3(0.25f,0.25f,0.25f));
yellowBrick = new Model;
yellowBrick->loadModel(device, "yellowbrick.x");
yellowBrick->setScale(D3DXVECTOR3(0.25f,0.25f,0.25f));
greenBrick = new Model;
greenBrick->loadModel(device, "greenbrick.x");
greenBrick->setScale(D3DXVECTOR3(0.25f,0.25f,0.25f));
loadLevel(filename);
return true;
}
When the init has finished loading the brick models,
it calls loadLevel to load the level data out of
the text file. The process works like this, a file reading object of type ifstream
is created, and open is called on the object to
open the level text file. With the text file open getline
is called to get a line of text from the file. getline
takes a character buffer, and the number of characters to read from the file.
Once the line is in the character buffer strtok is
called to break up the string into tokens based a delimiter. The delimiter is
the character, or string of characters on which we wish to break the string.
In this case I am separating the data on spaces. Each token can then be evaluated
or stored. In the first instance I check if the token contains the string 'levelStart'
as an indication to start loading the level data. If it finds 'levelStart' each
subsequent line is loaded and the lines of numbers are split into tokens, converted
to integers, and then stored into the level data vector. When the 'levelEnd'
string is found the loop is broken and the file is closed.
bool Level::loadLevel(std::string filename){
int *intarray;
//file in object
ifstream file;
//open the file for reading
file.open(filename.c_str());
if (!file.good()){
return false;
}
// a buffer to hold each line
char lineBuffer[CHARS_TO_READ];
// read a line into memory
file.getline(lineBuffer, CHARS_TO_READ);
char* token = 0; // initialize the token to 0
//splits the string on spaces
token = strtok(lineBuffer, " "); // first token
//do the workfor getting level data
if (strstr(token, "levelStart")){
while (!file.eof()){
//get the next line it should contain level data
file.getline(lineBuffer, CHARS_TO_READ);
//get the first token
token = strtok(lineBuffer, " ");
//check for the end level token
if (strstr(token, "levelEnd")){ break; }
//create a temp buffer, and fill it with zeros
intarray = new int[LEVEL_LENGTH];
for (int i = 0; i < LEVEL_LENGTH; i++){intarray[i] = 0;}
//place the first brick data into the array
intarray[0] = atoi(token);
//fill the array with the rest of the level data
for (unsigned int n = 1; n < LEVEL_LENGTH; n++)
{
token = strtok(0, " ");
//if there are no tokens left break
if (!token) break;
//convert the token to an integer and store it in the array
intarray[n] = atoi(token);
}
//place the line of level data into the leveldata vector
levelData.push_back((int*)intarray);
}
} //end get level data
The next function in the Level class is render.
Render uses the BRICK_LENGTH and BRICK_HEIGHT defined
at the top of the file to space the bricks. The position each brick is rendered
at is based on the index into the level data vector. For example, brick 1,3
is rendered at the start height minus 1 times the BRICK_HEIGHT and the start
left position plus 3 times the BRICK_LENGTH. The color of brick rendered is
based on the number stored in the level data. I have coded 1 as red, 2 as yellow,
and 3 as green.
void Level::render(LPDIRECT3DDEVICE9 device){
D3DXVECTOR3 position = D3DXVECTOR3(0.0f,0.0f,0.0f);
for (unsigned int i = 0; i < levelData.size(); i++){
for (int j = 0; j < LEVEL_LENGTH; j++){
position.x = STARTX + (float)j*BRICK_LENGTH;
position.y = STARTY - (float)i*BRICK_HEIGHT;
switch(levelData[i][j]){
case 0: break;
case 1: {
redBrick->setPosition(position);
redBrick->render(device);
break;
}
case 2: {
yellowBrick->setPosition(position);
yellowBrick->render(device);
break;
}
case 3: {
greenBrick->setPosition(position);
greenBrick->render(device);
break;
}
}
}
}
}
The last complicated function in the Level is getTileAtCoords,
getTileAtCoords takes an X and Y coordinate to check, and a pointer to
an indexi and indexj for the function to fill with the index to a brick if it
finds one. Passing pointers to a function is a common method for returning more
than one value. The function determines if the X and Y coordinates are in fact
colliding with a brick by calculating the top left and bottom right of each
brick and checking if the point lies within. When a brick is found the brick
number is returned and the indexi and indexj are set with the index to the brick
that has been collided.
//Go through each row of the level
for (unsigned int i = 0; i < levelData.size(); i++){
//calculate the top and bottom Y coord for this row of bricks
brickTopY = STARTY - (i * BRICK_HEIGHT) + HalfBrickHeight;
brickBottomY = STARTY - (i * BRICK_HEIGHT) - HalfBrickHeight;
//Go through each brick of the row
for (int j = 0; j < LEVEL_LENGTH; j++){
//calculate the left and right X value of each brick in the row
brickLeftX = (j * BRICK_LENGTH) - HalfBrickLength + STARTX;
brickRightX = (j * BRICK_LENGTH) + HalfBrickLength + STARTX;
// the request coords fall between the top and bottom and left and right of
the brick
// return the current brick as the brick at the coords.
if (x > brickLeftX && x < brickRightX){
if (y < brickTopY && y > brickBottomY){
brickAtLocation = levelData[i][j];
*indexi = i;
*indexj = j;
}
}
} //End each brick
}//End Each Row
return brickAtLocation;
}
The last function in Level is destroyBrick, and
it simply erases a brick at a given index by setting its value to 0.
void Level::destroyBrick(int i, int j){
//set the brick to 0, (no brick)
levelData[i][j] = 0;
}
Now that we have finished our Level class we need to call it from the Breakout
class. After making the appropriate additions to the header, I added the calls
to create the Level to the Breakout class's init. I have also added a camera
and a light, so we can see our level when we render it.
Changes to Breakout init:
bool Breakout::init(InputManager* input, SoundManager*
sounds, LPDIRECT3DDEVICE9 device){
myInput = input;
mySounds = sounds;
message = NO_MESSAGE;
//load the background image
background = new Surface();
background->loadSurface(device, "background.jpg"); //create the level object
level = new Level();
level->init("level1.txt", device);
//create the ball controller
//create the paddle controller
//setup the camera
camera = new Camera();
camera->create(device, 1.0f, 1000.f);
camera->setLookAt(D3DXVECTOR3(0.0f,0.0f,0.0f));
camera->setPosition(D3DXVECTOR3(0.0f,0.0f,-40.0f));
//create lighting
lights = new LightManager(device);
int light_one = lights->createLight();
lights->setPosition(light_one, D3DXVECTOR3(0.0f,10.0f,-20.0f));
lights->setRange(light_one, 100.0f);
return true;
}
The only other change required is to add a render call to the render in breakout.
Changes made to Breakout render:
void Breakout::render(LPDIRECT3DDEVICE9 device){
//render the background
background->render(device);
//render the level level->render(device);
//render the ball
//render the paddle
}
The Level has now been added to breakout. Compiling and running the project now,
will allow you to start a new game, and view the level stored in level1.txt.
Summing Up - Chapter 4
Loading and Storing level information can be a difficult task. However, tile
based games can easily store the level tiles in text the way we did in this
chapter. To return the location of each brick we cycled through the bricks and
checking the space they occupy. In a tile based game where the bricks do not
move such as this one, we could have figured out a formula to return the brick
number. Cycling through is less efficient, however if the tiles were to be moving
often there is little choice other than to cycle each tile in range. In this
case I chose to check each brick because it was easier to code.