Voltar para o Blog
Quest Log

SerializeField vs public na Unity: Variáveis no Inspector

Painel de propriedades de um editor de jogos com campos, sliders e títulos organizados ao lado de um personagem

SerializeField na Unity: por que usar private com [SerializeField] em vez de public, Header, Tooltip e Range, e o valor do Inspector que ignora o código.

Ferramenta:UnityC#

SerializeField vs public na Unity: Variáveis no Inspector

[SerializeField] na Unity é o atributo que faz um campo private aparecer no Inspector e ser salvo na cena ou no prefab. Ele dá ao campo o mesmo efeito visual de public, mas sem abrir a variável para qualquer outro script mexer. Por isso a regra da casa é simples: configurável no editor, [SerializeField] private; public só quando outro script realmente precisa daquilo.

Aqui você vai ver o porquê dessa regra, os atributos que deixam o Inspector organizado (Header, Tooltip, Range, Space), o bug do valor que você muda no código e não muda no jogo, e o que fazer quando um campo simplesmente não aparece. Tudo testado contra as regras de serialização da Unity 6.3 LTS.

SerializeField na Unity: o que faz

Serializar é transformar dados em algo que pode ser salvo e recarregado. Quando você ajusta a velocidade de um inimigo no Inspector e salva a cena, a Unity grava aquele número no arquivo da cena ou do prefab. Ao abrir o projeto de novo, ela lê o arquivo e devolve o valor ao campo.

A regra do manual ("Serialization rules") diz que um campo é serializado quando é public ou tem [SerializeField], e desde que não seja static, const nem readonly. Além disso, o tipo precisa ser suportado: primitivos (int, float, bool, string), enums, tipos da Unity (Vector2, Color, AnimationCurve), referências a objetos da Unity (GameObject, Transform, componentes, assets), structs e classes marcadas com [Serializable], e arrays ou List<T> desses tipos.

Campo serializado aparece no Inspector. Campo não serializado não aparece e não é salvo. É essa a relação que você precisa ter na cabeça.

public vs private com SerializeField

Veja os dois jeitos lado a lado. Arquivo EnemyStats.cs:

using UnityEngine;

public class EnemyStats : MonoBehaviour
{
    // Jeito que aparece em tutorial antigo: qualquer script pode mudar.
    public float speedPublic = 3f;

    // Jeito recomendado: aparece no Inspector, mas só esta classe muda.
    [SerializeField] private float speed = 3f;
    [SerializeField] private int maxHealth = 4;

    private int currentHealth;

    public float Speed => speed;
    public bool IsDead => currentHealth <= 0;

    private void Awake()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int amount)
    {
        currentHealth = Mathf.Max(currentHealth - amount, 0);
    }
}

No Inspector, speedPublic e speed ficam idênticos. A diferença está no resto do código. Com public, qualquer script pode escrever enemy.speedPublic = 0; e o inimigo para, sem deixar rastro. Quando o bug aparecer ("o inimigo congela às vezes"), você vai ter que procurar em todos os scripts do projeto quem mexe nesse campo.

Com [SerializeField] private, a única forma de mudar speed em jogo é por um método da própria classe. Outros scripts leem pela propriedade Speed, que é só leitura. Se a vida precisa mudar, passa por TakeDamage, que é um ponto só, onde você pode botar um log, tocar um som ou disparar um evento.

Existe ainda um ganho de leitura. Quando você abre um script e vê [SerializeField] private, sabe que aquele número é ajuste de design, feito no editor. Quando vê public, precisa descobrir se é ajuste de design, se é dado que outro sistema escreve, ou as duas coisas ao mesmo tempo. Isso também protege você de você mesmo. Num projeto pequeno parece exagero; com 40 scripts, saber que só uma classe mexe em cada valor é o que mantém o código legível.

O valor do Inspector ganha do código

Este é o bug que mais confunde quem está começando. Você escreve [SerializeField] private float speed = 3f;, adiciona o script num inimigo, testa, acha lento, volta no código, troca para 6f, salva, dá Play. O inimigo continua lento.

O motivo: o 3f do código é só o valor inicial, usado no momento em que o componente é criado. A partir daí, o valor que vale é o que está salvo na cena ou no prefab. Ao mudar para 6f no código, você mudou o padrão para componentes novos, não os que já existem.

