Isometric Tile Selection in Godot

Isometric games have a unique problem that “lower” tiles (with higher Y values) can overlap those that are higher up. Likewise, a tile texture will most likely not be fully contained within the cell that it belongs to. Because of this, when we want to select an item in a cell, we can’t rely on the local_to_map function that would typically be used for non-isometric 2D tiles.

Take the following. The object we want to click on is the green cube. If we were to simply use local_to_map we might get one of the tiles that sits behind it, depending on where the object was clicked.

Mouse clicks at these points would result in incorrect tile selection

To solve this, we can setup collision shapes in the TileSet properties, perform a 2d point ray query at the cursor position, detect the area2ds that we “collided” with and return the coordinates at the highest Y position. This is actually quite simple to implement.

Add Tile Collision

Add a Collision Layer to the TileSet. The layer properties aren’t particularly important so the defaults are fine.

Add a default Collision layer to the TileSet

Add collision shapes in TileSet properties to the tiles we’re interested in detecting.

Add a collision shape that surrounds the tile
The collision shapes will overlap

Query Collisions

In the _unhandled_input (or whichever method you’re using to detect), use intersect_point to detect the areas in the TileMap. The position of the point is the mouse position.

We’re interested in the rid that’s returned in the collision data. In this case, it corresponds to the collision body. We can retrieve the cell coordinates using the get_coords_for_body_rid method on the TileMap. With that information, the data from the cell can also be retrieved if required.
If multiple hits are detected, we want to filter them by the highest Y value, this will return the tile coord “closest” to the camera.

The code for this is below, this is in a class extending TileMap.

func _unhandled_input(event):
	# listen to a click action (you may want to use an action instead)
	if event is InputEventMouseButton and event.is_pressed():
		# get the current mouse position
		var mousepos = get_global_mouse_position()
		# construct our point query
		var query = PhysicsPointQueryParameters2D.new()
		query.position = mousepos
		# get the screen space
		var space_state = get_world_2d().direct_space_state
		# fetch collision results
		var collisions = space_state.intersect_point(query)
		if !collisions.is_empty():
			var highest := get_coords_for_body_rid(collisions[0].rid)
			for collision in collisions:
				var current = get_coords_for_body_rid(collision.rid)
				if current.y > highest.y:
					highest = current
			print(highest)
GDScript

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *