Aula 11 — Introdução a Jogos 2D: construindo um jogo completo
Objetivos
Ao final desta aula você deve ser capaz de:
- Decompor um jogo simples em objetos, componentes, comportamentos e regras.
- Importar sprites em pixel art com o PPU e o filtro corretos e relacionar PPU,
Sizeda câmera e resolução ao mapeamento janela → viewport da Aula 03. - Redimensionar sprites sem distorcer as bordas usando 9-slice (Draw Mode = Sliced).
- Configurar
Rigidbody2DeCollider2Dem uma cena 2D e distinguir um collider sólido de um trigger. - Criar uma Input Action e usá-la para aplicar um impulso físico ao personagem.
- Modelar um recurso limitado (a bateria) e exibi-lo numa barra que escala em torno de um pivô.
- Construir um prefab composto e reutilizá-lo durante a execução com
InstantiateeDestroy. - Distribuir responsabilidades entre componentes que controlam entrada, obstáculos, geração, pontuação e estado do jogo.
- Implementar o ciclo completo de uma partida: aguardar, jogar, pontuar, perder e reiniciar.
- Relacionar as luzes 2D do URP (Global e Spot) à luz ambiente, ao spot e à atenuação da Aula 08.
- Simular profundidade num cenário 2D com camadas de parallax e relacioná-las ao termo da projeção perspectiva da Aula 05.
- Testar cada subsistema isoladamente e diagnosticar erros de configuração pelo Inspector, pelos Gizmos e pelo Console.
Conteúdo
O jogo que vamos construir
Nesta aula construiremos Drone Gate, um pequeno jogo arcade de visão lateral. O jogador controla um drone que perde altitude continuamente e recebe um impulso para cima quando o comando é acionado. Cada impulso gasta uma carga da bateria — que só liga quando o drone se aproxima do primeiro portal — e a carga só volta ao atravessar um portal. Portais aparecem à direita, movem-se para a esquerda e deixam uma passagem em alturas variadas. Cada travessia vale um ponto; tocar uma estrutura ou um limite da pista encerra a partida.
O resultado é pequeno, mas já contém o ciclo essencial de um jogo completo:
- o jogador recebe uma situação inicial;
- executa uma ação;
- o mundo responde segundo regras;
- o jogo reconhece sucesso ou falha;
- a interface comunica o resultado;
- uma nova tentativa pode começar.
Ponto de partida: Flappy Bird
Drone Gate pertence a um gênero de jogos móveis em que um único comando basta para jogar. O ponto de partida é Flappy Bird (Dong Nguyen, .Gears, 2013): a gravidade puxa o pássaro para baixo, cada toque dá uma batida de asas, canos com fendas em alturas sorteadas passam sempre à mesma distância, cada cano vale um ponto e qualquer contato encerra a partida. Drone Gate mantém esse núcleo e muda o resto.
| Aspecto | Flappy Bird | Drone Gate |
|---|---|---|
| comando | toques ilimitados | cada impulso gasta uma carga da bateria; cada portal atravessado devolve quatro |
| estratégia | acertar o ritmo | acertar o ritmo e economizar: um toque por arco gasta metade do que uma rajada de toques |
| obstáculos | canos | portais montados com um único sprite fatiado (9-slice) |
| cenário | dia ou noite, fixo durante a partida | a cada 5 pontos, um setor escuro iluminado só pelo farol do drone (Atividade 1) |
| dificuldade | constante | os portais aceleram conforme o placar cresce (Atividade 2) |
| reinício | toque | espaço, botão sul do controle ou botão Reiniciar |
Observe também uma decisão de projeto do gênero: a falha custa pouco. A partida dura segundos e recomeça com um comando. Isso só funciona se o reinício for rápido e previsível, e é por isso que o reinício ganha uma seção própria nesta aula.
O roteiro começa em um projeto novo, criado com Unity 6.3 LTS —
6000.3.24f1 e o template Universal 2D. O projeto da Aula 10 não será
reutilizado: ele foi criado para uma cena 3D, enquanto este já nasce com o
Universal Render Pipeline, o 2D Renderer e os pacotes próprios do fluxo 2D.
Usaremos também o pacote Input System. Em código da Unity 6, a velocidade
de um Rigidbody2D é acessada por linearVelocity; materiais antigos podem
mostrar a propriedade obsoleta velocity ou o Input Manager legado.
Antes do código: decompor o jogo
Uma cena pronta pode parecer um único sistema. Para construí-la, precisamos separar o que existe, o que cada objeto faz e quais regras ligam esses objetos.
| Objeto | Componentes principais | Responsabilidade |
|---|---|---|
| Drone | Rigidbody2D, CircleCollider2D, Bateria, ControleDrone; filho Visual com Sprite Renderer | cair, receber impulso gastando carga e informar uma colisão fatal |
| Portal | Rigidbody2D cinemático, MovimentoPortal; três filhos | deslocar o conjunto e removê-lo fora da tela |
| Zona de pontuação | BoxCollider2D como trigger, ZonaPontuacao | detectar uma travessia válida uma única vez e recarregar a bateria |
| Zona da bateria | BoxCollider2D como trigger, ZonaBateria | ligar a bateria quando o drone chega perto do primeiro portal |
| Gerador de portais | GeradorPortais | criar portais em intervalos e alturas controlados |
| Controlador do jogo | ControladorJogo | guardar estado e pontuação, atualizar a interface e reiniciar |
| Interface | textos, barra da bateria (IndicadorBateria), painel de fim e botão | comunicar pontuação, carga e derrota |
O jogador parece avançar, mas sua posição horizontal permanece fixa. São os portais que se movem para a esquerda. Esse truque cria movimento relativo, simplifica a câmera e dispensa um mundo infinito.
O fluxo da Figura 1 mostra como esses componentes colaboram sem concentrar todas as decisões em um único script.
Essa divisão evita um “script que faz tudo”. O ControleDrone, por exemplo,
não precisa saber como um texto é atualizado. Ele informa a colisão ao
ControladorJogo, e o controlador toma a decisão correspondente.
A Figura 2 mostra o jogo pronto, no enquadramento da câmera que vamos configurar.

Planejar e obter os assets
Toda a arte do jogo cabe em três PNGs pequenos, desenhados em pixel art. Há duas formas de começar:
- Pacote inicial (recomendado em laboratório). O pacote
aula-11-drone-gate-inicio.unitypackagetraz os sprites já configurados (os três do jogo e as três camadas da Atividade 3) e a cenaAula11-Inicio, com a câmera, o cenário (fundo e limites) e a interface (placar, aviso, barra da bateria e painel de fim) montados, sem nenhum script. Você começa direto no drone. - Do zero. O arquivo com os sprites contém só os PNGs. Cada um também pode ser obtido separadamente: fundo, drone, portal e, para a Atividade 3, céu, planeta e montanhas.
Pacotes de checkpoint
Cada checkpoint do roteiro tem um pacote com o projeto exatamente naquele ponto: cena, prefab e scripts como estão quando a página pede os testes. Servem para quem perdeu parte da aula ou quer comparar o próprio projeto com a referência. Para importar:
- Use um projeto criado como em Criar o projeto 2D do zero, com o Input System ativo e os TMP Essential Resources importados.
- Apague antes a pasta
Assets/Aula11do projeto (ou importe num projeto novo). Os scripts e oControles.inputactionsdo pacote têm os mesmos nomes dos seus; importados por cima, a Unity criaria cópias renomeadas e cada classe passaria a existir duas vezes. - Assets → Import Package → Custom Package, escolha o pacote e clique em Import.
- Abra
Assets/Aula11/Aula11-CheckpointN.unity(N é o número do checkpoint). Se quiser, salve uma cópia com File → Save As emAssets/Aula11/Aula11.unitye continue a partir dela.
| Recurso | Arquivo ou origem | Uso nesta aula |
|---|---|---|
| Fundo espacial | T_Fundo.png, 320 × 180 px | uma imagem parada atrás do jogo |
| Drone | T_Drone.png, 18 × 14 px, transparência | aparência do personagem |
| Módulo do portal | T_Portal.png, 18 × 18 px, borda de 4 px | obstáculos do portal e limites da pista, fatiados em 9-slice |
| Camadas do fundo | T_Ceu.png, T_Planeta.png, T_Montanhas.png, 320 × 180 px | parallax da Atividade 3; somadas, formam o T_Fundo |
| Zona de pontuação | sem imagem | BoxCollider2D invisível, configurado como trigger |
| Placar e mensagens | TextMeshPro da Unity | textos da interface |
| Barra da bateria | duas Images da UI | carga restante |
| Botão | UI da Unity | reinício da partida |
| Entrada | Controles.inputactions, criado no Editor | espaço, clique e botão sul do controle |
| Áudio e animação | não usados | ficam para aulas posteriores |
Os PNGs são originais desta disciplina, distribuídos sob a licença CC BY-SA 4.0 como o restante do material, e podem ser usados sem cadastro.
Alternativas gratuitas: a Kenney oferece pacotes 2D CC0, como Pixel Shmup e Background Elements. A atribuição não é exigida pelo CC0, mas é uma boa prática indicar a origem. Ao substituir um sprite, confira sua licença específica, a silhueta, o tamanho em unidades e o collider. Um pacote dito “grátis” da Asset Store pode ter outra licença e não deve ser redistribuído com o projeto sem verificar suas condições.
Criar o projeto 2D do zero
- Abra o Unity Hub e escolha New project.
- Selecione Unity 6.3 LTS — 6000.3.24f1 e o template Universal 2D.
- Dê ao projeto o nome
drone-gatee clique em Create project. - No Editor, abra Window → Package Management → Package Manager e confirme que Input System está instalado.
- Em Edit → Project Settings → Player, confirme Active Input Handling = Input System Package (New). Aceite a reinicialização, se solicitada.
- Importe os recursos do TextMeshPro em Window → TextMeshPro → Import TMP Essential Resources. A interface do pacote inicial depende deles.
- Crie a pasta
Assets/Aula11e, dentro dela,PrefabseScripts.
Agora, conforme o caminho escolhido:
- Pacote inicial: Assets → Import Package → Custom Package, escolha
aula-11-drone-gate-inicio.unitypackagee clique em Import. AbraAssets/Aula11/Aula11-Inicio.unitye salve uma cópia com File → Save As emAssets/Aula11/Aula11.unity. Leia as duas seções seguintes para entender o que já vem pronto e siga a partir de “O drone”. - Do zero: crie
Assets/Aula11/Artes/Sprites, copie os três PNGs para lá pela pasta do sistema e aguarde a importação. Abra a cenaAssets/Scenes/SampleScene.unityque o template criou e salve-a com File → Save As emAssets/Aula11/Aula11.unity.
Este é um projeto independente. A aula não exige que você importe a cena
3D da Aula 10. O template Universal 2D já fornece o URP com 2D Renderer, e a
cena dele já contém dois objetos: Main Camera e Global Light 2D. A luz
global ilumina todos os sprites por igual; sem ela, sprites com o material
padrão do 2D Renderer ficariam escuros. Não a apague.
Sprites em pixel art: PPU, filtro e câmera
Selecione cada PNG na janela Project. No Inspector, confira ou configure:
| Propriedade | Valor | Por quê |
|---|---|---|
| Texture Type | Sprite (2D and UI) | a imagem vira um sprite utilizável pelo Sprite Renderer |
| Sprite Mode | Single | um desenho por arquivo; spritesheets ficam para a Aula 12 |
| Pixels Per Unit | 18 | 18 texels da imagem ocupam 1 unidade do mundo |
| Mesh Type | Full Rect em T_Portal; Tight nos outros | o 9-slice exige Full Rect |
| Filter Mode | Point (no filter) | cada texel vira um quadrado nítido, sem borrar a borda |
| Compression | None | compressão cria artefatos visíveis em arte de poucos pixels |
Clique em Apply. O tamanho em unidades é o número de texels dividido por 18: o drone mede 1 × 0,78 unidade, o módulo do portal 1 × 1 e o fundo 17,78 × 10.
A Figura 3 mostra essa configuração no Inspector do T_Portal.

