Skip to content

Commit 1802203

Browse files
authored
Merge pull request #22 from xorus/groups
Add Godot groups on layers and support for Area2D and Node2D nodes
2 parents 9bfcc1a + cbbbf5d commit 1802203

File tree

3 files changed

+189
-20
lines changed

3 files changed

+189
-20
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,24 @@ Legend: ticked = done, unticked = to do
146146

147147
\* The Godot tileset editor supports only Rectangle and Polygon. That's Tiled are supported and are converted to polygons in Godot.
148148

149+
## Supported entity types
150+
151+
Creating entities with these types will result in specific nodes to be created :
152+
153+
- type `Area2D`
154+
155+
Creates an `Area2D` node in the scene, containing a `CollisionShape2D` with the rectangle set in the tiled map.
156+
157+
You can add `collision_layer` and `collision_mask` integer custom properties to set these properties for Godot.
158+
159+
- type `Node2D`
160+
161+
Creates an empty `Node2D` at the specified position. Can be useful for defining spawn points for example.
162+
163+
If present, the `groups` custom string property will add the generated entity to the specified Godot scene groups. Accepts multiple groups via comma separation: `Group1, Group2`.
164+
149165
## Long term plans
166+
150167
I'm making a 2D platformer and I'm gonna focus on these needs for now.
151168
Generally, I would like to support everything Tiled offers because it's a very good level editor.
152169

export_to_godot_tilemap.js

