mapeditor/tiled

Tiled 1.2.x object properties backwards compatibility issues

Closed this issue · 3 comments

I saw that properties were restructured into an array-like JSON format for object templates in #1868. This heavily breaks the way that my application was previously using properties, and is not at all intuitive for how one would use properties for in-game objects. Properties inherently should be a JSON object or map of properties -> values. I can't upgrade to Tiled 1.2.x with this break in backwards compatibility.

I understand that the specific use-case sited was Unity3d serialization, but for folks looking to parse Tiled objects, selecting properties.foo is much more intuitive than iterating over every single tile and creating a property map for each tile.

Here's an example use-case of an NPC, whose Tiled object properties defines its dialog & whether nor not that NPC wanders around the map.

{
  "gid": 4816,
  "height": 32,
  "id": 165,
  "name": "NPC",
  "properties": {
    "bubbleData": "8629,9469",
    "dialog": "BARTENDER",
    "dialogBubbleEnabled": true,
    "entity_type": "NPC",
    "wanders": false
  },
  "propertytypes": {
    "bubbleData": "string",
    "dialog": "string",
    "dialogBubbleEnabled": "bool",
    "entity_type": "string",
    "wanders": "bool"
  },
  "rotation": 0,
  "type": "",
  "visible": true,
  "width": 32,
  "x": 224,
  "y": 320
}

Here is the resulting object as a result of #1868.

{
    "gid":4816,
    "height":32,
    "id":165,
    "name":"NPC",
    "properties":[
           {
            "name":"bubbleData",
            "type":"string",
            "value":"8629,9469"
           }, 
           {
            "name":"dialog",
            "type":"string",
            "value":"BARTENDER"
           }, 
           {
            "name":"dialogBubbleEnabled",
            "type":"bool",
            "value":true
           }, 
           {
            "name":"entity_type",
            "type":"string",
            "value":"NPC"
           }, 
           {
            "name":"wanders",
            "type":"bool",
            "value":false
           }],
    "rotation":0,
    "type":"",
    "visible":true,
    "width":32,
    "x":224,
    "y":320
   }

At the very least, a visible warning and alert that this massively breaks backwards compatibility for those using Tiled objects would be appreciated!

Here's some more supporting examples of how I used the previous implementation of properties for objects. This is a code snippet that takes one of these objects, creates either an Actor or Item from the object, and places it in the game.

createActorFromObject(object) {
		const { gid, x, y, properties } = object
		if (properties.entity_type === undefined) {
			console.error(`No entity type given in properties for object ${object}`)
			return
		}
		const { entity_type } = properties
		let nx = x / 32
		let ny = y / 32 - 1 // for some reason, TILED objects y position are 1-indexed..?
		let entity = properties.item ? createItem(entity_type, nx, ny, gid - 1) : createActor(entity_type, nx, ny, gid - 1)
		if (properties.item) entity.inInventory = false
		if (properties.items !== undefined) {
			let items = properties.items.split(',').map(i => createItem(i.trim(), nx, ny))
			items.forEach(item => entity.addToInventory(item))
		}
		/* Post entity creation clean up... */
		if (entity_type === 'NPC') {
			// add NPC routines if any exist...
			// NPCs may have items too!
			entity.wanders = properties.wanders
			if (properties.dialog !== undefined) {
				const dialogID = properties.dialog
				if (dialogID in DIALOGUES) {
					entity.dialogData = DIALOGUES[dialogID]
					entity.dialogBubbleEnabled = 'dialogBubbleEnabled' in properties ? properties.dialogBubbleEnabled : true
					if ('bubbleData' in properties) {
						let [id, animated_id] = properties.bubbleData.split(',')
						entity.bubbleData = { id, animated_id }
					} else {
						entity.bubbleData = null
					}
				}
			}
		} else if (entity_type === 'LADDER' || entity_type === 'LEVEL_TRANSITION') {
			// ladders & level transitions have portal ID's
			entity.portal = properties.portalID
			entity.createDungeon = properties.createDungeon
			if (entity_type === 'LADDER') entity.direction = properties.direction
		}
		this.getTile(nx, ny).actors.push(entity)
	}
bjorn commented

I'm sorry that this compatibility change affected your project and likely wasted some of your time. I did mention it in the blog post about the Tiled 1.2 release under "Compatibility Notes" and provided details on the JSON format page.

It wasn't only Unity that was having trouble parsing the old format, but generally all annotation-based automated parsing systems would have trouble dealing with JSON objects with arbitrary members. Also, the properties had become quite a mess with the additional propertytypes and tilepropertytypes members. Hence I'm reluctant to re-introduce support for the old format, even though I understand it was easier previously to access the properties in dynamically typed languages like JavaScript.

Fortunately, it should be easy to support both formats in the map loader. After you have loaded the JSON, you could iterate your objects and perform the following kind of operation:

function ensureObject(properties) {
    if (Array.isArray(properties)) {
        const array = properties;
        properties = {};
        for (const p of array)
            properties[p.name] = p.value;
    }
    return properties;
}

// For each object / tile / layer:
something.properties = ensureObject(something.properties)

I hope that helps you support Tiled 1.2 in your game.

Thanks for your swift response. I can see where this could present some issues across the board for other parsing systems, and where having the unified properties list with its respective type present can be really helpful. I'll be sure to do as you suggested above, and add an additional step to parse the properties array into a map as it was before.