Glusoft

Import tiled maps

Tiled map from Tiled after import

In this tutorial we will use the tileset from Namatnieks and the goal is to import tiled maps with SFML
You can download the map and tilesets I will use : map.7z

I will use RapidJSON to parse the json files.

Why use json instead of the default tmx and tsx files?
JSON tend to be less verbose and easier to parse.

But you can do the same thing with XML files, if you use a fast parser like RapidXML you will not see the difference in speed.

Loading the map and the tilesets

The first thing to do in the main function is creating the window, then load the map.

sf::RenderWindow window(sf::VideoMode(512, 320), "Tiled map loader");

const char* mapStr = readFile("map.json");
Map map(mapStr);

Read file

I load the whole map file in memory because the map is small. For that we can use a helper function:

inline const char* readFile(const char* filename) {
	FILE* f = fopen(filename, "r");

	fseek(f, 0, SEEK_END);
	size_t size = ftell(f);

	char* buffer = new char[size];

	rewind(f);
	fread(buffer, sizeof(char), size, f);

	return buffer;
}

Map class

After the JSON is loaded in memory, we create a new class Map representing the data of the map file after the parsing.

class Map {
public:
	Map(const char* map);
	~Map();

	int getHeight() { return height; }
	bool isInfinite() { return infinite; }
	std::vector<Layer> getLayers(){ return layers; }
	int getNextLayerId() { return nextlayerid; }
	int getNextObjectId() { return nextobjectid; }
	std::string getOrientation() { return orientation; }
	std::string getRenderOrder() { return renderorder; }
	std::string getTiledVersion(){ return tiledversion; }
	int getTileHeight() { return tileheight; }
	std::vector<TileSetMap> getTileSetMap() { return tilesets; }
	int getTileWidth() { return tilewidth; }
	std::string getType() { return type; }
	float getVersion() { return version; }
	int getWidth() { return width; }

private: 
	int height;
	bool infinite;
	std::vector<Layer> layers;
	int nextlayerid;
	int nextobjectid;
	std::string orientation;
	std::string renderorder;
	std::string tiledversion;
	int tileheight;
	std::vector<TileSetMap> tilesets;
	int tilewidth;
	std::string type;
	float version;
	int width;
};

Layer class

Inside each map we have an array of Layer:

class Layer {
public:
	Layer(){};
	Layer(rapidjson::Value& value);
	~Layer();

	std::vector<int> getData() { return data; }
	int getHeight() { return height; }
	int getId() { return id; }
	std::string getImage() { return image; }
	std::string getName() { return name; }
	int getOpacity() { return opacity; }
	std::string getType() { return type; }
	bool isVisible() { return visible; }
	int getWidth() { return width; }
	int getX() { return x; }
	int getY() { return y; }

private:
	std::vector<int> data;
	int height;
	int id;
	std::string image;
	std::string name;
	int opacity;
	std::string type;
	bool visible;
	int width;
	int x;
	int y;
};

But also an array of TileSetMap, the constructor is short so I put everything in the header.

class TileSetMap {
public:
	TileSetMap(rapidjson::Value& value) {
		firstgid = value["firstgid"].GetInt();
		source = value["source"].GetString();
	}

	~TileSetMap() {}

	int getFirstGId() { return firstgid; }
	std::string getSource() { return source; }
private: 
	int firstgid;
	std::string source;
};

All the fields are in the Tiled Documentation

Map constructor

In the map constructor we parse the JSON using RapidJSON:

Map::Map(const char* map) {
	rapidjson::Document mapDoc;
	mapDoc.Parse(map);

	height = mapDoc["height"].GetInt();
	infinite = mapDoc["infinite"].GetBool();

	rapidjson::Value::Array layersA = mapDoc["layers"].GetArray();
	for (size_t i = 0; i < layersA.Capacity(); i++) {
		Layer layer(layersA[i]);
		layers.push_back(layer);
	}

	std::reverse(layers.begin(), layers.end());

	nextlayerid = mapDoc["nextlayerid"].GetInt();
	nextobjectid = mapDoc["nextobjectid"].GetInt();
	orientation = mapDoc["orientation"].GetString();
	renderorder = mapDoc["renderorder"].GetString();
	tiledversion = mapDoc["tiledversion"].GetString();
	tileheight = mapDoc["tileheight"].GetInt();

	rapidjson::Value::Array tileSetArray = mapDoc["tilesets"].GetArray();
	for (size_t i = 0; i < tileSetArray.Capacity(); i++) {
		TileSetMap tileset(tileSetArray[i]);
		tilesets.push_back(tileset);
	}

	std::reverse(tilesets.begin(), tilesets.end());

	tilewidth = mapDoc["tilewidth"].GetInt();
	type = mapDoc["type"].GetString();
	version = mapDoc["version"].GetFloat();
	width = mapDoc["width"].GetInt();

	delete[](map);
}

We do the same thing with the layer constructor:

Layer::Layer(rapidjson::Value& value) {
	if (value.HasMember("data")) {
		for (auto& a : value["data"].GetArray()) {
			data.push_back(a.GetInt());
		}
	}

	if (value.HasMember("height")) {
		height = value["height"].GetInt();
	}

	id = value["id"].GetInt();

	if (value.HasMember("image")) {
		image = value["image"].GetString();
	}

	name = value["name"].GetString();
	opacity = value["opacity"].GetInt();
	type = value["type"].GetString();
	visible = value["visible"].GetBool();
	
	if (value.HasMember("width")) {
		width = value["width"].GetInt();
	}

 	x = value["x"].GetInt();
	y = value["y"].GetInt();
}

For all these classes the destructor is empty.

Once the map is loaded, we can load the tilesets used in the map. For that we will create a class TileSet.

std::vector<TileSet> tilesets;
std::vector<TileSetMap> tilesetMaps = map.getTileSetMap();

for (size_t i = 0; i < tilesetMaps.size(); i++) {
	std::string file = tilesetMaps[i].getSource();
	file = file.substr(0, file.size() - 4) + ".json";

	TileSet tileset(tilesetMaps[i].getFirstGId(), readFile(file.c_str()));
	tilesets.push_back(tileset);
}

To load the right tileset file we replace the extension .tsx with .json

The tileset class do the same thing as before:

class TileSet {
public:
	TileSet() {}
	TileSet(int firstgid, const char* tileset);
	~TileSet();

	int getFirstGid() { return firstgid; }
	int getColumns() { return columns; }
	std::string getImage() { return image; }
	int getImageHeight() { return imageHeight; }
	int getImageWidth() { return imageWidth; }
	int getMargin() { return margin; }
	std::string getName() { return name; }
	int getSpacing() { return spacing; }
	int getTileCount(){ return tilecount; }
	std::string getTiledVersion() { return tiledversion; }
	int getTileHeight() { return tileheight; }
	int getTileWidth() { return tilewidth; }
	std::string getType() { return type; }
	float getVersion() { return version; }

private:
	int firstgid;
	int columns;
	std::string image;
	int imageHeight;
	int imageWidth;
	int margin;
	std::string name;
	int spacing;
	int tilecount;
	std::string tiledversion;
	int tileheight;
	int tilewidth;
	std::string type;
	float version;
};

And the constructor parse all the field.

TileSet::TileSet(int firstgid, const char* tileset) {
	this->firstgid = firstgid;
	rapidjson::Document tilesetDoc;
	tilesetDoc.Parse(tileset);

	columns = tilesetDoc["columns"].GetInt();
	image = tilesetDoc["image"].GetString();
	imageHeight = tilesetDoc["imageheight"].GetInt();
	imageWidth = tilesetDoc["imagewidth"].GetInt();
	margin = tilesetDoc["margin"].GetInt();
	name = tilesetDoc["name"].GetString();
	spacing = tilesetDoc["spacing"].GetInt();
	tilecount = tilesetDoc["tilecount"].GetInt();
	tiledversion = tilesetDoc["tiledversion"].GetString();
	tileheight = tilesetDoc["tileheight"].GetInt();
	tilewidth = tilesetDoc["tilewidth"].GetInt();
	type = tilesetDoc["type"].GetString();
	version = tilesetDoc["version"].GetFloat();

	delete[](tileset);
}

