Skip to content

Instantly share code, notes, and snippets.

@dwdvIl
Last active November 30, 2024 23:34
Show Gist options
  • Save dwdvIl/c4363de9bbc0ec97db91b54e6b5ebe28 to your computer and use it in GitHub Desktop.
Save dwdvIl/c4363de9bbc0ec97db91b54e6b5ebe28 to your computer and use it in GitHub Desktop.
[Add-on, Godot 4.3] Game Viewport with custom resolution for main scenes (with background!)
[gd_scene load_steps=2 format=3 uid="uid://8syhpi57ht3g"]
[sub_resource type="GDScript" id="GDScript_xp7q5"]
resource_name = "GameView"
script/source = "@tool
extends SubViewportContainer
## Height of the viewport, the width is determined by aspect_ratio
## If free_aspect is true, then it's based on the aspect ratio of the window
@export_range(1, 16384) var resolution:int = 720 :
set(v):
resolution = v
queue_resize()
get:
return resolution
@export var aspect_ratio:float = 16.0/9.0 :
set(v):
aspect_ratio = v
queue_resize()
get:
return aspect_ratio
## Same as stretch scale of window mode
@export_range(0.5, 8) var gui_scale:float = 1 :
set(v):
gui_scale = v
queue_resize()
get:
return gui_scale
## If true, aspect_ratio is ignores and the window aspect ratio is used instead
@export var free_aspect:bool :
set(v):
free_aspect = v
if not v:
expand = false
else:
queue_resize()
get:
return free_aspect
## If true, the viewport will expand its size instead of scale
## This is equivalent to the \"expand\" aspect mode in project settings
@export var expand:bool :
set(v):
if free_aspect:
expand = v
queue_resize()
else:
expand = false
get:
return expand
var current_scene:Node
# Saving this to take it as a base for the viewport
# Design size is the area where your 2D elements exist in
# It should not change based on resolution, except for width, for unlimited aspect ratio
var design_size:Vector2i = Vector2i(
ProjectSettings.get_setting(\"display/window/size/viewport_width\"),
ProjectSettings.get_setting(\"display/window/size/viewport_height\")
)
var subviewport:SubViewport
var _resize_queued:float
func _init() -> void:
if not Engine.is_editor_hint():
RenderingServer.set_default_clear_color(Color.BLACK)
func _ready() -> void:
subviewport = get_child(0)
var scene = subviewport.get_child(0) if subviewport.get_child_count() > 0 else null
if scene and is_instance_valid(scene) and not scene.scene_file_path.is_empty():
current_scene = subviewport.get_child(0)
get_window().dpi_changed.connect(queue_resize.call_deferred)
get_window().size_changed.connect(queue_resize.call_deferred)
queue_resize()
func queue_resize() -> void:
_resize_queued = 3
func _process(_delta) -> void:
if _resize_queued < 1 or stretch:
if stretch:
push_warning(\"Don't use stretch for this SubViewportContainer, use the built-in variables instead!\")
stretch = false
return
_resize_queued -= 1
subviewport.size_2d_override = design_size
subviewport.size_2d_override_stretch = true
subviewport.size.y = resolution
var sizew = Vector2(get_window().size)
# Set aspect
var aspect = aspect_ratio if not free_aspect else sizew.aspect()
subviewport.size.x = resolution * aspect
#
if expand:
var ratio = aspect / aspect_ratio
subviewport.size_2d_override = (sizew * ratio if aspect >= aspect_ratio else sizew / (ratio)) / gui_scale
else:
subviewport.size_2d_override.x = (design_size.y * aspect) / gui_scale
subviewport.size_2d_override.y = design_size.y / gui_scale
# Center on screen
var ox = sizew.x - subviewport.size_2d_override.x
var oy = -sizew.y + design_size.y / gui_scale
if not Engine.is_editor_hint() and not expand:
if sizew.aspect() > aspect_ratio:
# Align horizontally
global_position.x = ox * .5 + oy * aspect * .5
global_position.y = 0
scale.y = sizew.y / float(subviewport.size.y)
else:
# Align vertically
global_position.x = 0
global_position.y = -ox / aspect * .5 - oy * .5
scale.y = sizew.x / float(subviewport.size.x)
else:
global_position = Vector2.ZERO
scale.y = sizew.y / float(resolution) if expand else design_size.y / float(resolution)
scale.x = scale.y
func _on_sub_viewport_size_changed() -> void:
if subviewport.size.y != resolution:
queue_resize()
# Scene functions if you need it
# Works the same as you'd do it with SceneTree
func change_scene_to_packed(packed:PackedScene) -> Error:
if not packed.can_instantiate(): return ERR_CANT_CREATE
if current_scene:
subviewport.remove_child(current_scene)
current_scene.queue_free()
current_scene = null
_change_scene.call_deferred(packed)
return OK
func change_scene_to_file(path:String) -> Error:
var packed = load(path)
if packed is not PackedScene:
packed = null
return ERR_CANT_OPEN
return change_scene_to_packed(packed)
func reload_current_scene() -> Error:
var scene:Node = subviewport.get_child(0) if subviewport.get_child_count() == 1 else null
if not scene: return ERR_UNCONFIGURED
return change_scene_to_file(scene.scene_file_path)
func unload_current_scene() -> void:
var scene = subviewport.get_child(0) if subviewport.get_child_count() == 1 else null
if scene:
scene.queue_free()
current_scene = null
func _change_scene(packed:PackedScene) -> void:
var scene = packed.instantiate()
subviewport.add_child(scene)
current_scene = scene
"
[node name="Background" type="TextureRect"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="GameView" type="SubViewportContainer" parent="."]
layout_mode = 0
offset_right = 1280.0
offset_bottom = 720.0
script = SubResource("GDScript_xp7q5")
[node name="SubViewport" type="SubViewport" parent="GameView"]
handle_input_locally = false
audio_listener_enable_2d = true
audio_listener_enable_3d = true
size = Vector2i(1280, 720)
size_2d_override = Vector2i(1280, 720)
size_2d_override_stretch = true
render_target_update_mode = 4
[connection signal="size_changed" from="GameView/SubViewport" to="GameView" method="_on_sub_viewport_size_changed"]

Some notes

This add-on doesn't play well with the stretch options in the project settings, it manages that by itself, so make sure to disable them!

It uses the viewport width and height as the "design size" for the game, don't change them at runtime, use the properties of the game viewport instead.

Usage

Instantiate your main scene as a child of the SubViewport:

Main scene

Set the properties of the GameView:

Viewport properties

  • Resolution: The height of the viewport
  • Aspect: The aspect ratio of the viewport, this determines its width.

Note: Divide the width by the height of your desired resolution to get the aspect ratio, i.e: 320.0 / 240.0. You can do it in the field.

  • GUI Scale: Affects the scale factor of 2D elements, but scales user interface accordingly.
  • Free aspect: When enabled, the aspect ratio of the window is used instead, this allows for support of a wide of screens, while preserving the same rendered height.
  • Expand: When enabled, the design size will correspond to the window size, when used together with GUI Scale, it acts as a sort of dpi changer.

4:3 aspect, 320x240 example:

Displayed on a 1280x720 window.

4:3

Setting background images

Set a texture to the Background TextureRect.

Background node

Background texture

Same scene, now displayed on an even wider window with a background image.

wider

Yes it works for 3D scenes too!

gnarpy

@dwdvIl
Copy link
Author

dwdvIl commented Nov 27, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment