Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Multiple resolutions with C# demo #1037

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions mono/gui/multiple_resolutions/Main.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// The root Control node ("Main") and AspectRatioContainer nodes are the most important
// pieces of this demo.
// Both nodes have their Layout set to Full Rect
// (with their rect spread across the whole viewport, and Anchor set to Full Rect).

using Godot;

namespace MultipleResolutions
{
public partial class Main : Control
{
Vector2 baseWindowSize = new(
(float)ProjectSettings.GetSetting("display/window/size/viewport_width"),
(float)ProjectSettings.GetSetting("display/window/size/viewport_height"));

// These defaults match this demo's project settings. Adjust as needed if adapting this
// in your own project.
Window.ContentScaleModeEnum stretchMode = Window.ContentScaleModeEnum.CanvasItems;
Window.ContentScaleAspectEnum stretchAspect = Window.ContentScaleAspectEnum.Expand;

float scaleFactor = 1.0f;
float guiAspectRatio = -1.0f;
float guiMargin = 0.0f;

Panel panel = null;
AspectRatioContainer arc = null;

public override void _Ready()
{
// The `resized` signal will be emitted when the window size changes, as the root Control node
// is resized whenever the window size changes. This is because the root Control node
// uses a Full Rect anchor, so its size will always be equal to the window size.
panel = FindChild("Panel") as Panel;
arc = panel.FindChild("AspectRatioContainer") as AspectRatioContainer;

Resized += this.OnResized;
CallDeferred("UpdateContainer");
}

public void UpdateContainer()
{
// The code within this function needs to be run deferred to work around an issue with containers
// having a 1-frame delay with updates.
// Otherwise, `panel.size` returns a value of the previous frame, which results in incorrect
// sizing of the inner AspectRatioContainer when using the Fit to Window setting.
for (int i = 0; i < 2; i++)
{
if (Mathf.IsEqualApprox(guiAspectRatio, -1.0f))
{
// Fit to Window. Tell the AspectRatioContainer to use the same aspect ratio as the window,
// making the AspectRatioContainer not have any visible effect.
arc.Ratio = panel.Size.Aspect();
// Apply GUI offset on the AspectRatioContainer's parent (Panel).
// This also makes the GUI offset apply on controls located outside the AspectRatioContainer
// (such as the inner side label in this demo).
panel.OffsetTop = guiMargin;
panel.OffsetBottom = -guiMargin;
}
else
{
// Constrained aspect ratio.
arc.Ratio = Mathf.Min(panel.Size.Aspect(), guiAspectRatio);
// Adjust top and bottom offsets relative to the aspect ratio when it's constrained.
// This ensures that GUI offset settings behave exactly as if the window had the
// original aspect ratio size.
panel.OffsetTop = guiMargin / guiAspectRatio;
panel.OffsetBottom = -guiMargin / guiAspectRatio;
}

panel.OffsetLeft = guiMargin;
panel.OffsetRight = -guiMargin;
}
}

public void OnGuiAspectRatioItemSelected(int index)
{
switch (index)
{
case 0: // Fit to Window
guiAspectRatio = -1.0f;
break;
case 1: // 5:4
guiAspectRatio = 5.0f / 4.0f;
break;
case 2: // 4:3
guiAspectRatio = 4.0f / 3.0f;
break;
case 3: // 3:2
guiAspectRatio = 3.0f / 2.0f;
break;
case 4: // 16:10
guiAspectRatio = 16.0f / 10.0f;
break;
case 5: // 16:9
guiAspectRatio = 16.0f / 9.0f;
break;
case 6: // 21:9
guiAspectRatio = 21.0f / 9.0f;
break;
}
CallDeferred("UpdateContainer");
}

private void OnResized()
{
CallDeferred("UpdateContainer");
}

private void OnGuiMarginDragEnded(float valueChanged)
{
guiMargin = (float)GetNode<HSlider>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/GUIMargin/HSlider").Value;
GetNode<Label>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/GUIMargin/Value").Text = guiMargin.ToString();
CallDeferred("UpdateContainer");
}

private void OnWindowBaseSizeItemSelected(int index)
{
Vector2I baseWindowSize;
switch (index)
{
case 0: // 648×648 (1:1)
baseWindowSize = new Vector2I(648, 648);
break;
case 1: // 640×480 (4:3)
baseWindowSize = new Vector2I(640, 480);
break;
case 2: // 720×480 (3:2)
baseWindowSize = new Vector2I(720, 480);
break;
case 3: // 800×600 (4:3)
baseWindowSize = new Vector2I(800, 600);
break;
case 4: // 1152×648 (16:9)
baseWindowSize = new Vector2I(1152, 648);
break;
case 5: // 1280×720 (16:9)
baseWindowSize = new Vector2I(1280, 720);
break;
case 6: // 1280×800 (16:10)
baseWindowSize = new Vector2I(1280, 800);
break;
case 7: // 1680×720 (21:9)
baseWindowSize = new Vector2I(1680, 720);
break;
default:
GD.Print("Invalid index selected");
return;
}

GetViewport().GetWindow().ContentScaleSize = baseWindowSize;
CallDeferred("UpdateContainer");
}

private void OnWindowStretchModeItemSelected(int index)
{
stretchMode = (Window.ContentScaleModeEnum)index;
GetViewport().GetWindow().ContentScaleMode = stretchMode;

// Disable irrelevant options when the stretch mode is Disabled.
GetNode<OptionButton>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/WindowBaseSize/OptionButton").Disabled =
stretchMode == Window.ContentScaleModeEnum.Disabled;
GetNode<OptionButton>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/WindowStretchAspect/OptionButton").Disabled =
stretchMode == Window.ContentScaleModeEnum.Disabled;
}

private void OnWindowStretchAspectItemSelected(int index)
{
stretchAspect = (Window.ContentScaleAspectEnum)index;
GetViewport().GetWindow().ContentScaleAspect = stretchAspect;
}

private void OnWindowScaleFactorDragEnded(float valueChanged)
{
scaleFactor = (float)GetNode<HSlider>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/WindowScaleFactor/HSlider").Value;
GetNode<Label>("Panel/AspectRatioContainer/Panel/CenterContainer/Options/WindowScaleFactor/Value").Text = string.Format("{0}%", scaleFactor * 100);
GetViewport().GetWindow().ContentScaleFactor = scaleFactor;
}

private void OnWindowStretchScaleModeItemSelected(int index)
{
GetViewport().GetWindow().ContentScaleStretch = (Window.ContentScaleStretchEnum)index;
}
}
}
6 changes: 6 additions & 0 deletions mono/gui/multiple_resolutions/MultipleResolutions.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Godot.NET.Sdk/4.2.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>
100 changes: 100 additions & 0 deletions mono/gui/multiple_resolutions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Multiple Resolutions and Aspect Ratios with C#

**Note:** This demo is intended to showcase what Godot can do in terms of
supporting multiple resolutions and aspect ratios. As such, this demo very
full-featured but it's also fairly complex to understand.

If you're in a hurry and want to implement *decent* support for multiple
resolutions and aspect ratios in your game, see [Multiple resolutions crash
course](#multiple-resolutions-crash-course).

**Note:** There is a GDScript version available [here](https://github.com/godotengine/godot-demo-projects/tree/master/gui/multiple_resolutions)
___

This project demonstrates how to set up a project to handle screens of multiple
resolutions and aspect ratios.

This demo allows you to adjust the window's base resolution, stretch mode,
stretch aspect, and scale factor (internally known as "stretch shrink"). This
lets you see what happens when adjusting those properties. Make sure to resize
the project window in any direction to see the difference with the various
stretch mode and stretch aspect settings.

The GUI can be made to fit the window or constrained to a specific aspect ratio
from a list of common aspect ratios. On ultrawide aspect ratios, this can be
used to prevent HUD elements from being too spread apart, which can harm the
gameplay experience. For non-essential HUD elements, specific controls can be
made to ignore this aspect ratio constraint when it makes sense (e.g. a list of
players on the side of the screen).

Additionally, a GUI margin setting is provided to better handle TVs with an
overscan area to prevent GUI elements from being cut off. This can also improve
the gameplay experience on large monitors by bringing HUD elements closer to the
center of the screen.

A DynamicFont with multichannel signed distance field (MSDF) rendering is also used.
This allows for crisp font rendering at any resolution, without having to re-rasterize
the font when the font size changes. This makes changing the various settings in this
demo faster, especially when large font sizes are used as a result of the GUI scale factor
setting being increased.

Note that by default, Godot uses font oversampling for traditional rasterized
DynamicFonts. This means MSDF fonts are *not* required to have crisp fonts at
higher-than-default screen resolutions.

Language: GDScript

Renderer: Compatibility

## Technical notes

The demo works with the following project settings:

- `canvas_items` stretch mode (this was called `2d` in Godot 3.x).
Recommended for most non-pixel art games.
- `expand` stretch aspect (allows support for multiple aspect ratios without
distortion or black bars).
- Using a base window size with a 1:1 aspect ratio (`648×648` in this demo).
This prevents GUI elements from automatically shrinking, even in portrait
mode.
- With this setting, to prevent the GUI from breaking at narrow aspect ratios,
the GUI must be designed to work with a 1:1 aspect ratio. This is not
feasible in most complex games, so a base window size with a wider aspect
ratio (such as 4:3 or 16:10) can be used instead. The wider the aspect
ratio, the easier design becomes, but the GUI will automatically become
smaller at narrow aspect ratios unless the user overrides its scale with the
stretch shrink setting. Many devices such as the Steam Deck and MacBooks
feature 16:10 displays, so it's recommended to use a 16:10 resolution or
narrower as a base window size to ensure a good gameplay experience out of
the box on those devices.
- Using a window size override with a 16:9 aspect ratio (`1152×648` in this demo).
This way, the project starts in a 16:9 window even if the base window size has
a 1:1 aspect ratio.
- The test window height matches the width and height of the base window size,
so GUI elements are still at the same size.

## Multiple resolutions crash course

**Not everything in this demo is critical to all games.** For gamejam projects or mobile games, most of this can be skipped.
See the [Common use case scenarios](https://docs.godotengine.org/en/stable/tutorials/rendering/multiple_resolutions.html#common-use-case-scenarios)
section in the Multiple resolutions documentation.

With the simpler setup described in the above documentation, there are a few
limitations compared to this demo:

- The HUD will shrink when the aspect ratio becomes narrower than the base
window size. As such, it's recommended to use a base window size with a 16:10
aspect ratio to prevent the HUD from shrinking on Steam Deck and MacBooks.
- Players will not be able to define a margin, which can be problematic when
playing on a TV (as overscan can obstruct some HUD elements). This can be
worked around by ensuring the entire HUD always has a small margin around it.
This can be done by increasing the Margin properties on all sides on the root
Control node by 10-30 pixels or so.

If you're releasing a full-fledged game on a desktop platform such as Steam,
consider implementing full support as this demo suggests. Your players will
thank you :slightly_smiling_face:

## Screenshots

![Screenshot](screenshots/multiple_resolutions.png)
Binary file added mono/gui/multiple_resolutions/icon.webp
Binary file not shown.
34 changes: 34 additions & 0 deletions mono/gui/multiple_resolutions/icon.webp.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://brwp8bimc75uu"
path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://icon.webp"
dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Loading