C# para Unity: o Essencial para Começar a Programar Jogos
C# para Unity é um recorte pequeno de uma linguagem grande: no primeiro mês de projeto você usa tipos, condições, loops, métodos, classes, listas, null e eventos, e quase nada além disso. Este guia mostra cada um desses pedaços dentro de um script de jogo de verdade, um MonoBehaviour que você cola na Unity 6.3 LTS e roda, e diz com todas as letras o que pode ficar para depois.
Se você ainda está decidindo se a Unity é a engine certa ou qual linguagem ela usa, o post Qual linguagem a Unity usa resolve essa parte. Aqui a pergunta é outra: o que estudar de C# para conseguir escrever o código do seu primeiro jogo sem travar a cada linha.
C# para Unity: quanto da linguagem você precisa
C# é usado em backend, aplicativo de desktop, ferramenta corporativa e jogo. Livro de C# cobre tudo isso, e é por isso que tanta gente desiste no capítulo de interfaces genéricas sem nunca ter movido um personagem. Script de jogo usa uma fatia bem menor.
A Unity 6.3 LTS compila seus scripts com o compilador Roslyn e suporta C# 9.0, segundo o manual oficial (página "C# compiler", conferida em outubro de 2026). Algumas partes do C# 9 não funcionam na engine, como init-only setters e covariant return types, e o manual avisa que records não funcionam com a serialização da Unity. Nada disso aparece no código de quem está começando.
O runtime dos scripts é o Mono. A Unity vem publicando atualizações sobre a migração para o CoreCLR (o runtime moderno do .NET), com o alvo declarado em fórum oficial de um Editor completo em CoreCLR no Unity 6.8. Para você, hoje, isso não muda uma linha do que está neste post: tipos, if, for, classes e listas são iguais em qualquer runtime.
A lista do primeiro mês:
- variáveis e tipos (
int,float,bool,string,Vector2,Vector3); if,else,switch,foreforeach;- métodos com parâmetro e retorno;
- classes, objetos e o
MonoBehaviour; List<T>, arrays eDictionary<TKey, TValue>;nulle referências;- eventos com
Action.
Variáveis e tipos que você mais vai usar
Num jogo, quase toda variável representa uma coisa que o jogador vê ou sente: vida, velocidade, nome, se está no chão. Arquivo PlayerStats.cs:
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
[SerializeField] private int maxHealth = 5;
[SerializeField] private float moveSpeed = 4.5f;
[SerializeField] private string playerName = "Aventureira";
private int currentHealth;
private bool isGrounded;
private Vector2 spawnPoint;
private void Awake()
{
currentHealth = maxHealth;
spawnPoint = transform.position;
Debug.Log(playerName + " começa com " + currentHealth + " de vida.");
}
}
Três detalhes que pegam iniciante. O f no fim de 4.5f é obrigatório: sem ele o C# lê 4.5 como double e não deixa guardar num float. int não tem casa decimal, então 7 / 2 dá 3, não 3.5. E Vector2/Vector3 são tipos da Unity (struct), não do C#: eles vêm do using UnityEngine;.
O [SerializeField] private faz o campo aparecer no Inspector sem abrir ele para qualquer outro script mexer. Esse hábito evita muito bug: se outro script precisar ler o valor, crie um método ou uma propriedade só de leitura.
Condições e loops em código de jogo
if é a forma mais comum de decisão do jogo: tomou dano, está morto, pode pular. Loop aparece quando você percorre uma coleção: todos os inimigos, todos os itens do inventário, todas as moedas.
Arquivo Health.cs:
using UnityEngine;
public class Health : MonoBehaviour
{
[SerializeField] private int maxHealth = 5;
private int currentHealth;
private void Awake()
{
currentHealth = maxHealth;
}
public void TakeDamage(int amount)
{
if (amount <= 0)
{
return;
}
currentHealth -= amount;
if (currentHealth <= 0)
{
currentHealth = 0;
Debug.Log(name + " morreu.");
}
else if (currentHealth == 1)
{
Debug.Log(name + " está por um fio.");
}
}
public int CountAliveEnemies(Health[] enemies)
{
int alive = 0;
foreach (Health enemy in enemies)
{
if (enemy != null && enemy.currentHealth > 0)
{
alive++;
}
}
return alive;
}
}
O return logo no começo de TakeDamage é um padrão que você vai usar o tempo todo: sai do método cedo quando a entrada não faz sentido, e o resto do código fica sem um if gigante em volta. foreach serve para ler a coleção; quando você precisa do índice (para remover item ou comparar com o anterior), use for (int i = 0; i < enemies.Length; i++).
Um cuidado que não existe em livro de C#: loop dentro de Update roda a cada frame. Percorrer 10 inimigos é nada; percorrer 5 mil objetos com GetComponent dentro do loop, 60 vezes por segundo, pesa.
Métodos e parâmetros
Método é um bloco de código com nome. Na Unity você escreve dois tipos: os que a engine chama sozinha (Awake, Start, Update, OnTriggerEnter2D) e os seus, chamados por você ou por outro script, como o TakeDamage acima.
A regra prática: se você escreveu o mesmo trecho duas vezes, ele vira método. Se o método passa de uns 30 linhas, provavelmente faz duas coisas e pode ser dividido. Arquivo Coin.cs:
using UnityEngine;
public class Coin : MonoBehaviour
{
[SerializeField] private int value = 1;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
int total = AddToScore(value);
Debug.Log("Pontuação: " + total);
Destroy(gameObject);
}
}
private int AddToScore(int amount)
{
int current = PlayerPrefs.GetInt("score", 0);
int updated = current + amount;
PlayerPrefs.SetInt("score", updated);
return updated;
}
}
AddToScore recebe um int e devolve um int. Quando o método não devolve nada, o tipo de retorno é void. Os nomes dos métodos da Unity precisam estar escritos exatamente assim: OnTriggerEnter2D com "T" maiúsculo e o "2D" no fim. Um erro de digitação não dá erro de compilação, o método só nunca é chamado.
Classes, objetos e MonoBehaviour
Toda vez que você cria um script na Unity, cria uma classe. Quando arrasta esse script para um GameObject, a Unity cria um objeto (uma instância) daquela classe, preso àquele GameObject. Dez inimigos com o script Enemy são dez objetos Enemy, cada um com sua própria vida.
Duas regras que a Unity cobra:
- O nome da classe precisa ser igual ao nome do arquivo.
Enemy.cscontémpublic class Enemy : MonoBehaviour. Se não bater, o componente não pode ser adicionado ao objeto. - Classe que herda de
MonoBehaviournão é criada comnew. Você adiciona pelo Inspector, comAddComponent<T>()ou instanciando um prefab.
Classes que não herdam de MonoBehaviour também têm lugar: dados puros, como um item de inventário. Essas você cria com new normalmente. Arquivo InventoryItem.cs:
using System;
[Serializable]
public class InventoryItem
{
public string id;
public int amount;
public InventoryItem(string id, int amount)
{
this.id = id;
this.amount = amount;
}
}
O [Serializable] permite que uma lista dessas classes apareça no Inspector de quem a usa. Aqui os campos são públicos de propósito: é um pacote de dados, sem comportamento para proteger.
Até aqui, cada conceito cabe num script. Quando o jogo cresce, a parte difícil deixa de ser a sintaxe: é fazer input, física, animação, câmera e interface conversarem sem que um script quebre o outro. É aí que quem estuda sozinho costuma parar, com cinco scripts que funcionam isolados e um jogo que não fecha.
List, arrays e Dictionary
Array tem tamanho fixo e é bom para coisas que não mudam: os pontos de patrulha de um inimigo, os sprites de uma animação. List<T> cresce e encolhe: inimigos vivos, itens coletados. Dictionary<TKey, TValue> busca por chave: quantidade de cada item pelo id.
Arquivo Inventory.cs:
using System.Collections.Generic;
using UnityEngine;
public class Inventory : MonoBehaviour
{
// Array: tamanho fixo, preenchido no Inspector (aqui, ícones dos slots).
[SerializeField] private Sprite[] slotIcons;
// List: cresce e encolhe durante o jogo.
[SerializeField] private List<InventoryItem> startingItems = new List<InventoryItem>();
private readonly Dictionary<string, int> counts = new Dictionary<string, int>();
private void Awake()
{
foreach (InventoryItem item in startingItems)
{
Add(item.id, item.amount);
}
}
public void Add(string id, int amount)
{
if (counts.TryGetValue(id, out int current))
{
counts[id] = current + amount;
}
else
{
counts.Add(id, amount);
}
}
public int GetCount(string id)
{
return counts.TryGetValue(id, out int value) ? value : 0;
}
}
Repare que o Dictionary não tem [SerializeField]. A Unity não serializa dicionários (o manual de serialização lista isso entre os tipos não suportados), então ele não aparece no Inspector. O padrão comum é o que está no código: uma lista serializada para configurar no editor e um dicionário montado no Awake para buscar rápido em jogo.
Null, referência e o erro mais comum
Variável de classe guarda uma referência, não o objeto em si. Se ninguém atribuiu nada, ela vale null, e qualquer acesso a ela lança NullReferenceException. É o erro número um de quem começa na Unity, e quase sempre tem uma de três causas: esqueceu de arrastar no Inspector, o GetComponent não achou o componente, ou o objeto foi destruído.
Arquivo Door.cs:
using UnityEngine;
public class Door : MonoBehaviour
{
[SerializeField] private Animator animator;
private void Awake()
{
if (animator == null)
{
Debug.LogError("Door sem Animator em " + name, this);
}
}
public void Open()
{
if (animator != null)
{
animator.SetTrigger("Open");
}
}
}
O Debug.LogError com this no segundo parâmetro faz um clique no Console selecionar o objeto com problema na Hierarchy. Isso economiza muito tempo quando há 40 portas na cena.
Uma pegadinha específica da Unity: os operadores ?. e ?? do C# não devem ser usados com componentes e GameObjects. A documentação de UnityEngine.Object diz que eles não são suportados porque não enxergam o objeto destruído como nulo. Para objetos da Unity, compare com == null ou use o próprio objeto como bool (if (animator)).
Eventos: avisar sem conhecer quem escuta
O último conceito do mês é o que mais organiza o projeto. Em vez do script de vida chamar a UI, o som e a câmera diretamente, ele dispara um evento e quem se interessa se inscreve. Arquivo PlayerHealth.cs:
using System;
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public event Action<int> HealthChanged;
public event Action Died;
[SerializeField] private int maxHealth = 5;
private int currentHealth;
private void Awake()
{
currentHealth = maxHealth;
}
public void TakeDamage(int amount)
{
currentHealth = Mathf.Max(currentHealth - amount, 0);
HealthChanged?.Invoke(currentHealth);
if (currentHealth == 0)
{
Died?.Invoke();
}
}
}
Aqui o ?. está correto: HealthChanged é um delegate do C#, não um objeto da Unity. E quem escuta, arquivo HealthLabel.cs:
using UnityEngine;
public class HealthLabel : MonoBehaviour
{
[SerializeField] private PlayerHealth playerHealth;
private void OnEnable()
{
playerHealth.HealthChanged += UpdateLabel;
}
private void OnDisable()
{
playerHealth.HealthChanged -= UpdateLabel;
}
private void UpdateLabel(int value)
{
Debug.Log("Vida na tela: " + value);
}
}
Inscrever no OnEnable e desinscrever no OnDisable é a regra que evita o erro de um objeto destruído continuar recebendo evento. Esse é o padrão Observer, um dos poucos padrões de projeto que valem desde o primeiro jogo.
O que estudar depois
Fica para depois, sem culpa:
- LINQ (
Where,Select,OrderBy): é útil em ferramenta de editor e em código que roda uma vez, mas aloca memória e não combina comUpdate. - async/await e Awaitable: a Unity 6 tem o tipo
Awaitablepara código assíncrono. Antes dele, aprenda coroutines, que resolvem os mesmos timers e sequências com menos regra nova. - Generics próprios, interfaces e herança profunda: você vai usar
List<T>eGetComponent<T>()desde o dia um, mas escrever suas classes genéricas só faz sentido quando aparecer código repetido entre tipos diferentes. - Delegates avançados, reflection, unsafe: fora do escopo de quem está fechando o primeiro jogo.
O próximo passo prático: pegue os scripts deste post, coloque num projeto 2D vazio e faça uma cena com um jogador que toma dano de um inimigo, coleta moedas e mostra a vida no Console. Se você travar em if, for e variável, volte um degrau com lógica de programação para jogos. Se a dúvida é se vale pagar um curso de C# separado antes da Unity, o post curso de C# para jogos vale a pena compara os dois caminhos.
Perguntas frequentes
Preciso aprender C# antes de abrir a Unity?
Não. Dá para aprender C# já dentro da Unity, escrevendo scripts pequenos que mexem em objetos de verdade. O que ajuda muito é ter lógica de programação básica (variável, condição, repetição), porque aí a única novidade é a sintaxe e a API da engine.
Qual versão do C# a Unity 6.3 usa?
A Unity 6.3 LTS compila com Roslyn e suporta C# 9.0, com algumas exceções listadas no manual, como init-only setters e covariant return types. Na prática, quase tudo que um iniciante vai escrever funciona sem pensar em versão.
Onde aprender C# para Unity de graça?
A documentação oficial da Unity (Manual e Scripting API) é gratuita e é a referência que você vai consultar a vida toda. Combine com exercícios pequenos dentro de um projeto seu: um script por conceito, testado na cena, ensina mais do que uma playlist assistida sem abrir o editor.
LINQ e async são necessários para fazer jogo na Unity?
No primeiro mês, não. LINQ gera alocação de memória e quase nunca aparece em código que roda todo frame, e async tem regras próprias na Unity (o tipo Awaitable). Aprenda primeiro tipos, condições, loops, métodos, classes e listas, e volte nesses tópicos quando um problema real pedir.



