What's a best practice of storing extracted data from a tmx parser?

Hi all,

I’m developing my own parser in c++ and so far so good, I can extract all the needed data to render tiles correctly. However being a noob as I am, I would like to learn from the more experienced developers here on what’s the best way to structure the extracted data from a tmx file.

Can anyone suggest a good practice in storing the data (like many small objects linked to each other through some index, or one big array for the tiles and images, or others)?

Many thanks for the advice, I really appreciate it.

Thanks.

Keeping data together as much as possible is usually a good thing for performance reasons. Yet, I wouldn’t worry too much about that early on.

Here is an incomplete list of data types that could be used to store a TMX map in C++:

#ifndef TILED_TYPES
#define TILED_TYPES

#include <map>
#include <memory>
#include <string>
#include <vector>

namespace Tiled {

enum Orientation
{
    Orthogonal,
    Isometric,
    Hexagonal
};

enum TileIdFlags
{
    FlipHorizontal      = 0x80000000,
    FlipVertical        = 0x40000000,
    FlipAntiDiagonal    = 0x20000000,
    AllTileIdFlags      = FlipHorizontal | FlipVertical | FlipAntiDiagonal,
};

enum ObjectShape
{
    ShapeRectangle,
    ShapeEllipse,
    ShapePolygon,
    ShapePolyline
};

enum LayerType
{
    LayerTile,
    LayerObject,
};

typedef std::map<std::string, std::string> Properties;

struct Frame
{
    unsigned tileId;
    unsigned duration;
};

struct Animation
{
    std::vector<Frame> frames;
};

struct Terrain
{
    std::string name;
    Properties properties;
};

struct Tile
{
    unsigned id;
    unsigned terrain;
    std::string imageFilename;
    std::shared_ptr<Animation> animation;
    Properties properties;
};

// todo: Consider using polymorphism for Tileset / ImageCollection
struct Tileset
{
    std::string name;
    std::string imageFilename;
    unsigned firstTileId;
    int tileWidth;
    int tileHeight;
    int tileSpacing;
    int margin;
    Properties properties;
    std::map<unsigned, Tile> tiles;
    std::vector<Terrain> terrains;
};

struct Point
{
    float x;
    float y;
};

struct TileLayer
{
    int x;
    int y;
    int width;
    int height;
    std::vector<unsigned> tileIds;
};

struct Color
{
    unsigned char r, g, b, a;
};

struct Object
{
    std::string name;
    std::string type;
    ObjectShape shape;
    unsigned tileId;
    float x;
    float y;
    float width;
    float height;
    float rotation;
    bool visible;
    Properties properties;
    std::vector<Point> points;
};

struct ObjectLayer
{
    std::string name;
    Color color;
    std::vector<Object> objects;
};

struct Layer
{
    LayerType type;
    std::string name;
    float opacity;
    bool visible;
    Point offset;
    Properties properties;

    std::shared_ptr<TileLayer> tileLayer;
    std::shared_ptr<ObjectLayer> objectLayer;
    // todo: image layer
};

struct Map
{
    Orientation orientation;
    int width;
    int height;
    int tileWidth;
    int tileHeight;
    Color backgroundColor;

    std::vector<Tileset> tilesets;
    std::vector<Layer> layers;

    Tileset *findTileset(const std::string &name) noexcept;
    Layer *findLayer(const std::string &name) noexcept;

    Tile *findTile(unsigned tileId) noexcept;
};

} // namespace Tiled

#endif // TILED_TYPES

I wrote this in order to experiment with C++ code generation that would remove the need for having a map loader at all. Here’s an example of how the above classes could be initialized using C++11 (it works on clang++ 3.7 this way, but not with g++ 5.2, which says “sorry, unimplemented: non-trivial designated initializers not supported”):

#include "tiled.h"

#include <iostream>

using namespace Tiled;

static Map map {
    .orientation = Orthogonal,
    .width = 10,
    .height = 10,
    .tileWidth = 32,
    .tileHeight = 32,
    .backgroundColor = Color { 0, 0, 0, 0 },

    .tilesets = {
        Tileset {
            .name = "desert",
            .imageFilename = "desert.png",
            .firstTileId = 1,
            .tileWidth = 32,
            .tileHeight = 32,
            .tileSpacing = 1,
            .margin = 1,
            .properties = {},
            .tiles = {
                {
                    1,
                    Tile {
                        .id = 1,
                        .terrain = 0,
                    },
                },
                {
                    2,
                    Tile {
                        .id = 2,
                        .terrain = 0,
                    },
                },
            },
            .terrains = {},
        },
    },

    .layers = {
        Layer {
            .type = LayerTile,
            .name = "ground",
            // todo: not supported by GCC 5.2
            .tileLayer = std::shared_ptr<TileLayer>(new TileLayer {
                .x = 0,
                .y = 0,
                .width = 10,
                .height = 10,
                .tileIds = {
                    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, 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,
                },
            }),
        },

        Layer {
            .type = LayerObject,
            // todo: not supported by GCC 5.2
            .objectLayer = std::shared_ptr<ObjectLayer>(new ObjectLayer {
                .objects = {
                },
            }),
        },
    },
};

int main()
{
    std::cout << map.tilesets[0].name << " "
              << map.findTile(0) << " "
              << map.findTile(1)->id << std::endl;
    return 0;
}

In any case, I guess my main point of advice is not to make things more complicated than necessary unless you have a good reason for it. It may very well be that the above code goes against my own advice though. :smile:

Oh wow! Thanks for the advice and the sample code, especially the data format. I can readily use that in my current parser with a little modification, though more testing should be done to accommodate my use cases.

Thank you very much, really appreciate it.

Hi bjorn,

About the exporter initializer list, is this planned for the intended next release? Also, a suggestion, what about a written guide if one wants to export back to tmx format based on the data formatted above? That should help any developer to iterate through the data to export it back so that it could be opened in Tiled.

Another suggestion, on ImageCollection of Tileset node, can’t the image(s) listed in the Tileset segment be saved in a vector? That means for a normal Tileset with a single image, the vector would be of size 1. That could simplify the whole image(s) relationship with Tileset with no special use case at all for a collection of images, plus it would standardized the format for image information in the Tileset node.

Best regards,

It was an experiment. I don’t know how many would be interested in this, but when it works it could be included as an export option in Tiled in some future release.

For that it should suffice to just read the TMX Map Format documentation. If something is missing there please let me know.

I think I should have removed that “todo” entry from my code. Actually using polymorphism here seems to be not possible in combination with using initializer lists, or at least I didn’t figure out how to do that. And in fact the current approach is fine. For image collection tilesets, the image file name of the tileset is simply ignored and the image file name of each tile is used instead.

I don’t see much value in using a vector of image file names. Then you’d need an additional variable to be able to tell the different between a tileset image based tileset and a collection of a single image, right? Also the image file name would no longer be readily available in the Tile structure.

Having Tiled to be able to generate and export c++ code would tremendously help to integrate it with game engines. This is absolutely brilliant! I am sure many game developers would love it.

[quote=“bjorn, post:5, topic:951, full:true”]
I don’t see much value in using a vector of image file names. Then you’d need an additional variable to be able to tell the different between a tileset image based tileset and a collection of a single image, right? Also the image file name would no longer be readily available in the Tile structure.
[/quote] You’re right. I was thinking of suggesting it since we already have some “enums” like the ones used by renderorder tag, perhaps the same could be applied to image tag in the tileset node and to eliminate a “special use case” for image collection parsing? But either way it’s not a big deal that would require some drastic changes to the format currently used.

Cheers,

This sounds interesting. With what exactly did you experiment there?

Regards,
Ablu

I personally expect the interest to be a little less big because adding the map to the game this way would require you to compile your game while working on the content, which could really slow down your iteration time. Also, there is nothing stopping you from converting maps to code yourself, apart from the time investment I guess. But sure I can see it being useful for some.

Hmm, actually I wasn’t sure so I looked it up, and it’s unfortunately a combination of C++11 and C99 features… From C++11 it uses initializer lists (as supported by STL containers) and from C99 it uses tagged structure initialization. For some reason neither GCC nor Clang warned about this not being standardized in C++. It will be rather less readable without the C99 feature, or require generating a function that sets up the map instead of a static initialization.

Ah. I thought you wanted to generate the maploader itself somehow :stuck_out_tongue: