@@ -237,6 +237,13 @@ struct CustomDataLayer
237
237
int index = 0 ;
238
238
};
239
239
240
+ // Remove any special chars from a string
241
+ static QString sanitizeSpecialChars (QString str)
242
+ {
243
+ static QRegularExpression sanitizer (" [^a-zA-Z0-9]" );
244
+ return str.replace (sanitizer, " " );
245
+ }
246
+
240
247
// For collecting information about the tilesets we're using
241
248
struct TilesetInfo
242
249
{
@@ -253,6 +260,8 @@ struct AssetInfo
253
260
QMap<QString, TilesetInfo> tilesetInfo;
254
261
QList<const TileLayer*> layers;
255
262
QSet<QString> tilesetIds;
263
+ QMap<QString, QString> objectIds; // Map resPaths to unique IDs
264
+ QList<const MapObject*> objects;
256
265
QString resRoot;
257
266
std::map<QString, CustomDataLayer> customDataLayers;
258
267
};
@@ -329,6 +338,57 @@ static void findUsedTilesets(const TileLayer *layer, AssetInfo &assetInfo)
329
338
}
330
339
}
331
340
341
+ // Search an object layer for all object resources and save them in assetInfo
342
+ static void findUsedObjects (const ObjectGroup *objectLayer, AssetInfo &assetInfo)
343
+ {
344
+ static QRegularExpression resPathValidator (" ^res://(.*)\\ .tscn$" );
345
+
346
+ for (const MapObject *object : objectLayer->objects ()) {
347
+ const auto resPath = object->resolvedProperty (" resPath" ).toString ();
348
+
349
+ if (resPath.isEmpty ()) {
350
+ Tiled::WARNING (TscnPlugin::tr (" Only objects with the resPath property will be exported" ),
351
+ Tiled::JumpToObject { object });
352
+ continue ;
353
+ }
354
+
355
+ QRegularExpressionMatch match;
356
+ if (!resPath.contains (resPathValidator, &match)) {
357
+ Tiled::ERROR (TscnPlugin::tr (" resPath must be in the form of 'res://<filename>.tscn'." ),
358
+ Tiled::JumpToObject { object });
359
+ continue ;
360
+ }
361
+
362
+ const QString baseName = sanitizeSpecialChars (match.captured (1 ));
363
+ int uniqueifier = 1 ;
364
+ QString id = baseName;
365
+
366
+ // Create the objectId map such that every resPath has a unique ID.
367
+ while (true ) {
368
+ // keys() is slow. If this becomes a problem, we can create a reverse map.
369
+ auto keys = assetInfo.objectIds .keys (id);
370
+
371
+ if (keys.empty ()) {
372
+ assetInfo.objectIds [resPath] = id;
373
+ break ;
374
+ }
375
+
376
+ // The key already exists with the right value
377
+ if (keys[0 ] == resPath)
378
+ break ;
379
+
380
+ // The baseName is based off of a file path, which is unique by definition,
381
+ // but because we santized it, paths like res://ab/c.tscn and res://a/bc.tscn
382
+ // would both get santitized into the non-unique name of abc, so we need to
383
+ // add a uniqueifier and try again.
384
+ ++uniqueifier;
385
+ id = baseName + QString::number (uniqueifier);
386
+ }
387
+
388
+ assetInfo.objects .append (object);
389
+ }
390
+ }
391
+
332
392
// Used by collectAssets() to search all layers and layer groups
333
393
static void collectAssetsRecursive (const QList<Layer*> &layers, AssetInfo &assetInfo)
334
394
{
@@ -346,10 +406,11 @@ static void collectAssetsRecursive(const QList<Layer*> &layers, AssetInfo &asset
346
406
347
407
break ;
348
408
}
349
- case Layer::ObjectGroupType:
350
- Tiled::WARNING ( TscnPlugin::tr ( " The Godot exporter does not yet support objects " ),
351
- Tiled::SelectLayer { layer } );
409
+ case Layer::ObjectGroupType: {
410
+ auto objectLayer = static_cast < const ObjectGroup*>(layer);
411
+ findUsedObjects (objectLayer, assetInfo );
352
412
break ;
413
+ }
353
414
case Layer::ImageLayerType:
354
415
Tiled::WARNING (TscnPlugin::tr (" The Godot exporter does not yet support image layers" ),
355
416
Tiled::SelectLayer { layer });
@@ -548,17 +609,30 @@ static bool writeProperties(QFileDevice *device, const QVariantMap &properties,
548
609
return first;
549
610
}
550
611
612
+ // Write the ext_resource lines for any objects exported
613
+ static void writeExtObjects (QFileDevice *device, const AssetInfo &assetInfo)
614
+ {
615
+ for (auto it = assetInfo.objectIds .begin (); it != assetInfo.objectIds .end (); ++it) {
616
+ device->write (formatByteString (
617
+ " [ext_resource type=\" PackedScene\" path=\" %1\" id=\" %2\" ]\n " ,
618
+ sanitizeQuotedString (it.key ()),
619
+ it.value ()));
620
+ }
621
+
622
+ device->write (" \n " );
623
+ }
624
+
551
625
// Write the tileset
552
626
// If you're creating a reusable tileset file, pass in a new file device and
553
627
// set isExternal to true, otherwise, reuse the device from the tscn file.
554
628
static void writeTileset (const Map *map, QFileDevice *device, bool isExternal, AssetInfo &assetInfo)
555
629
{
556
630
bool foundCollisions = false ;
557
631
558
- // One Texture2D and one TileSetAtlasSource per tileset, plus a resource node
559
- auto loadSteps = assetInfo.tilesetInfo .size () * 2 + 1 ;
560
-
561
632
if (isExternal) {
633
+ // One Texture2D and one TileSetAtlasSource per tileset, plus a resource node
634
+ auto loadSteps = assetInfo.tilesetInfo .size () * 2 + 1 ;
635
+
562
636
device->write (formatByteString (
563
637
" [gd_resource type=\" TileSet\" load_steps=%1 format=3]\n\n " ,
564
638
loadSteps));
@@ -574,6 +648,7 @@ static void writeTileset(const Map *map, QFileDevice *device, bool isExternal, A
574
648
sanitizeQuotedString (it.key ()),
575
649
sanitizeQuotedString (it->id )));
576
650
}
651
+
577
652
device->write (" \n " );
578
653
579
654
// TileSetAtlasSource nodes
@@ -799,9 +874,14 @@ bool TscnPlugin::write(const Map *map, const QString &fileName, Options options)
799
874
// (unless we're writing the tileset to an external .tres file)
800
875
auto loadSteps = !tilesetResPath.isEmpty () ? 2 : assetInfo.tilesetInfo .size () * 2 + 2 ;
801
876
877
+ // And an extra load step per object resource
878
+ loadSteps += assetInfo.objectIds .size ();
879
+
802
880
// gdscene node
803
881
device->write (formatByteString (" [gd_scene load_steps=%1 format=3]\n\n " , loadSteps));
804
882
883
+ writeExtObjects (device, assetInfo);
884
+
805
885
// tileset, either inline, or as an external file
806
886
if (tilesetResPath.isEmpty ()) {
807
887
writeTileset (map, device, false , assetInfo);
@@ -811,7 +891,7 @@ bool TscnPlugin::write(const Map *map, const QString &fileName, Options options)
811
891
throw tscnError (tr (" tilesetResPath must be in the form of 'res://<filename>.tres'." ));
812
892
813
893
device->write (formatByteString (
814
- " [ext_resource type=\" TileSet\" path=\" %1\" id=\" TileSet_0\" ]\n\n " ,
894
+ " [ext_resource type=\" TileSet\" path=\" %1\" id=\" TileSet_0\" ]\n " ,
815
895
sanitizeQuotedString (tilesetResPath)));
816
896
817
897
QString resFileName = assetInfo.resRoot + ' /' + match.captured (1 );
@@ -837,10 +917,13 @@ bool TscnPlugin::write(const Map *map, const QString &fileName, Options options)
837
917
}
838
918
}
839
919
840
- // TileMap node
841
- device->write (formatByteString (" [node name=\" %1\" type=\" TileMap \" ]\n " ,
920
+ // Root node
921
+ device->write (formatByteString (" [node name=\" %1\" type=\" Node2D \" ]\n \n" ,
842
922
sanitizeQuotedString (fi.baseName ())));
843
923
924
+ // TileMap node
925
+ device->write (" [node name=\" TileMap\" type=\" TileMap\" parent=\" .\" ]\n " );
926
+
844
927
if (tilesetResPath.isEmpty ())
845
928
device->write (" tile_set = SubResource(\" TileSet_0\" )\n " );
846
929
else
@@ -932,6 +1015,37 @@ bool TscnPlugin::write(const Map *map, const QString &fileName, Options options)
932
1015
933
1016
layerIndex++;
934
1017
}
1018
+
1019
+ device->write (" \n " );
1020
+
1021
+ // Object scene nodes
1022
+ for (const MapObject *object : assetInfo.objects ) {
1023
+ device->write (" \n " );
1024
+
1025
+ auto name = object->name ();
1026
+ if (name.isEmpty ())
1027
+ name = " Object" + QString::number (object->id ());
1028
+
1029
+ const auto resPath = object->resolvedProperty (" resPath" ).toString ();
1030
+
1031
+ device->write (formatByteString (
1032
+ " [node name=\" %1\" parent=\" .\" instance=ExtResource(\" %2\" )]\n " ,
1033
+ sanitizeQuotedString (name),
1034
+ sanitizeQuotedString (assetInfo.objectIds [resPath]))
1035
+ );
1036
+
1037
+ // Convert Tiled's alignment position to Godot's centre-aligned position.
1038
+ QPointF pos =
1039
+ object->position () -
1040
+ Tiled::alignmentOffset (object->size (), object->alignment ()) +
1041
+ QPointF (object->width ()/2 , object->height ()/2 );
1042
+
1043
+ device->write (formatByteString (
1044
+ " position = Vector2(%1, %2)\n " ,
1045
+ pos.x (),
1046
+ pos.y ()
1047
+ ));
1048
+ }
935
1049
} catch (std::exception & e) {
936
1050
mError = e.what ();
937
1051
return false ;
0 commit comments