Dynamically resize pixel art game in fixed zooms

I’ve seen some libraries that seem to resize pixel art games dynamically but they do so in an unclean way. I want to do so only allowing 1x size as the smallest, then 2x size, 3x size, and 4x size to have clean pixel art.

The logic would be as follows:
Start the game at x2 zoom.
If the user resizes the window:
If the window size is smaller than the x1 zoom window size, set the window size to x1 zoom window size to fit all the contents. In other words, don’t allow the user to size the window smaller than the x1 zoom window size.
If the window size is smaller than the x2 zoom window size, set the window size to x1 zoom and center the game contents in the window.
If the window size is larger than the x2 zoom window size, but not larger than x3 zoom window size, set the zoom to x3 and center the contents in the window.
If the window size is larger than x3, set the window size to x3 zoom and center the game contents in the window.
If the window size is “maximized”, set the window mode to full screen and set the zoom to the largest one (up to x4 zoom) that fits the new window dimensions.
If the device the game is running on is a phone, set the zoom to the max zoom that will fit all of the contents.

How would I go about implementing this? So far I have the following in main.script:

    function init(self)
    	msg.post(".", "acquire_input_focus")
    	msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })
    end

Yep, you got that part right:

msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })

Use window.set_listener() to detect a window resize:

window.set_listener(function(self, event, data)
    if event == window. window.WINDOW_EVENT_RESIZED then
        print(data.width, data.height)
    end
end)

Next you need to compare the current window size to the default window size. You get the default window size like this:

local default_width = sys.get_config_int("display.width")
local default_height = sys.get_config_int("display.height")
local zoom = 2
if data.width < default_width or data.height < default_height then
    zoom = 1
end

msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = zoom })

And then you just keep going with your various checks, checking the thresholds for when changing the zoom.

Thank you, I’m struggling with how to set the window size from a Defold script though.
@britzl
Defold API allows me to react to a window resize, but doesn’t seem to allow me to change the window size (other than the initial window size on startup).

This is particularly important when the user tries to size the window smaller than the contents of the game at 1x zoom.

The DefOS plugin seems to allow sizing the window directly on Operating Systems

Yes, DefOS can be used if you need to control window size.

Thank you @britzl

Here is a main.script which does most of what I want. I still haven’t figured out how to detect if the game is running on a phone or browser so that this code may be ignored but, this at least solves things for desktop:

There were several tricky things I had to solve:

  1. When resizing a window by mouse clicking and dragging, constant window resize messages get sent, trying to handle a resize action at that time leads to very bad behavior. Therefore, we have to detect when the mouse button is not pressed down - signaling that a resize operation is not still ongoing.
  2. We cannot handle the window resize logic in the window_callback because the mouse button may still be down, so we unfortunately have to use the update function. What a mess.
  3. I have not found a way to avoid passing around 4 global variables just to accomplish this. Passing around self to every function seems worse.
  4. DefOS is required, and we have to use both defos.get_view_size() and defos.get_window_size() in order to know how what to use for defos.set_view_size(), because defos.set_view_size() needs the x and y from the window size and the width and height from the view size.

Edit: heavily refactored to be better written code.

-- Constants
local DEFAULT_ZOOM = 2
local ALLOWED_ZOOMS = {4, 3, 2, 1}
local SMALLEST_ZOOM = 1

-- State variables
local is_window_resized = false
local is_touch_pressed = false
local view_width, view_height

-- Initialization
function init(self)
	window.set_listener(window_callback)
	msg.post(".", "acquire_input_focus")
	msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = DEFAULT_ZOOM })
end

-- Window event callback
function window_callback(self, event, data)
	if event == window.WINDOW_EVENT_FOCUS_LOST then
		print("Window focus lost")
	elseif event == window.WINDOW_EVENT_FOCUS_GAINED then
		print("Window focus gained")
	elseif event == window.WINDOW_EVENT_ICONFIED then
		print("Window iconified")
	elseif event == window.WINDOW_EVENT_DEICONIFIED then
		print("Window deiconified")
	elseif event == window.WINDOW_EVENT_RESIZED then
		print("Window resized: ", data.width, data.height)
		is_window_resized = true
		view_width = data.width
		view_height = data.height
	end
end

-- Input event handler
function on_input(self, action_id, action)
	if action_id == hash("touch") and action.pressed then
		is_touch_pressed = true
		print("Touch detected!")
	elseif action.released then
		is_touch_pressed = false
	end
end

-- Handle window resizing
function on_window_resized()
	is_window_resized = false
	local base_width = sys.get_config_int("display.width") / DEFAULT_ZOOM
	local base_height = sys.get_config_int("display.height") / DEFAULT_ZOOM

	local view_x, view_y, view_width, view_height = defos.get_view_size()
	local zoom_to_use = SMALLEST_ZOOM

	for _, zoom in ipairs(ALLOWED_ZOOMS) do
		if view_width >= base_width * zoom and view_height >= base_height * zoom then
			zoom_to_use = zoom
			break
		end
	end

	if zoom_to_use == SMALLEST_ZOOM then
		local window_x, window_y, window_width, window_height = defos.get_window_size()
		defos.set_view_size(window_x, window_y, base_width * zoom_to_use, base_height * zoom_to_use)
	end

	msg.post("@render:", "use_fixed_projection", { zoom = zoom_to_use })
end

-- Update function
function update(self, dt)
	if is_window_resized and not is_touch_pressed then
		on_window_resized()
	end
end
1 Like