Ordem real dos acontecimentos quando o jogo carrega um objeto:

  1. A Unity cria a instância do script; os inicializadores do código rodam (speed = 6f).
  2. A Unity aplica os dados serializados da cena por cima (speed = 3f, o valor salvo).
  3. O Awake roda, já com 3f.

Como resolver, conforme o caso:

  • Quer o valor novo só neste objeto: mude no Inspector.
  • Quer voltar ao valor do código: clique nos três pontos do componente no Inspector e escolha Reset. Isso reaplica todos os valores iniciais do código, então confira os outros campos depois.
  • Quer mudar em todos os inimigos: mude no prefab, não nas cópias da cena. Cópias sem override herdam do prefab.
  • O valor nunca deveria ser configurável: tire o [SerializeField] e deixe private. Aí o código manda sempre.

Outro sintoma do mesmo mecanismo: mudar um valor no Inspector durante o Play e perder a mudança ao parar. Mudança em Play Mode não é salva. Copie o componente (três pontos, Copy Component), pare o jogo e use Paste Component Values.

Header, Tooltip, Range e Space

Um script com 15 campos soltos vira uma parede no Inspector. Quatro atributos resolvem a maior parte disso. Arquivo PlayerConfig.cs:

using UnityEngine;

public class PlayerConfig : MonoBehaviour
{
    [Header("Movimento")]
    [Tooltip("Velocidade horizontal em unidades por segundo.")]
    [SerializeField, Range(1f, 15f)] private float moveSpeed = 6f;

    [Tooltip("Velocidade vertical aplicada no início do pulo.")]
    [SerializeField, Min(0f)] private float jumpSpeed = 12f;

    [Space(12)]
    [Header("Combate")]
    [SerializeField, Range(1, 10)] private int maxHealth = 5;

    [Tooltip("Segundos de invencibilidade depois de tomar dano.")]
    [SerializeField, Range(0f, 3f)] private float invincibleTime = 1f;

    [Header("Referências")]
    [SerializeField] private Rigidbody2D body;
    [SerializeField] private Animator animator;

    [Header("Texto")]
    [SerializeField, TextArea(2, 5)] private string deathMessage = "Você caiu.";
}
  • [Header("...")] escreve um título em negrito acima do campo.
  • [Tooltip("...")] mostra o texto quando o mouse para sobre o nome do campo. Use para dizer a unidade (segundos, graus, unidades por segundo), que é o que mais se esquece.
  • [Range(min, max)] troca a caixa de número por um slider e impede valores fora do intervalo pelo Inspector. Funciona com float e int.
  • [Space(pixels)] adiciona um respiro vertical.
  • [Min(valor)] e [TextArea] são dois extras úteis: um impede número abaixo do mínimo sem slider, o outro deixa um campo de texto com várias linhas.

Os atributos podem ir em colchetes separados ou juntos, separados por vírgula, como em [SerializeField, Range(1f, 15f)]. O resultado é o mesmo.

Um Inspector bem organizado é o que permite você ajustar o jogo sem abrir código. Isso fica crítico quando o projeto tem jogador, inimigos, armas e fases inteiras para balancear, e é exatamente nessa hora, de fazer muita coisa funcionar junto, que quem estuda sozinho costuma travar.

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

Serializar classes e listas próprias

Você pode agrupar dados numa classe sua e mostrar no Inspector, desde que ela tenha [System.Serializable]. Arquivo WaveSpawner.cs:

using System;
using System.Collections.Generic;
using UnityEngine;

public class WaveSpawner : MonoBehaviour
{
    [Serializable]
    private class Wave
    {
        public GameObject enemyPrefab;
        [Range(1, 30)] public int count = 5;
        [Min(0.1f)] public float interval = 1f;
    }

    [SerializeField] private List<Wave> waves = new List<Wave>();

    public int WaveCount => waves.Count;
}

No Inspector aparece uma lista que você expande, e cada item tem os três campos. Os campos da classe Wave são public porque ela é um pacote de dados interno, private dentro do WaveSpawner. Ninguém de fora acessa.

