Configurações de Vídeo no Godot 4: Resolução, Tela Cheia e VSync

Monte uma tela de configurações de vídeo no Godot 4: troca de resolução, tela cheia, VSync e content scale sem distorcer a UI, com GDScript tipado.
Configurações de Vídeo no Godot 4: Resolução, Tela Cheia e VSync
Toda tela de opções de jogo tem uma aba de vídeo, e ela é enganosamente simples de descrever: um botão de tela cheia, uma lista de resoluções, um interruptor de VSync. Só que na hora de montar isso no Godot 4 batem as dúvidas de sempre. Qual a diferença entre os modos de janela? Por que a UI distorce quando troco a resolução? O VSync fica em qual API? Este tutorial monta uma tela de configurações de vídeo no Godot 4 completa, cobrindo troca de resolução, tela cheia em todas as variantes e VSync, com uma UI de opções de verdade e todo o GDScript estaticamente tipado.
A boa notícia é que quase tudo aqui passa por uma única classe, o DisplayServer, mais os enums da classe Window. Depois que você entende esses dois nomes, o resto é ligar um OptionButton e uns CheckButton na lógica. Vamos por partes.
O DisplayServer e os modos de janela
No Godot 4, quem controla a janela do sistema operacional é o singleton DisplayServer. Ele é global, você chama de qualquer lugar, e é por ele que se troca modo de janela, tamanho, posição e VSync. A propriedade central é o modo da janela, que aceita os valores do enum Window:
func aplicar_modo_janela(modo: Window.Mode) -> void:
DisplayServer.window_set_mode(modo)
Os modos que interessam numa tela de vídeo são quatro:
Window.MODE_WINDOWED: janela normal, com borda e barra de título, que o jogador pode mover e redimensionar.Window.MODE_FULLSCREEN: tela cheia "borderless", uma janela sem borda do tamanho do monitor mantendo a resolução do desktop. O alt-tab é instantâneo. É o padrão que a maioria dos jogos deveria usar.Window.MODE_EXCLUSIVE_FULLSCREEN: tela cheia exclusiva. O jogo assume o monitor e pode trocar o modo de vídeo. Às vezes reduz a latência, mas o alt-tab fica mais lento.Window.MODE_MINIMIZEDeWindow.MODE_MAXIMIZED: existem, mas raramente entram numa aba de vídeo.
Para o modo sem borda (borderless) em janela, o caminho é um pouco diferente: você fica em MODE_WINDOWED e liga a flag de "sem borda" separadamente. Uma função que cobre os quatro casos práticos fica assim:
func trocar_janela(indice: int) -> void:
match indice:
0: # Janela
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
1: # Janela sem borda
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
2: # Tela cheia
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
3: # Tela cheia exclusiva
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN)
Repare em um detalhe que confunde muita gente: o DisplayServer usa constantes com o prefixo WINDOW_MODE_ (por exemplo, DisplayServer.WINDOW_MODE_FULLSCREEN), enquanto a classe Window usa MODE_ (por exemplo, Window.MODE_FULLSCREEN). Os dois se referem à mesma coisa e têm os mesmos valores numéricos. Se você tiver a referência da janela em mãos, dá pra trocar o modo por ela também:
func tela_cheia_pela_window(ligada: bool) -> void:
var janela: Window = get_window()
janela.mode = Window.MODE_FULLSCREEN if ligada else Window.MODE_WINDOWED
Use o que for mais legível no seu contexto. Eu prefiro o DisplayServer na tela de opções porque ela costuma ser um autoload ou um node solto, e nem sempre get_window() aponta pra janela principal.
Resolução: window_set_size e recentralizar
Trocar a resolução da janela é uma chamada só, window_set_size, que recebe um Vector2i:
func aplicar_resolucao(resolucao: Vector2i) -> void:
DisplayServer.window_set_size(resolucao)
O problema prático é que, depois de redimensionar, a janela costuma "escorregar" pro canto da tela. A correção é recalcular a posição pra centralizar no monitor onde a janela está. O DisplayServer te dá o tamanho da tela útil (descontando barra de tarefas), então a conta é direta:
func centralizar_janela() -> void:
var id_tela: int = DisplayServer.window_get_current_screen()
var tamanho_tela: Vector2i = DisplayServer.screen_get_size(id_tela)
var tamanho_janela: Vector2i = DisplayServer.window_get_size()
var pos: Vector2i = (tamanho_tela - tamanho_janela) / 2
pos += DisplayServer.screen_get_position(id_tela)
DisplayServer.window_set_position(pos)
Uma observação importante: trocar resolução só faz sentido no modo janela. Em tela cheia, a janela já ocupa o monitor inteiro, e é o content scale (o Stretch, que vou explicar já já) que decide como a arte se encaixa ali. Então na hora de aplicar, o padrão é: se estiver em tela cheia, aplico o modo e ignoro o window_set_size; se estiver em janela, aplico o tamanho e recentralizo.
Content scale: por que a UI não pode distorcer
Aqui está o detalhe que separa uma tela de opções amadora de uma profissional. Se você trocar a resolução e a interface esticar ou achatar, o problema não está no seu código de vídeo, está no Stretch do projeto.
O Godot renderiza o jogo numa resolução "base" (a que você define em Project Settings, Display, Window, Viewport Width e Height) e depois escala isso pra janela real. Como ele escala é decidido por duas opções na mesma seção, dentro de Stretch:
- Mode: deixe em
canvas_items. Assim o Godot escala os elementos de UI e sprites de forma limpa, sem pixelar as fontes. O modoviewportrenderiza numa resolução fixa e faz upscale bruto (bom pra pixel art), e odisablednão escala nada (é o que causa a distorção clássica). - Aspect: deixe em
keep. Isso mantém a proporção da resolução base. Se a janela tem outra proporção, o Godot adiciona barras pretas (letterbox) em vez de deformar a imagem. As alternativasexpand,keep_widthekeep_heightservem pra casos específicos, maskeepé o que garante que nada distorça.
Com canvas_items + keep configurados, você troca a resolução à vontade que a UI acompanha proporcionalmente. O seu código GDScript não precisa fazer nada disso na mão: é configuração de projeto, aplicada uma vez, e vale pra qualquer resolução que o jogador escolher. Vale a mesma lógica de quando você monta o menu de pause do jogo, onde a UI também precisa se manter íntegra em qualquer tamanho de tela.
VSync: window_set_vsync_mode
O VSync (sincronização vertical) alinha a entrega dos frames à taxa de atualização do monitor, o que elimina o "tearing", aquela linha horizontal de imagem cortada em movimentos rápidos. O custo é limitar o FPS e adicionar um pouco de latência de input. É clássico oferecer como opção, e no Godot 4 mora no DisplayServer:
func aplicar_vsync(ligado: bool) -> void:
var modo: DisplayServer.VSyncMode = DisplayServer.VSYNC_ENABLED if ligado else DisplayServer.VSYNC_DISABLED
DisplayServer.window_set_vsync_mode(modo)
Existem quatro modos no enum DisplayServer.VSyncMode:
VSYNC_DISABLED: desligado. FPS livre, com tearing possível.VSYNC_ENABLED: ligado, o padrão. Sem tearing, FPS travado na taxa do monitor.VSYNC_ADAPTIVE: liga quando o FPS está acima da taxa e desliga quando cai abaixo, reduzindo o "stutter" em quedas.VSYNC_MAILBOX: renderiza mais rápido que o monitor e mostra sempre o frame mais recente. Menos latência, mais uso de GPU.
Para uma tela de opções, oferecer só ligado/desligado com um CheckButton já cobre 90% dos casos.
A UI de opções
Agora juntamos tudo numa cena. A ideia é um Control com três controles principais: um OptionButton pra lista de resoluções, um CheckButton pra tela cheia e outro pra VSync. A estrutura fica assim:
VideoOptions (Control)
└── VBoxContainer
├── ResolucaoOption (OptionButton)
├── TelaCheiaCheck (CheckButton, "Tela cheia")
├── VSyncCheck (CheckButton, "VSync")
└── AplicarButton (Button, "Aplicar")
O script começa declarando as resoluções suportadas como uma lista tipada de Vector2i e populando o OptionButton. Prefira @onready tipado pra pegar os nodes:
extends Control
const RESOLUCOES: Array[Vector2i] = [
Vector2i(1280, 720),
Vector2i(1600, 900),
Vector2i(1920, 1080),
Vector2i(2560, 1440),
]
@onready var resolucao_option: OptionButton = $VBoxContainer/ResolucaoOption
@onready var tela_cheia_check: CheckButton = $VBoxContainer/TelaCheiaCheck
@onready var vsync_check: CheckButton = $VBoxContainer/VSyncCheck
func _ready() -> void:
_popular_resolucoes()
_sincronizar_com_estado_atual()
func _popular_resolucoes() -> void:
resolucao_option.clear()
for res: Vector2i in RESOLUCOES:
resolucao_option.add_item("%d x %d" % [res.x, res.y])
O _sincronizar_com_estado_atual lê a janela na hora que a tela abre e marca os controles conforme o que já está valendo, pra UI não mentir pro jogador:
func _sincronizar_com_estado_atual() -> void:
var modo_atual: int = DisplayServer.window_get_mode()
var em_tela_cheia: bool = (
modo_atual == DisplayServer.WINDOW_MODE_FULLSCREEN
or modo_atual == DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN
)
tela_cheia_check.button_pressed = em_tela_cheia
var vsync_atual: int = DisplayServer.window_get_vsync_mode()
vsync_check.button_pressed = (vsync_atual != DisplayServer.VSYNC_DISABLED)
var tamanho_atual: Vector2i = DisplayServer.window_get_size()
var indice: int = RESOLUCOES.find(tamanho_atual)
if indice != -1:
resolucao_option.select(indice)
Com os controles no lugar, falta a função que pega o estado da UI e aplica de fato. Ela concentra a decisão que comentei antes: em tela cheia, aplica o modo; em janela, aplica tamanho e recentraliza. E aplica o VSync sempre.
func _on_aplicar_button_pressed() -> void:
_aplicar_configuracoes()
func _aplicar_configuracoes() -> void:
var em_tela_cheia: bool = tela_cheia_check.button_pressed
var vsync_ligado: bool = vsync_check.button_pressed
var indice_res: int = resolucao_option.selected
var resolucao: Vector2i = RESOLUCOES[indice_res]
# VSync vale pros dois modos
var modo_vsync: DisplayServer.VSyncMode = (
DisplayServer.VSYNC_ENABLED if vsync_ligado else DisplayServer.VSYNC_DISABLED
)
DisplayServer.window_set_vsync_mode(modo_vsync)
if em_tela_cheia:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(resolucao)
_centralizar_janela()
Não esqueça de ligar os sinais no editor (ou por código) do AplicarButton.pressed pra _on_aplicar_button_pressed. Se quiser aplicar na hora, sem botão "Aplicar", conecte também o item_selected do OptionButton e o toggled de cada CheckButton às mesmas funções. A função _centralizar_janela é a mesma que mostrei lá em cima, só copiada pra dentro deste script.
Persistindo as escolhas
Aqui vem o ponto que pega muita gente de surpresa: nada disso é salvo. Quando o jogador fecha o jogo, o DisplayServer esquece tudo, porque ele só mexe na janela da sessão atual. Na próxima abertura, o jogo volta pro que está definido no Project Settings.
A solução é gravar as três escolhas (modo de janela, resolução e VSync) num arquivo quando o jogador aplica, e ler esse arquivo logo no início do jogo pra reaplicar. O caminho padrão no Godot pra isso é o ConfigFile, que grava um .cfg legível em user://. O fluxo é: no _aplicar_configuracoes, além de mexer no DisplayServer, você chama uma função que salva os valores; e num autoload de boot, você lê e reaplica antes da primeira cena aparecer. Montei esse lado do sistema em detalhe em como salvar as opções do jogador com ConfigFile, então aqui deixo só o gancho: separe bem a camada que "aplica na janela" da que "grava em disco", porque são responsabilidades diferentes e você vai reusar a de aplicar no boot.
Um esqueleto mínimo da parte de salvar, pra fechar o raciocínio:
func salvar_video() -> void:
var config: ConfigFile = ConfigFile.new()
config.set_value("video", "tela_cheia", tela_cheia_check.button_pressed)
config.set_value("video", "vsync", vsync_check.button_pressed)
config.set_value("video", "resolucao", RESOLUCOES[resolucao_option.selected])
config.save("user://opcoes.cfg")
No boot, você faz o caminho inverso: config.load("user://opcoes.cfg"), lê cada get_value com um valor padrão e chama as mesmas funções de aplicação. Assim a tela de vídeo e o carregamento inicial compartilham a lógica.
Fechando
Uma tela de configurações de vídeo no Godot 4 se resume a três ferramentas: DisplayServer.window_set_mode com os enums de Window pra alternar janela e tela cheia, window_set_size mais o recentralizar pra resolução, e window_set_vsync_mode pro VSync. O Stretch em canvas_items + keep garante que a UI não distorça, e o ConfigFile fecha o ciclo persistindo tudo. Note que nenhuma dessas peças é complicada isolada; o trabalho é conectá-las com estados coerentes e uma UI que reflete o que está valendo.
Se você está montando o menu de opções do zero, faz sentido ter a base de UI e sinais bem firme antes. Caso ainda esteja começando, vale seguir uma trilha de Godot para iniciantes pra não ficar tropeçando em Control nodes no meio do caminho. Com a fundação no lugar, essa tela de vídeo vira quase um exercício de encaixe.
Perguntas frequentes
Como colocar o jogo em tela cheia no Godot 4?
Use DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) ou, quando você tem a referência da janela, atribua get_window().mode = Window.MODE_FULLSCREEN. Para tela cheia exclusiva (que troca o modo de vídeo do monitor), use WINDOW_MODE_EXCLUSIVE_FULLSCREEN. Voltar pro modo janela é WINDOW_MODE_WINDOWED.
Como trocar a resolução da janela no Godot 4?
A resolução da janela se troca com DisplayServer.window_set_size(Vector2i(largura, altura)). Depois vale recentralizar a janela com window_set_position. Para a arte do jogo escalar junto sem distorcer, configure o Stretch em Project Settings, Display, Window com o modo canvas_items e aspect keep.
Qual a diferença entre tela cheia e tela cheia exclusiva no Godot?
A tela cheia normal (WINDOW_MODE_FULLSCREEN) é uma janela sem borda do tamanho do monitor, mantendo a resolução do desktop, e o alt-tab é instantâneo. A exclusiva (WINDOW_MODE_EXCLUSIVE_FULLSCREEN) assume o controle do monitor e pode trocar o modo de vídeo, o que às vezes reduz a latência mas deixa o alt-tab mais lento.
Como ligar e desligar o VSync no Godot 4?
Use DisplayServer.window_set_vsync_mode com DisplayServer.VSYNC_ENABLED para ligar e DisplayServer.VSYNC_DISABLED para desligar. Existem ainda VSYNC_ADAPTIVE e VSYNC_MAILBOX para casos específicos. O VSync elimina o tearing na tela ao custo de limitar o FPS à taxa de atualização do monitor.
Por que a interface distorce quando eu troco a resolução no Godot?
Isso acontece quando o modo de Stretch está em disabled ou o aspect está em ignore, fazendo a UI esticar junto com a janela. A correção é ir em Project Settings, Display, Window, Stretch e usar o modo canvas_items com aspect keep, assim o conteúdo escala proporcionalmente com barras pretas em vez de deformar.
As configurações de vídeo do Godot são salvas sozinhas?
Não. Trocar o modo da janela ou a resolução em tempo de execução não persiste nada em disco. Você precisa gravar as escolhas do jogador em um arquivo, tipicamente com ConfigFile, e ler esse arquivo no início do jogo para reaplicar tudo. O DisplayServer só mexe na janela da sessão atual.


