Added exporter for Game Maker Studio room files #1386

Merged
merged 6 commits into from Nov 14, 2016

Projects

None yet

3 participants

@JonesBlunt
Contributor

Objects are exported into instances, they just need a name. Using an optional Depth property on both tile layers and object layers will export using that depth.

@JonesBlunt JonesBlunt Add files via upload
Added exporter for Game Maker Studio room files.

Objects are exported into instances, they just need a name. Using an optional Depth property on both tile layers and object layers will export using that depth.
36679ec
@bjorn

@JonesBlunt Thanks for working on this! I've yet to try it out, but for now I've provided some inline comments. One of them means almost completely rewriting it, but I think it's needed to resolve possible encoding and escaping issues.

I assume this is meant to close issue #1369? In that case please mention this at the bottom of your commit message (like: Closes #1369).

@cursedtoast If you want to try this out to see if it fits your needs, know that you can find automated installer builds for pull requests by following the link to the AppVeyor continuous integration service. Just click on Details, then choose 32-bit or 64-bit and then click to Artifacts. You'll need to get the .7z archive until this new plugin has been added to the installer.

src/plugins/gmx/gmxplugin.cpp
@@ -0,0 +1,154 @@
+/*
+ * GMX Tiled Plugin
+ * Copyright 2014, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
@bjorn
bjorn Oct 28, 2016 Owner

Please put your name here. :-)

src/plugins/gmx/gmxplugin.cpp
+ file.write(" <height>" + QByteArray::number(map->tileHeight() * map->height()) + "</height>\n");
+ file.write(" <vsnap>" + QByteArray::number(map->tileWidth()) + "</vsnap>\n");
+ file.write(" <hsnap>" + QByteArray::number(map->tileHeight()) + "</hsnap>\n");
+ file.write(" <isometric>" + QByteArray::number((map->orientation() == Tiled::Map::Orientation::Isometric ? 1 : 0)) + "</isometric>\n");
@bjorn
bjorn Oct 28, 2016 Owner

Since you're writing an XML file, please use QXmlStreamWriter instead. It will automatically take care of the formatting, the encoding and any necessary escaping of values.

@JonesBlunt JonesBlunt Closes #1369
Exporter for Game Maker Studio, now using QXmlStreamWriter.
037bea8
@JonesBlunt
Contributor

Hey @bjorn thanks for the feedback. I appreciate the advice regarding switching to QXmlStreamWriter, it's pretty useful. My latest commit hopefully addresses all your concerns, let me know what you think.

src/plugins/gmx/gmxplugin.cpp
+ stream.writeStartElement("caption");
+ stream.writeEndElement();
+
+ stream.writeTextElement("width", QByteArray::number(map->tileWidth() * map->width()));
@bjorn
bjorn Oct 29, 2016 Owner

I'll have a closer look on Monday latest, but in the meantime, please use QString::number instead because all the QXmlStreamWriter functions take QString.

@cursedtoast

Wow!! Thank you Jones!
I am going to give this a test ASAP.

@cursedtoast

Hi @JonesBlunt
First off, thank you for fulfilling this. Myself and the rest of the GMS community thank you. We've needed this. So seriously, thank you. This helps a lot.

Second, maybe this is user error, but I'm running into an issue with the wrong tiles being mapped out in GMS.

1.) I draw my map in Tiled, and export it in .room format for GMS.
2.) I add a matching background as a tileset in GMS with the same name as the tileset in Tiled.
3.) I add an existing room in GMS, and choose the file Tiled exports.
4.) I see the wrong tiles but the coordinates of the tiles are correct.

Here's two screenshots:
What the map looks like in Tiled:
http://l.eaf.io/LF6KroZk.png

What it looks like in GMS:
http://l.eaf.io/fADGHdiQ.png

Am I doing things correctly or is this user error? Do you need a copy of GMS pro to test with? I can provide you with one. I can also provide you with the tileset I'm using.

Again, thanks a million for this!

@JonesBlunt JonesBlunt Removed QByteArray::number references
Removed QByteArray::number references in QXmlStreamWriter function calls.
38af770
@JonesBlunt
Contributor

Hey there @cursedtoast!
Sounds like you set up the map perfectly, so that's a little odd. There's a layer related fix in the second commit, though it looks like you're probably using 1 layer.

The tileset could be useful to see, did you set any offsets by chance?

Thanks for trying it.

@cursedtoast

Hi @JonesBlunt
A lot of people are very happy you're working on this :D

Here is the tileset I'm using, there is no offsets being used. I configure it as 32x32 tiles in GM as well as Tiled.

Tileset: http://l.eaf.io/DpxyOqSv.png

Someone on Reddit also had this issue with one of their tilesets, but one of them was fine.

Here is the one that worked fine for them:
https://www.dropbox.com/sh/cql1w0n6sa7qfvo/AAADkXducM8gqQvX2MGY6mFTa?dl=0&preview=bg_tiles.png

Here's the one that caused this issue:
https://www.dropbox.com/sh/cql1w0n6sa7qfvo/AAADkXducM8gqQvX2MGY6mFTa?dl=0&preview=spikes.png

Let me know if you're able to duplicate the problem with the tileset I'm using. Maybe you can find a correlation between both problematic sets.

@JonesBlunt JonesBlunt Fixed tile id calculations
Mistakenly used Tileset rowCount() instead of columnCount().
a6be367
@JonesBlunt
Contributor

Ok @cursedtoast, I managed to reproduce it. It was a simple mistake, only revealed when textures weren't power of two textures.

@cursedtoast

Thanks @JonesBlunt I'll give it a test shortly. I appreciate your responsiveness.

Someone on Reddit wanted to know if rotations in Tiled work in GM, did you by chance code that? Just relaying the question, no huge worries if it's not supported yet - someone can get around to it eventually :D

@JonesBlunt
Contributor

There is currently no support for rotations. I will try to implement object rotations in the next commit, though tiled rotations would require more work considering there are no native tile rotations in GM as far as I'm aware.

@bjorn

Just some more comments on the code for now.

Thanks @cursedtoast for trying it out!

src/plugins/gmx/gmxplugin.cpp
+ stream.setAutoFormatting(true);
+ stream.setAutoFormattingIndent(2);
+
+ stream.writeComment("This Document is generated by GameMaker, if you edit it by hand then you do so at your own risk!");
@bjorn
bjorn Oct 31, 2016 Owner

I'm sure you mean to write "Tiled" instead of "GameMaker" here. :-)

src/plugins/gmx/gmxplugin.cpp
+
+ stream.writeStartElement("room");
+ stream.writeStartElement("caption");
+ stream.writeEndElement();
@bjorn
bjorn Oct 31, 2016 Owner

There is writeEmptyElement for this.

src/plugins/gmx/gmxplugin.cpp
+ stream.writeStartElement("instances");
+
+ uint objectId = 0u;
+ foreach (const Layer *layer, map->layers()) {
@bjorn
bjorn Oct 31, 2016 Owner

You can use a more modern loop here:

for (const Layer *layer : map->layers()) {
src/plugins/gmx/gmxplugin.cpp
+ stream.writeAttribute("x", QString::number((int)(object->x())));
+ stream.writeAttribute("y", QString::number((int)(object->y() - object->height())));
+
+ stream.writeAttribute("id", QString::number(++objectId));
@bjorn
bjorn Oct 31, 2016 Owner

Is there a reason you can't use object->id() here? Does it need to be a consecutive numbering?

src/plugins/gmx/gmxplugin.cpp
+ stream.writeAttribute("x", QString::number((int)(x * map->tileWidth())));
+ stream.writeAttribute("y", QString::number((int)(y * map->tileHeight())));
+ stream.writeAttribute("w", QString::number(map->tileWidth()));
+ stream.writeAttribute("h", QString::number(map->tileHeight()));
@bjorn
bjorn Oct 31, 2016 Owner

The tiles do not need to be the same size as the map tile size, so you should use tile->size() here.

src/plugins/gmx/gmxplugin.cpp
+
+ stream.writeAttribute("yo", QString::number((int)(tile->id() / tile->tileset()->columnCount()) * tile->tileset()->tileWidth()));
+
+ stream.writeAttribute("depth", QString::number(layer->hasProperty(QLatin1String("Depth")) ? layer->property(QLatin1String("Depth")).toInt() : currentLayer));
@bjorn
bjorn Oct 31, 2016 Owner

Please initialize the depth only once per layer.

src/plugins/gmx/gmxplugin.cpp
+ stream.writeAttribute("depth", QString::number(layer->hasProperty(QLatin1String("Depth")) ? layer->property(QLatin1String("Depth")).toInt() : currentLayer));
+ stream.writeAttribute("id", QString::number(++tileId));
+ stream.writeAttribute("scaleX", QString::number(1));
+ stream.writeAttribute("scaleY", QString::number(1));
@bjorn
bjorn Oct 31, 2016 Owner

Is it necessary to write all these out? The file could be kept smaller if GameMaker defaults to 1 anyway. Using an XML element for every tile is already rather expensive. :-/

@JonesBlunt
JonesBlunt Nov 1, 2016 Contributor

Other than for rotations on objects which require a scaleY inverse, scaleX/Y are indeed redundant. All tiles and objects need to have a unique id attribute or the room will fail to load in GM. object->id() does work well for objects, though tile->id() will not.

src/plugins/gmx/gmxplugin.cpp
+ result.append(completePath);
+
+ if (result.size() == 1)
+ result[0] = fileName;
@bjorn
bjorn Oct 31, 2016 Owner

What are you trying to achieve here? This function is effectively always just returning fileName, which is what the default implementation already does.

@JonesBlunt
JonesBlunt Nov 1, 2016 Contributor

Yes, this shouldn't be there.

@JonesBlunt JonesBlunt Object rotations and removed redundancies.
3e001ac
@bjorn

Some more comments.

src/plugins/gmx/gmxplugin.cpp
if (layer->layerType() != Layer::ObjectGroupType) continue;
const ObjectGroup *objectLayer = static_cast<const ObjectGroup*>(layer);
- foreach(const MapObject *object, objectLayer->objects()) {
+ bool rotating = false;
@bjorn
bjorn Nov 1, 2016 Owner

Please declare this variable inside the loop.

src/plugins/gmx/gmxplugin.cpp
- stream.writeAttribute("id", QString::number(++objectId));
+ stream.writeAttribute("scaleY", QString::number(rotating ? -1 : 1));
@bjorn
bjorn Nov 1, 2016 Owner

What is the idea behind inverting the Y axis when an object is rotated? Doesn't that suddenly flip the entire graphic around?

@JonesBlunt
JonesBlunt Nov 1, 2016 Contributor

Unless I'm mistaken, the origin for Tiled rotations is always the bottom left vertex, whereas in GM it's the top left tile vertex. ScaleY did indeed flip the graphic, which appeared to be reset to it's original graphic after rotating the object, though that was just a glitch in GM's room editor. My current solution disregards scaleY but uses QTransform() for computing x/y offsets to correctly translate objects following rotation.

src/plugins/gmx/gmxplugin.cpp
- stream.writeAttribute("w", QString::number(map->tileWidth()));
- stream.writeAttribute("h", QString::number(map->tileHeight()));
+ stream.writeAttribute("x", QString::number((int)(x * tile->width())));
+ stream.writeAttribute("y", QString::number((int)(y * tile->height())));
@bjorn
bjorn Nov 1, 2016 Owner

When I asked you to use the tile size, I meant just for w and h. The position of the tile depends on the map tile size, not on the size of each tile.

@JonesBlunt JonesBlunt Fixed rotations
3a1ec66
@bjorn

It looks like image layers could be exported as (non-tiling) backgrounds?

Also just a comment to add back the grid snap.

Overall it looks fine to me. I guess to be really useful, some more GM properties should be supported, like the various physics related properties and well as settings like "speed", "persistent", etc.

src/plugins/gmx/gmxplugin.cpp
stream.writeTextElement("height", QString::number(map->tileHeight() * map->height()));
- stream.writeTextElement("vsnap", QString::number(map->tileHeight()));
- stream.writeTextElement("hsnap", QString::number(map->tileWidth()));
@bjorn
bjorn Nov 7, 2016 Owner

Please add back these vsnap and hsnap variables, because without them GameMaker will display empty red-colored input boxes for these fields. Writing the tile size in there seems reasonable.

@bjorn
Owner
bjorn commented Nov 7, 2016

Since I was just trying it, I also noticed there is a GameMaker Studio 2 beta going now and I've shortly played around with it. They have vastly improved their tile map editor capabilities as well as changing their map format from XML to JSON (and storing tile references much more efficiently, basically like Tiled is doing it). I don't think there will be too many people still looking for a better tile map editor, but it would of course be interesting to support exporting to the new format eventually as well.

@bjorn bjorn merged commit 5172ad3 into bjorn:master Nov 14, 2016

2 checks passed

continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@bjorn
Owner
bjorn commented Nov 14, 2016

Alright, I decided to merge this as-is and make some adjustments myself that I think were necessary:

  • Use QSaveFile (8814db8). In your original code you did, but you somehow took it out while moving to QXmlStreamWriter. Was there any particular reason why, @JonesBlunt?
  • Added support for several GameMaker properties (6625eff). Since I figured otherwise it would be quite annoying if you need to change these after every export. It also adds back the vsnap and hsnap attributes like I had requested.
  • Sanitize names and changed the way type and name are used (7c33041). GameMaker is quite restrictive about what can be in a name, and won't load the exported map when it contains any invalid characters in the name. Also, I thought it made more sense to map the object type to objName and the name to name (instance name).
  • Adding the plugin to the Windows installer (c2e6bb2).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment