Menu de Opções na Unity: Volume, Resolução e Tela Cheia
Um unity settings menu que funciona tem cinco controles e um sistema de salvar: um dropdown de resolução preenchido com Screen.resolutions, um toggle de tela cheia que usa FullScreenMode, um dropdown de qualidade com QualitySettings, sliders de volume ligados a um Audio Mixer e tudo gravado em PlayerPrefs e reaplicado quando o jogo abre. O que costuma dar errado não é ligar cada controle, é o detalhe: a lista de resolução com 1920x1080 repetido quatro vezes e a opção que o jogador escolheu e sumiu ao reabrir o jogo.
Aqui você monta a tela de opções inteira no Unity 6.3 LTS, com o código para cada controle e a parte de salvar e carregar, que é onde a maioria dos tutoriais para.
O que todo menu de opções precisa ter
Para um jogo de PC lançado, o mínimo que o jogador espera:
- Resolução, em dropdown, só com as resoluções que o monitor suporta.
- Tela cheia ou janela, em toggle ou dropdown.
- Qualidade gráfica, ligada aos níveis de Edit > Project Settings > Quality.
- Volume geral, de música e de efeitos, separados.
- Persistência: o que foi escolhido continua escolhido na próxima vez.
Controles e legendas entram na lista em jogos maiores, mas a estrutura do código é a mesma. Monte no painel de opções (o PainelOpcoes do tutorial de menu principal na Unity, se você seguiu aquele post) estes elementos, todos de GameObject > UI: um Dropdown - TextMeshPro chamado DropdownResolucao, um Toggle chamado ToggleTelaCheia, outro Dropdown - TextMeshPro chamado DropdownQualidade e três Slider: SliderGeral, SliderMusica, SliderEfeitos. Nos sliders, ajuste Min Value para 0.0001 e Max Value para 1. O motivo do 0.0001 aparece na seção de volume.
Dropdown de resolução com Screen.resolutions
Screen.resolutions devolve as resoluções de tela cheia que o monitor suporta, ordenadas por largura e depois por altura. Cada item é um Resolution com width, height e refreshRateRatio (a taxa de atualização em formato de razão; o campo refreshRate inteiro está obsoleto no Unity 6).
O problema: um monitor que roda em 60 Hz e 144 Hz gera duas entradas para cada tamanho. Jogue isso direto no dropdown e o jogador vê "1920 x 1080" repetido. A solução é filtrar por tamanho.
Duas outras decisões sobre resolução que valem tomar antes de escrever código. A primeira: qual opção aparece marcada quando o menu abre. O script compara cada item com Screen.width e Screen.height, que é o tamanho atual da janela do jogo, então o dropdown sempre mostra o que está valendo. A segunda: o que fazer se o jogador escolher uma resolução que o monitor dele aceita mas que deixa a tela preta ou cortada, coisa que acontece com TV e monitor ultrawide. Jogos lançados resolvem com uma confirmação: aplicam a resolução nova, mostram "Manter esta resolução?" com uma contagem de 10 segundos e, se ninguém confirmar, voltam para a anterior. É um painel e uma corrotina com WaitForSecondsRealtime, e evita o pior caso, que é o jogador sem conseguir ver o menu para desfazer a escolha.
Tela cheia e modo janela com FullScreenMode
O FullScreenMode tem quatro valores:
- FullScreenWindow: janela sem borda do tamanho da tela. Troca de janela com Alt+Tab rápido. É o padrão recomendado.
- ExclusiveFullScreen: tela cheia exclusiva, só no Windows. Muda a resolução do monitor.
- MaximizedWindow: janela maximizada, no macOS.
- Windowed: janela comum.
Para um toggle simples, alterne entre FullScreenWindow e Windowed. Para trocar resolução e modo de uma vez, use Screen.SetResolution(largura, altura, modo), a sobrecarga que recebe FullScreenMode.
Qualidade gráfica com QualitySettings
QualitySettings.names devolve os nomes dos níveis configurados em Project Settings > Quality, na ordem. QualitySettings.SetQualityLevel(indice, true) aplica o nível. O segundo parâmetro, applyExpensiveChanges, aplica também as mudanças caras, como anti-aliasing; para uma tela de opções, deixe em true. Preencher o dropdown com names faz a lista acompanhar o projeto: se você criar um nível "Ultra" amanhã, ele aparece sem mexer no código.
Volume ligado ao Audio Mixer
Volume em jogo se controla num Audio Mixer (Assets > Create > Audio Mixer), com um grupo Master e dois filhos, Musica e Efeitos. Cada AudioSource do jogo aponta para um desses grupos no campo Output.
Para o script mexer no volume, exponha o parâmetro: selecione o grupo, clique com o botão direito no campo Volume do Inspector e escolha Expose 'Volume (of Musica)' to script. No topo da janela Audio Mixer, em Exposed Parameters, renomeie para VolumeMusica. Repita para VolumeGeral e VolumeEfeitos.
O Mixer trabalha em decibéis, de -80 dB (mudo) a 0 dB (volume original). O ouvido percebe volume em escala logarítmica, então converter o slider direto (0 a 1 virando -80 a 0) faz o volume "sumir" na metade do slider. A conversão certa é Mathf.Log10(valor) * 20: 1 vira 0 dB, 0.5 vira cerca de -6 dB, 0.0001 vira -80 dB. Log de zero é infinito negativo, e por isso o slider começa em 0.0001. O post sobre Audio Mixer na Unity aprofunda grupos, snapshots e efeitos; aqui basta a ligação com o menu.
Montar cada um desses controles leva minutos. Fazer a tela inteira se comportar como a de um jogo lançado, com tudo salvo, aplicado antes da primeira cena e testado numa build em monitor diferente do seu, é o tipo de acabamento que separa protótipo de jogo publicado, e é onde quem aprende sozinho costuma parar.
O script completo do menu de opções
Agora o código que liga tudo. Coloque num objeto que fica ativo (o próprio painel de opções serve, desde que o script só rode quando ele abre; a parte de carregar no início fica num script separado, mais abaixo).
Arquivo MenuDeOpcoes.cs:
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;
public class MenuDeOpcoes : MonoBehaviour
{
[SerializeField] private TMP_Dropdown dropdownResolucao;
[SerializeField] private Toggle toggleTelaCheia;
[SerializeField] private TMP_Dropdown dropdownQualidade;
[SerializeField] private Slider sliderGeral;
[SerializeField] private Slider sliderMusica;
[SerializeField] private Slider sliderEfeitos;
[SerializeField] private AudioMixer mixer;
private readonly List<Vector2Int> resolucoes = new List<Vector2Int>();
private void Start()
{
PreencherResolucoes();
PreencherQualidade();
toggleTelaCheia.SetIsOnWithoutNotify(Screen.fullScreenMode != FullScreenMode.Windowed);
sliderGeral.SetValueWithoutNotify(PlayerPrefs.GetFloat(ChavesDeOpcoes.VolumeGeral, 1f));
sliderMusica.SetValueWithoutNotify(PlayerPrefs.GetFloat(ChavesDeOpcoes.VolumeMusica, 1f));
sliderEfeitos.SetValueWithoutNotify(PlayerPrefs.GetFloat(ChavesDeOpcoes.VolumeEfeitos, 1f));
dropdownResolucao.onValueChanged.AddListener(MudarResolucao);
toggleTelaCheia.onValueChanged.AddListener(MudarTelaCheia);
dropdownQualidade.onValueChanged.AddListener(MudarQualidade);
sliderGeral.onValueChanged.AddListener(valor => MudarVolume(ChavesDeOpcoes.VolumeGeral, valor));
sliderMusica.onValueChanged.AddListener(valor => MudarVolume(ChavesDeOpcoes.VolumeMusica, valor));
sliderEfeitos.onValueChanged.AddListener(valor => MudarVolume(ChavesDeOpcoes.VolumeEfeitos, valor));
}
private void PreencherResolucoes()
{
resolucoes.Clear();
List<string> textos = new List<string>();
int indiceAtual = 0;
foreach (Resolution resolucao in Screen.resolutions)
{
Vector2Int tamanho = new Vector2Int(resolucao.width, resolucao.height);
if (resolucoes.Contains(tamanho))
{
continue;
}
resolucoes.Add(tamanho);
textos.Add(tamanho.x + " x " + tamanho.y);
if (tamanho.x == Screen.width && tamanho.y == Screen.height)
{
indiceAtual = resolucoes.Count - 1;
}
}
dropdownResolucao.ClearOptions();
dropdownResolucao.AddOptions(textos);
dropdownResolucao.SetValueWithoutNotify(indiceAtual);
}
private void PreencherQualidade()
{
dropdownQualidade.ClearOptions();
dropdownQualidade.AddOptions(new List<string>(QualitySettings.names));
dropdownQualidade.SetValueWithoutNotify(QualitySettings.GetQualityLevel());
}
private void MudarResolucao(int indice)
{
Vector2Int tamanho = resolucoes[indice];
Screen.SetResolution(tamanho.x, tamanho.y, Screen.fullScreenMode);
PlayerPrefs.SetInt(ChavesDeOpcoes.Largura, tamanho.x);
PlayerPrefs.SetInt(ChavesDeOpcoes.Altura, tamanho.y);
PlayerPrefs.Save();
}
private void MudarTelaCheia(bool telaCheia)
{
FullScreenMode modo = telaCheia ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed;
Screen.fullScreenMode = modo;
PlayerPrefs.SetInt(ChavesDeOpcoes.TelaCheia, telaCheia ? 1 : 0);
PlayerPrefs.Save();
}
private void MudarQualidade(int indice)
{
QualitySettings.SetQualityLevel(indice, true);
PlayerPrefs.SetInt(ChavesDeOpcoes.Qualidade, indice);
PlayerPrefs.Save();
}
private void MudarVolume(string parametro, float valor)
{
mixer.SetFloat(parametro, Mathf.Log10(valor) * 20f);
PlayerPrefs.SetFloat(parametro, valor);
}
private void OnDisable()
{
PlayerPrefs.Save();
}
}
As chaves ficam numa classe estática, para não ter texto solto espalhado e errar uma letra entre o salvar e o carregar.
Arquivo ChavesDeOpcoes.cs:
public static class ChavesDeOpcoes
{
public const string Largura = "opcoes_largura";
public const string Altura = "opcoes_altura";
public const string TelaCheia = "opcoes_tela_cheia";
public const string Qualidade = "opcoes_qualidade";
public const string VolumeGeral = "VolumeGeral";
public const string VolumeMusica = "VolumeMusica";
public const string VolumeEfeitos = "VolumeEfeitos";
}
As chaves de volume têm o mesmo nome dos parâmetros expostos no Mixer de propósito: o MudarVolume usa a mesma string para os dois. Repare também no SetValueWithoutNotify e no SetIsOnWithoutNotify: eles mostram o valor salvo sem disparar o evento de mudança, então abrir o menu não reaplica nem regrava tudo.
Salvando e carregando as opções
O MenuDeOpcoes grava no disco a cada troca de resolução, tela cheia ou qualidade. O volume é diferente: o slider dispara o evento várias vezes por segundo enquanto o jogador arrasta, então ele só atualiza o valor na memória, e o OnDisable grava tudo quando o painel fecha. Falta aplicar o que foi salvo quando o jogo abre, antes de o jogador abrir o menu. Isso fica num script na primeira cena (a do menu principal), num objeto sempre ativo.
Arquivo AplicarOpcoesSalvas.cs:
using UnityEngine;
using UnityEngine.Audio;
public class AplicarOpcoesSalvas : MonoBehaviour
{
[SerializeField] private AudioMixer mixer;
private void Start()
{
if (PlayerPrefs.HasKey(ChavesDeOpcoes.Qualidade))
{
QualitySettings.SetQualityLevel(PlayerPrefs.GetInt(ChavesDeOpcoes.Qualidade), true);
}
bool telaCheia = PlayerPrefs.GetInt(ChavesDeOpcoes.TelaCheia, 1) == 1;
FullScreenMode modo = telaCheia ? FullScreenMode.FullScreenWindow : FullScreenMode.Windowed;
if (PlayerPrefs.HasKey(ChavesDeOpcoes.Largura))
{
int largura = PlayerPrefs.GetInt(ChavesDeOpcoes.Largura);
int altura = PlayerPrefs.GetInt(ChavesDeOpcoes.Altura);
Screen.SetResolution(largura, altura, modo);
}
else
{
Screen.fullScreenMode = modo;
}
AplicarVolume(ChavesDeOpcoes.VolumeGeral);
AplicarVolume(ChavesDeOpcoes.VolumeMusica);
AplicarVolume(ChavesDeOpcoes.VolumeEfeitos);
}
private void AplicarVolume(string parametro)
{
float valor = PlayerPrefs.GetFloat(parametro, 1f);
mixer.SetFloat(parametro, Mathf.Log10(valor) * 20f);
}
}
Por que Start e não Awake? O AudioMixer.SetFloat chamado no Awake pode não ter efeito, porque o sistema de áudio ainda não terminou de inicializar o Mixer naquele momento. No Start ele aplica sempre.
PlayerPrefs grava num lugar do sistema (no Windows, no registro do usuário, sob o nome da empresa e do produto do Player Settings). Ele salva sozinho quando o jogo fecha normalmente, mas um crash perde o que não foi gravado. O PlayerPrefs.Save() nas mudanças pontuais e ao fechar o painel evita isso, sem gravar no disco a cada movimento do slider. Para opções, PlayerPrefs é suficiente. Para progresso do jogador (fases, inventário), use um arquivo próprio em JSON.
Erros comuns: resoluções repetidas e opção que não persiste
Resoluções repetidas. É o caso das taxas de atualização. O filtro com Vector2Int resolve. Se você quer mostrar a taxa também ("1920 x 1080 @ 144 Hz"), use resolucao.refreshRateRatio.value e não filtre, mas aí use a sobrecarga de SetResolution que recebe RefreshRate.
Lista de resolução com uma opção só no editor. O editor não simula o monitor de verdade. Teste o dropdown numa build.
Opção que não persiste. Três suspeitos: a chave de salvar é diferente da de carregar (por isso a classe ChavesDeOpcoes), o AplicarOpcoesSalvas não está na primeira cena ou está num objeto desligado, ou o jogo foi fechado pelo editor antes de gravar e você não chamou PlayerPrefs.Save().
Volume que some na metade do slider ou estoura em zero. Falta a conversão Log10 * 20, ou o slider começa em 0 e o log de zero vira infinito negativo.
Toggle de tela cheia sem efeito no editor. No Game View do editor, a tela cheia não muda nada. Teste na build.
Próximo passo: usar a mesma tela na pausa
Transforme o painel de opções num prefab e abra o mesmo prefab pelo menu principal e pelo menu de pausa. Depois, acrescente uma opção de sensibilidade de câmera usando o mesmo padrão de chave e PlayerPrefs. Se você trabalha com Godot também, o post de menu de opções no Godot monta a mesma tela com ConfigFile, e o guia de UI e UX em jogos ajuda a decidir onde cada opção fica para o jogador achar sem procurar.
Perguntas frequentes
Por que o dropdown de resolução da Unity mostra resoluções repetidas?
Porque Screen.resolutions devolve uma entrada para cada combinação de tamanho e taxa de atualização. Um monitor de 60 Hz e 144 Hz gera 1920x1080 duas vezes. Filtre por largura e altura antes de preencher o dropdown.
Como deixar o jogo em tela cheia por script na Unity?
Use Screen.fullScreenMode = FullScreenMode.FullScreenWindow para tela cheia em janela sem borda, ou FullScreenMode.Windowed para janela. Para trocar resolução e modo juntos, use Screen.SetResolution(largura, altura, modo).
Como salvar as opções do jogo na Unity?
Para configurações simples, PlayerPrefs resolve: SetInt, SetFloat e SetString com uma chave, e PlayerPrefs.Save para gravar na hora. No início do jogo, leia as chaves e aplique os valores antes de o jogador ver a primeira tela.
Por que o volume do Audio Mixer não aplica quando o jogo abre?
AudioMixer.SetFloat chamado no Awake pode não surtir efeito. Aplique o volume salvo no Start. E converta o valor do slider para decibéis com Mathf.Log10(valor) * 20, com o slider começando em 0.0001 e não em 0.



