How is texture data represented in TMX file? and how to get that data in C++

Hi there. I have re-worded this question as it was perhaps a bit vague. Basically I have my map tmx file imported into a C++ project. I believe it is imported as the game runs without errors, but of course as yet no map tiles have appeared on screen.

I’m struggling to understand where the information is stored that tells each tile what exact sprite sheet area it should draw. I have some code that finds my ‘sky’ layer for example, but I cannot find way to tell my own Tile class which texture to draw in each tile.

Here is my code that loads the map file.

void LevelManager::start(b2World* world)
{

worldPtr = world;
//BuildLevelFromString();

size_t level_width = 100; // TODO: Fix it so the code can find this number each level.
tmx::Map map;
if (map.load("LevelTmx/1-1.tmx"))
{
	const std::vector<std::unique_ptr<tmx::Layer>>& layers = map.getLayers();
	for (const std::unique_ptr<tmx::Layer>& layer : layers)
	{
		if (layer->getType() == tmx::Layer::Type::Object)
		{
			const tmx::ObjectGroup& objectLayer = layer->getLayerAs<tmx::ObjectGroup>();
			const std::vector<tmx::Object>& objects = objectLayer.getObjects();
			for (const tmx::Object object : objects)
			{
				// do stuff with object properties
			}
		}
		else if (layer->getType() == tmx::Layer::Type::Tile)
		{
			const tmx::TileLayer& tileLayer = layer->getLayerAs<tmx::TileLayer>();
			if (tileLayer.getName() == "sky")
			{
				for (int i = 0; i < tileLayer.getTiles().size(); i++)
				{
					Tile tile;
					unsigned long long tile_x = i % level_width;
					unsigned long long tile_y = i / level_width;

					// HELP IM A LOST HERE....
				}
			}
		}
	}

	const std::vector<tmx::Tileset>& tilesets = map.getTilesets();
	for (const auto& tileset : tilesets)
	{
		//read out tile set properties, load textures etc...
	}
}
camera.initView(level_width * Constants::PPU, Constants::LEVEL_TILECOUNT_Y * Constants::PPU);



guy.start(worldPtr);
      }

Documentation:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset

If your tilesets are in an external tileset file (TSX), then the <tileset> data will be in that, and the <tileset> elements in the TMX file will have a src attribute that points to the TSX files (or to JSON files, in which case the format will be JSON instead of XML).

Within <tileset>, the firstgids will help you decide which tile belongs to which tileset, and the <image> element will tell you which file to use as the texture. The portion of the texture to draw is determined by the ID of the tile within the tileset (i.e. tileID - the tileset’s firstgid), the tilewidth, the tileheight, and the texture’s width.

X and Y coordinates of a tile within the texture, in pixels:

tilesPerRow = floor(textureWidth / tileWidth)
x = ( (tileID - firstgid) % tilesPerRow )  * tileWidth
y = floor( (tileID - firstgid) / tilesPerRow ) * tileHeight

(If you’re using integers for these values, you don’t need the explicit flooring, int division rounds down implicitly.)

1 Like

Thank you for this. I haven’t yet started trying to implement it but its great to have these concise instructions.
There is still something i’m a little confused over…

My tilesheet is one png file, the Tiled software seems to have automatically split that into 16x16 tiles as I requested (ie when i made the map in Tiled, i could use each tile individually). Do I need to manually in my code specify these grids on the spritesheet when i ‘find’ the correct texture, or is there a parameter/property in the Tiled output (TMX etc) that already contains this information?

I was kinda hoping (and from memory it was similar to this when i used Tiled in LibGDX), that I could just point all the tiles to one spritesheet, and then pull information from Tiled files to tell it what position (and size, although in my case it is always 16x16) of the area in the spritesheet to draw.

EDIT: I guess I am actually still confused exactly what a ‘tileset’ is. Is that not the entire file (ie. All tiles?) What is the importance of knowing the first global ID for the first tile? …very confused now :confused:

