Apostila de C# para Unity
Quem sabe C# e começa em Unity descobre, aos poucos, que várias coisas não funcionam como esperava: construtores que não se usam, propriedades que o Inspector ignora, um null que não é bem null, ?. que falha, alocações que fazem o jogo engasgar. Quem aprendeu C# dentro do Unity costuma ter o problema inverso. Esta apostila é a ponte: o C# que o Unity usa de maneira própria, e porquê.
A linguagem em si — tipos, orientação a objetos, genéricos, LINQ, async — está em C# do Básico ao Avançado. O editor e os conceitos de Unity estão em Unity Essencial e Unity do zero ao mercado. A arquitetura de projetos maiores está em Unity para Aplicações. Aqui, só o que muda quando o C# roda dentro do Unity. Referência: Unity 6.
O C# do Unity não é o C# do .NET atual
Objetivo: saber que versão da linguagem e que bibliotecas estão disponíveis, e por que o ambiente de execução é diferente do de uma aplicação .NET comum.
1.1 Linguagem, bibliotecas e runtime
| Aplicação .NET moderna | Unity 6 | |
|---|---|---|
| Versão da linguagem | A mais recente | C# 9, com algumas exceções documentadas |
| Bibliotecas base | .NET atual | .NET Standard 2.1 ou .NET Framework (perfil de compatibilidade escolhido nas Player Settings) |
| Runtime no editor | CoreCLR | Mono |
| Runtime nos builds | CoreCLR (JIT) | Mono ou IL2CPP (C# convertido em C++ e compilado antes — módulo 9) |
| Garbage collector | Geracional, compactador | Não geracional, com modo incremental (módulo 7) |
A Unity anunciou a migração para o runtime moderno do .NET (CoreCLR); até lá, confira sempre se um recurso da linguagem ou uma biblioteca NuGet funciona no Unity antes de construir em cima dele. Alguns recursos do C# 9 que dependem de tipos do runtime novo (como init e record) exigem declarar um tipo auxiliar ou não funcionam como esperado.
1.2 O que mais surpreende quem vem do .NET
- Não há
Main. O motor controla o laço principal e chama os seus componentes (módulo 2). - Não se usam construtores em MonoBehaviour. O motor cria os componentes; a inicialização vai para
Awake/Start. - Quase toda a API do motor só funciona na thread principal (módulo 5).
- Alocar é caro de uma forma que num servidor web não seria — o GC pode causar engasgos visíveis (módulo 7).
- Objetos do motor têm dois lados: um objeto C# e um objeto nativo em C++. Daí o "null falso" (módulo 4).
Entrevistas para vagas de Unity quase sempre incluem perguntas sobre o que é específico do motor: ciclo de vida, serialização, null falso, GC, corrotinas. Saber C# "do .NET" não basta, e é exatamente aí que muitos candidatos vindos de back-end tropeçam. O módulo 10 reúne as perguntas mais comuns.
✏️ Exercício 1 — Porta a biblioteca?
Você quer usar no Unity uma biblioteca NuGet que depende de System.Text.Json e de reflexão para gerar código em tempo de execução. Que dois riscos verifica primeiro?
Gabarito: (1) Compatibilidade: se a biblioteca e as suas dependências têm uma versão para .NET Standard 2.1 compatível com o perfil do projeto. (2) IL2CPP: bibliotecas que geram código em tempo de execução (Reflection.Emit, expressões compiladas) não funcionam em plataformas AOT como iOS, consolas e WebGL (módulo 9). É por isso que em Unity se usa tanto o pacote Newtonsoft JSON da própria Unity.
MonoBehaviour e o ciclo de vida
Objetivo: saber quando o motor chama cada método, em que ordem, e onde pôr cada tipo de código.
2.1 Os métodos principais
| Método | Quando | Para quê |
|---|---|---|
Awake | Uma vez, quando o objeto é carregado (mesmo com o componente desativado) | Inicializar o próprio estado e obter referências internas |
OnEnable | Sempre que o componente é ativado | Inscrever-se em eventos |
Start | Uma vez, antes do primeiro Update, se ativo | Inicialização que depende de outros objetos já terem feito o Awake |
FixedUpdate | Em passos fixos de física (0 a várias vezes por quadro) | Física: forças, Rigidbody |
Update | Uma vez por quadro | Input, lógica de jogo |
LateUpdate | Depois de todos os Update | Câmaras que seguem objetos |
OnDisable | Sempre que é desativado ou destruído | Desinscrever-se de eventos |
OnDestroy | Quando o objeto é destruído | Libertar recursos |
2.2 A regra de ouro da inicialização
Em Awake, prepare-se a si próprio; em Start, fale com os outros. A ordem de Awake entre objetos diferentes não é garantida. Se o objeto A lê em Awake um valor que B só define no seu Awake, o resultado depende da sorte — e muda entre o editor e o build. A ordem pode ser forçada em Script Execution Order, mas é melhor não depender dela.
2.3 Tempo
void Update() { // independente do frame rate: unidades por segundo × segundos deste quadro transform.position += direcao * velocidade * Time.deltaTime; } void FixedUpdate() { // física: dentro do passo fixo; não multiplique forças por deltaTime corpo.AddForce(empurrao); }
Ler "o botão foi carregado neste quadro" em FixedUpdate perde toques: há quadros sem nenhum passo de física e quadros com vários. Leia o input em Update, guarde a intenção num campo, e aplique-a em FixedUpdate.
✏️ Exercício 2 — Onde vai cada linha?
Distribua pelos métodos: (a) rb = GetComponent<Rigidbody>(); (b) GameManager.Instance.Registrar(this); (c) if (Input.GetKeyDown(KeyCode.Space)) querPular = true;; (d) rb.AddForce(Vector3.up * forca) quando querPular; (e) jogador.Morreu += AoMorrer e o seu oposto.
Gabarito: (a) Awake — referência interna. (b) Start — depende de outro objeto já inicializado. (c) Update. (d) FixedUpdate, voltando querPular a falso. (e) Inscrever em OnEnable, desinscrever em OnDisable.
Serialização: o que o Inspector guarda
Objetivo: saber exatamente que campos o Unity guarda nas cenas e prefabs, porque é que uma propriedade não aparece no Inspector, e como não perder dados ao mudar o código.
3.1 As regras
O Unity guarda (serializa) um campo se ele for público ou marcado com [SerializeField], não estático, não const, não readonly, e de um tipo serializável: tipos primitivos, string, enums, tipos do Unity (Vector3, Color, AnimationCurve…), referências a UnityEngine.Object (GameObject, componentes, assets), classes e structs marcadas com [Serializable], e arrays ou List<T> desses tipos.
Não são serializados: propriedades, dicionários, arrays multidimensionais, listas de listas, campos estáticos, e referências a interfaces ou classes abstratas (a não ser com [SerializeReference]).
3.2 Três armadilhas
- O valor do Inspector vence o inicializador. Mudar
public float velocidade = 5f;para= 8fno código não altera objetos já existentes: eles guardaram 5 na cena. Só objetos novos recebem o valor novo. - Renomear um campo perde os dados. O Unity guarda pelo nome. Use
[FormerlySerializedAs("nomeAntigo")]ao renomear campos que já têm dados em cenas e prefabs. - Público só para serializar é mau encapsulamento. Prefira
[SerializeField] private: o Inspector vê, o resto do código não.
public class Inimigo : MonoBehaviour { [SerializeField] private float velocidade = 5f; // no Inspector, privado para o código [field: SerializeField] public int Vida { get; private set; } // serializa o campo por trás da propriedade [FormerlySerializedAs("dano")] [SerializeField] private int danoPorGolpe; // renomeado sem perder os valores }
✏️ Exercício 3 — O inventário que não aparece
Um programador escreve public Dictionary<string, int> inventario; e o campo não aparece no Inspector. Proponha duas soluções.
Gabarito: (1) Uma lista de pares serializáveis — [Serializable] struct Item { public string id; public int qtd; } e List<Item> — convertida para dicionário em Awake se precisar de busca rápida. (2) Implementar ISerializationCallbackReceiver, guardando em duas listas antes de serializar e reconstruindo o dicionário depois de desserializar.
O "null falso" e a vida dos objetos
Objetivo: entender por que um objeto destruído "é null" para o == e não é para o ?., e evitar os bugs que isso causa.
4.1 Dois objetos em um
Cada GameObject, componente ou asset tem um lado C# (o objeto gerenciado) e um lado nativo (em C++, dentro do motor). Quando se chama Destroy, o lado nativo desaparece, mas o objeto C# continua a existir enquanto houver referências a ele. Para esconder isso, UnityEngine.Object sobrecarrega o operador ==: um objeto destruído compara como igual a null, embora a referência C# não seja nula.
4.2 Onde isso morde
| Código | Com objeto destruído |
|---|---|
if (alvo == null) | ✓ verdadeiro — usa a sobrecarga do Unity |
if (alvo) | ✓ falso — a conversão para bool também usa o Unity |
alvo?.Atacar() | ✗ chama o método num objeto destruído — o ?. não usa a sobrecarga |
var x = alvo ?? reserva; | ✗ fica com o objeto destruído — o ?? também não usa |
if (alvo is null) | ✗ falso — o is testa a referência C# pura |
Para qualquer tipo que herda de UnityEngine.Object, não use ?., ??, ??= nem is null. Use == null, != null ou if (obj). Os analisadores de código do Unity avisam sobre isto — mantenha-os ativos. Para tipos C# comuns (os seus modelos de domínio), os operadores funcionam normalmente.
4.3 Destroy não é imediato
Destroy(obj) marca o objeto e só o destrói no fim do quadro. No resto do quadro atual, ele ainda existe e responde. Código que destrói e, na linha seguinte, conta os objetos na cena, conta também o destruído. DestroyImmediate é para scripts de editor, não para o jogo.
4.4 Procurar componentes
// TryGetComponent: não aloca quando o componente não existe (GetComponent aloca no editor) if (outro.TryGetComponent(out Vida vida)) vida.ReceberDano(10); // guarde as referências; não procure em cada Update Rigidbody _rb; void Awake() => _rb = GetComponent<Rigidbody>();
✏️ Exercício 4 — O míssil teimoso
Um míssil guarda uma referência ao alvo e, em Update, faz transform.LookAt(alvo?.transform.position ?? Vector3.zero). Quando o alvo é destruído, surge MissingReferenceException. Porquê, e como corrigir?
Gabarito: O ?. não vê o objeto destruído como nulo; tenta ler .transform num componente cujo lado nativo já não existe. Correção: if (alvo == null) { Explodir(); return; } transform.LookAt(alvo.position); — decidindo explicitamente o que fazer quando o alvo desaparece.
Corrotinas, async e Awaitable
Objetivo: saber escrever código que se espalha por vários quadros ou espera por algo, escolhendo entre corrotinas e async, sem deixar operações órfãs.
5.1 Corrotinas
IEnumerator Piscar(int vezes) { for (int i = 0; i < vezes; i++) { render.enabled = !render.enabled; yield return new WaitForSeconds(0.1f); // continua no quadro certo, na thread principal } render.enabled = true; } // StartCoroutine(Piscar(6));
- Correm na thread principal — não são paralelismo, são uma forma de espalhar trabalho pelo tempo.
- Param quando o GameObject é desativado ou destruído — o que é quase sempre o que se quer.
- Não devolvem valores nem tratam exceções com facilidade.
new WaitForSecondsaloca; num laço muito repetido, guarde-o num campo.
5.2 async/await com Awaitable
No Unity 6, o tipo Awaitable é a forma recomendada de usar async no motor: é integrado ao laço do Unity, tem métodos para esperar o próximo quadro, segundos, o próximo passo de física, e para mudar de thread.
async Awaitable AbrirPortaAsync() { var ct = destroyCancellationToken; // cancela se o objeto for destruído animador.SetTrigger("abrir"); await Awaitable.WaitForSecondsAsync(1.2f, ct); colisor.enabled = false; await Awaitable.BackgroundThreadAsync(); // trabalho pesado fora da thread principal var caminho = CalcularRota(mapa); // só C# puro aqui, nada de API do Unity await Awaitable.MainThreadAsync(); // volta antes de tocar na cena guia.Mostrar(caminho); }
Um async Task comum não para quando se sai do Play Mode no editor nem quando o objeto é destruído — continua a correr e a tocar em objetos que já não existem. Passe sempre um CancellationToken (o destroyCancellationToken do componente, ou o Application.exitCancellationToken). A biblioteca comunitária UniTask é outra opção popular, com zero alocações.
5.3 Qual usar
| Situação | Escolha |
|---|---|
| Sequência visual curta ligada a um objeto (piscar, esperar uma animação) | Corrotina — para sozinha com o objeto |
| Esperar um resultado (rede, carregamento) e usá-lo | async Awaitable com cancelamento |
| Cálculo pesado sem API do motor | Awaitable.BackgroundThreadAsync — ou Jobs, se for muito (módulo 8) |
✏️ Exercício 5 — A requisição órfã
Um painel faz uma requisição de 3 segundos em async void Start() e, no fim, escreve o resultado num texto. Se o jogador fecha o painel antes, aparece um erro. Corrija.
Gabarito: Trocar async void por async Awaitable (exceções em async void são difíceis de apanhar), passar destroyCancellationToken à espera/requisição, e tratar OperationCanceledException em silêncio. Assim, fechar o painel cancela a continuação em vez de a deixar escrever num texto destruído.
Eventos e comunicação entre objetos
Objetivo: escolher a forma certa de um objeto avisar outros, sem acoplar tudo a tudo nem vazar memória.
6.1 As opções
| Mecanismo | Vantagem | Custo / risco |
|---|---|---|
Evento C# (event Action<T>) | Rápido, tipado, claro no código | Não aparece no Inspector; esquecer de desinscrever vaza e causa erros |
| UnityEvent | Ligável no Inspector por designers, sem código | Mais lento, ligações invisíveis no código (difícil de rastrear) |
| Canal em ScriptableObject | Desacopla emissor e ouvintes, que só conhecem o asset do canal | Mais assets para gerir; fluxo menos explícito |
| Interface + TryGetComponent | Ótimo para interações pontuais (IDanificavel) | Exige uma referência ao objeto |
SendMessage | — | Por nome de método em string, lento, sem verificação — evitar |
6.2 O par OnEnable / OnDisable
void OnEnable() => jogador.VidaMudou += AtualizarBarra; void OnDisable() => jogador.VidaMudou -= AtualizarBarra;
Inscrever em OnEnable e desinscrever em OnDisable cobre ativação, desativação e destruição. Um evento que continua a apontar para um componente destruído é uma das fontes mais comuns de MissingReferenceException intermitente e de vazamento de memória.
✏️ Exercício 6 — Escolha o mecanismo
(a) Um botão de UI que um designer quer ligar a várias ações sem programador; (b) a vida do jogador que atualiza a barra, o som e a câmara; (c) um projétil que causa dano em qualquer coisa que o possa receber.
Gabarito: (a) UnityEvent (o próprio Button.onClick já é um). (b) Evento C# no componente de vida, com os três ouvintes a inscrever-se em OnEnable. (c) Interface IDanificavel e TryGetComponent no objeto atingido.
Memória, alocações e o GC
Objetivo: entender por que alocar memória por quadro causa engasgos, reconhecer as alocações escondidas mais comuns, e eliminá-las.
7.1 Por que o GC incomoda mais no Unity
O coletor de lixo do Unity não é geracional: quando corre, examina todo o heap gerenciado. Com o GC incremental (ativo por padrão nas versões recentes), o trabalho é dividido por vários quadros, o que reduz os picos — mas não elimina o custo. A regra de ouro continua: no código que corre a cada quadro, alocar zero.
7.2 Alocações escondidas
| Código | Aloca porque… | Alternativa |
|---|---|---|
texto.text = "Vida: " + vida; em Update | Cria uma string nova a cada quadro | Atualizar só quando a vida muda |
LINQ em Update (inimigos.Where(...).ToList()) | Enumeradores, delegates e a lista | Laço for numa lista reutilizada |
| Lambda que captura variáveis locais | Cria um objeto de fecho (closure) | Método normal ou lambda sem capturas |
Passar um struct como object ou interface | Boxing | Genéricos |
Physics.RaycastAll, GetComponents | Devolvem arrays novos | Versões NonAlloc / que recebem uma lista |
Instantiate/Destroy de balas, partículas | Criação e destruição constantes | Pooling |
7.3 Pooling
using UnityEngine.Pool; ObjectPool<Bala> _balas; void Awake() => _balas = new ObjectPool<Bala>( createFunc: () => Instantiate(prefab), actionOnGet: b => b.gameObject.SetActive(true), actionOnRelease: b => b.gameObject.SetActive(false), defaultCapacity: 32); // disparar: var b = _balas.Get(); ao acertar: _balas.Release(b);
Para medir, use a coluna GC Alloc do Profiler (em build de desenvolvimento no aparelho): o objetivo é 0 B por quadro durante a jogabilidade. Performance em aparelhos móveis e XR está em Meta Quest 3, módulos 6 e 7.
✏️ Exercício 7 — Caça às alocações
Encontre as quatro alocações neste Update: var perto = FindObjectsOfType<Inimigo>().Where(e => Vector3.Distance(e.transform.position, transform.position) < raio).ToList(); placar.text = "Perto: " + perto.Count;
Gabarito: (1) FindObjectsOfType devolve um array novo (e é lento — mantenha uma lista registada de inimigos). (2) A lambda captura this/raio → fecho. (3) Where/ToList → enumerador e lista nova. (4) Concatenação de string. Reescrita: laço for sobre uma lista estática de inimigos, contando num int, comparando sqrMagnitude com raio*raio, e atualizar o texto só quando a contagem muda.
C# de alto desempenho: Jobs e Burst
Objetivo: conhecer o sistema de Jobs e o compilador Burst, o subconjunto de C# que eles aceitam, e quando o esforço compensa.
8.1 O problema e a resposta
O código de jogo típico corre numa só thread, com objetos espalhados pela memória. Para processar dezenas de milhares de elementos (partículas próprias, multidões, simulações, geração procedural), a Unity oferece duas peças:
- C# Job System: divide o trabalho por vários núcleos de forma segura, com verificação de concorrência (o motor avisa se dois jobs escrevem nos mesmos dados).
- Burst: compila jobs para código nativo altamente otimizado (com vetorização SIMD) — frequentemente muitas vezes mais rápido do que o mesmo C# em Mono.
8.2 Um job em miniatura
using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; [BurstCompile] struct MoverJob : IJobParallelFor { public NativeArray<float3> posicoes; [ReadOnly] public NativeArray<float3> velocidades; public float dt; public void Execute(int i) => posicoes[i] += velocidades[i] * dt; } // agendar: var h = new MoverJob { posicoes = p, velocidades = v, dt = Time.deltaTime }.Schedule(p.Length, 64); // h.Complete(); // antes de ler os resultados // NativeArray não é gerido pelo GC: faça Dispose() quando não precisar mais.
8.3 O C# que o Burst aceita
Burst compila um subconjunto chamado HPC# (High Performance C#): structs e tipos não gerenciados, NativeArray e outras coleções nativas, e o pacote Mathematics. Não aceita classes, strings gerenciadas, referências a GameObject ou componentes, nem exceções no caminho normal. É outro estilo de programar — orientado a dados.
8.4 ECS / DOTS
O pacote Entities (a parte "ECS" da pilha orientada a dados, DOTS) leva a ideia ao extremo: entidades sem GameObject, componentes como dados puros, sistemas que processam tudo em lote. Compensa em jogos com enormes quantidades de elementos; para a maioria dos projetos, Jobs + Burst em pontos quentes específicos dão a maior parte do ganho com muito menos mudança.
✏️ Exercício 8 — Vale a pena?
Para cada caso, diga se Jobs/Burst compensam: (a) um menu com 20 botões; (b) 30 000 peixes num cardume com regras de vizinhança; (c) a IA de 5 inimigos com árvores de decisão complexas; (d) gerar um terreno procedural de 1024×1024 ao carregar o nível.
Gabarito: (a) Não. (b) Sim — é o caso clássico: muitos elementos, dados simples, a mesma operação. (c) Normalmente não — poucos elementos e lógica cheia de ramificações e referências. (d) Sim — cálculo massivo e independente por ponto, ideal para IJobParallelFor com Burst.
IL2CPP, stripping, editor e domain reload
Objetivo: entender o que acontece ao seu C# no build e no editor, e os bugs que só aparecem num dos dois.
9.1 IL2CPP e AOT
Com IL2CPP, o código C# é convertido em C++ e compilado antes da execução (AOT). É obrigatório em plataformas como iOS, WebGL e consolas, e costuma ser mais rápido. Consequência: não há geração de código em tempo de execução. Reflection.Emit e expressões compiladas não funcionam, e alguns usos de genéricos só descobertos em execução podem falhar se o compilador não os tiver previsto.
9.2 Managed code stripping
Para reduzir o tamanho do build, o Unity remove código que parece não ser usado. Código chamado só por reflexão (serializadores, injeção de dependências, alguns plugins) parece não usado — e desaparece. O sintoma: funciona no editor, falha no build com erros de tipo ou método em falta. Soluções: [Preserve] nos tipos e métodos, ou um arquivo link.xml listando o que manter, e um nível de stripping menos agressivo enquanto se investiga.
9.3 Código de editor
- Código que usa
UnityEditornão existe no build. Coloque-o em pastasEditor, num Assembly Definition só para editor, ou entre#if UNITY_EDITOR. - Ferramentas úteis:
[MenuItem]para comandos, inspetores personalizados, validações comOnValidate,[ContextMenu]para ações rápidas num componente. - Assembly Definitions separam o código em módulos: compilação mais rápida e dependências explícitas (ver Unity para Aplicações, módulo 2).
9.4 Domain reload e campos estáticos
Ao entrar no Play Mode, o Unity recarrega por padrão todo o código (domain reload), o que repõe os campos estáticos. Muitas equipas desativam esse recarregamento (Enter Play Mode Options) para entrar em Play em segundos em vez de dezenas de segundos. Consequência: campos estáticos e eventos estáticos mantêm os valores da sessão anterior. Um singleton que "já existe" ou uma contagem que começa em 37 são os sintomas. A solução é repor o estado explicitamente:
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Repor() { _instancia = null; TotalInimigos = 0; }
✏️ Exercício 9 — Funciona no editor
Um sistema de save usa uma biblioteca que encontra, por reflexão, todas as classes que implementam ISalvavel. No editor tudo funciona; no build Android com IL2CPP, nada é salvo e não há erro. Hipótese e correção?
Gabarito: Stripping: as classes ISalvavel só são referenciadas por reflexão, e o linker removeu-as (ou removeu os seus construtores). Correção: marcar as classes com [Preserve] ou incluí-las num link.xml; melhor ainda, registar as classes explicitamente num ponto do código em vez de depender de descoberta por reflexão.
Entrevistas, portfólio e fontes
Objetivo: preparar as perguntas técnicas mais comuns sobre C# em Unity e mostrar essas competências num portfólio.
10.1 Perguntas frequentes
"Qual a diferença entre Awake e Start?"
Awake corre quando o objeto é carregado, mesmo com o componente desativado, e serve para inicializar o próprio estado; Start corre antes do primeiro Update, só se ativo, e é onde é seguro depender de outros objetos. A ordem de Awake entre objetos não é garantida.
"Por que não usar ?. com componentes?"
Porque UnityEngine.Object sobrecarrega == para objetos destruídos, mas ?. e ?? não usam essa sobrecarga: um objeto destruído passa pelo ?. e causa MissingReferenceException.
"O jogo engasga a cada poucos segundos. O que investiga?"
Coleta de lixo: Profiler no aparelho, coluna GC Alloc e marcador GC.Collect. Procuro alocações por quadro — strings, LINQ, closures, boxing, Instantiate/Destroy constantes — e corrijo com cache, laços simples, NonAlloc e pooling.
"Corrotina ou async?"
Corrotina para sequências curtas ligadas a um objeto (param com ele); async Awaitable para esperar resultados, sempre com CancellationToken. Nenhuma das duas é paralelismo — para isso, trabalho em background sem API do motor, ou Jobs.
"Funciona no editor mas não no build. Por onde começa?"
Ordem de Awake diferente, stripping de código usado só por reflexão, código de editor que não existe no build, geração de código em runtime em IL2CPP, e estado estático preso com domain reload desativado (esse é o inverso: afeta o editor).
10.2 Portfólio
- Um projeto com código limpo e comentado no GitHub:
[SerializeField] private, eventos desinscritos, sem alocações por quadro (com captura do Profiler a mostrar 0 B). - Uma demonstração de Jobs + Burst: a mesma simulação em MonoBehaviour e em jobs, com os tempos medidos.
- Uma ferramenta de editor que poupa trabalho real (validador de cenas, gerador de níveis) — mostra que você sabe estender o motor.
- Testes automatizados com o Unity Test Framework na lógica do jogo.
10.3 Fontes para continuar
- Manual do Unity: ordem de execução das funções de eventos; regras de serialização; Awaitable; Job System; Burst; managed code stripping; configurar o Enter Play Mode.
- E-books gratuitos da Unity sobre padrões de programação em C# para Unity e sobre otimização de performance.
- Blog técnico da Unity: os textos sobre o comportamento do
==emUnityEngine.Objecte sobre o GC incremental. - Na Uniana: C#, Unity Essencial, Unity, Unity para Aplicações, Meta Quest 3.
Quatro ideias sustentam o C# no Unity: (1) o motor manda no tempo — ciclo de vida, quadros e passos de física decidem quando o seu código corre; (2) o Inspector tem regras próprias — serialização por campos, não por propriedades, e o valor guardado vence o do código; (3) objetos do motor têm dois lados — use == null, não ?., e lembre-se de que Destroy espera pelo fim do quadro; (4) memória é desempenho — zero alocações por quadro, pooling, e Jobs/Burst onde o volume justifica. E teste no build, não só no editor: IL2CPP, stripping e ordem de inicialização só se revelam lá.