Then we can start loading the data from the layers :
If the type is a tilelayer we create a Tilemap
If the type is an image, we know this is the bg for the map.

std::vector<TileMap> tilemap_render;
std::vector<Layer> layers = map.getLayers();
sf::Texture bg;
	
for (size_t i = 0; i < layers.size(); i++) {
	if (layers[i].getType() == "tilelayer") {
		tilemap_render.push_back(TileMap());
		tilemap_render.back().load(findTileSet(layers[i], tilesets), layers[i]);
	}
	else if(layers[i].getType() == "imagelayer"){
		bg.loadFromFile(layers[i].getImage());
	}
}

The function findTileSet find the tileset used in the layer according to the Tiled documentation.

TileSet findTileSet(Layer& layer, std::vector<TileSet>& tilesets) {
	for (auto d : layer.getData()) {
		if (d != 0) {
			for (auto& t : tilesets) {
				if (d >= t.getFirstGid()) {
					return t;
				}
			}
		}
	}
}

The TileMap is a SFML Entity, for that the class inherit from sf::Drawable and sf::Transformable.
The load function initialize the vertex arrays with the right tile.
The function draw, render the vertex arrays with the texture.

class TileMap : public sf::Drawable, public sf::Transformable {
public:
	bool load(TileSet tileset, Layer layer) {
		tex.loadFromFile(tileset.getImage());

		verts.setPrimitiveType(sf::Quads);
		verts.resize(layer.getWidth() * layer.getHeight() * 4);

		std::vector<int> data = layer.getData();

		for (size_t i = 0; i < data.size(); i++) {
			if (data[i] != 0) {
				size_t columns = tileset.getColumns();
				size_t val = data[i] - tileset.getFirstGid();

				size_t y = floor(val / columns);
				size_t x = val % columns;

				size_t yPos = floor(i / layer.getWidth());
				size_t xPos = i % layer.getWidth();

				sf::Vertex* quad = &verts[i*4];

				float xPos1 = xPos * tileset.getTileWidth();
				float xPos2 = (xPos + 1) * tileset.getTileWidth();
				float yPos1 = yPos * tileset.getTileHeight();
				float yPos2 = (yPos + 1) * tileset.getTileHeight();

				float x1 = x * tileset.getTileWidth();
				float x2 = (x + 1) * tileset.getTileWidth();
				float y1 = y * tileset.getTileHeight();
				float y2 = (y + 1) * tileset.getTileHeight();

				quad[0].position = sf::Vector2f(xPos1, yPos1);
				quad[1].position = sf::Vector2f(xPos2, yPos1);
				quad[2].position = sf::Vector2f(xPos2, yPos2);
				quad[3].position = sf::Vector2f(xPos1, yPos2);

				quad[0].texCoords = sf::Vector2f(x1, y1);
				quad[1].texCoords = sf::Vector2f(x2, y1);
				quad[2].texCoords = sf::Vector2f(x2, y2);
				quad[3].texCoords = sf::Vector2f(x1, y2);
			}
		}
		return true;
	}
private:
	virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const {
		states.transform *= getTransform();
		states.texture = &tex;
		target.draw(verts, states);
	}

	sf::VertexArray verts;
	sf::Texture tex;
};

Next we can create a small event loop, and render the tilemaps in the reverse order.
The order is reversed when we used a vector, a LIFO data structure.

sf::Sprite bgSpr(bg);

// run the main loop
while (window.isOpen())	{
	// handle events
	sf::Event event;
	while (window.pollEvent(event))	{
		if (event.type == sf::Event::Closed)
			window.close();
	}

	// draw the map
	window.clear();

	window.draw(bgSpr);

	for (int i = tilemap_render.size()-1; i >= 0 ; i--) {
		window.draw(tilemap_render[i]);
	}

	window.display();
}

Everything is finished 🙂

You can download the project import tiled maps: TiledMapLoader.7z