Skip to content

Commit

Permalink
Merge pull request #22 from xorus/groups
Browse files Browse the repository at this point in the history
Add Godot groups on layers and support for Area2D and Node2D nodes
  • Loading branch information
MikeMnD authored Dec 28, 2020
2 parents 9bfcc1a + cbbbf5d commit 1802203
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 20 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `Area2D`

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 `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

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.

Expand Down
122 changes: 102 additions & 20 deletions export_to_godot_tilemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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("groups"));
}
}
} 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: splitCommaSeparated(layer.property("groups"))
});

// add entities
for (const object of layer.objects) {
const groups = splitCommaSeparated(object.property("groups"));

if (object.tile) {
let tilesetsIndexKey = object.tile.tileset.name + "_Image";
let textureResourceId = 0;
Expand All @@ -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 == "Area2D" && 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} )`
});
}
}
}
Expand Down Expand Up @@ -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}
`;
Expand All @@ -370,14 +446,20 @@ ${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, groups = undefined) {
groups = splitCommaSeparated(groups);
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) {
Expand Down
70 changes: 70 additions & 0 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,73 @@ function getTilesetColumns(tileset) {
// so we need to return as Math.floor to avoid throwing off the tile indices.
return Math.floor(calculatedColumnCount);
}

/**
* @param {string} str comma separated items
*/
function splitCommaSeparated(str) {
if (!str) {
return undefined;
}
return str.split(',').map(s => s.trim());
}

/**
* 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
if (Array.isArray(value)) {
value = '[\n"' + value.join('","') + '",\n]';
} else if (quote) {
value = `"${value}"`;
}
if (!spaces) {
return `${key}=${value}`;
}
return `${key} = ${value}`;
}

0 comments on commit 1802203

Please sign in to comment.