The tileset is the image plus some metadata about it (the image size, the tile size, and it can also contain other info, like collision shapes if you use that feature in Tiled).

The tileset image is just your tileset texture that has all your tiles in it. The tiles are sub-rectangles from that texture. The x and y coordinates are described above are the top left corner of it. When you want to draw a tile, just draw the tileWidth x tileHeight (16x16 in your case) rectangle that has its top left corner at those coordinates.

The firstgid is mostly important if you use multiple tilesets. If you only have one tileset in your map, the firstgid will always be 1 (it should still be subtracted from the tile ID so that you can calculate the tile coordinates correctly). When the tile ID is 0 in the map file, that means “no tile” (so don’t draw anything), and tile ID 1 is the first (top left) tile in the tileset (local ID 0, since it’s row 0, column 0).

If you only have one tileset in the map, then what you want to do is exactly how it should be done - point all your tiles to the one tileset image, and just define their positions within the tileset.

While I’m here, there is one more complicating factor in Tiled - flipping. The tile IDs are stored as an int containing the tile ID and three bits used as flip flags. The details are here:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile-flipping

1 Like

Thank you , i think i get it now… was over complicating it a bit in my head haha. I think you’ve given me enough info to complete this function, so i will give it a try now. Thanks again

1 Like

Scratch my last comment (deleted and replaced with this one). I think ive made progress by using tileset.Tiles[i].imagePosition.x and .y . But now I just need a way to add that info, to my existing grid. If I struggle more I will make a new topic . cheers for the help :smiley:

Hey @supermoneylife, it’s great to hear that you’re making progress and I think @eishiya is amazing for taking the time to explaining this stuff in detail. Indeed this is a topic where the Tiled manual is severely lacking.

Btw, you seem to be using some C++ library to load and process your maps. In this case, please also let us know which library it is that you’re using. It can really help us provide more specific instructions or to guide you to the right place or person to ask for help.

In any case, good luck with your project!

1 Like

Hi @bjorn . I am indeed thankful to @eishiya and all others who are kind enough to help out.

I am using Tmxlite (although I also tried Tileson) . Somehow I have managed to fudge through it and I now have a rudimentary platforrmer with box2d and Tiled map support. There was one bit of my code that I don’t fully understand, at first when i got it to work, all the ‘empty’ tiles where rendering some other tile everywhere (so not empty in other words). I had to get the sourceTiles_index minus 1 when i accessed my ‘sourceTiles’ array which contains the member ‘ImagePosition’.

I might be doing it wrong, as it seemed I had to make to iterating loops, one to get the ‘Tile’ which only really contained the ‘ImagePosition’ vector, then i had to iterate through the layers to find each ‘object layer’ and ‘tile layer’.

Maybe i have done it wrong but i will post it here (whilst editing out the non-Tiled related code where possible) in case it helps someone, or if someone can see any obvious mistakes:

#include <tmxlite/Map.hpp>
#include <tmxlite/Layer.hpp>
#include <tmxlite/TileLayer.hpp>
#include <tmxlite/ObjectGroup.hpp>