Selecione a Main Camera. O template já cria a câmera em Projection =
Orthographic, Position = (0, 0, −10) e Size = 5; confira os três
valores. Na janela Game, escolha a proporção 16:9.
Do Módulo 1 para a Unity: glOrtho e o mapeamento janela → viewport
O Size da câmera ortográfica é a metade da altura visível, em unidades do mundo. Com Size 5 e proporção 16:9, a câmera enxerga de −5 a 5 em Y e de −8,89 a 8,89 em X. É exatamente o volume de visualização que você montava na Aula 05 com
glOrtho(-meia_h * aspecto, meia_h * aspecto, -meia_h, meia_h, 0.1, 100.0) # meia_h = 5
Em projeção ortográfica o tamanho aparente não diminui com a distância, por isso a coordenada Z dos sprites só importa para ficar dentro do volume.
O PPU fecha a outra ponta do caminho. Na Aula 03 você mapeou a janela do SRU para a viewport do SRD com um fator de escala por eixo. Aqui a janela tem 10 unidades de altura e a viewport de referência tem 720 pixels, portanto o fator é 720 / 10 = 72 pixels por unidade. Como cada unidade contém 18 texels, cada texel ocupa 72 / 18 = 4 × 4 pixels da tela. Um fator inteiro deixa todos os texels do mesmo tamanho; com PPU 100, por exemplo, o fator seria 0,72 pixel por texel e a arte seria reamostrada. O Filter Mode = Point completa o cuidado: em vez de interpolar entre texels vizinhos, como o filtro bilinear, repete o texel mais próximo, e a borda do pixel continua dura.
Em outra resolução (1920 × 1080, por exemplo) o fator deixa de ser inteiro (6 pixels por texel funciona, 5,4 não). O componente Pixel Perfect Camera, que ajusta o Size para manter fatores inteiros, é tema da Aula 12.
Montar o cenário: fundo, limites e 9-slice
Se você importou o pacote, o Cenario já está pronto. Leia esta seção
selecionando os objetos na Hierarchy e conferindo os valores no Inspector.
Crie um objeto vazio Cenario em (0, 0, 0). Arraste T_Fundo para dentro
dele, renomeie para Fundo, deixe Position = (0, 0, 0), Scale =
(1, 1, 1) e Order in Layer = −10. Com PPU 18, a imagem de 320 × 180
texels cobre a câmera exatamente.
Os limites da pista usam o módulo T_Portal, que tem só 1 × 1 unidade.
Esticá-lo com Scale até 18 × 0,5 multiplicaria também a borda metálica:
ela ficaria 18 vezes mais larga nas laterais e metade da altura em cima e
embaixo. O 9-slice resolve isso. A imagem é dividida por quatro linhas
em nove regiões: os quatro cantos são desenhados sem alteração, as bordas se
esticam em uma direção só e o miolo se estica nas duas.
O 9-slice resolve a borda, mas não faz milagre com o miolo: com Draw
Mode = Sliced, bordas e miolo são esticados, e em pixel art um texel
esticado vira um retângulo. No obstáculo do portal (1,5 × 4,5 unidades), o
miolo de 10 × 10 texels ocupa 19 × 73 texels — quase o dobro na largura e
sete vezes na altura; no limite (18 × 0,5), ocupa 316 × 1. Por isso o
T_Portal foi desenhado para ser fatiado: todo detalhe fica nos quatro
cantos de 4 × 4 texels; cada faixa de borda é igual ao longo do próprio
comprimento (as linhas amarela e ciano correm paralelas à direção em que a
faixa é esticada, então só ficam mais compridas); e o miolo é liso. Esticar
uma cor uniforme não deforma nada. Uma listra ou um desenho no miolo, ao contrário, apareceria como uma
mancha alongada que muda de proporção com o comprimento de cada peça. Quando a
arte precisa de um padrão repetido, o caminho é Draw Mode = Tiled, que
repete o miolo e as bordas em vez de esticá-los.
- Selecione
T_Portale abra Sprite Editor no Inspector. No painel do sprite, digite L = 4, R = 4, T = 4, B = 4 em Border, clique em Apply e feche. As linhas verdes marcam as nove regiões. (É o único uso do Sprite Editor nesta aula; o restante dele é assunto da Aula 12.) - Arraste
T_PortalparaCenarioe renomeie paraLimiteSuperior. - No Sprite Renderer, escolha Draw Mode = Sliced e Size = (18, 0.5). Deixe Scale = (1, 1, 1) e Order in Layer = 3.
- Position = (0, 4.75, 0): o limite ocupa de 4,5 a 5,0 em Y.
- Adicione
BoxCollider2Dcom Size = (18, 0.5), Offset = (0, 0), Is Trigger e Auto Tiling desmarcados. - Duplique (
Ctrl/Cmd + D), renomeie paraLimiteInferiore mude a posição para (0, −4.75, 0).
A Figura 4 mostra o Inspector de um limite fatiado.

