Parsing and drawing a tile map

Now that we are relatively familiar with creating tile maps in the Tiled application we will move on to parsing them and drawing them in our game. We are going to create quite a few new classes starting with a class called Level that will hold our tilesets and also draw and update our separate layers. Let's go ahead and create Level.h in our project and add the following code:

class Level
{
  public:

  Level();
  ~Level() {}

  void update();
  void render();
};

We will also define a struct at the top of this file called Tileset:

struct Tileset
{
  int firstGridID;
  int tileWidth;
  int tileHeight;
  int spacing;
  int margin;
  int width;
  int height;
  int numColumns;
  std::string name;
};

This struct holds any information we need to know about our tilesets. Our Level class will now also hold a vector of Tileset objects:

private:

  std::vector<Tileset> m_tilesets;

Next we will create a public getter function that returns a pointer to this Tileset vector:

std::vector<Tileset>* getTilesets() 
{ 
    return &m_tilesets;  
}

We will pass this into our parser when we come to load the map.

The next class we will create is an abstract base class called Layer. All of our layer types will derive from this class. Create Layer.h and add the following code:

class Layer
{
  public:

  virtual void render() = 0;
  virtual void update() = 0;

  protected:

  virtual ~Layer() {}
};

Now that we have the Layer class we will store a vector of the Layer* objects in the Level class. Back in Level.h add our vector:

std::vector<Layer*> m_layers;

And a getter function:

std::vector<Layer*>* getLayers() 
{ 
    return &m_layers; 
}

Now we have a basic Level class in place; its purpose is to store, draw, and update our layers. We will define the functions for Level in a Level.cpp file:

void Level::render()
{
  for(int i = 0; i < m _layers.size(); i++)
  {
    m_layers[i]->render();
  }
}
void Level::update()
{
  for(int i = 0; i < m_layers.size(); i++)
  {
    m_layers[i]->update();
  }
}

Creating the TileLayer class

Our first layer type is going to be a TileLayer. This type of layer is made up entirely of tiles and does not contain anything else. We have already created a layer like this in the Tiled application. Create TileLayer.h and we can start to write up this class:

class TileLayer : public Layer
{
  public:

    TileLayer(int tileSize, const std::vector<Tileset> &tilesets);

    virtual void update();
    virtual void render();

    void setTileIDs(const std::vector<std::vector<int>>& data)  
    {  
        m_tileIDs = data;  
    }

    void setTileSize(int tileSize)  
    {  
        m_tileSize = tileSize;  
    }

    Tileset getTilesetByID(int tileID);

  private:

    int m_numColumns;
    int m_numRows;
    int m_tileSize;

    Vector2D m_position;
    Vector2D m_velocity;

    const std::vector<Tileset> &m_tilesets;

    std::vector<std::vector<int>> m_tileIDs;
};

There is nothing too complicated about this class; it holds data for our tile layer. The Vector2D variables are used when we start to scroll our maps. We will not define this class' functions properly right now, but you will need to create empty definitions along with defining the vector constants in a TileLayer.cpp file.

Creating the LevelParser class

Now that we have the basic level and layer classes in place, we can move onto creating a parser for our .tmx files and creating levels from them. Create LevelParser.h:

Class LevelParser
{
  public:

    Level* parseLevel(const char* levelFile);

  private:

    void parseTilesets(TiXmlElement* pTilesetRoot,std::vector<Tileset>* pTilesets);

    void parseTileLayer(TiXmlElement* pTileElement,std::vector<Layer*> *pLayers, const std::vector<Tileset>*pTilesets);

    int m_tileSize;
    int m_width;
    int m_height;
};

The parseLevel function is what we will call whenever we want to create a level. To ensure that this function must be used to create a Level object, we will make the Level class' constructor private and make it a friend class of LevelParser:

private:

  friend class LevelParser;
  Level();

