Voltar para o Blog
Quest Log

Assembly Definition na Unity: Compilar Mais Rápido

Blocos de código separados em caixas conectadas por cabos, com um cronômetro marcando o tempo de compilação

Unity assembly definition (.asmdef) para recompilar menos ao salvar: divisão Runtime, Editor e Tests, referências entre assemblies e o erro CS0246.

Ferramenta:UnityC#

Assembly Definition na Unity: Compilar Mais Rápido

Assembly Definition na Unity é um arquivo .asmdef que separa uma pasta de scripts num assembly próprio, para que salvar um arquivo recompile só aquele pedaço e quem depende dele, e não o projeto inteiro. Se a barra de "compiling scripts" está comendo dez segundos a cada Ctrl+S, uma divisão mínima com unity assembly definition em Runtime, Editor e Tests é o primeiro ajuste a fazer. Este tutorial mostra como criar, como ligar as referências, como medir o ganho e como resolver o CS0246 que aparece logo depois.

Os nomes de menu e de campos são do Unity 6.3 LTS.

Por que a Unity demora para compilar

Sem nenhum .asmdef, todo script seu vai para um assembly predefinido chamado Assembly-CSharp (e os de pastas Editor vão para Assembly-CSharp-Editor). É um bloco único. Mudou uma linha no PlayerController? A Unity recompila o Assembly-CSharp inteiro, com todos os scripts do jogo, e todos os assemblies que dependem dele.

Depois da compilação vem o domain reload: a Unity descarrega e recarrega o código gerenciado no editor, o que reinicia estados estáticos e roda os inicializadores de editor. O tempo que você sente entre salvar e poder voltar a trabalhar é a soma dos dois.

Em projeto novo, isso é rápido. Com o crescimento do código, com pacotes da Asset Store que trazem centenas de scripts soltos em Assets e com ferramentas de editor, o tempo cresce. Esse é um custo que não aparece no Profiler do jogo, mas aparece em cada iteração sua. Otimizar o ciclo de trabalho é tão importante quanto otimizar a performance do jogo.

O que é Assembly Definition

Um assembly é a unidade de código compilado do .NET (uma DLL). Quando você cria um .asmdef numa pasta, todos os scripts daquela pasta e das subpastas, exceto as que tiverem o próprio .asmdef, viram um assembly com o nome que você escolher.

Três consequências práticas:

  1. Recompilação parcial. Mudou um script do assembly Game.UI? A Unity recompila Game.UI e os assemblies que referenciam Game.UI. O resto é reaproveitado.
  2. Dependências explícitas. Um assembly só enxerga o código dos assemblies que ele referencia. Isso força uma arquitetura em camadas e impede que a UI dependa do inimigo e o inimigo dependa da UI.
  3. Controle de plataforma. Dá para dizer que um assembly só existe no editor ou só em certas plataformas.

Criando o primeiro .asmdef

Parto de uma estrutura de pastas por área, como a sugerida no post sobre organizar o projeto (o princípio vale para qualquer engine):

Assets/_Project/Scripts/
  Core/        (utilitários, eventos, interfaces)
  Gameplay/    (player, inimigos, armas)
  UI/          (HUD, menus)
  Editor/      (ferramentas de editor)
  Tests/       (testes)

Na janela Project, entre em Scripts/Core, clique com o botão direito e escolha Create > Scripting > Assembly Definition. Dê o nome Game.Core. A Unity cria Game.Core.asmdef e recompila: agora todo script dentro de Core pertence a esse assembly.

Selecione o arquivo e veja o Inspector. Os campos que importam no começo:

  • Name: o nome do assembly (pode ser diferente do nome do arquivo, mas manter igual evita confusão).
  • Auto Referenced: quando ligado, os assemblies predefinidos (Assembly-CSharp) referenciam este automaticamente. Deixe ligado enquanto houver código seu fora de asmdef que precisa usar este.
  • Root Namespace: namespace sugerido para scripts novos criados nesta pasta.
  • Assembly Definition References: a lista de outros asmdefs que este pode usar.
  • Platforms: com Any Platform marcado, compila para todas, exceto as que você desmarcar; desmarcado, só para as que você marcar.
  • Define Constraints: o assembly só é compilado se todos os símbolos listados estiverem definidos.

