Skip to content

Commit 59d443e

Browse files
committed
Add a demo on supporting multiple resolutions and aspect ratios
This demo intends to showcase what Godot can do in terms of supporting multiple resolutions and aspect ratios.
1 parent 2e40f67 commit 59d443e

File tree

10 files changed

+587
-0
lines changed

10 files changed

+587
-0
lines changed

gui/multiple_resolutions/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Multiple Resolutions and Aspect Ratios
2+
3+
**Note:** This demo is intended to showcase what Godot can do in terms of
4+
supporting multiple resolutions and aspect ratios. As such, this demo very
5+
full-featured but it's also fairly complex to understand.
6+
7+
If you're in a hurry and want to implement *decent* support for multiple
8+
resolutions and aspect ratios in your game, see [Multiple resolutions crash
9+
course](#multiple-resolutions-crash-course).
10+
11+
___
12+
13+
This project demonstrates how to set up a project to handle screens of multiple
14+
resolutions and aspect ratios.
15+
16+
This demo allows you to adjust the window's base resolution, stretch mode,
17+
stretch aspect, and scale factor (internally known as "stretch shrink"). This
18+
lets you see what happens when adjusting those properties. Make sure to resize
19+
the project window in any direction to see the difference with the various
20+
stretch mode and stretch aspect settings.
21+
22+
The GUI can be made to fit the window or constrained to a specific aspect ratio
23+
from a list of common aspect ratios. On ultrawide aspect ratios, this can be
24+
used to prevent HUD elements from being too spread apart, which can harm the
25+
gameplay experience. For non-essential HUD elements, specific controls can be
26+
made to ignore this aspect ratio constraint when it makes sense (e.g. a list of
27+
players on the side of the screen).
28+
29+
Additionally, a GUI margin setting is provided to better handle TVs with an
30+
overscan area to prevent GUI elements from being cut off. This can also improve
31+
the gameplay experience on large monitors by bringing HUD elements closer to the
32+
center of the screen.
33+
34+
A DynamicFont is also used to ensure font rendering remains crisp at high
35+
resolutions, thanks to Godot's built-in support for font oversampling. In other
36+
words, the engine will automatically re-rasterize fonts at different resolutions
37+
than the base window size when the window is resized (or when the window scale
38+
factor is changed).
39+
40+
Language: GDScript
41+
42+
Renderer: GLES 2
43+
44+
## Technical notes
45+
46+
The demo works with the following project settings:
47+
48+
- `2d` stretch mode (recommended for most non-pixel art games).
49+
- `expand` stretch aspect (allows support for multiple aspect ratios without
50+
distortion or black bars).
51+
- Using a base window size with a 1:1 aspect ratio (`648×648` in this demo).
52+
This prevents GUI elements from automatically shrinking, even in portrait
53+
mode.
54+
- With this setting, to prevent the GUI from breaking at narrow aspect ratios,
55+
the GUI must be designed to work with a 1:1 aspect ratio. This is not
56+
feasible in most complex games, so a base window size with a wider aspect
57+
ratio (such as 4:3 or 16:10) can be used instead. The wider the aspect
58+
ratio, the easier design becomes, but the GUI will automatically become
59+
smaller at narrow aspect ratios unless the user overrides its scale with the
60+
stretch shrink setting. Many devices such as the Steam Deck and MacBooks
61+
feature 16:10 displays, so it's recommended to use a 16:10 resolution or
62+
narrower as a base window size to ensure a good gameplay experience out of
63+
the box on those devices.
64+
- Using a test window size with a 16:9 aspect ratio (`1152×648` in this demo).
65+
This way, the project starts in a 16:9 window even if the base window size has
66+
a 1:1 aspect ratio.
67+
- The test window height matches the width and height of the base window size,
68+
so GUI elements are still at the same size.
69+
70+
## Multiple resolutions crash course
71+
72+
**Not everything in this demo is critical to all games.** For gamejam projects or mobile games, most of this can be skipped.
73+
See the [Common use case scenarios](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#common-use-case-scenarios)
74+
section in the Multiple resolutions documentation.
75+
76+
With the simpler setup described in the above documentation, there are a few
77+
limitations compared to this demo:
78+
79+
- The HUD will shrink when the aspect ratio becomes narrower than the base
80+
window size. As such, it's recommended to use a base window size with a 16:10
81+
aspect ratio to prevent the HUD from shrinking on Steam Deck and MacBooks.
82+
- Players will not be able to define a margin, which can be problematic when
83+
playing on a TV (as overscan can obstruct some HUD elements). This can be
84+
worked around by ensuring the entire HUD always has a small margin around it.
85+
This can be done by increasing the Margin properties on all sides on the root
86+
Control node by 10-30 pixels or so.
87+
88+
If you're releasing a full-fledged game on a desktop platform such as Steam,
89+
consider implementing full support as this demo suggests. Your players will
90+
thank you :slightly_smiling_face:
91+
92+
## Screenshots
93+
94+
![Screenshot](screenshots/multiple_resolutions.png)

gui/multiple_resolutions/icon.png

684 Bytes
Loading
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="StreamTexture"
5+
path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
6+
metadata={
7+
"vram_texture": false
8+
}
9+
10+
[deps]
11+
12+
source_file="res://icon.png"
13+
dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
14+
15+
[params]
16+
17+
compress/mode=0
18+
compress/lossy_quality=0.7
19+
compress/hdr_mode=0
20+
compress/bptc_ldr=0
21+
compress/normal_map=0
22+
flags/repeat=0
23+
flags/filter=true
24+
flags/mipmaps=false
25+
flags/anisotropic=false
26+
flags/srgb=2
27+
process/fix_alpha_border=true
28+
process/premult_alpha=false
29+
process/HDR_as_SRGB=false
30+
process/invert_color=false
31+
process/normal_map_invert_y=false
32+
stream=false
33+
size_limit=0
34+
detect_3d=true
35+
svg/scale=1.0

gui/multiple_resolutions/main.gd

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# The root Control node ("Main") and AspectRatioContainer nodes are the most important
2+
# pieces of this demo.
3+
# Both nodes have their Layout set to Full Rect
4+
# (with their rect spread across the whole viewport, and Anchor set to Full Rect).
5+
extends Control
6+
7+
var base_window_size = Vector2(ProjectSettings.get_setting("display/window/size/width"), ProjectSettings.get_setting("display/window/size/height"))
8+
9+
# These defaults match this demo's project settings. Adjust as needed if adapting this
10+
# in your own project.
11+
var stretch_mode = SceneTree.STRETCH_MODE_2D
12+
var stretch_aspect = SceneTree.STRETCH_ASPECT_EXPAND
13+
14+
var scale_factor = 1.0
15+
var gui_aspect_ratio = -1.0
16+
var gui_margin = 0.0
17+
18+
onready var panel = $Panel
19+
onready var arc = $Panel/AspectRatioContainer
20+
21+
22+
func _ready():
23+
# The `resized` signal will be emitted when the window size changes, as the root Control node
24+
# is resized whenever the window size changes. This is because the root Control node
25+
# uses a Full Rect anchor, so its size will always be equal to the window size.
26+
# warning-ignore:return_value_discarded
27+
connect("resized", self, "_on_resized")
28+
update_container()
29+
30+
31+
func update_container():
32+
# The code within this function needs to be run twice to work around an issue with containers
33+
# having a 1-frame delay with updates.
34+
# Otherwise, `panel.rect_size` returns a value of the previous frame, which results in incorrect
35+
# sizing of the inner AspectRatioContainer when using the Fit to Window setting.
36+
for _i in 2:
37+
if is_equal_approx(gui_aspect_ratio, -1.0):
38+
# Fit to Window. Tell the AspectRatioContainer to use the same aspect ratio as the window,
39+
# making the AspectRatioContainer not have any visible effect.
40+
arc.ratio = panel.rect_size.aspect()
41+
# Apply GUI margin on the AspectRatioContainer's parent (Panel).
42+
# This also makes the GUI margin apply on controls located outside the AspectRatioContainer
43+
# (such as the inner side label in this demo).
44+
panel.margin_top = gui_margin
45+
panel.margin_bottom = -gui_margin
46+
else:
47+
# Constrained aspect ratio.
48+
arc.ratio = min(panel.rect_size.aspect(), gui_aspect_ratio)
49+
# Adjust top and bottom margins relative to the aspect ratio when it's constrained.
50+
# This ensures that GUI margin settings behave exactly as if the window had the
51+
# original aspect ratio size.
52+
panel.margin_top = gui_margin / gui_aspect_ratio
53+
panel.margin_bottom = -gui_margin / gui_aspect_ratio
54+
55+
panel.margin_left = gui_margin
56+
panel.margin_right = -gui_margin
57+
58+
59+
func _on_gui_aspect_ratio_item_selected(index):
60+
match index:
61+
0: # Fit to Window
62+
gui_aspect_ratio = -1.0
63+
1: # 5:4
64+
gui_aspect_ratio = 5.0 / 4.0
65+
2: # 4:3
66+
gui_aspect_ratio = 4.0 / 3.0
67+
3: # 3:2
68+
gui_aspect_ratio = 3.0 / 2.0
69+
4: # 16:10
70+
gui_aspect_ratio = 16.0 / 10.0
71+
5: # 16:9
72+
gui_aspect_ratio = 16.0 / 9.0
73+
6: # 21:9
74+
gui_aspect_ratio = 21.0 / 9.0
75+
76+
update_container()
77+
78+
79+
func _on_resized():
80+
update_container()
81+
82+
83+
func _on_gui_margin_drag_ended(_value_changed):
84+
gui_margin = $"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/GUIMargin/HSlider".value
85+
$"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/GUIMargin/Value".text = str(gui_margin)
86+
update_container()
87+
88+
89+
func _on_window_base_size_item_selected(index):
90+
match index:
91+
0: # 648×648 (1:1)
92+
base_window_size = Vector2(648, 648)
93+
1: # 640×480 (4:3)
94+
base_window_size = Vector2(640, 480)
95+
2: # 720×480 (3:2)
96+
base_window_size = Vector2(720, 480)
97+
3: # 800×600 (4:3)
98+
base_window_size = Vector2(800, 600)
99+
4: # 1152×648 (16:9)
100+
base_window_size = Vector2(1152, 648)
101+
5: # 1280×720 (16:9)
102+
base_window_size = Vector2(1280, 720)
103+
6: # 1280×800 (16:10)
104+
base_window_size = Vector2(1280, 800)
105+
7: # 1680×720 (21:9)
106+
base_window_size = Vector2(1680, 720)
107+
108+
get_tree().set_screen_stretch(stretch_mode, stretch_aspect, base_window_size, scale_factor)
109+
update_container()
110+
111+
112+
func _on_window_stretch_mode_item_selected(index):
113+
stretch_mode = index
114+
get_tree().set_screen_stretch(stretch_mode, stretch_aspect, base_window_size, scale_factor)
115+
116+
# Disable irrelevant options when the stretch mode is Disabled.
117+
$"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/WindowBaseSize/OptionButton".disabled = stretch_mode == SceneTree.STRETCH_MODE_DISABLED
118+
$"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/WindowStretchAspect/OptionButton".disabled = stretch_mode == SceneTree.STRETCH_MODE_DISABLED
119+
120+
121+
func _on_window_stretch_aspect_item_selected(index):
122+
stretch_aspect = index
123+
get_tree().set_screen_stretch(stretch_mode, stretch_aspect, base_window_size, scale_factor)
124+
125+
126+
func _on_window_scale_factor_drag_ended(_value_changed):
127+
scale_factor = $"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/WindowScaleFactor/HSlider".value
128+
$"Panel/AspectRatioContainer/ColorRect/CenterContainer/Options/WindowScaleFactor/Value".text = "%d%%" % (scale_factor * 100)
129+
get_tree().set_screen_stretch(stretch_mode, stretch_aspect, base_window_size, scale_factor)

0 commit comments

Comments
 (0)