Post tutorial RSS Supporting multiple resolutions in Godot Engine

One of the most common problems of pixel art games is supporting multiple resolutions. The difficulty is because such graphics can’t be scaled with any ratio you need without artifacts. In this article, I’ll show different approaches to solving this issue.

Posted by on - Basic Client Side Coding

One of the most common problems of pixel art games is supporting multiple resolutions. The difficulty is because such graphics can’t be scaled with any ratio you need without artifacts. In this article, I’ll show different approaches to solving this issue.

The most common way is to use Stretch in project properties. Godot documentation describes this very clearly here. But stretching doesn’t solve the problem of scaling pixel art graphics. Here is the approach I’ve used in my game — Comrade Architect.

First of all, we need to construct a scene with game objects. I didn’t mind about resolutions at this point. In Comrade Architect, I need to draw available tiles in the middle of the screen. But what if the available space is much bigger than my game view? I needed to scale up objects view somehow. I knew that it’s possible to scale pixel art by an integer factor. So, I’ve used a camera with a script in it, which knows how to show a rectangle. Here it is:

class_name PlanetCamera
extends Camera2D

export var allow_downscale: bool = false
export var precise_zoom: bool = false

var current_rect: Rect2


func _process(_delta):
	if current_rect != null:
		_perform_show_current_rect()


func show_rect(rect: Rect2):
	current_rect = rect
	position = rect.position + Vector2(rect.size.x / 2, rect.size.y / 2)
	
	_perform_show_current_rect()


func _perform_show_current_rect():
	var viewport_rect: Rect2 = get_viewport_rect()
	
	var viewport_size = min(viewport_rect.size.x, viewport_rect.size.y)
	var tile_map_size = max(current_rect.size.x, current_rect.size.y)
	
	if tile_map_size == 0:
		return
	
	var new_zoom
	if viewport_size == 0:
		new_zoom = 1
	else:
		new_zoom = tile_map_size / viewport_size
	
	if precise_zoom:
		zoom = Vector2(new_zoom, new_zoom)
		return
	
	if new_zoom > 1:
		if allow_downscale:
			var _new_zoom = 1.0
			while _new_zoom < new_zoom:
				_new_zoom *= 2
			new_zoom = _new_zoom
		else:
			new_zoom = 1
	else: if new_zoom == 1:
		new_zoom = 1
	else:
		var _new_zoom = 1.0
		while 1 / _new_zoom > new_zoom:
			_new_zoom += 1
		new_zoom = 1 / (_new_zoom - 1)

	zoom = Vector2(new_zoom, new_zoom)

As you can see, I increment the scale by an integer amount until I get as close to the desired value as possible. Now I had all game objects centered in a viewport.

The next step is to build a UI for the game. I’m an Android developer by profession, so the solution came to me from my day-to-day routine. On mobile devices, we use adaptive layouts and resizable vector backgrounds to support many different screens. Godot has several ways to create scalable pixel art UI. The first is to use NinePatchRect as a background. I don’t advise this approach because it doesn’t work with themes and different UI states. Better stick with the second one. It’s about configuring margins in StyleBoxTexture. You only need to select an area to stretch on scaling. Then you can set the result to any Control you need, from Button to Panel.

Now it’s time to combine it all. I’ve used a ViewportContainer to add the game objects to my UI. I liked this approach because it gives full control over player view. At this point, game objects were scaling nice, but UI elements were too little on HIDPI screens. I added a global script to manipulate root Viewport stretch to adjust UI scale to screen size. The idea was to use the same approach as for game objects. I increment the Stretch setting until it’s below the desired amount. Here is the script, which I added to autoload:

extends Node

var project_size = Vector2(
		ProjectSettings.get_setting("display/window/size/width"),
		ProjectSettings.get_setting("display/window/size/height")
	)
var current_scale = -1


func _ready() -> void:
	OS.min_window_size = Vector2(project_size.x, project_size.y)


func _process(_delta: float) -> void:
	var new_scale = _calculate_interface_scale()
	if  new_scale != current_scale:
		get_tree().set_screen_stretch(
				SceneTree.STRETCH_MODE_DISABLED, \
				SceneTree.STRETCH_ASPECT_EXPAND, \
				Vector2.ZERO, \
				new_scale
		)
		current_scale = new_scale


func _calculate_interface_scale() -> int:
	var window_size = OS.get_window_size()
	
	var desired = min(project_size.x, project_size.y)
	var current = min(window_size.x, window_size.y)
	
	var scale = 1
	
	while current / scale > desired:
		scale += 1
	
	return max(scale - 1, 1) as int

Here I used the desired project screen size to determine the scale factor. Pixel art can’t be downscaled without artifacts, so I didn’t allow to resize the window smaller than the minimum. Now I had a brilliant solution that supports different scales for game objects and UI without artifacts.

You can check the result on Itch.io. Thanks for reading!

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.