Responsive to fit multiple resolutions

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Rasmus
:warning: Old Version Published before Godot 3 was released.

Introduction

I am having a problem bulding responsiveness.

The build-in methods of stretching and scaling, found in the project settings, is not quiet what i want, or at least i can’t make them behave as i want.

I kind of want a mix of the “keep aspect ratio” and ignoring stretching completely.

I want the game to scale, keeping the aspect ratio, as long a certain area of the game fits, and if the aspect ratio doesn’t fill the whole screen i just want more of the game to be shown.

More in depth explanation - With fancy pictures and everything
Full
The red frame is the minimum, and should always be shown.
As long as this minimum frame of the game can be scaled up (keeping the aspect ratio) the whole game should just scale up like the “keep aspect ratio” option would do.

If the screen has more room to the sides or at the top/bottom, more of the background should just be visible instead of adding black bars or such.

For example running on a mobile in portrait mode it would look like this:
Portrait

Running on a mobile in landscape mode it would look like this:
Landscape

Summary

  • How can i dynamically define how much of my game should be shown at runtime?

So i can just show more of my background instead of black bars/clear color generated by Godot, of course as long as my texture is big enough.

  • How can i define a minimum size?

So i make sure my whole game is always visible, and at the same time i have a “zeroing point” to base the scaling off.

  • How do i scale up the game, keeping the aspect ratio of the defined minimum size?

So my game scales to fit all resolutions, just like the build-in “keep aspect ratio stretch_mode” would.

Side notes
I have played a bunch around with the stretching and scaling, i have tried a few ideas i had with controlling the viewport of the root node through scripting.

I just really can’t seem to find a mix of the functionalities already in Godot.

I am looking forward to your ideas, suggestions, solutions or whatever you might have to add.

Thanks
Keetz

:bust_in_silhouette: Reply From: Rasmus

I have come up with a solution, so for everyone looking for this kind of behavior, here is one way it can be done.

I would like to thanks bojidar-bg (bojidar-bg (Bojidar Marinov) · GitHub) for helping me out on IRC by directing me to a previous game that has similar behavior, GitHub - KOBUGE-Games/minilens: Cute puzzle platformer starring a cleaning robot on post-apocalyptic Earth

Most of the scripting is based on this game, so if my solution doesn’t quiet fit for you, give minilens a look yourself :wink:

TL;DR
This ended up being quiet long so i added a TL;DR at the bottom, if you know your way around Godot.

All right, let’s get on with it!
First of all we need to setup our project settings and our scene. I will just show the bare minimum scene structure.

In Project Settings->Display i set the width and height to my minimum size so i have a reference for setting up my scene correct. Again, it is just a reference so it is easy to visualize how much is visible at all times, we will manipulate the actual size of the viewport later in script.

Next up in Project Settings->Display we should set the stretch_mode to 2d and the stretch_aspect to ignore, have a look at http://docs.godotengine.org/en/stable/tutorials/engine/multiple_resolutions.html for more information on how stretching works.

That’s it for Project Settings.

Next up we add some stuff to our scene, we need a Control with a child Sprite.

Simple scene setup
The Texture on the sprite is our oversized background that goes beyond the minimum size we want, and the Sprite itself is positioned to the center of our minimum sized frame. For example check the Centered property in the Inspector and set the Position to (minimum width/2, minimum height/2).
The Control nodes anchor is set to Center so our background stays centered.

With our current project setup our image will just stretch and skew to whatever dimensions are needed to fill out the window, thanks to the stretch_mode and stretch_aspect.

Time to code!

This is where we set our minimum size that actual matters, the scaling will be based on the size we set here in the script, and not the Project Settings that we set earlier.

We will make use of the signal “size_changed” found on a Viewport (Viewport — Godot Engine (stable) documentation in English) so we don’t have to check for changes in screen size every frame, every second or whatever.

Whenever the screen size changes, we:

  • Calculate a scale factor based on the minimum height and the current height of the screen
  • Calculate a new size for the viewport
  • Check if we should scale down (if the new width/height is smaller than our minimum width/height)
  • Set the new size on our viewport

