Voltar para o Blog
Quest Log

Menu de Opções na Unity: Volume, Resolução e Tela Cheia

Painel de configurações de jogo com sliders de volume, um dropdown de resolução e uma chave de tela cheia

Unity settings menu completo: dropdown de resolução sem duplicata, tela cheia com FullScreenMode, qualidade, volume no Audio Mixer e opções salvas no início.

Ferramenta:UnityC#

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.

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.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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.