O manual da Unity sugere marcar Auto Tiling no BoxCollider2D de um sprite
fatiado, para o collider acompanhar o Size do Sprite Renderer. Neste projeto,
na 6000.3.24f1, o collider com Auto Tiling ficou maior que o sprite durante
o Play (um obstáculo de 1,5 × 4,5 passou a colidir como 2 × 8, fechando a
passagem). Por isso a aula desliga a opção e digita o Size do collider igual
ao Size do sprite. Ative os Gizmos da janela Game para ver o contorno
verde dos colliders e conferir.
Ordem de desenho: o algoritmo do pintor
Todos os sprites estão no plano Z = 0; quem decide quem aparece na frente é o Order in Layer (dentro de uma Sorting Layer). O 2D Renderer desenha do menor para o maior valor: fundo (−10), portal (1), drone (2), limites (3). É o algoritmo do pintor citado na Aula 07: pintar do fundo para a frente, cada camada cobrindo a anterior. Ele funciona aqui sem depth buffer porque sprites têm transparência e a ordem é conhecida de antemão; quando dois sprites têm a mesma ordem, a Unity desempata por distância à câmera, e o resultado pode oscilar. Dê valores distintos a tudo o que se sobrepõe.
O drone: corpo, forma e gravidade
O objeto Drone separa física e aparência em dois objetos:
Drone ← Rigidbody2D, CircleCollider2D, Bateria, ControleDrone, tag Player
└── Visual ← Sprite Renderer (T_Drone)
O pai é o corpo que a física simula; o filho só desenha. Essa separação
parece exagero agora, mas permite girar a arte sem girar o corpo (uma das
extensões opcionais) e prender ao desenho um farol, na Atividade 1. Bateria
e ControleDrone entram no Drone mais adiante, quando os scripts existirem.
- Crie um objeto vazio
Droneem Position = (−3, 0, 0). Em Tag, selecione Player; essa tag será usada na pontuação. - Arraste
T_Dronepara dentro deDrone, renomeie paraVisual, deixe a posição local em (0, 0, 0) e Order in Layer = 2. - No
Drone, adicioneRigidbody2Dcom os valores da tabela abaixo. - Adicione
CircleCollider2Dcom Radius = 0.36 e Is Trigger desmarcado.
| Propriedade | Valor | Motivo |
|---|---|---|
| Body Type | Dynamic | o corpo responde à gravidade, forças e colisões |
| Gravity Scale | 1.5 | queda perceptível sem ser imediatamente punitiva |
| Collision Detection | Continuous | reduz a chance de atravessar colliders finos em alta velocidade |
| Constraints → Freeze Rotation Z | marcado | impede que uma colisão gire o corpo |
O sprite mede 1 × 0,78 unidade; o círculo de raio 0,36 é um pouco menor que a arte. Um collider simples e ligeiramente menor que a imagem produz uma interação mais justa: o jogador perdoa um raspão que não vê, mas não perdoa uma colisão com o ar.
Corpo dinâmico e obstáculos estáticos
O drone é dinâmico porque a física calcula sua queda e seus impulsos.
Os limites não têm Rigidbody2D e por isso funcionam como colliders
estáticos. O portal será movido por um Rigidbody2D cinemático no objeto
pai: ele não cai com a gravidade, mas desloca seus colliders de forma
integrada à física 2D.
A física 2D roda em passo fixo, no FixedUpdate: por padrão 50 vezes por
segundo (Fixed Timestep = 0,02), independentemente da taxa de quadros.
É o mesmo acumulador de tempo que você viu no laco_de_jogo.py da
Aula 10: o Update roda uma
vez por quadro para desenhar e ler estado; o passo da física roda quantas
vezes o tempo acumulado pedir.
Não misture componentes 2D e 3D. Rigidbody2D interage com
BoxCollider2D, CircleCollider2D e outros componentes terminados em 2D;
ele não colide com BoxCollider, SphereCollider ou Rigidbody.
Estado, pontuação e interface mínima
Antes de adicionar os outros scripts, crie a fonte do estado da partida. Assim todos os arquivos compilarão quando forem usados. Um estado representa qual conjunto de regras vale agora. Drone Gate usa três:
- aguardando o primeiro comando: drone parado, aviso visível e nenhum portal gerado;
- partida iniciada: aceita impulso, gera e move portais, permite pontuar;
- derrota: bloqueia impulso e geração, interrompe portais, exibe o fim e aceita reinício.
A interface é um Canvas com o placar, o aviso, a barra da bateria e um
painel. Com o pacote inicial, ela já está na cena; do zero, siga o roteiro
abaixo.
Montar a interface do zero
- Em GameObject → UI → Canvas, crie
Interface. Configure Render Mode = Screen Space - Overlay e Canvas Scaler → UI Scale Mode = Scale With Screen Size, com Reference Resolution = 1280 × 720. A Unity cria também umEventSystemna raiz da cena, fora do Canvas. - Crie um texto UI → Text - TextMeshPro como filho do Canvas. Nomeie
TextoPontuacao, escreva0, tamanho 64, alinhamento central, âncora superior central, Pos = (0, −55) e Size = (220, 90). - Crie outro texto filho do Canvas:
AvisoInicio, conteúdo Espaço ou clique para começar, tamanho 28, âncora central, Pos = (0, 110) e Size = (600, 80). - Crie um UI → Image filho do Canvas chamado
BarraBateria: âncora e pivô no canto superior esquerdo (Anchor Min = Max = (0, 1), Pivot = (0, 1)), Pos = (32, −54), Size = (260, 28) e cor#140F24com Alpha 217. - Dentro dela, crie outro UI → Image chamado
Preenchimento: Anchor Min = (0, 0) e Max = (1, 1) (esticado nos dois eixos), Left, Top, Right e Bottom = 4, Pivot = (0, 0.5) e cor#59E6FF. O pivô na borda esquerda é o que importa; a seção da bateria explica por quê. Desmarque Raycast Target nas duas imagens, para a barra não interceptar cliques. - Crie um UI → Image chamado
PainelFimDeJogo, centralizado, com Size = (500, 280) e cor#140F24com Alpha 240. - Dentro do painel, crie
TextoFimDeJogo(Fim de jogo, tamanho 46, Pos = (0, 80)) eTextoDica(Espaço ou botão sul do controle também reinicia, tamanho 20, Pos = (0, 30), Size = (460, 40)). - Crie UI → Button - TextMeshPro como filho do painel:
BotaoReiniciar, Size = (320, 66), Pos = (0, −80), cor#FFCC40, texto Reiniciar em tamanho 28. - Selecione o
EventSystem. Se o Inspector oferecer a troca para Input System UI Input Module, aceite. - Desative
PainelFimDeJogona Hierarchy.
Crie um objeto vazio ControladorJogo na raiz da cena e, em
Assets/Aula11/Scripts, o script ControladorJogo.cs:
📥 Baixe o arquivo:
ControladorJogo.cs
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Aula11
{
public class ControladorJogo : MonoBehaviour
{
[Header("Interface")]
[SerializeField] private TMP_Text textoPontuacao;
[SerializeField] private GameObject painelFimDeJogo;
[SerializeField] private GameObject avisoInicio;
[Header("Reinício")]
[Tooltip("Segundos entre a derrota e o primeiro reinício aceito pelo comando Impulsionar.")]
[Min(0f)] [SerializeField] private float esperaReinicio = 0.6f;
public bool PartidaAtiva { get; private set; }
public bool PartidaIniciada { get; private set; }
public int Pontuacao => pontuacao;
private int pontuacao;
private float instanteDerrota;
private void Start()
{
pontuacao = 0;
PartidaAtiva = true;
PartidaIniciada = false;
painelFimDeJogo.SetActive(false);
avisoInicio.SetActive(true);
AtualizarPontuacao();
}
public void IniciarPartida()
{
if (!PartidaAtiva || PartidaIniciada) return;
PartidaIniciada = true;
avisoInicio.SetActive(false);
}
public void AdicionarPonto()
{
if (!PartidaAtiva || !PartidaIniciada) return;
pontuacao++;
AtualizarPontuacao();
}
public void RegistrarDerrota()
{
if (!PartidaAtiva) return;
PartidaAtiva = false;
instanteDerrota = Time.time;
painelFimDeJogo.SetActive(true);
}
public void SolicitarReinicio()
{
if (PartidaAtiva || Time.time - instanteDerrota < esperaReinicio) return;
ReiniciarPartida();
}
public void ReiniciarPartida()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
private void AtualizarPontuacao()
{
textoPontuacao.text = pontuacao.ToString();
}
}
}
Adicione o script ao objeto ControladorJogo e arraste TextoPontuacao,
PainelFimDeJogo e AvisoInicio para os respectivos campos no Inspector.
PartidaAtiva indica que não houve derrota; PartidaIniciada indica que o
primeiro comando já foi dado. Essas propriedades têm leitura pública, mas
escrita privada: outros componentes podem consultar o estado, porém somente o
controlador pode alterá-lo. Pontuacao segue a mesma ideia para o placar:
é só de leitura, e as duas atividades de laboratório vão consultá-la.
RegistrarDerrota começa verificando o próprio estado. Assim duas colisões
no mesmo instante não processam a derrota duas vezes. Essa proteção torna o
método idempotente depois da primeira chamada: repetir a solicitação não
altera mais o resultado. AdicionarPonto exige os dois estados: sem o
teste de PartidaIniciada, um portal colocado à mão na cena poderia render
ponto antes de o jogador começar.
SolicitarReinicio e ReiniciarPartida são usados na seção “Fechar o
ciclo”; por ora, basta que compilem.
Não usamos Time.timeScale = 0. Cada sistema consulta explicitamente o
estado e decide parar. Para este projeto, isso deixa visível quais regras
mudam na derrota e evita efeitos colaterais em interface e temporizadores.
Bateria: cada impulso tem custo
Em Flappy Bird o jogador pode tocar quantas vezes quiser. Em Drone Gate o drone carrega uma bateria de 10 cargas: cada impulso gasta uma, cada portal atravessado devolve quatro (sem passar de 10) e, com a bateria vazia, o comando é ignorado e o drone cai. A bateria começa desligada e só passa a gastar quando o drone chega perto do primeiro portal. A regra é pequena, mas muda o jeito de jogar.
Por que a bateria muda o jogo
O impulso zera a velocidade vertical e a deixa em 5,5 unidades/s para cima. Se o comando chega quando o drone cai a , o ganho de velocidade é . Ao longo de uma partida estável, a velocidade vertical volta sempre aos mesmos valores; então a soma dos ganhos precisa compensar exatamente o que a gravidade retirou:
Entre dois portais passam s, e a gravidade retira unidades/s. Quem aperta uma vez por arco, no fim da queda, pega o drone descendo a −5,5 e ganha : são cerca de 3,2 cargas por portal. Quem aperta em rajadas gasta o segundo e o terceiro toque com o drone ainda subindo, e cada um rende pouco: numa simulação com rajadas de três toques, o gasto sobe para cerca de 6,4 cargas por portal — o dobro; com rajadas de dois, fica perto de 5,4. Como cada portal devolve 4, quem faz um arco por toque ganha menos de uma carga por portal e mantém a bateria alta; quem faz rajadas perde de 1,4 a 2,4 por portal e esvazia as 10 cargas em poucos portais (num teste automático, rajadas de dois fizeram 4 pontos e rajadas de três, 3; um toque por arco passou de 19 pontos com a bateria sempre perto de cheia).
Por que a bateria começa desligada
O primeiro portal só chega uns 5 s depois do primeiro comando, e esse trecho custa de 7 a 8 cargas mesmo para quem joga bem. Se ele gastasse carga, a capacidade teria de ser grande (15, por exemplo) só para o jogador chegar ao primeiro portal — e, com 15 cargas, a bateria passaria a partida inteira quase cheia e deixaria de pesar nas decisões. Por isso a bateria só liga quando o drone cruza uma linha invisível pouco antes do primeiro portal: um trigger montado no próprio portal, que você criará na seção Reconhecer uma travessia. Até lá, os impulsos são de graça e a barra fica cinza. A partir daí, o trecho até a passagem custa duas ou três cargas, e a capacidade pode ser pequena o bastante para punir o desperdício.
O script Bateria
Crie Assets/Aula11/Scripts/Bateria.cs:
📥 Baixe o arquivo:
Bateria.cs
using UnityEngine;
namespace Aula11
{
/// <summary>
/// Cargas do drone: cada impulso gasta uma, cada portal atravessado devolve algumas.
/// A bateria começa desligada e só passa a gastar quando o drone entra na ZonaBateria do primeiro portal.
/// </summary>
public class Bateria : MonoBehaviour
{
[Min(1)] [SerializeField] private int capacidade = 10;
[Tooltip("Cargas devolvidas a cada portal atravessado, sem passar da capacidade.")]
[Min(0)] [SerializeField] private int recargaPorPortal = 4;
public int Carga { get; private set; }
public bool Ligada { get; private set; }
public float Fracao => (float)Carga / capacidade;
private void Awake()
{
Carga = capacidade;
Ligada = false;
}
/// <summary>A partir daqui, cada impulso gasta carga. Chamar de novo não muda nada.</summary>
[ContextMenu("Ligar")]
public void Ligar()
{
Ligada = true;
}
/// <summary>Gasta uma carga, se houver. Desligada, não gasta nada. Devolve false com a bateria vazia.</summary>
public bool Consumir()
{
if (!Ligada) return true;
if (Carga == 0) return false;
Carga--;
return true;
}
public void Recarregar()
{
Carga = Mathf.Min(Carga + recargaPorPortal, capacidade);
}
}
}
Carga e Ligada têm leitura pública e escrita privada, como o estado do
controlador: só a própria bateria muda a carga e liga a si mesma. Enquanto
Ligada é falsa, Consumir devolve true sem gastar: o impulso acontece
normalmente. Ligar só muda a variável, e chamá-lo de novo não muda nada — é
isso que permite que todo portal tenha a zona que liga a bateria, e não só o
primeiro. O atributo [ContextMenu("Ligar")] acrescenta o item Ligar ao
menu ⋮ do componente no Inspector: em Play Mode, ele liga a bateria à mão,
útil para testar o consumo antes de haver portais. Consumir devolve bool porque quem pede
a carga decide o que fazer quando ela falta; a bateria só responde se havia o
que gastar. Recarregar usa Mathf.Min para não passar da capacidade.
Em Fracao, o (float) não é enfeite. Em C#, dividir um int por outro
int descarta a parte fracionária, como o // do Python: 11 / 15 vale 0, e
a barra ficaria vazia até a bateria encher. Converter um dos operandos para
float faz a divisão real, como o / do Python.
Selecione o Drone, adicione Bateria e mantenha Capacidade = 10 e
Recarga Por Portal = 4.
A barra: escala em torno de um ponto fixo
A barra BarraBateria tem um fundo escuro e, dentro dele, a imagem
Preenchimento. Para mostrar a carga, basta mudar a escala horizontal do
preenchimento para a fração da bateria: 1 com a bateria cheia, 0,5 pela
metade.
Uma escala acontece em torno de um ponto. Na Aula 04 você escalou em torno de um ponto fixo compondo três matrizes,
para que não saísse do lugar. Na UI da Unity, esse ponto é o pivô do
RectTransform. Com Pivot X = 0, o ponto fixo é a borda esquerda, e a
barra encolhe da direita para a esquerda; com o pivô padrão (0,5), ela
encolheria para o centro. Crie Assets/Aula11/Scripts/IndicadorBateria.cs:
📥 Baixe o arquivo:
IndicadorBateria.cs
using UnityEngine;
using UnityEngine.UI;
namespace Aula11
{
/// <summary>Mostra a carga como a largura de uma barra, encolhendo em direção ao pivô.</summary>
public class IndicadorBateria : MonoBehaviour
{
[Header("Referências")]
[SerializeField] private Bateria bateria;
[Tooltip("Imagem da carga, com o pivô na borda esquerda (Pivot X = 0).")]
[SerializeField] private Image preenchimento;
[Header("Cores")]
[SerializeField] private Color corNormal = new Color(0.35f, 0.9f, 1f);
[SerializeField] private Color corAlerta = new Color(1f, 0.3f, 0.25f);
[Tooltip("Cor da barra enquanto a bateria ainda não foi ligada.")]
[SerializeField] private Color corDesligada = new Color(0.5f, 0.52f, 0.62f);
[Tooltip("Abaixo desta fração da capacidade, a barra fica com a cor de alerta.")]
[Range(0f, 1f)] [SerializeField] private float limiarAlerta = 0.25f;
private void Update()
{
if (bateria == null || preenchimento == null) return;
float fracao = bateria.Fracao;
// Escala em X em torno do pivô: a borda esquerda fica parada, como T(p)·S·T(−p) na Aula 04.
preenchimento.rectTransform.localScale = new Vector3(fracao, 1f, 1f);
if (!bateria.Ligada) preenchimento.color = corDesligada;
else preenchimento.color = fracao < limiarAlerta ? corAlerta : corNormal;
}
}
}
Adicione IndicadorBateria ao objeto BarraBateria, arraste o Drone para
Bateria (a Unity preenche com o componente) e o filho Preenchimento para
Preenchimento. Enquanto a bateria está desligada, a barra fica cinza
(corDesligada): o jogador vê que o trecho inicial não custa nada. Depois,
com 10 cargas, ela fica vermelha a partir de 2 (2/10 = 0,2, abaixo do limiar
0,25).
O indicador lê a bateria a cada quadro, mas não a altera: a interface
mostra o estado, quem decide é o drone. Consultar um valor em todo Update é
simples e barato aqui; avisar a interface só quando a carga muda (com eventos)
fica para a interface completa da Aula 14.
Input Actions: da intenção ao comando
Na Aula 10 lemos Keyboard.current diretamente. Isso responde à pergunta
“a barra de espaço está pressionada?”, mas prende a regra a um dispositivo.
Nesta aula criaremos uma ação: o jogo pergunta se a intenção
Impulsionar ocorreu, e o Input System decide qual controle representa essa
intenção.
Na pasta Assets/Aula11, use Create → Input Actions e nomeie o asset como
Controles. Abra-o com duplo clique e então:
- crie um Action Map chamado
Jogador; - crie a ação
Impulsionare escolha Action Type = Button; - adicione o binding
<Keyboard>/space(Space [Keyboard]); - adicione
<Mouse>/leftButton(Left Button [Mouse]) como segundo binding; - adicione
<Gamepad>/buttonSouth(Button South [Gamepad]) como terceiro binding; - clique em Save Asset na janela de Input Actions.
Vários bindings podem produzir a mesma ação. O código do drone não precisa mudar quando um controle novo é acrescentado.
Crie Assets/Aula11/Scripts/ControleDrone.cs:
📥 Baixe o arquivo:
ControleDrone.cs
using UnityEngine;
using UnityEngine.InputSystem;
namespace Aula11
{
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(Bateria))]
public class ControleDrone : MonoBehaviour
{
[Header("Entrada")]
[SerializeField] private InputActionAsset controles;
[Header("Movimento")]
[Min(0f)] [SerializeField] private float intensidadeImpulso = 5.5f;
[Header("Referência da cena")]
[SerializeField] private ControladorJogo controladorJogo;
private Rigidbody2D corpo;
private Bateria bateria;
private float gravidadeOriginal;
private InputAction acaoImpulsionar;
private void Awake()
{
corpo = GetComponent<Rigidbody2D>();
bateria = GetComponent<Bateria>();
gravidadeOriginal = corpo.gravityScale;
corpo.gravityScale = 0f;
}
private void OnEnable()
{
if (controles == null) return;
acaoImpulsionar = controles.FindAction("Jogador/Impulsionar");
if (acaoImpulsionar == null) return;
acaoImpulsionar.performed += AoImpulsionar;
acaoImpulsionar.Enable();
}
private void OnDisable()
{
if (acaoImpulsionar == null) return;
acaoImpulsionar.performed -= AoImpulsionar;
acaoImpulsionar.Disable();
}
private void AoImpulsionar(InputAction.CallbackContext contexto)
{
if (controladorJogo == null) return;
if (!controladorJogo.PartidaAtiva)
{
// Depois da derrota o mouse fica para os botões; teclado e controle reiniciam.
if (contexto.control.device is not Mouse) controladorJogo.SolicitarReinicio();
return;
}
Impulsionar();
}
/// <summary>Comando do jogador: inicia a partida, se preciso, e gasta uma carga no impulso.</summary>
public void Impulsionar()
{
if (controladorJogo == null || !controladorJogo.PartidaAtiva) return;
if (!controladorJogo.PartidaIniciada)
{
controladorJogo.IniciarPartida();
corpo.gravityScale = gravidadeOriginal;
}
// Sem carga, o comando é ignorado e o drone continua caindo.
if (!bateria.Consumir()) return;
corpo.linearVelocity = new Vector2(corpo.linearVelocity.x, 0f);
corpo.AddForce(Vector2.up * intensidadeImpulso, ForceMode2D.Impulse);
}
private void OnCollisionEnter2D(Collision2D colisao)
{
if (controladorJogo != null) controladorJogo.RegistrarDerrota();
}
}
}
[RequireComponent] faz a Unity exigir um Rigidbody2D, um Collider2D e
uma Bateria no mesmo objeto. As referências são guardadas uma vez em
Awake, em vez de procurar os componentes a cada comando.
Em Awake, o script guarda a gravidade configurada no Inspector e a suspende.
O primeiro comando inicia a partida, restaura a gravidade e já aplica o
impulso. Assim o jogador tem tempo para ler o aviso antes de começar.
O evento performed ocorre quando a ação cumpre sua interação. Como a ação é
do tipo botão, cada pressionamento chama AoImpulsionar. Em OnEnable, o
script assina o evento e habilita a ação; em OnDisable, desfaz exatamente
essas duas operações. Essa simetria evita que um componente desativado
continue respondendo ou que o mesmo método seja chamado várias vezes após
reativações.
O comando em si fica em Impulsionar. A ordem das linhas conta: a partida
começa antes do teste da bateria, e só então uma carga é gasta. Se
Consumir devolve false, o método termina sem impulso: o drone continua
caindo até atravessar um portal (e recarregar) ou bater. Impulsionar é
público porque qualquer origem de comando pode usá-lo — um botão de toque na
tela, por exemplo, ou um teste automático — sem passar pelo Input System.
O impulso usa ForceMode2D.Impulse: uma alteração instantânea da quantidade
de movimento, apropriada para um comando breve. Antes dele, a velocidade
vertical é zerada. Sem essa linha, apertar durante uma queda forte primeiro
precisaria vencer toda a velocidade para baixo, deixando o comando menos
previsível. Com massa 1, o impulso de 5,5 deixa o drone subindo a 5,5 unidades
por segundo, independentemente de como ele estava caindo.
Depois da derrota, a mesma ação passa a pedir reinício. O teste
contexto.control.device is not Mouse separa os dispositivos: o clique do
mouse fica reservado ao botão Reiniciar do painel, para que um único
clique não dispare o botão e a ação ao mesmo tempo. Espaço e o botão sul do
controle reiniciam sem precisar do mouse.
No Inspector do Drone, arraste:
- o asset
Controlespara Controles; - o objeto
ControladorJogopara Controlador Jogo.
Aguarde a Unity compilar os dois arquivos antes de procurar o componente em
Add Component. O campo Controles recebe o asset inteiro. O script
procura a ação Jogador/Impulsionar pelo nome; confira a grafia se o comando
não funcionar.
A Figura 5 mostra o Editor com a cena montada até aqui e o Drone
selecionado.

