Finally took the plunge into SFML today, and managed to create a class that may be of use to others. It takes a Tiled .tmx file and gives you a Drawable class that you can just window.draw() onto the screen. I saw there was another forum post with something similar, but this one uses the VertexArray tutorial I read (I believe the other forum post drew everything as individual sprites).
DrawableMap.h
#pragma once
#include <SFML/Graphics.hpp>
#include "../TmxParser/Tmx.h"
#include <vector>
class DrawableMap : public sf::Drawable, public sf::Transformable {
public:
DrawableMap();
/**
* Load a map with the given filename from the specified path.
*/
bool Load(const std::string& path, const std::string& filename);
private:
/**
* Single-texture drawable layer
*/
class DrawableLayer : public sf::Drawable, public sf::Transformable {
public:
bool Load(const Tmx::Layer* layer, const Tmx::Tileset *tileset, const std::string& path);
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
bool inTileset(int tile_id, const Tmx::Tileset *tileset);
sf::VertexArray vertices_;
sf::Texture texture_;
};
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
Tmx::Map* map_;
std::vector<DrawableLayer*> layers;
};
DrawableMap.cpp
#include "DrawableMap.h"
bool DrawableMap::DrawableLayer::Load(const Tmx::Layer* layer, const Tmx::Tileset *tileset, const std::string& path) {
//Ensure non-null
if (layer == nullptr || tileset == nullptr) {
return false;
}
//Load tileset texture
if(!texture_.loadFromFile(path + tileset->GetImage()->GetSource())) {
return false;
}
vertices_.setPrimitiveType(sf::Quads);
vertices_.resize(layer->GetWidth() * layer->GetHeight() * 4);
for (int y = 0; y < layer->GetHeight(); ++y) {
for (int x = 0; x < layer->GetWidth(); ++x) {
//Get the tile ID
unsigned int tile_id = layer->GetTileId(x, y);
if(!inTileset(tile_id, tileset)) {
//Tile does not belong to our tileset, do not draw
continue;
}
int tileset_index = layer->GetTileTilesetIndex(x, y);
int tile_width = tileset->GetTileWidth();
int tile_height = tileset->GetTileHeight();
int tile_spacing = tileset->GetSpacing();
int tile_margin = tileset->GetMargin();
//Calculate tileset coordinates
tile_id -= tileset->GetFirstGid();
int tu = tile_id % ((tileset->GetImage()->GetWidth() - tile_margin)
/ (tile_width + tile_spacing));
int tv = tile_id / ((tileset->GetImage()->GetWidth() - tile_margin)
/ (tile_width + tile_spacing));
//Setup the tile
sf::Vertex* quad = &vertices_[(y * layer->GetWidth() + x) * 4];
//define its 4 corners
quad[0].position = sf::Vector2f(x * tile_width, y * tile_height);
quad[1].position = sf::Vector2f((x + 1) * tile_width, y * tile_height);
quad[2].position = sf::Vector2f((x + 1) * tile_width, (y + 1) * tile_height);
quad[3].position = sf::Vector2f(x * tile_width, (y + 1) * tile_height);
//define its 4 texture coordinates
quad[0].texCoords = sf::Vector2f(tu * (tile_width + tile_spacing) + tile_margin,
tv * (tile_height + tile_spacing) + tile_margin);
quad[1].texCoords = sf::Vector2f((tu + 1) * (tile_width + tile_spacing) + tile_margin
- tile_spacing, tv * (tile_height + tile_spacing) + tile_margin);
quad[2].texCoords = sf::Vector2f((tu + 1) * (tile_width + tile_spacing) + tile_margin
- tile_spacing, (tv + 1) * (tile_height + tile_spacing) + tile_margin - tile_spacing);
quad[3].texCoords = sf::Vector2f(tu * (tile_width + tile_spacing) + tile_margin,
(tv + 1) * (tile_height + tile_spacing) + tile_margin - tile_spacing);
}
}
}
bool DrawableMap::DrawableLayer::inTileset(int tile_id, const Tmx::Tileset *tileset) {
int tiles_x = (tileset->GetImage()->GetWidth() - tileset->GetMargin()) / (tileset->GetTileWidth() + tileset->GetSpacing());
int tiles_y = (tileset->GetImage()->GetHeight() - tileset->GetMargin()) / (tileset->GetTileHeight() + tileset->GetSpacing());
int total_tiles = tiles_x * tiles_y;
if((tile_id >= tileset->GetFirstGid()) && (tile_id <= tileset->GetFirstGid() + total_tiles)) {
return true;
}
return false;
}
void DrawableMap::DrawableLayer::draw(sf::RenderTarget& target, sf::RenderStates states) const {
// apply the transform
states.transform *= getTransform();
// apply the tileset texture
states.texture = &texture_;
// draw the vertex array
target.draw(vertices_, states);
}
DrawableMap::DrawableMap() : map_(nullptr) {
}
bool DrawableMap::Load(const std::string& path, const std::string& filename) {
//Load and parse the TMX Map
map_ = new Tmx::Map();
map_->ParseFile(path + filename);
if(map_->HasError()) {
fprintf(stderr, "[Error]::Map(%d) %s\n", map_->GetErrorCode(), map_->GetErrorText().c_str());
return false;
}
//Break up the map by layers
for (int l = 0; l < map_->GetNumLayers(); ++l) {
const Tmx::Layer *layer = map_->GetLayer(l);
//Break up the layers by tilesets
for(int t = 0; t < map_->GetNumTilesets(); ++t) {
DrawableLayer* d_layer = new DrawableLayer();
// TODO: Check for error?
d_layer->Load(layer, map_->GetTileset(t), path);
layers.push_back(d_layer);
}
}
return true;
}
void DrawableMap::draw(sf::RenderTarget& target, sf::RenderStates states) const {
for(unsigned int i = 0; i < layers.size(); ++i) {
target.draw(*layers[i], states);
}
}
You will need to use tmx-parser (
https://code.google.com/p/tmx-parser/) to load the file. I'm ok with reinventing the wheel for fun stuff like drawing, but not parsing XML files
.
It could still use some optimizations, such as a texture manager to prevent loading the same tileset more than once for each layer. But I was happy I could do this quickly, so thanks for SFML Laurent!