+102-20
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ class GodotTilemapExporter {
1010
this.tileOffset = 65536;
1111
this.tileMapsString = "";
1212
this.tilesetsString = "";
13+
this.subResourcesString = "";
1314
this.extResourceId = 0;
15+
this.subResourceId = 0;
1416

1517
/**
1618
* Tiled doesn't have tileset ID so we create a map
@@ -35,6 +37,28 @@ class GodotTilemapExporter {
3537
console.info(`Tilemap exported successfully to ${this.fileName}`);
3638
}
3739

40+
/**
41+
* Adds a new subresource to the genrated file
42+
*
43+
* @param {string} type the type of subresource
44+
* @param {object} contentProperties key:value map of properties
45+
* @returns {int} the created sub resource id
46+
*/
47+
addSubResource(type, contentProperties) {
48+
const id = this.subResourceId++;
49+
50+
this.subResourcesString += `
51+
52+
[sub_resource type="${type}" id=${id}]
53+
`;
54+
removeUndefined(contentProperties);
55+
for (const [key, value] of Object.entries(contentProperties)) {
56+
this.subResourcesString += stringifyKeyValue(key, value, false, true) + '\n';
57+
}
58+
59+
return id;
60+
}
61+
3862
/**
3963
* Generate a string with all tilesets in the map.
4064
* Godot allows only one tileset per tilemap so if you use more than one tileset per layer it's n ot going to work.
@@ -76,14 +100,22 @@ class GodotTilemapExporter {
76100
if (!ld.isEmpty) {
77101
const tileMapName = idx === 0 ? layer.name || "TileMap " + i : ld.tileset.name || "TileMap " + i + "_" + idx;
78102
this.mapLayerToTileset(layer.name, ld.tilesetID);
79-
this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight);
103+
this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight, layer.property("groups"));
80104
}
81105
}
82106
} else if (layer.isObjectLayer) {
83-
this.tileMapsString += `
84-
85-
[node name="${layer.name}" type="Node2D" parent="."]`;
107+
// create layer
108+
this.tileMapsString += stringifyNode({
109+
name: layer.name,
110+
type: "Node2D",
111+
parent: ".",
112+
groups: splitCommaSeparated(layer.property("groups"))
113+
});
114+
115+
// add entities
86116
for (const object of layer.objects) {
117+
const groups = splitCommaSeparated(object.property("groups"));
118+
87119
if (object.tile) {
88120
let tilesetsIndexKey = object.tile.tileset.name + "_Image";
89121
let textureResourceId = 0;
@@ -103,13 +135,54 @@ class GodotTilemapExporter {
103135
let objectPositionX = object.x + (object.tile.width / 2);
104136
let objectPositionY = object.y - (object.tile.height / 2);
105137

106-
this.tileMapsString += `
107-
108-
[node name="${object.name}" type="Sprite" parent="${layer.name}"]
109-
position = Vector2( ${objectPositionX}, ${objectPositionY} )
110-
texture = ExtResource( ${textureResourceId} )
111-
region_enabled = true
112-
region_rect = Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${object.tile.height} )`;
138+
this.tileMapsString += stringifyNode({
139+
name: object.name,
140+
type: "Sprite",
141+
parent: layer.name
142+
}, {
143+
position: `Vector2( ${objectPositionX}, ${objectPositionY} )`,
144+
texture: `ExtResource( ${textureResourceId} )`,
145+
region_enabled: true,
146+
region_rect: `Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${object.tile.height} )`
147+
});
148+
} else if (object.type == "Area2D" && object.width && object.height) {
149+
// Creates an Area2D node with a rectangle shape inside
150+
// Does not support rotation
151+
const width = object.width / 2;
152+
const height = object.height / 2;
153+
const objectPositionX = object.x + width;
154+
const objectPositionY = object.y + height;
155+
156+
this.tileMapsString += stringifyNode({
157+
name: object.name,
158+
type: "Area2D",
159+
parent: layer.name,
160+
groups: groups
161+
}, {
162+
collision_layer: object.property("collision_layer"),
163+
collision_mask: object.property("collision_mask")
164+
});
165+
166+
const shapeId = this.addSubResource("RectangleShape2D", {
167+
extents: `Vector2( ${width}, ${height} )`
168+
});
169+
this.tileMapsString += stringifyNode({
170+
name: "CollisionShape2D",
171+
type: "CollisionShape2D",
172+
parent: `${layer.name}/${object.name}`
173+
}, {
174+
shape: `SubResource( ${shapeId} )`,
175+
position: `Vector2( ${objectPositionX}, ${objectPositionY} )`,
176+
});
177+
} else if (object.type == "Node2D") {
178+
this.tileMapsString += stringifyNode({
179+
name: object.name,
180+
type: "Node2D",
181+
parent: layer.name,
182+
groups: groups
183+
}, {
184+
position: `Vector2( ${object.x}, ${object.y} )`
185+
});
113186
}
114187
}
115188
}
@@ -347,9 +420,12 @@ region_rect = Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${o
347420
* @returns {string}
348421
*/
349422
getSceneTemplate() {
350-
return `[gd_scene load_steps=2 format=2]
423+
const loadSteps = 2 + this.subResourceId;
424+
425+
return `[gd_scene load_steps=${loadSteps} format=2]
351426
352427
${this.tilesetsString}
428+
${this.subResourcesString}
353429
[node name="Node2D" type="Node2D"]
354430
${this.tileMapsString}
355431
`;
@@ -370,14 +446,20 @@ ${this.tileMapsString}
370446
* Template for a tilemap node
371447
* @returns {string}
372448
*/
373-
getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16) {
374-
return `[node name="${tileMapName}" type="TileMap" parent="${parent}"]
375-
tile_set = ExtResource( ${tilesetID} )
376-
cell_size = Vector2( ${tileWidth}, ${tileHeight} )
377-
cell_custom_transform = Transform2D( 16, 0, 0, 16, 0, 0 )
378-
format = 1
379-
tile_data = PoolIntArray( ${poolIntArrayString} )
380-
`;
449+
getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16, groups = undefined) {
450+
groups = splitCommaSeparated(groups);
451+
return stringifyNode({
452+
name: tileMapName,
453+
type: "TileMap",
454+
parent: parent,
455+
groups: groups
456+
}, {
457+
tile_set: `ExtResource( ${tilesetID} )`,
458+
cell_size: `Vector2( ${tileWidth}, ${tileHeight} )`,
459+
cell_custom_transform: `Transform2D( 16, 0, 0, 16, 0, 0 )`,
460+
format: "1",
461+
tile_data: `PoolIntArray( ${poolIntArrayString} )`
462+
});
381463
}
382464

383465
mapLayerToTileset(layerName, tilesetID) {

utils.js

+70
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,73 @@ function getTilesetColumns(tileset) {
4747
// so we need to return as Math.floor to avoid throwing off the tile indices.
4848
return Math.floor(calculatedColumnCount);
4949
}
50+
51+
/**
52+
* @param {string} str comma separated items
53+
*/
54+
function splitCommaSeparated(str) {
55+
if (!str) {
56+
return undefined;
57+
}
58+
return str.split(',').map(s => s.trim());
59+
}
60+
61+
/**
62+
* Removes any undefined value with its key from an object
63+
* @param {object} obj
64+
*/
65+
function removeUndefined(obj) {
66+
Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key])
67+
}
68+
69+
70+
/**
71+
* Translates key values defining a godot scene node to the expected TSCN format output
72+
* Passed keys must be strings. Values can be arrays (e.g. for groups)
73+
*
74+
* @param {object} nodeProperties pair key/values for the "node" properties
75+
* @param {object} contentProperties pair key/values for the content properties
76+
* @return {string} TSCN scene node like so :
77+
* ```
78+
* [node key="value"]
79+
* content_key = AnyValue
80+
* ```
81+
*/
82+
function stringifyNode(nodeProperties, contentProperties = {}) {
83+
// remove undefined values from objects
84+
removeUndefined(nodeProperties);
85+
removeUndefined(contentProperties);
86+
87+
let str = '\n';
88+
str += '[node';
89+
for (const [key, value] of Object.entries(nodeProperties)) {
90+
str += ' ' + this.stringifyKeyValue(key, value, true, false);
91+
}
92+
str += ']\n';
93+
for (const [key, value] of Object.entries(contentProperties)) {
94+
str += this.stringifyKeyValue(key, value, false, true) + '\n';
95+
}
96+
97+
return str;
98+
}
99+
100+
/**
101+
* Processes a key/value pair for a TSCN node
102+
*
103+
* @param {string} key
104+
* @param {string|array} value
105+
* @param {bool} quote
106+
* @param {bool} spaces
107+
*/
108+
function stringifyKeyValue(key, value, quote, spaces) {
109+
// flatten arrays
110+
if (Array.isArray(value)) {
111+
value = '[\n"' + value.join('","') + '",\n]';
112+
} else if (quote) {
113+
value = `"${value}"`;
114+
}
115+
if (!spaces) {
116+
return `${key}=${value}`;
117+
}
118+
return `${key} = ${value}`;
119+
}

0 commit comments

Comments
 (0)