Cannot figure out correct Isometric TileMap calculation

Hi there! I need a bit of help.
I made a Game Object called “Terrain” and added a TileMap to it called “grass_tilemap”, with a layer named “grass”. The tilesource uses a 512x512 texture, which I split into 64x64 tiles.

I made a function that puts a random tile wherever you click, and it works fine. But I wanted to make it look isometric, so I rotated the TileMap by 45 degrees and scaled the Game Object to X: 1.5 and Y: 0.75.

I figured out how to handle the scale in the function, but the tiles are still showing up too far to the left, probably because of the rotation. I’d love some help getting the placement to work properly with the rotation.

Here’s my code below!

function on_input(self, action_id, action)
    -- Check for touch input action
    if action_id == hash("touch") and action.pressed then
        print("Touch input detected")
        local tile_size_x = 64 * 1.5 -- Tile size in pixels (accounting for scale)
        local tile_size_y = 64 * 0.75 -- Tile size in pixels (accounting for scale)

        local worldx, worldy = screen_to_world(action.x, action.y, 0)
        print("World Coordinates in Terrain Editor: ", worldx, worldy)

        -- Convert touch position to tile indices (1-based)
        local tile_x = math.floor(worldx / tile_size_x) + 1
        local tile_y = math.floor(worldy / tile_size_y) + 1

        -- Get tilemap bounds to validate coordinates
        local x_start, y_start, width, height = tilemap.get_bounds("Terrain#grass_tilemap")

        -- Check if tile coordinates are within bounds
        if tile_x >= x_start and tile_x < x_start + width and
            tile_y >= y_start and tile_y < y_start + height then
            -- Generate random tile index (0-63 for 8x8 tiles)
            local random_tile = math.random(0, 63)
            -- Set the tile on the specified layer (replace "your_layer" with actual layer name)
            tilemap.set_tile("Terrain#grass_tilemap", "grass", tile_x, tile_y, random_tile)
        end
    end
end

Since my tiles are 96 in Width and 64 in Height. This code below does select the tile in right direction but its selection gets worse the more you get away from center of 0,0. I would really appreciate some guidance.

	local x_index = math.floor((worldx / 96 + worldy / 48) * 0.5 + 0.5)
	local y_index = math.floor((worldy / 48 - worldx / 96) * 0.5 + 0.5)

This is from 7 or 8 years ago so I can not remember how it works at all. But maybe it’s useful for you anyway.

My tiles were 40x20 though so keep that in mind.

The code includes a link to the stackoverflow post that helped me all those years ago!


pos_to_tile_pi_quarter = math.pi * 0.25
pos_to_tile_diag = 20 * math.sqrt(2)
function pos_to_tile(p)

    --https://stackoverflow.com/questions/19717770/screen-coordinates-to-isometric-coordinates
    --http://i.imgur.com/HnKpYmG.png

    if p == nil then
        print("error: pos_to_tile, p was nil", p)
        return
    end

    local pos = vmath.vector3(p.x, p.y, 0)

    local map_pixel_width = map_boundaries.y - map_boundaries.x         --map_boundaries is a 4-dimensional vector
    local col_range = (map_boundaries.w - map_boundaries.z) / 20

    --adjust x origin to account for 0,0 tile distance from 0,0 pixels
    pos.x = map_pixel_width - (pos.x - map_pixel_width * 0.5)

    --double (since height is half of width) the y position to account for compressed height perspective
    pos.y = pos.y * 2

    --rotate x and y positions by 45 degrees, counter clockwise
    local xr = math.cos(pos_to_tile_pi_quarter) * pos.x - math.sin(pos_to_tile_pi_quarter) * pos.y
    local yr = math.sin(pos_to_tile_pi_quarter) * pos.x + math.cos(pos_to_tile_pi_quarter) * pos.y

    --divide result by the side of a square-ised tile
    local row = round(xr / pos_to_tile_diag) + 1

    --invert y axis since a high y position is equal to a low y tile
    local col = col_range - (math.floor(yr / pos_to_tile_diag) - col_range)

    return {row=row, col=col, walkable=check_if_tile_walkable(row, col), flyable=check_if_tile_flyable(row, col)}
end
3 Likes

Thank you for your Helpful Comment. I just want to ask how did you make your tile-maps isometric?

I also converted one coordinate space to another, (screen to isometry and back again), for me it was easier than using maths tricks. (Google Cartesian coordinates to isometric for more info)

local map = {}
map.cell_size = 142
map.width = 8
map.height = 8
map.bounds = {left = 0, bottom = 0, right = 0, top = 0}
local v = vmath.vector3()
local q1 = vmath.quat_rotation_z(-math.pi / 4)
local q2 = vmath.quat_rotation_z(math.pi / 4)

function map.isometric_to_screen(ix, iy)

	v.x = (ix + map.bounds.left - 1) * map.cell_size - map.cell_size / 2
	v.y = (iy + map.bounds.bottom - 1) * map.cell_size - map.cell_size / 2
	local r = vmath.rotate(q1, v)
	return
		r.x,
		r.y / 2
end

function map.screen_to_isometric(x, y)
	
	v.x = x
	v.y = y * 2
	local r = vmath.rotate(q2, v)

	return
		math.ceil(r.x / map.cell_size) - map.bounds.left + 1,
		math.ceil(r.y / map.cell_size) - map.bounds.bottom + 1
end

5 Likes

I described my workflow in this post:

1 Like

Thank you for this. But i don’t know why its not working for me. Can you please check my setup?
My TileSource uses a 512x512 Texture. Which it divides into 64x64 parts.

This is my TileMap, that i have setup for debugging:

Then in the Main Collection I rotated it 45 degrees in the Z:

Using the tilemap.get_bounds() i get: -4 -4 10 10

Using the code you shared i set my cell_size to 64. Width to 10, Height to 10. Bounds remain 0 in all sides.

My Terrain Editor Script:

local DISPLAY_WIDTH = sys.get_config_int("display.width")
local DISPLAY_HEIGHT = sys.get_config_int("display.height")
local rts_camera

local MapConverter = require("main.Map")

function init(self)
	-- Inputs
	msg.post(".", "acquire_input_focus") -- Tells the game object to recieve input events for on_input

	-- screen_to_world function variables
	rts_camera = msg.url("RTSCamera#camera_component") -- Gets the Camera Component
end

local function screen_to_world(x, y, z)
	local projection = camera.get_projection(rts_camera)
	local view = camera.get_view(rts_camera)
	local w, h = window.get_size()
	w = w / (w / DISPLAY_WIDTH)
	h = h / (h / DISPLAY_HEIGHT)

	-- https://defold.com/manuals/camera/#converting-mouse-to-world-coordinates
	local inv = vmath.inv(projection * view)
	x = (2 * x / w) - 1
	y = (2 * y / h) - 1
	z = (2 * z) - 1
	local x1 = x * inv.m00 + y * inv.m01 + z * inv.m02 + inv.m03
	local y1 = x * inv.m10 + y * inv.m11 + z * inv.m12 + inv.m13
	local z1 = x * inv.m20 + y * inv.m21 + z * inv.m22 + inv.m23
	return x1, y1, z1
end

function on_input(self, action_id, action)
	-- Check for touch input action
	if action_id == hash("touch") and action.pressed then
		print("Touch input detected")

		local worldx, worldy = screen_to_world(action.x, action.y, 0)
		local tile_x, tile_y = MapConverter.screen_to_isometric(worldx, worldy)
		print("Tile:", tile_x, tile_y)

		-- Get tilemap bounds to validate coordinates
		local x_start, y_start, width, height = tilemap.get_bounds("Terrain#grass_tilemap")
		print("Tilemap bounds:", x_start, y_start, width, height)

		-- Check if tile coordinates are within bounds
		if tile_x >= x_start and tile_x < x_start + width and
			tile_y >= y_start and tile_y < y_start + height then
			-- Generate random tile index (0-63 for 8x8 tiles)
			local random_tile = math.random(0, 63)
			-- Set the tile on the specified layer (replace "your_layer" with actual layer name)
			tilemap.set_tile("Terrain#grass_tilemap", "grass", tile_x, tile_y, random_tile)
		end
	end
end