Can i set the window background color at runtime?

Godot Version

4.2.1

Question

i have a game that uses a fixed canvas ratio, so when you stretch the screen it stays square-shaped and reveals a black background

i would really like to be able to set that black color to the dark color from my palette, like i’ve photoshopped here

the thing is, i’m going to have multiple color palettes for different environments, so i need a way to be able to set this color at runtime, with a script


is there a way for me to set the default window color at runtime? thanks

I’m not 100% sure, but I think you might want RenderingServer.set_default_clear_color?


okay this is approaching the solution but something is missing. i’m setting the clear color to magenta here on the camera’s process and what happens is the background becomes magenta for the very first frame that a clear pixel is drawn, and then it immediately becomes black again. so something else is turning this region black retroactively…

Huh, that’s strange. I guess the black pixels aren’t actually the clear color, then. I can take another look at it but this might require someone who knows more about the internals of Godot to figure out.

@canslp This is a workaround, try using it :smiley:

Edit :

  1. I have now edited it to only update when size_changed signal is called.
  2. Set minimum window size to size specified in Project Settings

BTW settings
settings

extends Node2D
var size = Vector2i(ProjectSettings.get_setting("display/window/size/viewport_width"),ProjectSettings.get_setting("display/window/size/viewport_height"))

func _ready():
	RenderingServer.set_default_clear_color(Color.SKY_BLUE)
	DisplayServer.window_set_min_size(size)
	get_viewport().size_changed.connect(update_position)

func update_position():
	var a = DisplayServer.window_get_size()
	position.x = (a.x -size.x)/2
	position.y = (a.y -size.y)/2

Working example

For people trying to do this in 3.x you can use VisualServer.black_bars_set_images. This does not seem to have an equivalent in 4.x though, probably because the rewrite of the whole rendering system.

You can however create this yourself using a main ui scene which embeds your game using a subviewport container. This would be you best bet for 4.x I’d say. It also has the advantage that you can use the node system to style the bars which allows for fancy transitions were the bars overlap the viewport and such.

1 Like

Found my own implementation for colored aspect bars:

Scene structure:
image

The script (it replicates canvas item resizing with aspect keep. But adapting it for pixel games should be possible):

class_name BlackBars
extends MarginContainer


@export var color := Color.BLACK

@onready var __base_size := Vector2(
	ProjectSettings.get_setting("display/window/size/viewport_width"),
	ProjectSettings.get_setting("display/window/size/viewport_height"),
)

@onready var __oversampling: bool = ProjectSettings.get_setting("gui/fonts/dynamic_fonts/use_oversampling", true)

# Layer used for custom drawing the bars. You could also just change the bg color. This script originated from a version without subviewport. Were drawing above the scene was important.
@onready var __layer := CanvasLayer.new()
@onready var __base := Control.new()


func _ready() -> void:
	%SubViewport.size_2d_override = __base_size
	if __oversampling:
		# Handled by this script
		ProjectSettings.set_setting("gui/fonts/dynamic_fonts/use_oversampling", false)

	add_child(__layer, false, Node.INTERNAL_MODE_BACK)
	__layer.layer = 120
	__layer.add_child(__base)
	__base.set_anchors_preset(Control.PRESET_FULL_RECT, true)
	__base.mouse_filter = Control.MOUSE_FILTER_IGNORE

	__base.draw.connect(__draw_black_bars)


func __draw_black_bars() -> void:
		if (__oversampling):
			var factor = %SubViewport.size.x / __base_size.x
			TextServerManager.get_primary_interface().font_set_global_oversampling(factor)
			propagate_call(&"queue_redraw")

		var aspect: float = __base_size.x / __base_size.y
		if size.x / size.y > aspect:
			var bar_width: float = (size.x - aspect * size.y) / 2.0
			add_theme_constant_override(&"margin_left", bar_width)
			add_theme_constant_override(&"margin_right", bar_width)
			add_theme_constant_override(&"margin_top", 0)
			add_theme_constant_override(&"margin_bottom", 0)
			# Scaling of the viewport is handled by the margins. You could use anything to draw the bars. (e.g. textures or custom scenes)
			__base.draw_rect(Rect2(0, 0, bar_width, size.y), color)
			__base.draw_rect(Rect2(size.x - bar_width, 0, bar_width, size.y), color)
		elif size.x / size.y < aspect:
			var bar_height: float = (size.y - size.x / aspect) / 2.0
			add_theme_constant_override(&"margin_top", bar_height)
			add_theme_constant_override(&"margin_bottom", bar_height)
			add_theme_constant_override(&"margin_left", 0)
			add_theme_constant_override(&"margin_right", 0)
			__base.draw_rect(Rect2(0, 0, size.x, bar_height), color)
			__base.draw_rect(Rect2(0, size.y - bar_height, size.x, bar_height), color)
1 Like

i was really trying to avoid just drawing black bars onto the screen because i don’t want the camera to have to render stuff that’s out of frame anyway and then draw over it. it seems like this implementation creates some kind of sub-canvas though? is the camera still rendering the same amount of space here, or is it doing all the extra work on the sides and then clearing it?

ah that’s super annoying. certainly the color of the backmost layer is a value stored somewhere, i can’t imagine that it’s now impossible to change it

what is this script doing exactly, it seems to just lock the window at a minimum size? when stretch mode is disabled i don’t get those black bars at all, it just expands the camera range. my question is how to keep those black bars but change their color

okay follow-up question, where does this actually go in the scene structure? like where is that game that it is rendering in your example, is it in ‘holder2D’? i have a Node3D as the root of my game’s scene, can I just put that into the subviewport?

2D Cameras are really just a wrapper around canvas transforms. Not sure if and what clipping godot uses in 2d, but if it uses some clipping method it should apply them in the same way in the subviewport. (Same for 3d but there I know that godot uses clipping).

1 Like

okay thanks i’ll have to try this after work

Your game goes into the subviewport.

You can ignore the Holder2D and HolderUI thats just how I structure my games. The example is very integrated with my own main scene architecture, that’s why I don’t share the project.

Caveats to note: you will have to use a custom solution for scene changing. get_tree().change_scene won’t do, since you want to insert the new scene into the subviewport, not replace the subviewport structure. Should be easy enough to have an autoload which provides the same functions though.

1 Like

Yeah, it sets the minimum size so full game remains visible and after window is resized it changes nodes position to center. And You can set clear color to match the theme of your game.

I don’t think there is really any way to change color of those black bars.

Once I have also tried to fix that but ended up creating this workaround. Please let me know if you find a way to change its color

ah okay, when i do that it just makes the camera wider. what i was looking for was a fix for the window color

what are the settings you are using for this, do you have window stretching disabled? also, i’m not sure what those percentage signs in the inspector mean but i suspect they are important. i’ve changed all the references to %Subviewport to $SupviewportContainer/Subviewport because i was getting nullptr errors, maybe i put the script on the wrong node?

I have stretching in the project settings completly disabled. The nodes are configured as follows:
image
image

%Node is the syntax for scene unique nodes. You can also use the normal $Path/Node though.

1 Like

that’s interesting, i’m not sure why the script wasn’t recognizing them then. it was just returning null when i tried using %Subviewport

seems like this is working! thanks so much