I wont explain the math itself, but it is really not that advanced and it should be easy to find examples and explanations around the web for simple scaling like this.

The script, again bare minimum.

extends Node

onready var viewport = get_viewport()

var minimum_size = Vector2(1920, 1080)

func _ready():
	viewport.connect("size_changed", self, "window_resize")
	window_resize()

func window_resize():
	var current_size = OS.get_window_size()
	
	var scale_factor = minimum_size.y/current_size.y
	var new_size = Vector2(current_size.x*scale_factor, minimum_size.y)
	
	if new_size.y < minimum_size.y:
		scale_factor = minimum_size.y/new_size.y
		new_size = Vector2(new_size.x*scale_factor, minimum_size.y)
	if new_size.x < minimum_size.x:
		scale_factor = minimum_size.x/new_size.x
		new_size = Vector2(minimum_size.x, new_size.y*scale_factor)
	
	viewport.set_size_override(true, new_size)

Last up we have to save our script and then add it as a singleton in Project SettingsAutoLoad tab (http://docs.godotengine.org/en/stable/tutorials/step_by_step/singletons_autoload.html).

EDIT
I just thought i would add a screenshot of a setup where there is a game node added, to show how we make sure our game stays centered on the background.

With Game Node
So we have a Main node that just contains the rest of our game.
Then we have two Control nodes, one which is our background from earlier, and the other Control node is where we place our actual game (I have just added the godot logo as a sprite as an example).

And to make sure our game stays at the same position, we set the GameControl Anchor to Center just like we did with the background.

And voila! we now have a setup where we can replace the SomeGameSprite with a Scene containing our actual game.

TL;DR

  • In Project Settings->Display set stretch_mode = 2d and stretch_aspect = ignore.
  • Create a Control and anchor it to Center, then add a Sprite as a child and set your oversized background as the texture and center it.
  • Add the script, seen further up this answer, to AutoLoad

By the way this is a perfect example for a case where Autoload is the right solution.
As much as I criticize how most people use singletons I thought I should point that out.

It’s a good example, because the script doesn’t access any part of the scene tree, so it will work no matter what scene is the main scene, and it is by definition working on the application itself, so if you were to change the game so that the current main scene becomes a subscene of a new menu or something you would have to move this script to the new main scene if if wasn’t an autoload script.

Warlaan | 2016-11-16 10:33

I followed your instructions but I did not get.
Something is wrong, Sprite is not properly centered

Here are the results



P.S. Godot 2.1

dmklsv | 2016-11-17 16:57

My bad, Sprite has to be centered like this:

dmklsv | 2016-11-17 18:42

Ah yes i can see that it can be misunderstood, glad you got it working anyways.
I will edit my instructions to be more clear, hopefully.

Rasmus | 2016-11-18 07:13

Another way to center stuff is to calculate the translation required and using viewport.set_global_canvas_transform to center. The advantage is it doesn’t require any “centered” Control containers (which I couldn’t get to work for some reason).

chanon | 2017-01-05 12:01

The idea of using the Control node is to let Godot’s anchoring handle those calculations.

But if you want to, it is pretty basic indeed, something like this would do:

background.set_pos(Vector2(viewport.x/2, viewport.y/2))

But again, the idea was, why do this manually when Godot already does it for us?

If you want, i would be happy to help you out with the problems you have with the Control node.

Rasmus | 2017-01-05 12:19

Hey, this is working great, thanks. But what can I do if I have a 3D scene that I want to keep my minimum size? My UI Elements are scaling correctly, but my 3D seems not be be effected. I want my 3D scene to show everything it shows in the minimum size and show more of the scene instead. Any ideas?

TobiLa | 2018-03-30 08:15

This solution working well but issue with touch position when screen scale touch position is not scaling with it. Any fix for this issue?

rstar | 2020-05-14 03:29

:bust_in_silhouette: Reply From: Bluenix

First of all, this answer is an improvement after learning Rasmus’s answer and being left confused. That’s why I really want to make sure everyone leaves with an understanding of the code.

Let’s start with setting up Project Settings, first we go into Display/Windows and set Stretch/Mode to 2d. Stretch/Aspect should then be set to ignore.
Now we set Size/Width and Size/Height to our minimum area. The rest of the area should only be the background. This area is the marked red area in the original screenshot:
Original screenshot

Now, what do we want to achieve with this code?

  • Have a responsive viewport
  • Always show the core game
  • Resize viewport to show more background
  • Never have black bars

Great, now we have simple rules we need to follow, any solution needs to solve all of those criteria.
The solution Rasmus originally came up with is simple. First we get the current window size, after that we get a scaling factor for both axis and apply it for both axis. This resizes the whole viewport to fit the window size perfectly.

So let’s take a look at my improved code:

onready var viewport = get_viewport()
onready var game_size = Vector2(
    ProjectSettings.get_setting("display/window/size/width"),
    ProjectSettings.get_setting("display/window/size/height"))

func _ready():
    viewport.connect("size_changed", self, "resize_viewport")
    resize_viewport()

func resize_viewport():
    var new_size = OS.get_window_size()
    var scale_factor

    if new_size.x < game_size.x:
        scale_factor = game_size.x/new_size.x
        new_size = Vector2(new_size.x*scale_factor, new_size.y*scale_factor)
    if new_size.y < game_size.y:
        scale_factor = game_size.y/new_size.y
        new_size = Vector2(new_size.x*scale_factor, new_size.y*scale_factor)
    
    viewport.set_size_override(true, new_size)

Now let me explain
To begin we have two globally scoped variables, these get defined at startup thanks to the onready keyword. Since these get used throughout the code and we don’t want to get them each time we resize the viewport.

In the ready event we only connect the size_changed signal to our function and then call it. Otherwise we would start the game and need to resize it to get the viewport correctly sized.

Finally we can take a look at the resize_viewport function itself. The two initial variables are new_size ( the window size we want to scale to ) and scale_factor ( will be overwritten with the scaling factor ). Now comes the calculations themselves, the if statements check if any of the axis are smaller than the game size, which would mean that not the entire game would be showing. If any of the if statements come out true we apply this to both axis. In Rasmus’s solution this is only applied to the other axis, because x/y=z and then z*y = x meaning it’s an unnecessary calculation. But I left it in for the simplicity
And now, at last we can apply this new viewport.

But we have one last problem, keeping the game centered. This is actually simple, add a camera and offset it to the center. Don’t forget to check Current in the Inspector.
And that’s all!

TL;DR
Set Game size to size of core game. Copy in code and add a centered Camera

How to scale touch position? Getting wrong touch position!

rstar | 2020-05-14 03:30

I myself use this in my mobile game and I am not having any problems with it. It’s only scaling the viewport.
It is designed as a way to not have black bars, and instead have some other background. So as stated earlier the game project settings is where the actual game is, no touch input should be made outside this area. Is this the problem?

Bluenix | 2020-05-14 19:11

Viewport scaling good working as mentioned in answer but when i move object with touch getting wrong position.

project resolution is 1440x720 (wxh) (Working good, obviously! )

but when i set test resolution 1920x1280 or any other getting wrong mouse position.

i used get_viewport().get_mouse_position() to get mouse position.

check here when i pick object it move to slight bottom and this behaviour varies with resolution!
Imgur: The magic of the Internet

rstar | 2020-05-18 08:42

okay i fixed with adding offset to mouse position

on Click

offset=object_pos-get_viewport().get_mouse_position()

On Drag

 new_pos=get_viewport().get_mouse_position()+offset

rstar | 2020-05-18 14:23

Hello,

Thanks for the script and the explantations, but since my game is slightly different, my camera is not especially centered since it’s moving along with the player (it’s a platformer, the camera follow the player on the x axis, not the y). And I’m not allways in fullscreen. consequently the script is not working properly. Also the resize_viewport is called several time when launching the game, I do not get why. To be specific, I get that it’s called everytime the viewport size is changing, but since the script itself change the size, should’t the function be executed everytime we do viewport.set_size_override(true, new_size) ?

what is the difference between viewport.set_size and set_window_size() ? What should I use to set resolution in windowed mode?

Hephep | 2020-10-10 19:25