Voltar para o Blog
Quest Log

Destroy na Unity: Remover Objetos, Componentes e Filhos

Bloco de jogo se desfazendo em pequenos cubos luminosos enquanto peças filhas se soltam ao redor

Unity Destroy GameObject na prática: destruir a si mesmo, com atraso, só um componente, todos os filhos, e por que o objeto ainda existe até o fim do frame.

Ferramenta:UnityC#

Destroy na Unity: Remover Objetos, Componentes e Filhos

Para destruir um GameObject na Unity, você chama Destroy(gameObject). Para destruir depois de um tempo, Destroy(gameObject, 2f). Para remover só um componente, Destroy(componente). O detalhe que confunde: a destruição não acontece na linha em que você chamou, ela é agendada para depois do Update do frame atual, e até lá o objeto continua existindo.

Este tutorial mostra cada caso com código do Unity 6.3 LTS: remover a si mesmo, com atraso, só um componente, todos os filhos sem pular nenhum, a diferença para DestroyImmediate, o que esse "ainda existe até o fim do frame" causa no seu código, e quando a resposta certa é não destruir nada.

Unity destroy gameobject: o básico

Destroy é um método estático de UnityEngine.Object, disponível em qualquer MonoBehaviour. A declaração é Destroy(Object obj, float t = 0f). O primeiro parâmetro é o que vai ser destruído: um GameObject, um componente ou um asset. O segundo é o atraso em segundos.

Quando o alvo é um GameObject, vão junto todos os componentes dele e todos os filhos na hierarquia. A referência oficial diz que a destruição de verdade é sempre adiada para depois do loop de Update atual, mas sempre acontece antes da renderização. Na tela, o objeto some no mesmo frame. No código, ele ainda está lá até o fim dos Update.

Antes de sumir, a Unity chama OnDisable e OnDestroy nos scripts do objeto. OnDestroy é o lugar para limpar o que o objeto deixou no mundo: desinscrever eventos, avisar um gerenciador, soltar um item.

Destruir o próprio objeto e com atraso

O caso mais comum: uma moeda coletada, um inimigo que morreu. Arquivo Coin.cs:

using UnityEngine;

public class Coin : MonoBehaviour
{
    [SerializeField] private int value = 1;

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            Debug.Log("Coletou " + value);
            Destroy(gameObject);
        }
    }
}

gameObject (com g minúsculo) é a propriedade que aponta para o GameObject onde o script está. O erro clássico é escrever Destroy(this). Compila, roda, e a moeda continua lá: this é o componente Coin, então você removeu o script e deixou o objeto. A moeda para de funcionar e fica parada na tela para sempre.

Com atraso, para um efeito que precisa terminar antes de sumir. Arquivo TimedEffect.cs:

using UnityEngine;

public class TimedEffect : MonoBehaviour
{
    [SerializeField] private float lifetime = 1.5f;

    private void Start()
    {
        Destroy(gameObject, lifetime);
    }
}

Coloque esse script num prefab de explosão ou de texto de dano flutuante e ele se limpa sozinho. O cronômetro começa no momento da chamada.

Um detalhe da documentação que pega em menu de pausa: o atraso usa o tempo escalado. Com Time.timeScale em 0, a destruição espera o jogo voltar. Na maioria dos casos é exatamente o que você quer (a explosão não termina enquanto o jogo está pausado). Se não for, controle o tempo você mesmo com Time.unscaledDeltaTime e chame Destroy sem atraso.

E dá para cancelar? Não existe um "cancelar Destroy". Se o objeto pode precisar continuar vivo, não agende a destruição com atraso: use um timer próprio ou uma coroutine que você consegue parar.

Destruir só um componente

Destroy aceita qualquer componente. O exemplo da própria documentação remove um Rigidbody. Um caso de jogo: o inimigo morre, para de se mexer e de colidir, mas o corpo fica no chão por alguns segundos. Arquivo EnemyDeath.cs:

using UnityEngine;

public class EnemyDeath : MonoBehaviour
{
    [SerializeField] private Rigidbody2D body;
    [SerializeField] private Collider2D hitbox;
    [SerializeField] private MonoBehaviour[] behavioursToRemove;
    [SerializeField] private float corpseTime = 3f;

    public void Die()
    {
        Destroy(hitbox);
        Destroy(body);

        foreach (MonoBehaviour behaviour in behavioursToRemove)
        {
            Destroy(behaviour);
        }

        Destroy(gameObject, corpseTime);
    }
}

No array behavioursToRemove você arrasta a IA e o script de ataque. O sprite fica, sem física e sem comportamento, e o objeto inteiro some três segundos depois.

Duas restrições. Você não pode destruir o Transform: todo GameObject precisa de um. E se outro componente tem [RequireComponent] apontando para o que você quer remover, a Unity recusa. Remova primeiro o dependente.

Muitas vezes, em vez de destruir o componente, basta desligá-lo: hitbox.enabled = false;. Desligado, ele pode voltar com true. Destruído, só recriando.

Destruir todos os filhos sem pular nenhum

Limpar uma lista de UI antes de redesenhar, remover todos os itens de um baú, resetar uma fase gerada. O laço seguro percorre os filhos de trás para frente. Arquivo ChildCleaner.cs:

using UnityEngine;

public class ChildCleaner : MonoBehaviour
{
    public void DestroyAllChildren()
    {
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
            Destroy(transform.GetChild(i).gameObject);
        }
    }
}

Por que de trás para frente? Com Destroy, um laço para frente também funcionaria, porque nada sai da hierarquia até o fim do frame. O problema aparece no dia em que você trocar por DestroyImmediate (num script de editor) ou por algo que tira o filho da lista na hora, como SetParent(null). Aí a lista encolhe durante o laço: você remove o filho 0, o antigo filho 1 vira o novo 0, o índice avança para 1 e um filho é pulado. Indo do último para o primeiro, remover não mexe nos índices que ainda faltam. É um hábito que funciona em todos os casos.

Agora a pegadinha, ligada à seção sobre o fim do frame. Logo depois de chamar DestroyAllChildren, transform.childCount ainda devolve o número antigo. Se o código seguinte cria filhos novos e depois conta, a conta inclui os que vão morrer. Quando isso importa, desanexe antes de destruir:

using UnityEngine;

public class ListRebuilder : MonoBehaviour
{
    [SerializeField] private GameObject itemPrefab;

    public void Rebuild(int itemCount)
    {
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
            Transform child = transform.GetChild(i);
            child.SetParent(null);
            Destroy(child.gameObject);
        }

        for (int i = 0; i < itemCount; i++)
        {
            Instantiate(itemPrefab, transform);
        }

        Debug.Log("Filhos agora: " + transform.childCount);
    }
}

Com o SetParent(null), o childCount do fim mostra só os itens novos. Para recriar os itens, o post Instantiate na Unity mostra as sobrecargas com pai.

Destruir e recriar objetos é uma peça pequena. Num jogo completo, o mesmo inimigo precisa morrer, soltar item, atualizar a pontuação, tocar som e avisar a fase, e cada um desses sistemas pode estar segurando uma referência ao objeto que acabou de sumir. Organizar isso num projeto inteiro é onde 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

Destroy vs DestroyImmediate

DestroyImmediate destrói na hora, sem esperar o fim do frame. Parece mais simples, mas a documentação do Unity 6.3 é direta: ele é feito para scripts que rodam em Edit Mode, não em tempo de jogo, e em código de jogo você deve usar Destroy.

Os motivos práticos:

  • Outros scripts podem estar no meio do próprio Update usando o objeto. Com Destroy, eles terminam o frame em paz. Com DestroyImmediate, o objeto some no meio e o próximo acesso estoura.
  • DestroyImmediate num asset (um prefab ou material do Project) com allowDestroyingAssets em true apaga o arquivo do projeto de forma permanente. A documentação avisa sobre isso.

Onde ele faz sentido: ferramentas de editor, como um botão de Inspector que limpa os filhos gerados por um gerador de fase. Fora do Play Mode, o Destroy normal nem funciona, e a Unity mostra um erro no Console pedindo DestroyImmediate.

O objeto ainda existe até o fim do frame

Essa é a parte que mais gera bug sutil. Veja:

using UnityEngine;

public class DestroyTiming : MonoBehaviour
{
    [SerializeField] private GameObject target;