Clique em Apply depois de mudar qualquer campo. Repita para Gameplay (Game.Gameplay) e UI (Game.UI).

Referências entre assemblies

Agora as dependências. Game.Gameplay usa interfaces e eventos de Game.Core. Game.UI mostra dados de Game.Gameplay e de Game.Core. Configure:

  • Game.Core: nenhuma referência a outros asmdefs seus.
  • Game.Gameplay: referencia Game.Core.
  • Game.UI: referencia Game.Core e Game.Gameplay.

Para adicionar, selecione o .asmdef, clique no + em Assembly Definition References, arraste o outro .asmdef para o campo e clique em Apply.

Pacotes também são assemblies. Se o Game.Gameplay usa o Input System, ele precisa referenciar Unity.InputSystem. Se Game.UI usa TextMeshPro, precisa de Unity.TextMeshPro. Código no Assembly-CSharp enxerga esses pacotes automaticamente; código num asmdef precisa de cada dependência como uma linha na lista. Pode parecer burocracia, mas é exatamente essa lista que mostra, de relance, do que cada parte do jogo depende.

O desenho resultante é uma pirâmide: Core na base, sem depender de ninguém; Gameplay no meio; UI no topo. Mudar algo na UI recompila só a UI. Mudar algo no Core recompila tudo, então o Core deve ser pequeno e estável.

Separar código em camadas que não se enroscam é uma habilidade de arquitetura que só aparece quando o projeto passa do tamanho de tutorial. É nessa fase, com o jogo de verdade crescendo e o editor ficando lento, que quem estuda sozinho mais costuma travar e reescrever tudo do zero.

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

Assembly para Editor e para testes

Editor. Scripts de editor (inspectors customizados, janelas, menus) usam UnityEditor, que não existe na build. Se a pasta Editor está dentro de uma pasta com asmdef de runtime, os scripts dela entram no assembly de runtime e a build falha. Resolva com um asmdef próprio: em Scripts/Editor, crie Game.Editor.asmdef, em Platforms desmarque Any Platform e marque só Editor, e adicione as referências aos assemblies de jogo que as ferramentas inspecionam.

Testes. Com o pacote Test Framework instalado, a janela Window > General > Test Runner cria pastas de teste com o asmdef já configurado (referenciando o NUnit e o runner de testes). Os testes ficam num assembly separado que referencia Game.Core e Game.Gameplay, e não entram na build. Um bônus: lógica pura no Game.Core, sem depender de cena, é a mais fácil de testar.

Medindo o ganho de tempo de compilação

Medir evita a sensação enganosa de "parece mais rápido". Este script de editor registra no Console quanto tempo cada compilação levou e quais assemblies foram recompilados. Arquivo CompileTimer.cs, dentro de Scripts/Editor:

using System.Diagnostics;
using UnityEditor;
using UnityEditor.Compilation;
using Debug = UnityEngine.Debug;

[InitializeOnLoad]
public static class CompileTimer
{
    private static Stopwatch stopwatch;

    static CompileTimer()
    {
        CompilationPipeline.compilationStarted += OnCompilationStarted;
        CompilationPipeline.assemblyCompilationFinished += OnAssemblyFinished;
        CompilationPipeline.compilationFinished += OnCompilationFinished;
    }

    private static void OnCompilationStarted(object context)
    {
        stopwatch = Stopwatch.StartNew();
    }

    private static void OnAssemblyFinished(string assemblyPath, CompilerMessage[] messages)
    {
        Debug.Log($"Compilado: {System.IO.Path.GetFileName(assemblyPath)}");
    }

    private static void OnCompilationFinished(object context)
    {
        if (stopwatch == null)
        {
            return;
        }

        stopwatch.Stop();
        Debug.Log($"Compilação total: {stopwatch.Elapsed.TotalSeconds:F2} s");
    }
}