Checkpoint 1 — Drone sob controle
Pressione Play, clique na janela Game e use espaço ou clique principal.
- Antes do primeiro comando, o drone permanece parado e o aviso fica visível.
- Após o primeiro comando, o aviso desaparece e o drone passa a cair por causa da gravidade.
- Cada comando produz um impulso para cima; a barra continua cheia e cinza, porque ainda não há portal para ligar a bateria.
- Depois de Ligar no menu ⋮ do componente
Bateria(em Play), a barra fica ciano e cada comando a encolhe em direção à borda esquerda. - Abaixo de um quarto da carga, a barra fica vermelha; com a bateria vazia, o comando não tem efeito e o drone cai.
- A posição horizontal permanece estável.
- Ao tocar um limite, o painel de fim aparece e o drone não gira.
- O Console não mostra exceções.
Se o drone não responder, verifique se a ação foi atribuída, se o componente está habilitado, se a janela Game tem foco e se a bateria não está vazia. Se ele atravessar os limites, confira se todos os componentes envolvidos são da família 2D e se Is Trigger está desmarcado nos limites.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-1.unitypackagetraz o projeto neste ponto (drone, controlador, bateria e interface prontos; ainda sem portal). Veja como importar em Pacotes de checkpoint.
Construir um portal composto
Um portal é um único obstáculo lógico, mas possui partes diferentes. Ele é um objeto pai com três filhos:
P_Portal ← Rigidbody2D (Kinematic), MovimentoPortal
├── ObstaculoSuperior ← Sprite Renderer (Sliced), BoxCollider2D
├── ObstaculoInferior ← Sprite Renderer (Sliced), BoxCollider2D
└── ZonaPontuacao ← BoxCollider2D (Is Trigger), ZonaPontuacao
- Crie um objeto vazio
P_Portalcom Position = (−3, 0, 0) e Scale = (1, 1, 1) — nessa posição o drone fica no meio da passagem, o que facilita o Checkpoint 2. AdicioneRigidbody2Dcom Body Type = Kinematic. - Arraste
T_Portalpara dentro deP_Portale renomeie paraObstaculoSuperior. No Sprite Renderer: Draw Mode = Sliced, Size = (1.5, 4.5), Order in Layer = 1. Posição local (0, 4, 0). - Adicione
BoxCollider2Dcom Size = (1.5, 4.5), Is Trigger e Auto Tiling desmarcados. - Duplique o obstáculo, renomeie para
ObstaculoInferiore mude a posição local para (0, −4, 0). - Crie
ZonaPontuacaocomo filho vazio deP_Portal, em posição local (0.9, 0, 0), e adicioneBoxCollider2Dcom Size = (0.2, 3.4) e Is Trigger marcado.
Cada obstáculo cobre 4,5 unidades de altura a partir de ±4: o superior vai de 1,75 a 6,25 e o inferior de −6,25 a −1,75. A passagem central mede 3,5 unidades, cerca de quatro vezes e meia a altura do drone. A zona fica em X = 0,9, logo depois da borda direita dos obstáculos (0,75): o ponto só conta depois que o drone sai da abertura.
A zona é um sensor invisível: detecta a travessia, mas não empurra nem bloqueia o drone. Os três colliders filhos acompanham o corpo cinemático do pai; a física trata o conjunto como um único corpo composto.
A Figura 6 relaciona a aparência esquemática do portal à hierarquia do prefab.
Do Módulo 1 para a Unity: filhos e a pilha de matrizes
Mover P_Portal move os três filhos porque a posição de cada filho é
local: a Unity compõe a transformação do pai com a do filho. Na
Aula 04 você escreveria o
mesmo portal assim:
glPushMatrix()
glTranslatef(x_portal, y_portal, 0) # transformação do pai
glPushMatrix(); glTranslatef(0, 4, 0); desenhar_obstaculo(); glPopMatrix()
glPushMatrix(); glTranslatef(0, -4, 0); desenhar_obstaculo(); glPopMatrix()
glPopMatrix()
A hierarquia da Unity é essa pilha tornada permanente: o Transform do pai é
o glTranslatef externo, e cada filho guarda o seu glTranslatef interno.
Collider sólido ou trigger?
Os dois representam regiões geométricas; o que muda é a resposta:
| Configuração | O que a física faz | Evento recebido |
|---|---|---|
| collider sólido | impede a interpenetração e calcula contato | OnCollisionEnter2D |
| collider com Is Trigger | permite a passagem e apenas informa a sobreposição | OnTriggerEnter2D |
Na Figura 7, o círculo é apenas um corpo móvel abstrato. No primeiro quadro, ele para ao tocar o obstáculo; no segundo, atravessa a região do sensor.
Para que eventos 2D sejam enviados, pelo menos um dos objetos envolvidos
deve possuir Rigidbody2D. O drone já cumpre esse requisito.
O que o BoxCollider2D testa: AABB
Enquanto nenhum dos dois gira, um BoxCollider2D é uma axis-aligned
bounding box (AABB): um retângulo com lados paralelos aos eixos, descrito
por centro e meias-dimensões . Duas AABBs se
sobrepõem quando
São quatro comparações — por isso a física usa caixas (e círculos, que se testam pela distância entre centros) como primeira aproximação de quase tudo. O teste do drone contra um obstáculo é círculo × caixa: o ponto da caixa mais próximo do centro do círculo está a uma distância menor que o raio? A Unity faz isso por você, mas saber o teste explica por que um collider ligeiramente menor que a arte muda a sensação do jogo, e por que um collider maior que o sprite (como o do Auto Tiling, acima) fecha a passagem.
Transformar em prefab
Arraste P_Portal da Hierarchy para Assets/Aula11/Prefabs. O item azul
criado na janela Project é o asset de prefab; o objeto na cena passa a
ser uma instância dele. A partir de agora, altere a estrutura no Prefab
Mode (duplo clique no asset) para que toda instância receba as mesmas
partes e componentes. Mantenha a instância na cena para o teste a seguir.
Checkpoint 2 — Portal sólido e reutilizável
Com a instância em X = −3, o drone começa no meio da passagem. Arraste do Project uma segunda instância para Position = (4, 0.5, 0) e teste.
- O drone colide com as estruturas ao subir ou cair demais.
- A passagem central continua atravessável.
- Mover o objeto pai leva todas as partes juntas.
- Uma alteração feita no Prefab Mode (a cor do Sprite Renderer, por exemplo) aparece nas duas instâncias; desfaça-a depois.
- Com Gizmos ligados na janela Game, os contornos verdes dos colliders coincidem com a arte.
Ao terminar, apague a instância em X = 4 e mantenha a de X = −3 para o próximo checkpoint.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-2.unitypackagetraz o projeto neste ponto (acrescenta o prefabP_Portal, ainda sem scripts, e uma instância em X = −3). Veja como importar em Pacotes de checkpoint.
Movimento aparente do cenário
O portal se deslocará como um conjunto. Por isso, o script pertence ao
objeto pai, não aos filhos. Crie MovimentoPortal.cs:
📥 Baixe o arquivo:
MovimentoPortal.cs
using UnityEngine;
namespace Aula11
{
[RequireComponent(typeof(Rigidbody2D))]
public class MovimentoPortal : MonoBehaviour
{
[Min(0f)] [SerializeField] private float velocidade = 3f;
[SerializeField] private float limiteEsquerdo = -10f;
private Rigidbody2D corpo;
private ControladorJogo controladorJogo;
private void Awake()
{
corpo = GetComponent<Rigidbody2D>();
controladorJogo = FindFirstObjectByType<ControladorJogo>();
}
private void FixedUpdate()
{
if (controladorJogo == null || !controladorJogo.PartidaAtiva) return;
corpo.MovePosition(corpo.position + Vector2.left * (velocidade * Time.fixedDeltaTime));
if (corpo.position.x <= limiteEsquerdo) Destroy(gameObject);
}
}
}
Rigidbody2D.MovePosition move o corpo cinemático em FixedUpdate, no
passo da física 2D. A distância de cada passo é a velocidade multiplicada por
Time.fixedDeltaTime; portanto, o valor 3 significa 3 unidades por
segundo, independentemente da taxa de quadros da Game view.
Destroy(gameObject) não apaga o asset de prefab. Ele agenda a remoção
daquela instância ao final do quadro. Remover portais que já saíram da
câmera impede que uma partida longa acumule objetos invisíveis.
O prefab não pode guardar uma referência a um objeto de cena. Por isso cada
instância localiza o ControladorJogo uma vez, em Awake. Essa busca seria
inadequada dentro de Update, mas é aceitável na inicialização de um objeto
gerado. Em um jogo maior, um gerador poderia injetar a referência ou um
pool poderia reaproveitar instâncias; aqui isso esconderia os conceitos que
queremos observar.
Abra o prefab no Prefab Mode e adicione MovimentoPortal ao pai. Mantenha
Velocidade = 3 e Limite Esquerdo = −10. Com câmera 16:9 de Size 5, a
borda esquerda está em X = −8,89; a peça de largura 1,5 já terá saído da
tela quando o pai alcançar −10.
Checkpoint 3 — Mundo em movimento
Mova a instância que ficou na cena para Position = (8, 0, 0) e pressione
Play. O portal começa a andar antes do primeiro comando, porque
MovimentoPortal só consulta PartidaAtiva; não há problema, pois
AdicionarPonto recusa pontos enquanto a partida não foi iniciada.
- O portal inteiro se move para a esquerda, sem separar os filhos.
- A velocidade é visualmente a mesma com diferentes taxas de quadros.
- A instância desaparece da Hierarchy somente depois de sair da área visível.
- O script está no objeto pai do prefab.
Na janela Game, ative Stats e altere temporariamente o VSync: o número de quadros pode mudar, mas a velocidade em unidades por segundo deve permanecer a mesma. Saia do Play Mode e apague a instância da cena; daqui em diante o gerador cria os portais.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-3.unitypackagetraz o projeto neste ponto (acrescentaMovimentoPortalao pai do prefab e deixa a instância em X = 8). Veja como importar em Pacotes de checkpoint.
Gerar uma sequência de portais
Uma instância demonstra o obstáculo; uma sequência sustenta a partida. O gerador precisa conhecer:
- qual prefab criar;
- em que posição horizontal criar;
- o intervalo entre criações;
- os limites vertical mínimo e máximo.
Crie GeradorPortais.cs:
📥 Baixe o arquivo:
GeradorPortais.cs
using UnityEngine;
namespace Aula11
{
public class GeradorPortais : MonoBehaviour
{
[Header("Referências")]
[SerializeField] private GameObject prefabPortal;
[SerializeField] private ControladorJogo controladorJogo;
[Header("Tempo")]
[Min(0.1f)] [SerializeField] private float intervalo = 2.4f;
[Min(0f)] [SerializeField] private float atrasoInicial = 1f;
[Header("Posição")]
[SerializeField] private float posicaoX = 10f;
[SerializeField] private float alturaMinima = -1.2f;
[SerializeField] private float alturaMaxima = 1.2f;
private float tempoAteProximo;
private void Update()
{
if (controladorJogo == null || prefabPortal == null || !controladorJogo.PartidaAtiva) return;
if (!controladorJogo.PartidaIniciada)
{
tempoAteProximo = atrasoInicial;
return;
}
tempoAteProximo -= Time.deltaTime;
if (tempoAteProximo > 0f) return;
float altura = Random.Range(alturaMinima, alturaMaxima);
Instantiate(prefabPortal, new Vector3(posicaoX, altura, 0f), Quaternion.identity, transform);
tempoAteProximo = intervalo;
}
}
}
Crie um objeto vazio GeradorDePortais na raiz da cena, adicione o script e
mantenha os valores iniciais do código: Intervalo = 2.4, Atraso Inicial
= 1, Posicao X = 10, Altura Minima = −1.2 e Altura Maxima = 1.2.
Arraste o prefab da janela Project para Prefab Portal e o objeto
ControladorJogo da Hierarchy para Controlador Jogo.
Enquanto a partida não começou, o gerador mantém o cronômetro no atraso
inicial; o primeiro portal surge 1 s depois do primeiro comando. O quarto
argumento de Instantiate, transform, torna cada portal filho do
gerador: a Hierarchy fica organizada e, na Atividade 2, alcançar todos os
portais existentes — para mudar a velocidade de todos de uma vez — será
percorrer os filhos de um único objeto.
Random.Range não significa “qualquer altura”. Os limites devem preservar
uma passagem possível. Com o centro da passagem entre −1,2 e 1,2 e meia
passagem de 1,75, as bordas da abertura ficam entre −2,95 e 2,95, longe dos
limites da pista (±4,5). Aleatoriedade sem limites pode criar desafios
impossíveis; com limites, cria variedade sob as regras do projeto.
O intervalo temporal e a velocidade espacial trabalham juntos. Se os portais andam a 3 unidades/s e surgem a cada 2,4 s, a distância entre eles é 7,2 unidades. Aumentar a velocidade com o mesmo intervalo aumenta essa distância, mas reduz o tempo que cada portal leva para chegar ao drone.
Checkpoint 4 — Geração contínua
- O primeiro portal aparece cerca de 1 s depois do primeiro comando, fora da borda direita.
- As alturas variam dentro da faixa definida.
- Nenhuma passagem encosta nos limites da pista.
- A distância entre portais permite reação.
- Os portais aparecem como filhos de
GeradorDePortaise são removidos ao sair da tela.
Se nada surgir, a causa mais comum é o campo Prefab Portal vazio. Uma
NullReferenceException ou mensagem semelhante no Console deve ser lida da
primeira linha para baixo: ela indica o arquivo e a linha que tentaram usar
uma referência ausente.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-4.unitypackagetraz o projeto neste ponto (acrescenta oGeradorDePortais; nenhum portal colocado à mão na cena). Veja como importar em Pacotes de checkpoint.
Reconhecer uma travessia
O drone colide com a estrutura, mas atravessa a zona central. Essa
sobreposição é o evento de sucesso. Crie ZonaPontuacao.cs:
📥 Baixe o arquivo:
ZonaPontuacao.cs
using UnityEngine;
namespace Aula11
{
[RequireComponent(typeof(Collider2D))]
public class ZonaPontuacao : MonoBehaviour
{
private ControladorJogo controladorJogo;
private bool pontoRegistrado;
private void Awake()
{
controladorJogo = FindFirstObjectByType<ControladorJogo>();
}
private void OnTriggerEnter2D(Collider2D outro)
{
if (pontoRegistrado || controladorJogo == null || !outro.CompareTag("Player")) return;
pontoRegistrado = true;
controladorJogo.AdicionarPonto();
// Quem atravessou recarrega a própria bateria.
if (outro.TryGetComponent(out Bateria bateria)) bateria.Recarregar();
}
}
}
Adicione o script a ZonaPontuacao dentro do prefab (Prefab Mode). O drone já
tem a tag Player; CompareTag filtra a interação: outro objeto que entre
na zona não deve pontuar.
A última linha recarrega a bateria de quem atravessou. TryGetComponent
procura uma Bateria no objeto do collider que entrou na zona e, se ela
existe, a entrega em bateria; se não existe, devolve false e nada
acontece. É o jeito de perguntar “esse objeto tem tal componente?” sem
arriscar uma NullReferenceException.
O booleano pontoRegistrado protege a regra. Mesmo que a sobreposição gere
mais de uma entrada por causa de mudanças nos colliders ou de uma futura
alteração de movimento, aquela zona concede no máximo um ponto.
Ligar a bateria perto do portal
A bateria começa desligada (veja Bateria: cada impulso tem custo).
Quem a liga é um segundo trigger no prefab, à frente dos obstáculos:
quando o drone o cruza, o primeiro portal está a menos de um segundo. Crie
ZonaBateria.cs:
📥 Baixe o arquivo:
ZonaBateria.cs
using UnityEngine;
namespace Aula11
{
/// <summary>Trigger à frente dos obstáculos: liga a bateria de quem entra nele.</summary>
[RequireComponent(typeof(Collider2D))]
public class ZonaBateria : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D outro)
{
// Todo portal tem uma zona; do segundo em diante, Ligar não muda nada.
if (outro.TryGetComponent(out Bateria bateria)) bateria.Ligar();
}
}
}
No Prefab Mode, crie um filho vazio de P_Portal chamado ZonaBateria e
configure:
| Propriedade | Valor | Motivo |
|---|---|---|
| Position (local) | (−3.5, 0, 0) | 2,75 unidades antes da face dos obstáculos (que começa em X = −0,75): a 3 unidades/s, menos de um segundo |
BoxCollider2D → Is Trigger | marcado | o drone precisa atravessar a linha |
BoxCollider2D → Size | (0.2, 12) | altura maior que a pista para qualquer altura sorteada do portal (±1,2) |
| Script | ZonaBateria | liga a bateria de quem entra |
Todo portal gerado traz a sua ZonaBateria, mas só a do primeiro faz
diferença: do segundo em diante a bateria já está ligada, e Ligar não muda
nada. A zona não testa a tag Player porque o teste já está no próprio
TryGetComponent: só um objeto com Bateria reage. Repare que as duas zonas
do prefab usam o mesmo mecanismo (trigger + TryGetComponent) para regras
diferentes; a posição de cada uma no prefab é que decide quando a regra
dispara.
Checkpoint 5 — Pontuação e derrota
Teste sucesso e falha separadamente.
Ao atravessar uma passagem:
- a zona não bloqueia o drone;
- a pontuação aumenta exatamente uma vez;
- a barra, cinza desde o início, fica ciano pouco antes do primeiro portal e só então passa a encolher;
- a barra da bateria cresce quatro cargas, sem passar do máximo;
- outros colliders não aumentam a pontuação.
Ao tocar um obstáculo ou limite:
- a partida muda para derrota;
- novos impulsos deixam de ser aplicados;
- portais param de surgir e de se mover;
- o painel de fim aparece;
- a pontuação final permanece visível.
Se a colisão funciona, mas a pontuação não, confirme três pontos: Is
Trigger marcado na zona, tag Player no drone e ZonaPontuacao anexado ao
mesmo objeto que possui o trigger. Se o drone bate no meio da passagem,
ligue os Gizmos: um collider maior que o sprite indica Auto Tiling
marcado ou Size do collider diferente do Size do Sprite Renderer.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-5.unitypackagetraz o projeto neste ponto (acrescentaZonaPontuacaoe o filhoZonaBateriaao prefab; o botão Reiniciar ainda sem evento). Veja como importar em Pacotes de checkpoint.
Fechar o ciclo: reiniciar a cena
O estado inicial já está descrito pela cena: posição do drone, pontuação zero, painel oculto e ausência de portais gerados. Recarregar a cena restaura tudo isso de uma vez, sem escrever uma rotina de limpeza para cada componente.
- Abra File → Build Profiles.
- Na Scene List, adicione
Aula11.unity(Add Open Scenes). - Selecione
BotaoReiniciar. - Em Button → On Click(), pressione
+. - Arraste o objeto
ControladorJogopara o campo do evento. - Escolha
ControladorJogo → ReiniciarPartida().
O código recarrega a cena pelo nome. Ela precisa constar da Scene List
para que SceneManager.LoadScene a encontre em uma build. Confira também que
o EventSystem usa Input System UI Input Module, para o clique do botão
funcionar com o sistema de entrada configurado.
O botão chama ReiniciarPartida direto. Pelo teclado e pelo controle, o
caminho é ControleDrone → SolicitarReinicio, que acrescenta uma espera
(Espera Reinicio = 0.6 s): quem estava apertando espaço freneticamente no
instante da colisão não reinicia sem querer, antes de ver o placar.
Checkpoint 6 — Jogo completo
Jogue pelo menos três partidas seguidas e confira:
- o drone aguarda o primeiro comando sem cair e o aviso desaparece ao começar;
- a pontuação sempre começa em zero e a bateria, cheia;
- os impulsos antes do primeiro portal não gastam carga (barra cinza);
- um toque por arco mantém a bateria alta; rajadas de toques a esvaziam em poucos portais;
- a dificuldade inicial é possível para quem ainda está aprendendo;
- cada portal concede no máximo um ponto;
- colisões encerram a partida uma única vez;
- o botão reinicia com o mouse, e espaço ou botão sul reiniciam depois de 0,6 s;
- um clique do mouse no fim de jogo, fora do botão, não reinicia;
- o painel volta a ficar oculto e drone e portais retornam ao estado inicial;
- uma partida longa não acumula portais fora da tela;
- o Console permanece sem erros ou avisos.
📦 Ficou para trás? O pacote
aula-11-drone-gate-checkpoint-6.unitypackagetraz o projeto neste ponto (jogo completo, com o botão Reiniciar ligado; falta só adicionar a cena à Scene List em File → Build Profiles → Add Open Scenes). Veja como importar em Pacotes de checkpoint.
Revisão arquitetural
O jogo completo usa vários componentes pequenos porque cada mudança tem um motivo diferente:
| Mudança desejada | Onde alterar | Por quê |
|---|---|---|
| drone subir mais | intensidadeImpulso em ControleDrone | é uma regra do movimento do jogador |
| portais andarem mais rápido | velocidade em MovimentoPortal | pertence a cada obstáculo |
| portais surgirem mais vezes | intervalo em GeradorPortais | é uma regra de geração |
| aumentar a pontuação | ControladorJogo | ele é o dono desse estado |
| mudar o custo do impulso ou a recarga | capacidade e recargaPorPortal em Bateria | a carga é um recurso do drone |
| decidir quando a bateria passa a gastar | posição da ZonaBateria no prefab | é uma interação espacial, como a pontuação |
| mostrar a carga | IndicadorBateria | a interface lê o estado, não o altera |
| reconhecer uma travessia | ZonaPontuacao | é a interação espacial que dispara a regra |
| mostrar a derrota | ControladorJogo | a interface responde à mudança de estado |
| trocar a arte | Sprite Renderer do Visual e dos obstáculos | aparência não interfere nas regras de física |
Os campos serializados são dados configuráveis: intensidade, velocidade, intervalo e limites podem ser ajustados no Inspector. Os métodos expressam comportamentos e regras: aplicar impulso, criar, pontuar, perder. Separar esses dois tipos torna possível calibrar o jogo sem reescrever o algoritmo.
Instantiate e Destroy: o ciclo de vida das instâncias
O asset P_Portal é um molde persistente no projeto. Cada chamada a
Instantiate produz uma instância independente na cena. Destroy remove a
instância; o molde continua disponível para as próximas gerações.
A Figura 8 separa visualmente o asset persistente das instâncias temporárias. Essa distinção ajuda a interpretar a janela Project, que contém o molde, e a Hierarchy, que contém os objetos existentes naquele momento da partida.
Esse fluxo é adequado à pequena escala da aula e torna o ciclo visível na Hierarchy. Jogos que criam e removem milhares de objetos podem usar object pooling, isto é, desativar e reaproveitar instâncias. Essa otimização é uma extensão, não um requisito deste projeto.
Diagnóstico por sintomas
| Sintoma | Verificação mais provável |
|---|---|
| Drone não responde | ação não atribuída ou habilitada; janela Game sem foco; referência do controlador vazia |
| Drone para de responder no meio da partida | bateria vazia (confira a barra); só um portal atravessado a recarrega |
| Barra encolhe para o centro | Pivot X do Preenchimento diferente de 0 |
| Barra não muda ou só mostra cheia e vazia | referências do IndicadorBateria vazias; Fracao sem o (float) (divisão inteira) |
| Barra cinza a partida toda; a bateria nunca gasta | ZonaBateria ausente do prefab, sem Is Trigger ou sem o script; alteração feita na instância, não no prefab |
| Bateria gasta desde o primeiro toque | Ligada começando verdadeira; Consumir sem o teste if (!Ligada) |
| Bateria não recarrega | falta a linha do Recarregar em ZonaPontuacao; Bateria fora do objeto que tem o collider do drone |
| Drone sobe de forma incontrolável | impulso alto demais; velocidade vertical não zerada antes do impulso |
| Drone atravessa o portal | componente 2D ausente; Is Trigger marcado no obstáculo; layer sem colisão |
| Drone bate no meio da passagem | collider maior que a arte: Auto Tiling marcado ou Size do collider ≠ Size do sprite (ligue os Gizmos) |
| Borda do portal ou dos limites esticada | escala usada no lugar de Draw Mode = Sliced; borda não definida no Sprite Editor; Mesh Type ≠ Full Rect |
| Listras ou desenhos do portal alongados, diferentes em cada peça | sprite com detalhe no miolo ou variando ao longo das bordas: no Sliced só os cantos ficam intactos (use arte lisa no miolo ou Draw Mode = Tiled) |
| Arte borrada | Filter Mode = Bilinear; compressão ativada; PPU diferente de 18 |
| Portal se desmonta ao mover | MovimentoPortal anexado a um filho, não ao pai |
| Nenhum portal aparece | prefabPortal não atribuído; intervalo inválido; partida já encerrada ou não iniciada |
| Algumas passagens são impossíveis | limites verticais ignoram o tamanho da passagem e do drone |
| Pontuação nunca muda | trigger desmarcado; tag Player ausente; script no objeto errado |
| Um portal soma mais de um ponto | falta da proteção pontoRegistrado |
| Jogo continua após a colisão | componente não consulta PartidaAtiva |
| Botão não reinicia | evento não ligado; cena ausente da Scene List do Build Profile |
| Espaço não reinicia | SolicitarReinicio não chamado em AoImpulsionar; tempo menor que esperaReinicio |
| Textos aparecem como quadrados ou vazios | TMP Essential Resources não importados |
| Ajustes “somem” ao parar | valores foram alterados durante o Play Mode |
Tutorial antigo gera InvalidOperationException | código usa Input.GetKey/Input.GetAxis com o Input Manager legado |
Faça o diagnóstico na ordem: reproduza o sintoma, leia a primeira exceção do Console, selecione o objeto envolvido, confira componentes e referências e só então altere o código. Mudar várias coisas ao mesmo tempo torna impossível saber qual delas corrigiu — ou escondeu — o problema.
Experimentos de calibração
Depois que o jogo básico estiver estável, altere uma propriedade de cada vez e registre o efeito:
- aumente e diminua a intensidade do impulso;
- ajuste a gravidade do
Rigidbody2D; - varie a velocidade dos portais;
- varie o intervalo de geração;
- mude o tamanho da passagem no prefab (posições Y dos obstáculos);
- mude a capacidade e a recarga da bateria: com Recarga Por Portal = 3, o jogo ainda perdoa quem faz um arco por toque? E com Recarga Por Portal = 5, as rajadas ainda são punidas?
- mova a
ZonaBateriado prefab para X = −12 (bem antes do portal) e depois para X = −1,5 (quase na face): quantas cargas custa o trecho até a primeira passagem em cada caso, e o que muda na sensação do começo da partida?
O jogo se torna injusto quando uma combinação exige reação mais rápida do que o jogador consegue produzir ou cria uma trajetória fisicamente impossível. O objetivo da calibração não é maximizar a dificuldade: é produzir uma relação clara entre ação, resposta e consequência.
Extensões opcionais
Implemente uma extensão somente depois de todos os checkpoints do ciclo básico funcionarem.
- Portais que oscilam. Faça a passagem subir e descer enquanto o portal
anda. Em
MovimentoPortal, guarde o Y inicial emAwakee, emFixedUpdate, passe aMovePositionum Y igual ao inicial maisamplitude * Mathf.Sin(frequencia * tempo), somandoTime.fixedDeltaTimeatempoa cada passo. Cuide da faixa: as bordas da passagem já vão até ±2,95 e os limites da pista estão em ±4,5, então a amplitude não pode passar de 1,5. - Correntes de vento. Crie uma faixa invisível com
BoxCollider2D(Is Trigger e Used By Effector marcados) e umAreaEffector2Dcom Force Angle = 270 e Force Magnitude entre 5 e 10: dentro dela o drone é empurrado para baixo e gasta mais carga. É outro uso do trigger: em vez de avisar um script, a região aplica uma força. Como filho do prefab, a corrente anda com o portal. - Cristais coletáveis. Um cristal pequeno (Assets → Create → 2D →
Sprites → Circle, com cor) com
CircleCollider2Dem modo trigger e um script que, emOnTriggerEnter2D, devolve cargas à bateria de quem tem a tagPlayere chamaDestroy(gameObject). Acrescente àBateriaum método que some uma quantidade qualquer, com o mesmoMathf.Min. Posicione os cristais fora do centro da passagem: vale o desvio? - Inclinação visual. Incline a arte conforme a velocidade vertical: nariz
para cima ao subir, para baixo ao cair, sem liberar a rotação do corpo. O
esqueleto
InclinacaoVisual.csvai no filhoVisuale tem trêsTODO: convertercorpo.linearVelocity.ynum ângulo comMathf.Clamp(6° por unidade/s, entre −60° e 30°), suavizar comMathf.MoveTowardsAngle— que trata a volta de 360°, ao contrário deMoveTowards— e aplicar o resultado emtransform.localRotation = Quaternion.Euler(0f, 0f, angulo). Com os Gizmos ligados, o círculo do collider não gira: Freeze Rotation Z continua no pai. Se você já fez a Atividade 1, repare que o farol inclina junto.
Spritesheets, animação quadro a quadro, Tilemap e Pixel Perfect Camera ficam para a Aula 12. Áudio, partículas, menus, adaptação de interface, persistência de recorde e geração de uma build ficam para a Aula 14.
Atividades de Laboratório
As três atividades partem do jogo completo do Checkpoint 6. A primeira
escurece a pista em setores alternados e acende um farol no drone; a segunda
faz a partida acelerar conforme o placar cresce; a terceira dá profundidade ao
cenário com camadas de parallax. Antes de começar, salve uma
cópia da cena com File → Save As em Assets/Aula11/Aula11-Pratica.unity e
trabalhe nela.
Atividade 1 — Setor escuro com farol
Objetivo. A cada 5 pontos, a pista alterna entre um setor claro e um escuro. No escuro, a luz global cai para 0,2 e um farol à frente do drone acende; ao voltar ao claro, o farol apaga. As duas mudanças são graduais.
Do Módulo 1 para a Unity: luzes 2D
Os sprites respondem à luz porque o template Universal 2D os desenha com o
material Sprite-Lit-Default, cujo shader multiplica a cor de cada texel
pela luz acumulada naquele ponto — o papel do GL_LIGHTING ligado com um
material, na Aula 08. Um
sprite com Sprite-Unlit-Default ignora as luzes. A interface, num Canvas
Screen Space - Overlay, também não é iluminada: placar e barra continuam
legíveis no escuro.
| OpenGL (Aula 08) | Luz 2D do URP |
|---|---|
luz ambiente global, glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ...): sem posição nem direção, igual em todo ponto | Global Light 2D: ilumina todos os sprites por igual; a Intensity escala essa contribuição |
spot: GL_SPOT_DIRECTION e GL_SPOT_CUTOFF, o meio-ângulo do cone | Spot Light 2D: aponta para o +Y local; Inner / Outer Spot Angle são a abertura total (360 = círculo), e entre os dois fica a penumbra |
| atenuação | Radius (Inner e Outer): intensidade plena até o raio interno, caindo até zero no externo; Falloff Strength ajusta a curva da queda |
Roteiro
Passo 1 — O farol.
Clique com o botão direito no filho Visual do Drone e escolha Light →
Spot Light 2D; renomeie para Farol. Configure:
| Propriedade | Valor | Motivo |
|---|---|---|
| Position (local) | (0.35, 0, 0) | à frente do drone |
| Rotation (local) | (0, 0, −90) | o spot aponta para +Y; −90° em Z o faz apontar para +X |
| Color | #FFEDBF | branco quente, distinto do céu roxo |
| Intensity | 0 | começa apagado; o script o acende |
| Radius | Inner 0.5, Outer 8 | alcança os portais que se aproximam |
| Inner / Outer Spot Angle | 40 / 80 | cone com penumbra |
| Falloff Strength | 0.5 | queda suave até o raio externo |
| Shadows | desmarcado | não há objetos projetando sombra |
Para ver o efeito antes do código, mude temporariamente a Intensity da
Global Light 2D para 0.2 e a do Farol para 1.6; depois volte a 1 e 0.
Passo 2 — Traga o script.
📥 Baixe o arquivo-base:
SetorEscuro.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace Aula11
{
/// <summary>A cada tantos pontos, a pista alterna entre um setor claro e um escuro, iluminado só pelo farol.</summary>
public class SetorEscuro : MonoBehaviour
{
[Header("Referências")]
[SerializeField] private ControladorJogo controladorJogo;
[Tooltip("Global Light 2D da cena: faz o papel da luz ambiente.")]
[SerializeField] private Light2D luzGlobal;
[Tooltip("Spot Light 2D filha do Visual do drone.")]
[SerializeField] private Light2D farol;
[Header("Setores")]
[Min(1)] [SerializeField] private int pontosPorSetor = 5;
[Header("Intensidades")]
[Min(0f)] [SerializeField] private float intensidadeClara = 1f;
[Min(0f)] [SerializeField] private float intensidadeEscura = 0.2f;
[Min(0f)] [SerializeField] private float intensidadeFarol = 1.6f;
[Tooltip("Variação máxima de intensidade por segundo, nas duas luzes.")]
[Min(0f)] [SerializeField] private float velocidadeTransicao = 1.5f;
private void Update()
{
if (controladorJogo == null || luzGlobal == null || farol == null) return;
// TODO 1: descubra se o setor atual é escuro. De 0 a 4 pontos o setor é claro,
// de 5 a 9 é escuro, de 10 a 14 volta a ser claro, e assim por diante.
bool escuro = false;
// TODO 2: escolha a intensidade-alvo de cada luz conforme o setor
// (no escuro a luz global cai e o farol acende; no claro, o contrário).
float alvoGlobal = intensidadeClara;
float alvoFarol = 0f;
// TODO 3: aproxime a intensidade atual de cada luz do seu alvo, variando no
// máximo velocidadeTransicao por segundo.
}
}
}
Arraste o arquivo para Assets/Aula11/Scripts. Crie um objeto vazio
Iluminacao na raiz da cena, adicione Setor Escuro e arraste
ControladorJogo para Controlador Jogo, Global Light 2D para Luz
Global e Farol para Farol. O esqueleto compila e ainda não faz nada.
Passo 3 — Qual setor (TODO 1).
A divisão inteira controladorJogo.Pontuacao / pontosPorSetor dá o índice do
setor: 0 para 0 a 4 pontos, 1 para 5 a 9, 2 para 10 a 14. O resto da divisão
por 2 alterna entre 0 e 1: (controladorJogo.Pontuacao / pontosPorSetor) % 2 == 1
é verdadeiro nos setores escuros.
Passo 4 — Alvos (TODO 2).
Com o operador ternário, escuro ? intensidadeEscura : intensidadeClara para
a luz global e escuro ? intensidadeFarol : 0f para o farol.
Passo 5 — Transição (TODO 3).
Atribuir o alvo direto faria a pista escurecer num quadro. Use
Mathf.MoveTowards(luzGlobal.intensity, alvoGlobal, velocidadeTransicao * Time.deltaTime)
e o mesmo para o farol. MoveTowards anda no máximo o terceiro argumento
por chamada e para exatamente no alvo; com 1,5 por segundo, ir de 1 a 0,2
leva cerca de 0,5 s.
Passo 6 — Teste. Durante o Play, reduza Pontos Por Setor para 1 no Inspector para ver a alternância a cada portal. Volte a 5 ao terminar.
A Figura 9 mostra o resultado num setor escuro.

