Help with pixel-perfect GUI scaling

I’m running into lots and lots of issues trying to get just the right setup for a pixel art game. I want both the world and the GUI to scale with the window, but only at whole-level increments. After some work, I’ve managed to get that set up just right.

I’m using the defold orthographic framework for the camera, but I had to modify the render script to get the GUI to lock to the view instead of the window:

-- draw world per camera
local cameras = camera.get_cameras()
if #cameras > 0 then
	render.disable_state(render.STATE_DEPTH_TEST)
	render.disable_state(render.STATE_CULL_FACE)
	render.disable_state(render.STATE_STENCIL_TEST)
	for _,camera_id in ipairs(cameras) do
		local viewport = camera.get_viewport(camera_id)
		render.set_viewport(viewport.x, viewport.y, viewport.z, viewport.w)
		render.set_view(camera.get_view(camera_id))
		render.set_projection(camera.get_projection(camera_id))
		render.draw(self.tile_pred)
		render.draw(self.particle_pred)
		render.draw_debug3d()
	end

	-- draw gui in game space using an orthographic projection
	render.disable_state(render.STATE_DEPTH_TEST)
	render.disable_state(render.STATE_CULL_FACE)
	render.enable_state(render.STATE_STENCIL_TEST)
	--local camera_id = cameras[1]
	--local viewport = camera.get_viewport(camera_id)
	--render.set_viewport(viewport.x, viewport.y, viewport.z, viewport.w)
	--render.set_view(camera.get_view(camera_id))
	--render.set_projection(camera.get_projection(camera_id))
	render.draw(self.gui_pred)
	render.draw(self.text_pred)
end

(commented out some lines that weren’t doing anything)

For the camera I’ve got the projection set to FIXED_ZOOM and it’s working great, visually. The GUI stays in the right place even when I expand the window.

My problem right now, though, is getting the GUI interactable. Since I’m rendering it scaled to the view instead of the window, the targets are off. ALSO I have a retina screen, which adds another wrinkle to things.

I’ve tried so many combinations of camera.screen_to_world and camera.window_to_world and camera.world_to_screen and while I can get it to work when the window’s at its default size, as soon as it’s resized, everything is off. And I can’t for the life of me wrap my head around what I need to do to convert the action coordinates to the correct values.

This is what I have currently:

function convert_action_to_world(action)
	local cameras = camera.get_cameras()
	if #cameras > 0 and action.x and action.y then
		local dpi_ratio = get_scaling_factor()
		
		local WINDOW_WIDTH, WINDOW_HEIGHT = camera.get_window_size()
		WINDOW_WIDTH = WINDOW_WIDTH / dpi_ratio
		WINDOW_HEIGHT = WINDOW_HEIGHT / dpi_ratio
		local centerX, centerY = WINDOW_WIDTH / 2, WINDOW_HEIGHT/2
		local window = {x=0, y=0, w=WINDOW_WIDTH, h=WINDOW_HEIGHT}

		local DISPLAY_WIDTH, DISPLAY_HEIGHT = camera.get_display_size()
		local display = {x=centerX - DISPLAY_WIDTH/2, y=centerY - DISPLAY_HEIGHT/2, w=DISPLAY_WIDTH, h=DISPLAY_HEIGHT}

		local camera_id = cameras[1]

		local p = vmath.vector3(action.x, action.y, 0)
		p = p / dpi_ratio
		return p
	end
	return nil
end

This solution doesn’t use any of the orthographic coordinate conversion methods, but it’s at least easier for me to follow what it’s doing. But it has the same problem as all the other approaches I’ve tried – as soon as I resize the window, the mouse/action coordinates don’t line up with the buttons.

Help?

The GUI is designed to scale and adjust to changes in screen orientation and size. If you do not want this at all then you can set the adjust reference on the entire GUI to none.

that’s what I did!

The changes to the render_script were necessary to prevent it from moving around when the window changed dimensions, though.

What I’m stuck on is I can’t figure out how to convert the action coordinates into something that will properly trigger the GUI.

None of the numbers I’m seeing make any sense. I have a button that should be at (462,248)-(224,48).
I can log that and verify it.

If I have my mouse cursor hovering over the bottom left corner of the button, the action.x and action.y I get is (176.375, 114.125).

local w = camera.screen_to_world(camera_id, p)

w = 176.37498474121, 114.12499237061

local s = camera.world_to_screen(camera_id, p)

s = 176.37503051758, 114.125

This is all when the window is at the default size, so the buttons are activating correctly. I’m just trying to figure out how all these numbers are related and get something logging the “correct” numbers so I can try to figure out a solution for the actual problem.