Introduction to C++ with Game Development: Part 11, Tiles

Introduction

It's time to introduce you to a great concept for 2D graphics in C++, that you will find very useful and easy to use: Tiles. Tiles are a simple way of displaying 2D graphics for games such as Mahjong, strategic-type turn-based games and so on. We will be using a 2D array to store a grid of square images. We will also use Sprites.

Getting the stuff you need

Get prepared by extracting the familiar template package to a new directory, and load up the .sln file. Remove the stuff in the Tick function. Also, don't forget to enable debug mode.

Rendering a tile map

We will start by adding some terrain as a background for our tank game. The tiles are stored in 8 separate files (in assets/tiles), which we will load to small surfaces. The backdrop that we will draw will show 8x8 tiles; each of these positions will refer to one of the 8 tiles. We will use a 2-dimensional array for the backdrop.

Surface* tileSet[8];
int landTile[8][8];

Change the Game::Init() to the following:

void Game::Init()
{
   tileSet[0] = new Surface( "assets/tiles/tile1.png" );
   tileSet[1] = new Surface( "assets/tiles/tile2.png" );
   tileSet[2] = new Surface( "assets/tiles/tile3.png" );
   tileSet[3] = new Surface( "assets/tiles/tile4.png" );
   tileSet[4] = new Surface( "assets/tiles/tile5.png" );
   tileSet[5] = new Surface( "assets/tiles/tile6.png" );
   tileSet[6] = new Surface( "assets/tiles/tile7.png" );
   tileSet[7] = new Surface( "assets/tiles/tile8.png" );
   for (int indexX = 0; indexX <= 7; indexX++)
   {
      for (int indexY = 0; indexY <= 7; indexY++)
      {
          landTile[indexX][indexY] = 0; // sets terrian type to zero
      }
   }
}

In the Tick function, we will draw the tilemap:

void Game::Tick( float a_DT )
{
    m_Screen->Clear( 0 );
    for (int indexX = 0; indexX <= 7; indexX++)
    {
        for (int indexY = 0; indexY <= 7; indexY++)
        {
            int tile = landTile[indexY][indexX];
            tileSet[tile]->CopyTo( m_Screen, indexX * 64, indexY * 64 );
        }
    }
}

When you run this code, you will see that your entire backdrop now consists of one tile, repeated 8x8 times:

tiles

Time to draw a more interesting backdrop. Remove the double for-loop from the Init function. Effectively, this removes the bit that sets every tile to zero. Zero is the first tile in the tile set; this explains why we got an 8x8 map filled with tile1.png. Instead of that code, we fill the 2D tile array directly. Find the line that reads:

    int landTile[8][8];

And replace it by

int landTile[8][8] = {{0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 1, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0}};

This interesting but mysterious construct fills the array directly: 8x8 numbers are used; and to illustrate the concept, one is set to 1 instead of 0. When you run the application again, you'll see that this time, one tile looks different. Let's make some more changes:

int landTile[8][8] = {{0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {6, 6, 6, 6, 6, 6, 6, 6},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 0},
                      {0, 0, 0, 0, 0, 0, 0, 7},
                      {0, 0, 0, 0, 0, 0, 7, 7}};

tiles2

Now, editing your level right there in the .cpp file is not a good idea in the longer run: at some point you'll want to load levels from disk. We'll get to that at a later point in the course (lesson 18, on File I/O).

Your assignment for today will have you implement collision of a player sprite with tiles, so let's prepare ourselves. First, we need a player tank:

Sprite tank( new Surface( "assets/ctankbase.tga" ), 16 );
int tankX = 0, tankY = 0;

Then, controls for the tank. For this, we'll use a window function, called GetAsyncKeyState. This function allows you to probe the state of a single key. You'll probably find this useful in many of your future projects.

void Game::Tick( float a_DT )
{
    m_Screen->Clear( 0 );

    for (int indexX = 0; indexX <= 7; indexX++)
    {
        for (int indexY = 0; indexY <= 7; indexY++)
        {
            int tile = landTile[indexY][indexX];
            tileSet[tile]->CopyTo( m_Screen, indexX * 64, indexY * 64 );
        }
    }

    tank.Draw( tankX, tankY, m_Screen );

    if (GetAsyncKeyState( VK_UP ))    tankY -= 1;
    if (GetAsyncKeyState( VK_DOWN ))  tankY += 1;
    if (GetAsyncKeyState( VK_RIGHT )) tankX += 1;
    if (GetAsyncKeyState( VK_LEFT ))  tankX -= 1;

    Sleep( 10 );
}

There you go; one player controlled tank, on a backdrop that can be edited. By the way, the Sleep is there to slow everything down a bit: otherwise, your tank will leave the map before you know it.

Tile map collision

Tile maps are useful for many things. The most obvious reason for using tiles is easy level editing, as you have seen in the previous section. Another reason is saving memory: many older games used tiles exclusively for this reason. This is not really an issue anymore on more recent hardware, but quite a few games still use tiles nevertheless. One of the reasons is that they make your game logic easier. Let's have a look at how you would implement collision detection in a tile based game.

First of all, you need to decide where your player can't go. Let's say that the player may not walk on tile 6 in this case. Secondly, you need to know on which tile the player is. Note that most of the time, the player will touch more than one tile (up to 4, if the player is the same size as or smaller than a tile). Third, you need to decide what to do when your movement is blocked.

Regarding the second bullet point: if a tile is 64 pixels wide, and the player is at position 128, the player sprite top-left corner is at x-coordinate 128. Assuming the player sprite is 64 pixels wide, the player sprite top-right corner is at x-coordinate 128 + 63. This means the player overlaps tile column 2. Move the player 1 pixel to the right, and now he overlaps column 2 and 3. Draw out the situation on a piece of paper if you have trouble visualizing this in your mind.

Regarding blocking movement: when the player moves to a new position because of a keypress, it is probably a good idea to store the new position in a temporary variable, then test the new position, and only if the new position is valid (not blocked) you copy it to tankX and tankY.

Assignment

  • Design a cool backdrop using the numbers in the 8x8 array. Decide which tile is an obstacle.
  • Implement tile collision detection using the hints in the last section.
  • Adjust your code so that the player sprite faces in the correct direction when it moves.

Comments

Commenting will be coming soon. In the meantime, feel free to create a discussion topic on the forums.