O teste: antes de criar os asmdefs, mude uma linha num script de UI, salve e anote o tempo e a lista de assemblies. Depois da divisão, repita a mesma mudança. A lista deve encolher para Game.UI (e quem depende dele), e o tempo deve cair na proporção do código que deixou de ser recompilado. Não espero um número fixo: o ganho depende de quanto código seu e de terceiros estava no Assembly-CSharp.

O domain reload não entra nessa conta e pode ser a maior parte da espera. Para cortar a espera ao apertar Play, abra Edit > Project Settings > Editor, seção Enter Play Mode Settings, e no campo When entering Play Mode escolha Reload Scene only (desliga o domain reload ao entrar no Play Mode). A contrapartida: campos static e eventos estáticos não são zerados entre uma sessão de teste e outra, e o seu código precisa reiniciar esses valores por conta própria.

Erros comuns: CS0246 após criar asmdef e referência circular

CS0246: The type or namespace name 'X' could not be found. É o erro mais comum logo depois de criar um asmdef. Causas, em ordem de frequência:

  1. O tipo está noutro asmdef seu que não foi adicionado em Assembly Definition References. Adicione e clique em Apply.
  2. O tipo está num pacote (InputAction, TMP_Text, CinemachineCamera) e falta a referência ao assembly do pacote (Unity.InputSystem, Unity.TextMeshPro, Unity.Cinemachine).
  3. O tipo ficou no Assembly-CSharp, fora de qualquer asmdef. Asmdef não pode referenciar o assembly predefinido. Mova o script para um asmdef ou mova a dependência para baixo na pirâmide.

O post sobre o erro CS0246 na Unity cobre as outras origens desse erro, como using faltando e pacote não instalado.

Referência circular. Game.Gameplay referencia Game.UI, e Game.UI referencia Game.Gameplay. A Unity recusa e acusa dependência cíclica. Isso é um sinal de arquitetura: duas camadas que se conhecem mutuamente. A solução clássica é mover o que as duas compartilham (uma interface, um evento) para Game.Core e fazer a camada de baixo avisar a de cima por evento, sem referência direta.

Build quebrando por script de editor. Um script com using UnityEditor; caiu num asmdef de runtime. Mova para o asmdef de Editor ou envolva o trecho em #if UNITY_EDITOR.

Granularidade exagerada. Um asmdef por pasta pequena gera dezenas de assemblies, cada um com sua lista de referências para manter. Comece com três ou quatro e divida só onde a medição mostrar ganho.

Próximo passo

Instale o CompileTimer no seu projeto atual, meça três salvamentos típicos e anote os tempos. Depois crie só dois asmdefs (Game.Core e Game.Gameplay), corrija os CS0246 que aparecerem e meça de novo. Com os números na mão você decide se vale dividir mais.

Perguntas frequentes

O que é asmdef na Unity?

É um arquivo Assembly Definition (.asmdef) que transforma os scripts da pasta onde ele está, e das subpastas, num assembly separado (uma DLL própria). Assim a Unity recompila só os assemblies afetados por uma mudança, em vez de todo o código do projeto.

Assembly Definition deixa o jogo mais rápido?

Não o jogo em si. O ganho é no editor: menos código recompilado a cada vez que você salva um script. Em projetos pequenos a diferença é pequena; ela cresce com o volume de código e de pacotes.

Por que meus scripts pararam de achar outras classes depois do asmdef?

Porque um assembly só enxerga os assemblies que ele referencia. Se o script foi para um asmdef e a classe que ele usa ficou no Assembly-CSharp ou em outro asmdef não referenciado, aparece o erro CS0246. Adicione a referência em Assembly Definition References ou mova a classe para um assembly que possa ser referenciado.

Posso referenciar o Assembly-CSharp a partir de um asmdef?

Não. Os assemblies predefinidos (Assembly-CSharp e Assembly-CSharp-Editor) referenciam os seus asmdefs, mas não podem ser referenciados por eles. Por isso a migração costuma começar pelo código mais básico, de que os outros dependem.