Now LevelParser has access to the private constructor of Level and can return new instances. We can now define the parseLevel function and then go through it step-by-step. Create LevelParser.cpp and define the parseLevel function as follows:

Level* LevelParser::parseLevel(const char *levelFile)
{
    // create a TinyXML document and load the map XML
    TiXmlDocument levelDocument;
    levelDocument.LoadFile(levelFile);

    // create the level object
    Level* pLevel = new Level();

    // get the root node 
    TiXmlElement* pRoot = levelDocument.RootElement();

    pRoot->Attribute("tilewidth", &m_tileSize);
    pRoot->Attribute("width", &m_width);
    pRoot->Attribute("height", &m_height);

    // parse the tilesets
    for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
    {
      if(e->Value() == std::string("tileset"))
      {
         parseTilesets(e, pLevel->getTilesets());
      }
    }

    // parse any object layers
    for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
    {
      if(e->Value() == std::string("layer"))
      {
        parseTileLayer(e, pLevel->getLayers(), pLevel->getTilesets());
      }
    }

    return pLevel;
}

We covered XML files and TinyXML in the previous chapter so I won't go into detail again here. The first part of the function grabs the root node:

// get the root node 
TiXmlElement* pRoot = levelDocument.RootElement();

We can see from the map file that this node has several attributes:

<map version="1.0" orientation="orthogonal" width="60" height="15" tilewidth="32" tileheight="32">

We grab these values using the Attribute function from TinyXML and set the member variables of LevelParser:

pRoot->Attribute("tilewidth", &m_tileSize);
pRoot->Attribute("width", &m_width);
pRoot->Attribute("height", &m_height);

Next we must check for any tileset nodes and parse them, using the getTilesets function of our newly created Level instance to pass in the Tileset vector:

// parse the tilesets
for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
{
  if(e->Value() == std::string("tileset"))
  {
    parseTilesets(e, pLevel->getTilesets());
  }
}

Finally we can check for any tile layers and then parse them, again using the getter functions from our pLevel object, which we then return:

// parse any object layers
for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
{
  if(e->Value() == std::string("layer"))
  {
    parseTileLayer(e, pLevel->getLayers(), pLevel->getTilesets());
  }
}

return pLevel;
}

You can see that this function is very similar to our parseState function from the previous chapter. Now we must define the parseTilesets and parseTileLayer functions.

Parsing tilesets

Parsing tilesets is actually quite simple due to our TextureManager class:

void LevelParser::parseTilesets(TiXmlElement* pTilesetRoot, std::vector<Tileset>* pTilesets)
{
  // first add the tileset to texture manager
    TheTextureManager::Instance()->load(pTilesetRoot->FirstChildElement()->Attribute("source"), pTilesetRoot->Attribute("name"), TheGame::Instance()->getRenderer());

  // create a tileset object
  Tileset tileset;
  pTilesetRoot->FirstChildElement()->Attribute("width", &tileset.width);
  pTilesetRoot->FirstChildElement()->Attribute("height", &tileset.height);
  pTilesetRoot->Attribute("firstgid", &tileset.firstGridID);
  pTilesetRoot->Attribute("tilewidth", &tileset.tileWidth);
  pTilesetRoot->Attribute("tileheight", &tileset.tileHeight);
  pTilesetRoot->Attribute("spacing", &tileset.spacing);
  pTilesetRoot->Attribute("margin", &tileset.margin);
  tileset.name = pTilesetRoot->Attribute("name");

  tileset.numColumns = tileset.width / (tileset.tileWidth + tileset.spacing);

  pTilesets->push_back(tileset);
}

We add the tileset to the TextureManager class using its attributes and then create a Tileset object and push it into the pTilesets array. The pTilesets array is actually a pointer to the array from our pLevel object which we previously created in the parseLevel function. Here is our first tileset so that you can look at it alongside the preceding function:

<tileset firstgid="1" name="blocks1" tilewidth="32" tileheight="32"spacing="2" margin="2">
  <image source="blocks1.png" width="614" height="376"/>
