From 3c2848d03198550fa7ac6634171a4fb700267c10 Mon Sep 17 00:00:00 2001 From: Xorus Date: Wed, 16 Dec 2020 19:31:28 +0100 Subject: [PATCH 1/3] add godot groups on layers and support for Area2D and Node2D nodes --- README.md | 17 +++++ export_to_godot_tilemap.js | 126 +++++++++++++++++++++++++++++++------ utils.js | 72 +++++++++++++++++++++ 3 files changed, 195 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8551356..be44467 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,24 @@ Legend: ticked = done, unticked = to do \* The Godot tileset editor supports only Rectangle and Polygon. That's Tiled are supported and are converted to polygons in Godot. +## Supported entity types + +Creating entities with these types will result in specific nodes to be created : + +- type `Area` + + Creates an `Area2D` node in the scene, containing a `CollisionShape2D` with the rectangle set in the tiled map. + + You can add `collision_layer` and `collision_mask` integer custom properties to set these properties for Godot. + +- type `Node2D` + + Creates an empty `Node2D` at the specified position. Can be useful for defining spawn points for example. + +If present, the `group` custom string property will add the generated entity to the specified Godot scene group. + ## Long term plans + I'm making a 2D platformer and I'm gonna focus on these needs for now. Generally, I would like to support everything Tiled offers because it's a very good level editor. diff --git a/export_to_godot_tilemap.js b/export_to_godot_tilemap.js index a20c67e..2e802ff 100644 --- a/export_to_godot_tilemap.js +++ b/export_to_godot_tilemap.js @@ -10,7 +10,9 @@ class GodotTilemapExporter { this.tileOffset = 65536; this.tileMapsString = ""; this.tilesetsString = ""; + this.subResourcesString = ""; this.extResourceId = 0; + this.subResourceId = 0; /** * Tiled doesn't have tileset ID so we create a map @@ -35,6 +37,28 @@ class GodotTilemapExporter { console.info(`Tilemap exported successfully to ${this.fileName}`); } + /** + * Adds a new subresource to the genrated file + * + * @param {string} type the type of subresource + * @param {object} contentProperties key:value map of properties + * @returns {int} the created sub resource id + */ + addSubResource(type, contentProperties) { + const id = this.subResourceId++; + + this.subResourcesString += ` + +[sub_resource type="${type}" id=${id}] +`; + removeUndefined(contentProperties); + for (const [key, value] of Object.entries(contentProperties)) { + this.subResourcesString += stringifyKeyValue(key, value, false, true) + '\n'; + } + + return id; + } + /** * Generate a string with all tilesets in the map. * 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 { if (!ld.isEmpty) { const tileMapName = idx === 0 ? layer.name || "TileMap " + i : ld.tileset.name || "TileMap " + i + "_" + idx; this.mapLayerToTileset(layer.name, ld.tilesetID); - this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight); + this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight, layer.property("group")); } } } else if (layer.isObjectLayer) { - this.tileMapsString += ` - -[node name="${layer.name}" type="Node2D" parent="."]`; + // create layer + this.tileMapsString += stringifyNode({ + name: layer.name, + type: "Node2D", + parent: ".", + groups: singleDefinedItemToArray(layer.property("group")) + }); + + // add entities for (const object of layer.objects) { + const groups = singleDefinedItemToArray(object.property("group")); + if (object.tile) { let tilesetsIndexKey = object.tile.tileset.name + "_Image"; let textureResourceId = 0; @@ -103,13 +135,54 @@ class GodotTilemapExporter { let objectPositionX = object.x + (object.tile.width / 2); let objectPositionY = object.y - (object.tile.height / 2); - this.tileMapsString += ` - -[node name="${object.name}" type="Sprite" parent="${layer.name}"] -position = Vector2( ${objectPositionX}, ${objectPositionY} ) -texture = ExtResource( ${textureResourceId} ) -region_enabled = true -region_rect = Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${object.tile.height} )`; + this.tileMapsString += stringifyNode({ + name: object.name, + type: "Sprite", + parent: layer.name + }, { + position: `Vector2( ${objectPositionX}, ${objectPositionY} )`, + texture: `ExtResource( ${textureResourceId} )`, + region_enabled: true, + region_rect: `Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${object.tile.height} )` + }); + } else if (object.type == "Area" && object.width && object.height) { + // Creates an Area2D node with a rectangle shape inside + // Does not support rotation + const width = object.width / 2; + const height = object.height / 2; + const objectPositionX = object.x + width; + const objectPositionY = object.y + height; + + this.tileMapsString += stringifyNode({ + name: object.name, + type: "Area2D", + parent: layer.name, + groups: groups + }, { + collision_layer: object.property("collision_layer"), + collision_mask: object.property("collision_mask") + }); + + const shapeId = this.addSubResource("RectangleShape2D", { + extents: `Vector2( ${width}, ${height} )` + }); + this.tileMapsString += stringifyNode({ + name: "CollisionShape2D", + type: "CollisionShape2D", + parent: `${layer.name}/${object.name}` + }, { + shape: `SubResource( ${shapeId} )`, + position: `Vector2( ${objectPositionX}, ${objectPositionY} )`, + }); + } else if (object.type == "Node2D") { + this.tileMapsString += stringifyNode({ + name: object.name, + type: "Node2D", + parent: layer.name, + groups: groups + }, { + position: `Vector2( ${object.x}, ${object.y} )` + }); } } } @@ -347,9 +420,12 @@ region_rect = Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${o * @returns {string} */ getSceneTemplate() { - return `[gd_scene load_steps=2 format=2] + const loadSteps = 2 + this.subResourceId; + + return `[gd_scene load_steps=${loadSteps} format=2] ${this.tilesetsString} +${this.subResourcesString} [node name="Node2D" type="Node2D"] ${this.tileMapsString} `; @@ -370,14 +446,24 @@ ${this.tileMapsString} * Template for a tilemap node * @returns {string} */ - getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16) { - return `[node name="${tileMapName}" type="TileMap" parent="${parent}"] -tile_set = ExtResource( ${tilesetID} ) -cell_size = Vector2( ${tileWidth}, ${tileHeight} ) -cell_custom_transform = Transform2D( 16, 0, 0, 16, 0, 0 ) -format = 1 -tile_data = PoolIntArray( ${poolIntArrayString} ) -`; + getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16, group = undefined) { + let groups = undefined; + if (group) { + groups = [group]; + } + + return stringifyNode({ + name: tileMapName, + type: "TileMap", + parent: parent, + groups: groups + }, { + tile_set: `ExtResource( ${tilesetID} )`, + cell_size: `Vector2( ${tileWidth}, ${tileHeight} )`, + cell_custom_transform: `Transform2D( 16, 0, 0, 16, 0, 0 )`, + format: "1", + tile_data: `PoolIntArray( ${poolIntArrayString} )` + }); } mapLayerToTileset(layerName, tilesetID) { diff --git a/utils.js b/utils.js index a97684f..2598c89 100644 --- a/utils.js +++ b/utils.js @@ -47,3 +47,75 @@ function getTilesetColumns(tileset) { // so we need to return as Math.floor to avoid throwing off the tile indices. return Math.floor(calculatedColumnCount); } + +/** + * returns undefined if undefined is passed in, + * returns an array with the `single` item inside otherwise + */ +function singleDefinedItemToArray(single) { + if (!single) { + return undefined; + } + return [single]; +} + +/** + * Removes any undefined value with its key from an object + * @param {object} obj + */ +function removeUndefined(obj) { + Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]) +} + + +/** + * Translates key values defining a godot scene node to the expected TSCN format output + * Passed keys must be strings. Values can be arrays (e.g. for groups) + * + * @param {object} nodeProperties pair key/values for the "node" properties + * @param {object} contentProperties pair key/values for the content properties + * @return {string} TSCN scene node like so : + * ``` + * [node key="value"] + * content_key = AnyValue + * ``` + */ +function stringifyNode(nodeProperties, contentProperties = {}) { + // remove undefined values from objects + removeUndefined(nodeProperties); + removeUndefined(contentProperties); + + let str = '\n'; + str += '[node'; + for (const [key, value] of Object.entries(nodeProperties)) { + str += ' ' + this.stringifyKeyValue(key, value, true, false); + } + str += ']\n'; + for (const [key, value] of Object.entries(contentProperties)) { + str += this.stringifyKeyValue(key, value, false, true) + '\n'; + } + + return str; +} + +/** + * Processes a key/value pair for a TSCN node + * + * @param {string} key + * @param {string|array} value + * @param {bool} quote + * @param {bool} spaces + */ +function stringifyKeyValue(key, value, quote, spaces) { + // flatten arrays + console.log(value); + if (Array.isArray(value)) { + value = '[\n"' + value.join('","') + '",\n]'; + } else if (quote) { + value = `"${value}"`; + } + if (!spaces) { + return `${key}=${value}`; + } + return `${key} = ${value}`; +} \ No newline at end of file From 3ac34a00558e2d889c299c421781abe78e163097 Mon Sep 17 00:00:00 2001 From: Xorus Date: Thu, 17 Dec 2020 00:24:55 +0100 Subject: [PATCH 2/3] rename Area to Area2D --- README.md | 2 +- export_to_godot_tilemap.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be44467..8dc0585 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Legend: ticked = done, unticked = to do Creating entities with these types will result in specific nodes to be created : -- type `Area` +- type `Area2D` Creates an `Area2D` node in the scene, containing a `CollisionShape2D` with the rectangle set in the tiled map. diff --git a/export_to_godot_tilemap.js b/export_to_godot_tilemap.js index 2e802ff..a2104a2 100644 --- a/export_to_godot_tilemap.js +++ b/export_to_godot_tilemap.js @@ -145,7 +145,7 @@ class GodotTilemapExporter { region_enabled: true, region_rect: `Rect2( ${tileOffset.x}, ${tileOffset.y}, ${object.tile.width}, ${object.tile.height} )` }); - } else if (object.type == "Area" && object.width && object.height) { + } else if (object.type == "Area2D" && object.width && object.height) { // Creates an Area2D node with a rectangle shape inside // Does not support rotation const width = object.width / 2; From cbbbf5d88fee44430394f932f2c4fb206865a30f Mon Sep 17 00:00:00 2001 From: Xorus Date: Thu, 17 Dec 2020 00:42:01 +0100 Subject: [PATCH 3/3] allow multiple groups instead of a single one --- README.md | 2 +- export_to_godot_tilemap.js | 14 +++++--------- utils.js | 10 ++++------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8dc0585..ebe37f8 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Creating entities with these types will result in specific nodes to be created : Creates an empty `Node2D` at the specified position. Can be useful for defining spawn points for example. -If present, the `group` custom string property will add the generated entity to the specified Godot scene group. +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`. ## Long term plans diff --git a/export_to_godot_tilemap.js b/export_to_godot_tilemap.js index a2104a2..5bf0602 100644 --- a/export_to_godot_tilemap.js +++ b/export_to_godot_tilemap.js @@ -100,7 +100,7 @@ class GodotTilemapExporter { if (!ld.isEmpty) { const tileMapName = idx === 0 ? layer.name || "TileMap " + i : ld.tileset.name || "TileMap " + i + "_" + idx; this.mapLayerToTileset(layer.name, ld.tilesetID); - this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight, layer.property("group")); + this.tileMapsString += this.getTileMapTemplate(tileMapName, ld.tilesetID, ld.poolIntArrayString, ld.parent, layer.map.tileWidth, layer.map.tileHeight, layer.property("groups")); } } } else if (layer.isObjectLayer) { @@ -109,12 +109,12 @@ class GodotTilemapExporter { name: layer.name, type: "Node2D", parent: ".", - groups: singleDefinedItemToArray(layer.property("group")) + groups: splitCommaSeparated(layer.property("groups")) }); // add entities for (const object of layer.objects) { - const groups = singleDefinedItemToArray(object.property("group")); + const groups = splitCommaSeparated(object.property("groups")); if (object.tile) { let tilesetsIndexKey = object.tile.tileset.name + "_Image"; @@ -446,12 +446,8 @@ ${this.tileMapsString} * Template for a tilemap node * @returns {string} */ - getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16, group = undefined) { - let groups = undefined; - if (group) { - groups = [group]; - } - + getTileMapTemplate(tileMapName, tilesetID, poolIntArrayString, parent = ".", tileWidth = 16, tileHeight = 16, groups = undefined) { + groups = splitCommaSeparated(groups); return stringifyNode({ name: tileMapName, type: "TileMap", diff --git a/utils.js b/utils.js index 2598c89..d89eb3f 100644 --- a/utils.js +++ b/utils.js @@ -49,14 +49,13 @@ function getTilesetColumns(tileset) { } /** - * returns undefined if undefined is passed in, - * returns an array with the `single` item inside otherwise + * @param {string} str comma separated items */ -function singleDefinedItemToArray(single) { - if (!single) { +function splitCommaSeparated(str) { + if (!str) { return undefined; } - return [single]; + return str.split(',').map(s => s.trim()); } /** @@ -108,7 +107,6 @@ function stringifyNode(nodeProperties, contentProperties = {}) { */ function stringifyKeyValue(key, value, quote, spaces) { // flatten arrays - console.log(value); if (Array.isArray(value)) { value = '[\n"' + value.join('","') + '",\n]'; } else if (quote) {