Checklist de conclusão
- De 0 a 4 pontos a pista é clara e o farol está apagado; de 5 a 9 fica escura com o farol aceso; aos 10 volta a clarear.
- As transições são graduais, sem saltos.
- O cone aponta para a frente do drone e se move com ele.
- Placar, barra e painel de fim continuam legíveis no escuro.
- Reiniciar volta a partida ao setor claro.
- O Console não mostra erros.
Para responder
- Na Aula 08,
GL_SPOT_CUTOFF = 25produz um cone de que abertura total? Que Outer Spot Angle daria o mesmo cone na Unity? - Troque o material do
Fundopara Sprite-Unlit-Default e entre num setor escuro. O que acontece e por quê? - O
Farolé filho doVisual, não doDrone. Que diferença isso faz se você implementar a inclinação visual das extensões opcionais?
Atividade 2 — Dificuldade progressiva
Objetivo. A cada 5 pontos, os portais ficam 0,5 unidade/s mais rápidos, até o limite de 5 unidades/s — sem mudar a distância de 7,2 unidades entre portais consecutivos.
O problema a resolver. Hoje a velocidade mora em cada instância
(MovimentoPortal) e o intervalo mora no gerador. Aumentar só a velocidade
com o mesmo intervalo afasta os portais (o último exercício dissertativo faz
essa conta); e acelerar só os portais novos faz cada portal rápido
alcançar o lento da frente, encurtando a distância até fechar a reação do
jogador. A regra correta é: todos os portais andam na velocidade atual, e
o intervalo é calculado a partir da distância desejada,
Ponto de partida. O jogo completo e os trechos abaixo, com lacunas TODO.
Roteiro
Passo 1 — Ler a pontuação.
O gerador precisa ler o placar, mas não pode alterá-lo. A propriedade só de
leitura Pontuacao do ControladorJogo, usada também na Atividade 1, é
exatamente isso; não há nada a acrescentar.
Passo 2 — Velocidade configurável por instância (TODO A).
No MovimentoPortal, acrescente:
/// <summary>Chamado pelo gerador logo depois de Instantiate.</summary>
public void DefinirVelocidade(float novaVelocidade)
{
// TODO A: guarde a nova velocidade no campo velocidade, sem aceitar valor negativo.
}
O prefab continua com Velocidade = 3 no Inspector: é o valor que vale até alguém chamar o método.
Passo 3 — Configurar a instância recém-criada (TODO B).
No GeradorPortais, troque o bloco Tempo pelos campos abaixo e substitua o
Update:
[Header("Tempo e espaço")]
[Min(0f)] [SerializeField] private float atrasoInicial = 1f;
[Tooltip("Distância, em unidades, entre dois portais consecutivos.")]
[Min(1f)] [SerializeField] private float distanciaEntrePortais = 7.2f;
[Header("Dificuldade progressiva")]
[Min(0f)] [SerializeField] private float velocidadeInicial = 3f;
[Min(0f)] [SerializeField] private float acrescimoPorNivel = 0.5f;
[Min(1)] [SerializeField] private int pontosPorNivel = 5;
[Min(0f)] [SerializeField] private float velocidadeMaxima = 5f;
// (bloco Posição e campo tempoAteProximo continuam iguais)
private float velocidadeAplicada;
private void Update()
{
if (controladorJogo == null || prefabPortal == null || !controladorJogo.PartidaAtiva) return;
if (!controladorJogo.PartidaIniciada)
{
tempoAteProximo = atrasoInicial;
return;
}
float velocidade = VelocidadeAtual();
if (!Mathf.Approximately(velocidade, velocidadeAplicada)) AplicarVelocidade(velocidade);
tempoAteProximo -= Time.deltaTime;
if (tempoAteProximo > 0f) return;
float altura = Random.Range(alturaMinima, alturaMaxima);
GameObject portal = Instantiate(prefabPortal, new Vector3(posicaoX, altura, 0f), Quaternion.identity, transform);
// TODO B: obtenha o MovimentoPortal da instância criada e defina a velocidade atual.
// TODO C: calcule o próximo intervalo a partir da distância e da velocidade.
}
private float VelocidadeAtual()
{
int nivel = controladorJogo.Pontuacao / pontosPorNivel;
return Mathf.Min(velocidadeInicial + acrescimoPorNivel * nivel, velocidadeMaxima);
}
Instantiate devolve o GameObject criado, e GetComponent<MovimentoPortal>()
chega ao script dele. Esse é o jeito de configurar uma instância que o
prefab não conhece de antemão; a outra via, o próprio portal perguntar a
velocidade a alguém, repetiria o FindFirstObjectByType sem necessidade. A
divisão inteira em VelocidadeAtual transforma pontos em nível: de 0 a 4
pontos, nível 0; de 5 a 9, nível 1; e assim por diante.
Passo 4 — Todos juntos (TODO D).
Acrescente o método que o Update chama quando a velocidade muda:
private void AplicarVelocidade(float velocidade)
{
// TODO D: defina a nova velocidade em todos os portais já existentes
// (os filhos deste objeto).
// O tempo que faltava foi medido na velocidade antiga; reescalá-lo mantém a distância.
if (velocidadeAplicada > 0f) tempoAteProximo *= velocidadeAplicada / velocidade;
velocidadeAplicada = velocidade;
}
Como cada portal nasceu filho do gerador, foreach (Transform portal in transform)
percorre exatamente os portais vivos. A linha que reescala tempoAteProximo
corrige um detalhe fino: se faltava 1 s para o próximo portal na velocidade
antiga, na velocidade nova falta proporcionalmente menos para percorrer a
mesma distância.
Passo 5 — Teste.
Com Gizmos e Stats ligados, jogue até 15 pontos. Para conferir a
distância sem precisar jogar tão bem, reduza temporariamente Pontos Por
Nivel para 1 no Inspector durante o Play e observe a posição X dos filhos de
GeradorDePortais.
Checklist de conclusão
- Até 4 pontos, os portais andam a 3 unidades/s; a partir de 5, a 3,5; a partir de 25, param de acelerar em 5.
- Quando a velocidade muda, os portais que já estão na tela aceleram junto com os novos.
- A distância entre portais consecutivos permanece 7,2 unidades em qualquer velocidade.
- O prefab continua com Velocidade = 3 no Inspector; nenhuma instância altera o asset.
- Reiniciar volta a partida para a velocidade inicial.
- O Console não mostra erros.
Para responder
- Remova, de propósito, a chamada a
AplicarVelocidade(deixe só a configuração do portal novo). Jogue até mudar de nível e descreva o que acontece com a distância entre o último portal lento e o primeiro rápido. - Por que
DefinirVelocidadealtera a instância e não o asset de prefab? O que mudaria se o gerador alterasse o campo no próprioprefabPortal? - Flappy Bird não acelera: a dificuldade é constante do primeiro ao último cano. Cite uma vantagem de cada escolha para o jogador.
- Com os portais a 5 unidades/s e a mesma distância de 7,2 unidades, quantas cargas por portal gasta quem faz um arco por toque? A bateria fica mais folgada ou mais apertada do que a 3 unidades/s?
Atividade 3 — Parallax: profundidade num cenário 2D
Objetivo. Trocar o Fundo parado por três camadas que deslizam para a
esquerda, cada uma numa fração da velocidade dos portais: o céu a 10 %, o
planeta a 25 % e as montanhas a 50 %. O que está mais longe anda mais devagar,
e a pista ganha profundidade sem sair do 2D.
Do Módulo 1 para a Unity: parallax e projeção perspectiva
Na Aula 05, a projeção perspectiva leva um ponto a . Se um objeto à profundidade se desloca no mundo, a imagem dele se desloca
isto é, tanto menos quanto mais longe ele está. É a paralaxe: da janela
de um ônibus, o poste passa rápido e a montanha quase não se move. A câmera
de Drone Gate, porém, é ortográfica (o glOrtho da Aula 05): não
depende de , e um fundo que andasse junto com os portais pareceria colado
neles. O parallax 2D devolve o termo que a projeção ortográfica perdeu: cada
camada recebe um fator entre 0 e 1 e anda a vezes a
velocidade dos portais. é a profundidade dos portais; é o
infinito, que não se move (Figura 10a).
| Camada | Sprite | Fator | Profundidade equivalente | Order in Layer |
|---|---|---|---|---|
CamadaCeu | T_Ceu | 0,1 | −12 | |
CamadaPlaneta | T_Planeta | 0,25 | −11 | |
CamadaMontanhas | T_Montanhas | 0,5 | −10 |
A ordem de desenho continua sendo a do algoritmo do pintor: o que está mais longe tem Order in Layer menor. Profundidade e ordem de desenho são decisões separadas na Unity 2D — o fator decide o movimento, o Order in Layer decide quem cobre quem.
Uma imagem que nunca acaba
Cada camada é um único Sprite Renderer com Draw Mode = Tiled: a Unity
repete a imagem até preencher o Size pedido, sem esticá-la (o contrário do
Sliced). Com Size de três imagens de largura, a camada cobre de
a , com unidades. A janela da
câmera vai de −8,89 a 8,89, ou seja, exatamente . Depois de a camada
andar para a esquerda, sua borda direita chega a : a
borda da câmera. Nesse instante, o script a devolve para a direita. Como
a imagem se repete a cada , a tela mostra exatamente o mesmo conteúdo antes
e depois do salto, e ninguém o percebe (Figura 10b). É o x % L que você
usaria em Python para dar a volta num índice.
Ponto de partida. O jogo completo e o esqueleto abaixo, com três lacunas
TODO. Os sprites T_Ceu, T_Planeta e T_Montanhas já vêm no pacote
inicial, em Assets/Aula11/Artes/Sprites; são as três partes que, somadas,
formam o T_Fundo.
Roteiro
Passo 1 — Importação. Selecione os três sprites e confira PPU 18, Filter Mode = Point e compressão desligada, como nos outros. Em Mesh Type, escolha Full Rect: com Tight, a Unity avisa que o modo Tiled não funciona e desenha a imagem uma vez só.
Passo 2 — As camadas.
Desative o Fundo (desmarque a caixa ao lado do nome, no Inspector). Dentro
de Cenario, crie três objetos com Sprite Renderer, CamadaCeu,
CamadaPlaneta e CamadaMontanhas, conforme a tabela acima, todos em
Position (0, 0, 0), com Draw Mode = Tiled, Tile Mode = Continuous e
Size = (53.33, 10) — três imagens de largura e a altura de uma
(180/18 = 10). A cena deve ficar igual à de antes: as camadas sobrepostas
recompõem o T_Fundo.
Passo 3 — O script (TODOs 1 a 3).
Crie Assets/Aula11/Scripts/ParallaxCamada.cs a partir do esqueleto:
📥 Baixe o arquivo-base:
ParallaxCamada.cs
using UnityEngine;
namespace Aula11
{
/// <summary>
/// Desloca uma camada do fundo para a esquerda, mais devagar quanto mais distante ela está,
/// e a recoloca a cada imagem percorrida. O Sprite Renderer usa Draw Mode = Tiled.
/// </summary>
[RequireComponent(typeof(SpriteRenderer))]
public class ParallaxCamada : MonoBehaviour
{
[Header("Referência")]
[SerializeField] private ControladorJogo controladorJogo;
[Header("Movimento")]
[Tooltip("Velocidade dos portais, em unidades/s.")]
[Min(0f)] [SerializeField] private float velocidadeReferencia = 3f;
[Tooltip("Fração da velocidade dos portais: 0 = parada, como no infinito; 1 = junto com os portais.")]
[Range(0f, 1f)] [SerializeField] private float fator = 0.5f;
private float larguraImagem;
private float xInicial;
private void Awake()
{
// TODO 1: guarde em larguraImagem a largura, em unidades, de UMA cópia da imagem
// (o sprite, não o Size do Sprite Renderer em modo Tiled).
xInicial = transform.position.x;
}
private void Update()
{
if (controladorJogo == null || !controladorJogo.PartidaAtiva) return;
// TODO 2: desloque a camada para a esquerda a velocidadeReferencia * fator
// unidades por segundo.
// TODO 3: quando a camada tiver andado uma imagem inteira desde xInicial,
// devolva-a uma largura para a direita. A cópia seguinte ocupa o
// lugar da anterior, e o salto não aparece.
}
}
}
- TODO 1. A largura de uma imagem está no próprio sprite:
GetComponent<SpriteRenderer>().sprite.bounds.size.x. Osizedo Sprite Renderer em modo Tiled é o tamanho total (53,33), não o de uma cópia. - TODO 2. Mesmo padrão do
MovimentoPortal: velocidade vezes o tempo do quadro, somado atransform.positioncomVector3.left. - TODO 3. Compare
transform.position.xcomxInicial - larguraImageme, quando alcançar, someVector3.right * larguraImagem.
A camada não tem collider nem Rigidbody2D: é só aparência. Por isso ela
muda o Transform diretamente em Update, com Time.deltaTime, enquanto os
portais, que têm colliders, andam por MovePosition em FixedUpdate. O
teste de PartidaAtiva faz o fundo parar junto com os portais na derrota.
Passo 4 — Ligar e testar.
Adicione ParallaxCamada às três camadas, arraste ControladorJogo para o
campo de referência e ajuste Fator conforme a tabela. Em Play, selecione
CamadaMontanhas e observe o X no Inspector: ele desce de 0 até −17,78 e
volta a 0, sem que a Game view mostre salto. Para ver o salto de propósito,
troque temporariamente o Size para (35.56, 10) — duas imagens.
Checklist de conclusão
- Antes da partida e na derrota, o fundo fica parado junto com os portais.
- As montanhas andam mais rápido que o planeta, e o planeta mais que as estrelas; nenhuma camada anda tão rápido quanto os portais.
- Não aparece emenda, faixa vazia nem salto em nenhuma camada, mesmo depois de vários minutos.
- O
Fundodesativado não aparece, e as camadas recompõem a mesma imagem de antes. - Se você fez a Atividade 1, as três camadas escurecem no setor escuro.
- O Console não mostra erros nem o aviso do modo Tiled.
Para responder
- Pela tabela, as montanhas estão a . Que fator teria uma camada de
nuvens a , pouco atrás dos portais? E uma camada à frente
dos portais, a — o fator poderia passar de 1? O que o
[Range(0f, 1f)]impede? - Com Size de duas imagens em vez de três, a partir de que deslocamento aparece uma faixa vazia na borda direita da tela? Justifique com as bordas da câmera.
- Se você fez a Atividade 2, os portais aceleram mas as camadas continuam
com
velocidadeReferencia = 3. O que acontece com a ilusão de profundidade? Como a camada poderia ler a velocidade atual sem conhecer o gerador? - Por que as camadas podem mudar o
TransformemUpdate, e os portais precisam deMovePositionemFixedUpdate?
Exercícios (checkpoints)
Questões dissertativas
O drone realmente avança pelo mundo? Explique o movimento relativo usado no jogo e duas vantagens dessa decisão.
Por que o drone usa Rigidbody2D dinâmico e impulso, enquanto o portal usa Rigidbody2D cinemático e MovePosition?
Compare o obstáculo e a zona de pontuação: ambos têm Collider2D, mas por que somente um deles deve ser trigger?
Com câmera ortográfica de Size 5, resolução 1280 × 720 e sprites com PPU 18, quantos pixels da tela ocupa cada texel? Relacione o cálculo com o mapeamento janela → viewport da Aula 03 e explique o que mudaria com PPU 100.
Todo impulso deixa o drone subindo a 5,5 unidades/s. Por que, então, apertar em rajadas gasta mais bateria do que um toque por arco?
Por que o ControladorJogo protege RegistrarDerrota e AdicionarPonto com testes de PartidaAtiva e PartidaIniciada?
A velocidade dos portais passou de 3 para 5 unidades/s, mas o intervalo continuou em 2,4 s. O que muda na distância espacial entre portais e no tempo de reação?
Questões objetivas
1. Qual é a principal vantagem de o código ouvir a ação Impulsionar em vez de consultar diretamente a barra de espaço?
- a)A ação aplica física automaticamente
- b)A ação desacopla a intenção dos dispositivos e aceita vários bindings sem mudar o código do drone
- c)A barra de espaço não existe no Input System
- d)A ação elimina a necessidade de habilitação
- e)A ação faz Update rodar mais rápido
2. O que é necessário para que a interação 2D entre o drone e a ZonaPontuacao gere OnTriggerEnter2D?
- a)Os dois objetos precisam de Rigidbody2D
- b)A zona precisa ser um collider 3D
- c)Pelo menos um objeto precisa de Rigidbody2D e a zona deve ter Collider2D com Is Trigger
- d)O portal precisa ser filho do drone
- e)Time.timeScale precisa ser zero
3. Onde MovimentoPortal deve ser anexado?
- a)Em cada obstáculo filho
- b)Somente na ZonaPontuacao
- c)No objeto pai P_Portal
- d)Na câmera
- e)No sprite do drone
4. O que Destroy(gameObject) faz quando executado em uma instância de P_Portal?
- a)Apaga o asset do projeto
- b)Remove a instância da cena ao final do quadro e preserva o asset
- c)Remove apenas o Sprite Renderer
- d)Desativa todos os prefabs do projeto
- e)Recarrega a cena
5. Por que a altura aleatória usa alturaMinima e alturaMaxima calibradas?
- a)Random.Range não funciona sem campos serializados
- b)Para impedir que a Unity use números negativos
- c)Para garantir variedade sem criar passagens fora da área jogável ou impossíveis
- d)Para manter todos os portais na mesma altura
- e)Para substituir o collider
6. Quem deve alterar o valor de PartidaAtiva?
- a)Qualquer script por meio de um campo public
- b)Somente ControladorJogo, enquanto os demais componentes apenas consultam a propriedade
- c)Somente ZonaPontuacao
- d)O Input System
- e)O prefab do portal
7. Um sprite de 18 × 18 texels com borda de 4 texels é usado como limite de 18 × 0,5 unidades. Qual configuração preserva a espessura da borda?
- a)Scale = (18, 0.5, 1) com Draw Mode = Simple
- b)Pixels Per Unit = 1
- c)Filter Mode = Bilinear
- d)Draw Mode = Sliced com Size = (18, 0.5), borda definida no Sprite Editor e Mesh Type = Full Rect
- e)Order in Layer = 18
8. O IndicadorBateria muda a localScale.x do Preenchimento. O que garante que a barra encolha em direção à borda esquerda?
- a)Anchor Min = (0, 0)
- b)Pivot X = 0 no Preenchimento, que é o ponto fixo da escala, como em T(p)·S·T(−p)
- c)Raycast Target desmarcado
- d)A cor de alerta
- e)O Canvas Scaler em Scale With Screen Size
9. Fundo, portal, drone e limites estão todos em Z = 0. O que garante que o drone apareça na frente do portal?
- a)Order in Layer maior no drone, pois o 2D Renderer desenha do menor para o maior, como no algoritmo do pintor
- b)O depth buffer, que compara o Z de cada pixel
- c)A ordem dos objetos na Hierarchy, sempre
- d)O Rigidbody2D dinâmico
- e)A tag Player
Referências
Principais (essenciais)
- Unity Technologies. Unity User Manual 6.3 — 2D game development. Disponível em: docs.unity3d.com/6000.3/Documentation/Manual/Unity2D.html
- Unity Technologies. Unity User Manual 6.3 — 9-slicing e Set up your sprite for 9-slicing. Disponível em: docs.unity3d.com/6000.3/Documentation/Manual/sprite/9-slice/9-slicing.html e set-sprite-9slicing.html
- Unity Technologies. Input System package manual — Actions. Disponível em: docs.unity3d.com/Packages/com.unity.inputsystem@1.20/manual/Actions.html
- Unity Technologies. Scripting API: Rigidbody2D.linearVelocity. Disponível em: docs.unity3d.com/6000.3/Documentation/ScriptReference/Rigidbody2D-linearVelocity.html
- Unity Technologies. Scripting API: Object.Instantiate e Object.Destroy. Disponível em: docs.unity3d.com/6000.3/Documentation/ScriptReference/Object.Instantiate.html e Object.Destroy
- Unity Technologies. Scripting API: SceneManager.LoadScene. Disponível em: docs.unity3d.com/6000.3/Documentation/ScriptReference/SceneManagement.SceneManager.LoadScene.html
- AZEVEDO, E.; CONCI, A.; LETA, F. R. Computação Gráfica. Rio de Janeiro: Elsevier, 2003–2008. 2 v. (004.92 A994c) — capítulos de visualização 2D (mapeamento janela → viewport).
Aprofundamento (opcionais)
- NYSTROM, R. Game Programming Patterns. Genever Benning, 2014 — capítulos “Game Loop”, “Component” e “State”. Disponível em: gameprogrammingpatterns.com
- Unity Technologies. Unity User Manual — Introduction to collision. Disponível em: docs.unity3d.com/6000.3/Documentation/Manual/CollidersOverview.html
- Unity Technologies. Light 2D component reference for URP (Global e Spot Light 2D). Disponível em: docs.unity3d.com/6000.3/Documentation/Manual/urp/2DLightProperties.html
- Unity Technologies. Sprite Renderer component reference (Draw Mode, Order in Layer). Disponível em: docs.unity3d.com/6000.3/Documentation/Manual/sprite/renderer/sprite-renderer-reference.html
- Game Maker's Toolkit. The Unity Tutorial for Complete Beginners. YouTube, 2024. Disponível em: youtube.com/watch?v=XtQMytORBmM — referência estrutural para a construção incremental; adapte exemplos antigos de entrada para o Input System usado nesta aula.