void LevelManager::start(b2World* world)
{

worldPtr = world;


tmx::Map map;
unsigned int level_width = 0;
unsigned int level_height = 0;
unsigned int tile_width = 0;
unsigned int tile_height = 0;
if (map.load("LevelTmx/1-1.tmx"))
{
	level_width = map.getTileCount().x;
	level_height = map.getTileCount().y;
	tile_width = map.getTileSize().x;
	tile_height = map.getTileSize().y;
	std::vector<tmx::Tileset::Tile> sourceTiles;
	const auto& tilesets = map.getTilesets(); 
	for (const auto& tileset : tilesets) // as above
	{
		//read out tile set properties, load textures etc...

		
		sourceTiles = tileset.getTiles();
		for (int i = 0; i < sourceTiles.size(); i++)
		{
			//std::cout << "TILESET: i:" << i << " || sourceTile ID:" << sourceTiles[i].ID << std::endl;
			//std::cout << "TILESET: Image position: " << sourceTiles[i].imagePosition.x << " , " << sourceTiles[i].imagePosition.y << std::endl;
		}
		if (level_spriteSheet.loadFromFile(tileset.getImagePath()))
		{
			std::cout << "Level sprite sheet path: " << tileset.getImagePath() << std::endl;
		}
		else
		{
			std::cout << "Texture Not Loaded!" << std::endl;
		}
	}

	const auto& layers = map.getLayers();
	for (const auto& layer : layers)
	{
		if (layer->getType() == tmx::Layer::Type::Object)
		{
			const auto& objectLayer = layer->getLayerAs<tmx::ObjectGroup>();
			if (objectLayer.getName() == "solid")
			{
				const auto& objects = objectLayer.getObjects();
				for (const auto object : objects)
				{
					Collider collider;
					collider.start(object.getAABB(), worldPtr);
					colliders_ground.push_back(collider);
				}
			}
			
			if (objectLayer.getName() == "blocks_?")
			{
				const auto& objects = objectLayer.getObjects();
				for (const auto object : objects)
				{
					Block block;
					tmx::FloatRect aabb = object.getAABB();
					block.start(Block::BlockType::QUESTION_MARK, aabb.left, aabb.top, tile_width, tile_height, level_spriteSheet, sourceTiles[Constants::BLOCK_Q_ID].imagePosition.x, sourceTiles[Constants::BLOCK_Q_ID].imagePosition.y, worldPtr);
					all_blocks.push_back(block);
				}
			}

			if (objectLayer.getName() == "blocks_brick")
			{
				const auto& objects = objectLayer.getObjects();
				for (const auto object : objects)
				{
					Block block;
					tmx::FloatRect aabb = object.getAABB();
					block.start(Block::BlockType::BRICK, aabb.left, aabb.top, tile_width, tile_height, level_spriteSheet, sourceTiles[Constants::BLOCK_BRICK_ID].imagePosition.x, sourceTiles[Constants::BLOCK_BRICK_ID].imagePosition.y, worldPtr);
					all_blocks.push_back(block);
				}
			}
		}
		else if (layer->getType() == tmx::Layer::Type::Tile)
		{
			const auto& tileLayer = layer->getLayerAs<tmx::TileLayer>();
			// read out the layer properties etc...

			auto& tiles = tileLayer.getTiles();
			for (int i = 0; i < tiles.size(); i++)
			{
				uint32_t tileID = tiles[i].ID;
				uint8_t flipFlag = tiles[i].flipFlags;
				unsigned long long tile_x = i % level_width;
				unsigned long long tile_y = i / level_width;
				if (tileID <= 0)
					continue;
				Tile tile;
				// NOTE: Here is where i added minus 1, but couldnt quite understand why i needed it (it was from trial and error)
				tile.start(tile_x, tile_y, tile_width, tile_height, level_spriteSheet, sourceTiles[tileID - 1].imagePosition.x, sourceTiles[tileID - 1].imagePosition.y);
				levelTiles.push_back(tile);
			}
		}
	}
}
else
{
	std::cout << "Failed to load map" << std::endl;
}
camera.initView(level_width * tile_width, level_height * tile_height);



guy.start(worldPtr);
 }

The “minus 1” is the firstgid thing I mentioned in my second reply. You need to subtract the firstgid to get the local tile ID, and when you only have one tileset, the firstgid is always 1.

Your code’s looking somewhat similar to my parser code, if that’s any comfort :]

2 Likes

Just note that in these loop, you’re copying each object, which is probably not what you intended. To avoid the copy, write:

for (const auto& object : objects)

Most of your loops are already written like this, but for some reason not the ones iterating objects. :slight_smile:

1 Like

Thank you so much @eishiya ! I understand it now :smiley:
And @bjorn thank you too for pointing that out. I’ve made some huge strides since my last message, but still long way to go. Github repo here if you want to have a nose :smiley: https://github.com/Golden-Nuggs/Mario