    private void Update()
    {
        if (target == null)
        {
            return;
        }

        Destroy(target);
        Debug.Log(target == null);       // false: ainda existe neste frame
        Debug.Log(target.name);          // funciona normalmente
    }
}

Depois do Destroy, no mesmo frame, target == null é falso e você ainda acessa tudo nele. No frame seguinte, target == null é verdadeiro.

Isso acontece porque a Unity sobrescreve o operador == de UnityEngine.Object. A variável target continua apontando para um objeto C#, mas a parte nativa dele foi destruída, e o == da Unity checa justamente isso. É também por isso que os operadores ?. e ?? do C# não servem para objetos da Unity: a documentação diz que eles não enxergam o objeto destruído como nulo.

Consequências práticas:

  • Dano duplo. Dois tiros acertam o mesmo inimigo no mesmo frame, os dois chamam TakeDamage, a vida vai a zero no primeiro, Destroy é chamado duas vezes e o jogador ganha pontos duas vezes. A correção é uma flag: if (isDead) return; isDead = true;.
  • Lista com objeto morto. Se você guarda inimigos numa List, remova da lista no OnDestroy ou no momento da morte, não confie em checar null depois.
  • Destroy dentro de OnTriggerEnter2D. O objeto ainda recebe outros eventos de colisão no mesmo passo de física. Desligue o collider na hora (hitbox.enabled = false, com a referência ao Collider2D do objeto) se não quer mais eventos.

Quando usar SetActive ou pooling em vez de Destroy

Destruir não é a única forma de tirar algo de cena:

  • SetActive(false): esconde o objeto inteiro, para todos os scripts e colisões, e mantém tudo na memória. Volta com SetActive(true). É o certo para menu que abre e fecha, inimigo de checkpoint que reaparece, porta que some e volta. A própria documentação de DestroyImmediate sugere SetActive quando a intenção é só esconder.
  • Object pooling: para objetos que nascem e morrem muito (tiros, partículas, moedas de uma explosão), criar e destruir o tempo todo gera custo de CPU e lixo de memória. O pool desativa em vez de destruir e reaproveita depois. A Unity tem ObjectPool<T> pronto, e o conceito completo está em object pooling.
  • Destroy: objetos que não voltam, como o chefe derrotado, a moeda única da fase, o efeito que terminou.

Se o Profiler mostra picos de coleta de lixo durante combate, é bem provável que haja Instantiate e Destroy demais por segundo. O post otimização de performance para 60 FPS mostra como achar esse tipo de gargalo.

Próximo passo

Faça um teste: um inimigo com vida 1 e dois tiros que nascem no mesmo frame em cima dele. Conte quantas vezes o Debug.Log de morte aparece, corrija com a flag isDead e veja o número cair para um. Depois troque a destruição dos tiros por um pool e compare a aba de memória do Profiler com e sem. Esses dois exercícios cobrem os dois erros que mais aparecem em código de iniciante com Destroy.

Perguntas frequentes

Como destruir o próprio objeto na Unity?

Dentro do script, chame Destroy(gameObject). Cuidado com Destroy(this): isso remove só o componente do script e deixa o GameObject na cena. Para destruir depois de um tempo, passe o atraso em segundos: Destroy(gameObject, 2f).

Qual a diferença entre Destroy e DestroyImmediate?

Destroy agenda a destruição para depois do Update do frame atual e antes da renderização. DestroyImmediate destrói na hora. A documentação da Unity diz que DestroyImmediate é para scripts que rodam em Edit Mode e que código de jogo deve usar Destroy.

Como destruir todos os filhos de um objeto na Unity?

Percorra os filhos de trás para frente com for (int i = transform.childCount - 1; i >= 0; i--) e chame Destroy(transform.GetChild(i).gameObject). Esse laço funciona tanto com Destroy quanto com DestroyImmediate e não pula nenhum filho.

Destroy com atraso para quando o jogo está pausado?

Sim. O atraso do Destroy usa o tempo escalado, então com Time.timeScale em 0 a destruição espera o tempo voltar a correr. Se o objeto precisa sumir com o jogo pausado, chame Destroy sem atraso a partir de um timer com Time.unscaledDeltaTime.