Três regras do manual que valem aqui:

  1. [Serializable] não é herdado. Se BossWave herda de Wave, ela também precisa do atributo.
  2. Sem coleção dentro de coleção. List<List<int>>, int[,] e Dictionary não são serializados. Para uma grade, faça uma List de uma classe Row que contém uma List<int>.
  3. Records não. O manual avisa que a serialização da Unity não suporta records do C#.

Quando os mesmos dados aparecem em vários objetos (stats de cada tipo de inimigo, por exemplo), o passo seguinte é tirar esses dados do componente e colocar num ScriptableObject, um asset que vários prefabs referenciam. Está explicado em ScriptableObject na Unity.

Erros comuns: campo que não aparece no Inspector

Quando um campo some do Inspector, a causa quase sempre está nesta lista:

  • Erro de compilação no projeto. Se qualquer script tem erro, a Unity não recompila, e o Inspector mostra a versão antiga. Olhe o Console antes de tudo.
  • Campo static, const ou readonly. Esses nunca são serializados.
  • É uma propriedade. public float Speed { get; set; } não aparece. Veja a próxima seção.
  • Tipo não suportado. Dictionary, List<List<T>>, array multidimensional, interface, Action.
  • Classe própria sem [Serializable]. O campo do tipo Wave some inteiro.
  • [HideInInspector] ou [NonSerialized]. O primeiro esconde mas continua salvando; o segundo nem salva. Às vezes vêm de código copiado.
  • Nome da classe diferente do arquivo. O componente fica com aviso de script ausente e nenhum campo aparece.

Quando usar propriedades

Propriedade é o jeito do C# de expor um valor com controle. A Unity não serializa propriedades, mas tem duas formas de combinar as duas coisas.

A forma clássica, que funciona com qualquer atributo de Inspector:

using UnityEngine;

public class Weapon : MonoBehaviour
{
    [SerializeField, Range(1, 20)] private int damage = 3;

    public int Damage => damage;
}

A forma curta, com [field: SerializeField], que o manual documenta para serializar o campo gerado de uma auto-propriedade:

using UnityEngine;

public class Pickup : MonoBehaviour
{
    [field: SerializeField] public int Value { get; private set; } = 1;
}

O Value aparece no Inspector, outros scripts leem, só a própria classe escreve. O cuidado é que o nome salvo no arquivo é o do campo gerado pelo compilador; se você trocar de auto-propriedade para campo comum depois, o valor salvo se perde. Para dados que vão mudar de forma, prefira a versão clássica.

Próximo passo

Pegue um script seu que tenha campos public, troque todos por [SerializeField] private, compile e veja quais outros scripts quebram. Cada erro de compilação é um lugar que mexia no campo sem você saber: decida se ele precisa de um método ou se nem deveria estar mexendo. Depois organize o Inspector com Header e Range. Se a dúvida for de C# básico, o post qual linguagem a Unity usa dá o contexto, e o padrão de manter dados e comportamento separados aparece em padrões de projeto em jogos.

Perguntas frequentes

Qual a diferença entre public e [SerializeField] private na Unity?

Os dois fazem o campo aparecer no Inspector e ser salvo na cena ou no prefab. A diferença é que public também deixa qualquer outro script ler e alterar o valor, enquanto [SerializeField] private mantém o campo fechado para o código de fora. Para o Inspector o resultado é o mesmo; para o projeto, private é mais seguro.

Por que mudei o valor no script e nada mudou no jogo?

Porque o valor salvo no Inspector tem prioridade sobre o valor inicial escrito no código. O número do código só vale quando o componente é adicionado ou resetado. Mude no Inspector, ou use Reset no menu de três pontos do componente para voltar aos valores do código.

Como mostrar uma propriedade C# no Inspector da Unity?

A Unity não serializa propriedades. Para uma auto-propriedade, use [field: SerializeField], que serializa o campo gerado pelo compilador. A alternativa clássica é um campo [SerializeField] private com uma propriedade pública só de leitura apontando para ele.

Dictionary aparece no Inspector da Unity?

Não. O sistema de serialização da Unity não suporta Dictionary, arrays multidimensionais nem listas de listas. O padrão comum é serializar uma List de uma classe [Serializable] com chave e valor e montar o Dictionary no Awake.