</tileset>

Parsing a tile layer

Due to the compression and encoding of our tile IDs, this function is actually quite complicated. We are going to make use of a few different libraries that will help us to decode and decompress our data, the first of which is a Base64 decoder. We will be using a decoder created by René Nyffenegger, available from the source code downloads and also from https://github.com/ReneNyffenegger/development_misc/tree/master/base64. The base64.h and base64.cpp files can be added directly to the project.

The second library we will need is the zlib library, a compiled version is available at http://www.zlib.net and can be easily added to your project like any other library. Once these libraries are available to the project we can start parsing our tiles:

void LevelParser::parseTileLayer(TiXmlElement* pTileElement, std::vector<Layer*> *pLayers, const std::vector<Tileset>* pTilesets)
{
  TileLayer* pTileLayer = new TileLayer(m_tileSize, *pTilesets);

    // tile data
  std::vector<std::vector<int>> data;

  std::string decodedIDs;
  TiXmlElement* pDataNode;

  for(TiXmlElement* e = pTileElement->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
  {
    if(e->Value() == std::string("data"))
    {
      pDataNode = e;
    }
  }

  for(TiXmlNode* e = pDataNode->FirstChild(); e != NULL; e = e->NextSibling())
  {
    TiXmlText* text = e->ToText();
    std::string t = text->Value();
    decodedIDs = base64_decode(t);
  }

    // uncompress zlib compression
  uLongf numGids = m_width * m_height * sizeof(int);
  std::vector<unsigned> gids(numGids);
  uncompress((Bytef*)&gids[0], &numGids,(const Bytef*)decodedIDs.c_str(), decodedIDs.size());

  std::vector<int> layerRow(m_width);

  for(int j = 0; j < m_height; j++)
  {
    data.push_back(layerRow);
  }

  for(int rows = 0; rows < m_height; rows++)
  {
    for(int cols = 0; cols < m_width; cols++)
    {
      data[rows][cols] = gids[rows * m_width + cols];
    }
  }

  pTileLayer->setTileIDs(data);

  pLayers->push_back(pTileLayer);
}

Let's go through this function step-by-step. First we create a new TileLayer instance:

TileLayer* pTileLayer = new TileLayer(m_tileSize, *pTilesets);

Next we declare some needed variables; a multidimensional array of int values to hold our final decoded and uncompressed tile data, a std::string that will be our base64 decoded information and finally a place to store our XML node once we find it:

// tiledata
std::vector<std::vector<int>> data;

std::string decodedIDs;
TiXmlElement* pDataNode;

We can search for the node we need in the same way we have previously done:

for(TiXmlElement* e = pTileElement->FirstChildElement(); e != NULL; e = e->NextSiblingElement())
{
    if(e->Value() == std::string("data"))
    {
      pDataNode = e;
    }
}

Once we have found the correct node we can then get the text from within it (our encoded/compressed data) and use the base64 decoder to decode it:

for(TiXmlNode* e = pDataNode->FirstChild(); e != NULL; e = e->NextSibling())
{
  TiXmlText* text = e->ToText();
  std::string t = text->Value();
  decodedIDs = base64_decode(t);
}

Our decodedIDs variable is now a base64 decoded string. The next step is to use the zlib library to decompress our data, this is done using the uncompress function:

// uncompress zlib compression
uLongf sizeofids = m_width * m_height * sizeof(int);

std::vector<int> ids(m_width * m_height);

uncompress((Bytef*)&ids[0], &sizeofids,(const Bytef*)decodedIDs.c_str(), decodedIDs.size());

The uncompress function takes an array of Bytef* (defined in zlib's zconf.h) as the destination buffer; we are using an std::vector of int values and casting it to a Bytef* array. The second parameter is the total size of the destination buffer, in our case we are using a vector of int values making the total size the number of rows x the number of columns x the size of an int; or m_width * m_height * sizeof(int). We then pass in our decoded string and its size as the final two parameters. Our ids vector now contains all of our tile IDs and the function moves on to set the size of our data vector for us to fill with our tile IDs:

std::vector<int> layerRow(m_width);
for(int j = 0; j < m_height; j++)
{
  data.push_back(layerRow);
}

We can now fill our data array with the correct values:

for(int rows = 0; rows < m_height; rows++)
{
  for(int cols = 0; cols < m_width; cols++)
  {
    data[rows][cols] = ids[rows * m_width + cols];
  }
}

And finally we set this layer's tile data and then push the layer into the layers array of our Level.

We must now define the functions in our Level.cpp file.

Drawing the map

We are finally at a stage where we can start drawing our tiles to the screen. Inside the earlier created TileLayer.cpp file we will now need to define our functions for the layer. Starting with the constructor:

TileLayer::TileLayer(int tileSize, const std::vector<Tileset> &tilesets) : m_tileSize(tileSize), m_tilesets(tilesets), m_position(0,0), m_velocity(0,0)
{
  m_numColumns = (TheGame::Instance()->getGameWidth() / m_tileSize);
  m_numRows = (TheGame::Instance()->getGameHeight() / m_tileSize);
}

The new Game::getGameWidth and Game::getGameHeight functions are just simple getter functions that return variables set in the Game::init function:

int getGameWidth() const  
{  
  return m_gameWidth;  
}
int getGameHeight() const  
{  
  return m_gameHeight;  
}

The TileLayer update function uses velocity to set the map's position; we will cover this in more detail when we come to scroll our map:

void TileLayer::update()
{
  m_position += m_velocity;
}

The render function is where all the magic happens:

void TileLayer::render()
{
  int x, y, x2, y2 = 0;

  x = m_position.getX() / m_tileSize;
  y = m_position.getY() / m_tileSize;

  x2 = int(m_position.getX()) % m_tileSize;
  y2 = int(m_position.getY()) % m_tileSize;

  for(int i = 0; i < m_numRows; i++)
  {
    for(int j = 0; j < m_numColumns; j++)
    {
        int id = m_tileIDs[i][j + x];

          if(id == 0)
          {
            continue;
          }

        Tileset tileset = getTilesetByID(id);

        id--;

        TheTextureManager::Instance()->drawTile(tileset.name, 2, 2, (j * m_tileSize) - x2, (i * m_tileSize) - y2, m_tileSize, m_tileSize, (id - (tileset.firstGridID - 1)) / tileset.numColumns, (id - (tileset.firstGridID - 1)) % tileset.numColumns, TheGame::Instance()->getRenderer());
    }
  }
}

You will notice that there is a new function in the TextureManager, drawTile. This function is specifically for drawing tiles and includes margin and spacing values. Here it is:

void TextureManager::drawTile(std::string id, int margin, int spacing, int x, int y, int width, int height, int currentRow, int currentFrame, SDL_Renderer *pRenderer)
{
  SDL_Rect srcRect;
  SDL_Rect destRect;
  srcRect.x = margin + (spacing + width) * currentFrame;
  srcRect.y = margin + (spacing + height) * currentRow;
  srcRect.w = destRect.w = width;
  srcRect.h = destRect.h = height;
  destRect.x = x;
  destRect.y = y;

  SDL_RenderCopyEx(pRenderer, m_textureMap[id], &srcRect,&destRect, 0, 0, SDL_FLIP_NONE);
}

Let's look closer at the render function; we will ignore the positioning code for now:

for(int i = 0; i < m_numRows; i++)
{
  for(int j = 0; j < m_numColumns; j++)
  {
    int id = m_tileIDs[i][j + x];

    if(id == 0)
    {
      continue;
    }

    Tilesettileset = getTilesetByID(id);

    id--;

    TheTextureManager::Instance()->drawTile(tileset.name,tileset.margin, tileset.spacing, (j * m_tileSize) - x2, (i *m_tileSize) - y2, m_tileSize, m_tileSize, (id -(tileset.firstGridID - 1)) / tileset.numColumns, (id -(tileset.firstGridID - 1)) % tileset.numColumns,TheGame::Instance()->getRenderer());
  }
}

We loop through the number of columns and the number of rows:

for(int i = 0; i < m_numRows; i++)
{
  for(int j = 0; j < m_numColumns; j++)
{

This is not the number of rows and columns in the full tile ID array, it is actually the number of columns and rows needed to fill the size of our game. We do not want to be drawing anything that we do not have to. We obtained these values earlier in the constructor:

m_numColumns = (TheGame::Instance()->getGameWidth() / m_tileSize);
m_numRows = (TheGame::Instance()->getGameHeight() / m_tileSize);

Next we get the current tile ID from the array (ignore the + x for now):

int id = m_tileIDs[i + y][j + x];

We check if the tile ID is 0. If it is, then we do not want to draw anything:

if(id == 0)
{
  continue;
}

Otherwise we grab the correct tileset:

Tileset tileset = getTilesetByID(id);

Getting the tileset uses a very simple function, getTilesetByID, which compares each tileset's firstgid value and returns the correct tileset:

Tileset TileLayer::getTilesetByID(int tileID)
{
  for(int i = 0; i < m_tilesets.size(); i++)
  {
    if( i + 1 <= m_tilesets.size() - 1)
    {
      if(tileID >= m_tilesets[i].firstGridID&&tileID < m_tilesets[i + 1].firstGridID)
      {
        return m_tilesets[i];
      }
    }
    else
    {
      return m_tilesets[i];
    }
  }

  std::cout << "did not find tileset, returning empty tileset
";
  Tileset t;
  return t;
}

Next we move on to drawing the tiles:

id--;

TheTextureManager::Instance()->drawTile(tileset.name, 
  tileset.margin, tileset.spacing, (j * m_tileSize) - x2, (i * 
  m_tileSize) - y2, m_tileSize, m_tileSize, (id - 
  (tileset.firstGridID - 1)) / tileset.numColumns, (id - 
  (tileset.firstGridID - 1)) % tileset.numColumns, 
    TheGame::Instance()->getRenderer());
  }
}

First we decrement the ID so that we can draw the correct tile from the tilesheet, even if it is at position 0,0. We then use the drawTile function to copy across the correct tile using the tileset we grabbed earlier, to set the first parameter of the function, which is the name of the texture. Again, we can use the tileset for the next two parameters, margin and spacing:

tileset.margin, tileset.spacing

The next two parameters set the position we want to draw our tiles at:

(j * m_tileSize) - x2, (i * m_tileSize) - y2

Ignoring the x2 and y2 values for now (they are 0 anyway), we can set the current x position as the current column multiplied by the width of a tile and the y value as the current row multiplied by the height of a tile. We then set the width and height of the tile we are copying across:

m_tileSize, m_tileSize,

And finally we work out the location of the tile on the tilesheet:

(id - (tileset.firstGridID - 1)) / tileset.numColumns, 
(id - (tileset.firstGridID - 1)) % tileset.numColumns,

We subtract the firstGridID - 1 to allow us to treat each tilesheet the same and obtain the correct location. For example, the firstGridID of a tileset could be 50 and the current tile ID could be 70. We know that this is actually going to be tile 19 (after we decrement the ID) on the tilesheet itself.

Finally, we must create a level in our PlayState class:

bool PlayState::onEnter()
{
  LevelParser levelParser;
  pLevel = levelParser.parseLevel("assets/map1.tmx");

  std::cout << "entering PlayState
";
  return true;
}

Next, draw it in the render function, and also do the same with the update function:

void PlayState::render()
{
  pLevel->render();
}

We will also have to comment out any functions that use objects (such as collisionChecks) as we don't have any yet and this will cause a runtime error. Run our game and you will see our tile map being drawn to the screen.

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

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