Can I force coordinates of all Objects (Polygon, polyline, Rectangle) to be on integer positions?

I’m building a game with levels made up of shapes: polygons and rectangles.

My game doesn’t support fractional positions, so I need to make sure that when the level is saved, all points (nodes) are rounded off to the nearest integer.
I am aware of the View > Snapping > Snap To Pixels feature, but here’s the thing: I’m looking to share my template project with other level editors, and they might forget to turn that option on.

So I guess I’m looking for a plugin that rounds all Node positions to the nearest pixel on save. So far, I have been using the .tmj format to save levels, which works fine for my game. But my game can’t deal with fractional numbers, that’s why.
I though that if an extension is available for this, I could add that extension to the project, and then share the project as a template, right?

Is something available to do this? Or should I get help in building that extension?

I responded on Discord before I saw that you posted here. I don’t want to repeat myself, but here’s the script you’d need:

function forceIntegerCoordinates(mapOrLayer) {
	if(!mapOrLayer)
		return;
	if(mapOrLayer.isObjectLayer) {
		let objects = mapOrLayer.objects;
		for(object of objects) {
			object.x = Math.round(object.x);
			object.y = Math.round(object.y);
			if(object.shape == MapObject.Polygon || object.shape == MapObject.Polyline) {
				for(point of object.polygon) {
					point.x = Math.round(point.x);
					point.x = Math.round(point.y);
				}
			}
		}
	} else if(mapOrLayer.isTileMap || mapOrLayer.isGroupLayer) {
		let numLayers = mapOrLayer.layerCount;
		for(var i = 0; i < numLayers; i++) {
			forceIntegerCoordinates(mapOrLayer.layerAt(i));
		}
	} //else, do nothing
}

//Auto-apply on save:
tiled.assetAboutToBeSaved.connect(function(asset) {if(asset.isTileMap) forceIntegerCoordinates(asset); } );

//Allow manually applying via an Action:
let forceIntegerCoordinatesAction = tiled.registerAction("ForceIntegerCoordinates", function() { forceIntegerCoordinates(tiled.activeAsset); } );
forceIntegerCoordinatesAction.text = "Force Integer Coordinates";
//add this action to the Edit menu:
tiled.extendMenu("Edit", [
	{ action: "ForceIntegerCoordinates", before: "Preferences" }
]);

If you also want to apply this change to collision objects on Tiles in Tilesets, you’ll need to add an additional check for mapOrLayer.isTileset, and iterate all the tiles, and for each tile, and apply forceIntegerCoordinates to tile.objectGroup.

If you always want it manually applied, comment out the tiled.assetAboutToBeSaved line. If you always want it automatically applied and don’t need the action, comment out everything after the “allow manually applying via Action” comment.

I’ve tested this script via action, but not the signal, and I only tested it on Rectangle Objects. It should work, but you should test it before you send it to your teammates xP

If you need this to also apply to sizes, do that in the same part that does the positions. I imagine if your game can’t handle fractional positions, it can’t handle fractional sizes either…

I also want to reiterate that a smoother fix would be to round the coordinates in your game upon loading the map. Even with a script like this, you shouldn’t rely on people delivering perfect maps, your game should be able to handle reading in bad position coordinates, even if it can’t fully use them.

Thanks! There were a few small bugs in your code but I managed to solve them.

The polygon coordinates weren’t updated. I think the reason is that the points were updated as a copy and thus the original array was not affected. Working script, tested in Tiled 1.10 with rectangles, polygon, polyline and group layers. Thanks!

function forceIntegerCoordinates(mapOrLayer) {
	tiled.log("Running forceIntegerCoordinates:" + mapOrLayer);
	if(!mapOrLayer){
		return;
	};
	if(mapOrLayer.isObjectLayer) {
		let objects = mapOrLayer.objects;
		for(object of objects) {
			object.x = Math.round(object.x);
			object.y = Math.round(object.y);
			object.width = Math.round(object.width);
			object.height = Math.round(object.height);
			if(object.shape == MapObject.Polygon || object.shape == MapObject.Polyline) {
				object.polygon = object.polygon.map((p) => 
					Qt.point(
						Math.round(p.x), 
						Math.round(p.y)
					)
				); 
			}
		}
	} else if(mapOrLayer.isTileMap || mapOrLayer.isGroupLayer) {
		let numLayers = mapOrLayer.layerCount;
		for(var i = 0; i < numLayers; i++) {
			forceIntegerCoordinates(mapOrLayer.layerAt(i));
		}
	} else {
		//else, do nothing
	}
		
}

//Auto-apply on save:
tiled.assetAboutToBeSaved.connect(function(asset) {if(asset.isTileMap) forceIntegerCoordinates(asset); } );

//Allow manually applying via an Action:
let forceIntegerCoordinatesAction = tiled.registerAction("ForceIntegerCoordinates", function() { forceIntegerCoordinates(tiled.activeAsset); } );
forceIntegerCoordinatesAction.text = "Force Integer Coordinates";
//add this action to the Edit menu:
tiled.extendMenu("Edit", [
	{ action: "ForceIntegerCoordinates", before: "Preferences" }
]);
1 Like

Ah, good catch, I forgot that polygon needed to be assigned, sorry about that! Hope I didn’t waste too much of your time with that bug.

1 Like

Note that `assetAboutToBeSaved does not get triggered when exporting rather than saving. So, registering my own level format would be a better option after all, it seems