Experiments In Game Programming
Main
 Home
XNA - C#
 Coming Soon!
 Level Editing - GTK
DirectX 9 - C++
 Downloads
 Disclaimer
 Introduction
Part 1 - DirectX
 1 - Breakout
 2 - Create DX
 3 - 2d Images
 4 - 3d Models
 5 - Cameras & Lights
 6 - Animation Timing
 7 - Keyboard/Mouse
 8 - Sound
Part 2 - Breakout
 1 - Art and Sounds
 2 - The Menu
 3 - Starting Breakout
 4 - The Level
 5 - The Paddle
 6 - The Ball
 7 - Finishing Touches

Untitled Document

Chapter 4

 

Creating a Level

 

 

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

    
     file.close();
     //end token collection
     return true;
}


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.
int Level::getTileAtCoords(float x, float y, int* indexi, int* indexj){
     float HalfBrickLength = BRICK_LENGTH/2;
     float HalfBrickHeight = BRICK_HEIGHT/2;
     float brickLeftX;
     float brickRightX;
     float brickTopY;
     float brickBottomY;

     int brickAtLocation = 0;

     //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.
 ©2008 David Whittaker