Pular para o conteúdo principal

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, Size da 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 Rigidbody2D e Collider2D em 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 Instantiate e Destroy.
  • 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 1/z1/z 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:

  1. o jogador recebe uma situação inicial;
  2. executa uma ação;
  3. o mundo responde segundo regras;
  4. o jogo reconhece sucesso ou falha;
  5. a interface comunica o resultado;
  6. 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.

AspectoFlappy BirdDrone Gate
comandotoques ilimitadoscada impulso gasta uma carga da bateria; cada portal atravessado devolve quatro
estratégiaacertar o ritmoacertar o ritmo e economizar: um toque por arco gasta metade do que uma rajada de toques
obstáculoscanosportais montados com um único sprite fatiado (9-slice)
cenáriodia ou noite, fixo durante a partidaa cada 5 pontos, um setor escuro iluminado só pelo farol do drone (Atividade 1)
dificuldadeconstanteos portais aceleram conforme o placar cresce (Atividade 2)
reiníciotoqueespaç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.

Ambiente desta 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.

ObjetoComponentes principaisResponsabilidade
DroneRigidbody2D, CircleCollider2D, Bateria, ControleDrone; filho Visual com Sprite Renderercair, receber impulso gastando carga e informar uma colisão fatal
PortalRigidbody2D cinemático, MovimentoPortal; três filhosdeslocar o conjunto e removê-lo fora da tela
Zona de pontuaçãoBoxCollider2D como trigger, ZonaPontuacaodetectar uma travessia válida uma única vez e recarregar a bateria
Zona da bateriaBoxCollider2D como trigger, ZonaBaterialigar a bateria quando o drone chega perto do primeiro portal
Gerador de portaisGeradorPortaiscriar portais em intervalos e alturas controlados
Controlador do jogoControladorJogoguardar estado e pontuação, atualizar a interface e reiniciar
Interfacetextos, barra da bateria (IndicadorBateria), painel de fim e botãocomunicar 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.

Diagrama do fluxo de responsabilidades do jogo. A ação de entrada aciona o controle do drone, que consome uma carga da bateria antes de cada impulso; a barra da interface lê a carga; colisões e a zona de pontuação comunicam-se com o controlador, e a zona também recarrega a bateria; o gerador instancia portais; o controlador atualiza a interface e encerra os sistemas na derrota.
Fluxo de eventos e responsabilidades de Drone Gate. O estado central coordena os sistemas, mas cada componente conserva uma função específica; a bateria é um recurso do drone, gasto no impulso e devolvido na travessia.

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.

Tela do jogo em pixel art: céu roxo estrelado com um planeta de anéis, silhueta de montanhas escuras embaixo, faixas metálicas nas bordas superior e inferior, o drone branco com hélices azuis à esquerda, dois portais formados por colunas azul-escuras com moldura metálica e frisos amarelo e ciano, o número 3 no alto, ao centro, e no canto superior esquerdo uma barra ciano preenchida em cerca de sete décimos.
Drone Gate durante uma partida, renderizado pela câmera da cena em 1280 × 720. Cada texel da arte ocupa exatamente 4 × 4 pixels; a barra no canto é a bateria (7 de 10 cargas).

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.unitypackage traz os sprites já configurados (os três do jogo e as três camadas da Atividade 3) e a cena Aula11-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:

  1. Use um projeto criado como em Criar o projeto 2D do zero, com o Input System ativo e os TMP Essential Resources importados.
  2. Apague antes a pasta Assets/Aula11 do projeto (ou importe num projeto novo). Os scripts e o Controles.inputactions do 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.
  3. Assets → Import Package → Custom Package, escolha o pacote e clique em Import.
  4. Abra Assets/Aula11/Aula11-CheckpointN.unity (N é o número do checkpoint). Se quiser, salve uma cópia com File → Save As em Assets/Aula11/Aula11.unity e continue a partir dela.
RecursoArquivo ou origemUso nesta aula
Fundo espacialT_Fundo.png, 320 × 180 pxuma imagem parada atrás do jogo
DroneT_Drone.png, 18 × 14 px, transparênciaaparência do personagem
Módulo do portalT_Portal.png, 18 × 18 px, borda de 4 pxobstáculos do portal e limites da pista, fatiados em 9-slice
Camadas do fundoT_Ceu.png, T_Planeta.png, T_Montanhas.png, 320 × 180 pxparallax da Atividade 3; somadas, formam o T_Fundo
Zona de pontuaçãosem imagemBoxCollider2D invisível, configurado como trigger
Placar e mensagensTextMeshPro da Unitytextos da interface
Barra da bateriaduas Images da UIcarga restante
BotãoUI da Unityreinício da partida
EntradaControles.inputactions, criado no Editorespaço, clique e botão sul do controle
Áudio e animaçãonão usadosficam 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​

  1. Abra o Unity Hub e escolha New project.
  2. Selecione Unity 6.3 LTS — 6000.3.24f1 e o template Universal 2D.
  3. Dê ao projeto o nome drone-gate e clique em Create project.
  4. No Editor, abra Window → Package Management → Package Manager e confirme que Input System está instalado.
  5. Em Edit → Project Settings → Player, confirme Active Input Handling = Input System Package (New). Aceite a reinicialização, se solicitada.
  6. Importe os recursos do TextMeshPro em Window → TextMeshPro → Import TMP Essential Resources. A interface do pacote inicial depende deles.
  7. Crie a pasta Assets/Aula11 e, dentro dela, Prefabs e Scripts.

Agora, conforme o caminho escolhido:

  • Pacote inicial: Assets → Import Package → Custom Package, escolha aula-11-drone-gate-inicio.unitypackage e clique em Import. Abra Assets/Aula11/Aula11-Inicio.unity e salve uma cópia com File → Save As em Assets/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 cena Assets/Scenes/SampleScene.unity que o template criou e salve-a com File → Save As em Assets/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:

PropriedadeValorPor quê
Texture TypeSprite (2D and UI)a imagem vira um sprite utilizável pelo Sprite Renderer
Sprite ModeSingleum desenho por arquivo; spritesheets ficam para a Aula 12
Pixels Per Unit1818 texels da imagem ocupam 1 unidade do mundo
Mesh TypeFull Rect em T_Portal; Tight nos outroso 9-slice exige Full Rect
Filter ModePoint (no filter)cada texel vira um quadrado nítido, sem borrar a borda
CompressionNonecompressã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.

Inspector do Unity mostrando as opções de importação de T_Portal: Texture Type Sprite (2D and UI), Sprite Mode Single, Pixels Per Unit 18, Mesh Type Full Rect, Filter Mode Point (no filter) e Compression None.
Importação de um sprite em pixel art: PPU 18, filtro Point e sem compressão. No portal, Mesh Type = Full Rect prepara o sprite para o 9-slice.

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​

Pacote inicial

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.

  1. Selecione T_Portal e 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.)
  2. Arraste T_Portal para Cenario e renomeie para LimiteSuperior.
  3. No Sprite Renderer, escolha Draw Mode = Sliced e Size = (18, 0.5). Deixe Scale = (1, 1, 1) e Order in Layer = 3.
  4. Position = (0, 4.75, 0): o limite ocupa de 4,5 a 5,0 em Y.
  5. Adicione BoxCollider2D com Size = (18, 0.5), Offset = (0, 0), Is Trigger e Auto Tiling desmarcados.
  6. Duplique (Ctrl/Cmd + D), renomeie para LimiteInferior e mude a posição para (0, −4.75, 0).

A Figura 4 mostra o Inspector de um limite fatiado.

Inspector do objeto LimiteSuperior: Transform em (0, 4.75, 0) com escala 1; Sprite Renderer com sprite T_Portal, Draw Mode Sliced, Size 18 por 0.5 e Order in Layer 3; Box Collider 2D com Size 18 por 0.5, Is Trigger e Auto Tiling desmarcados.
Um limite fatiado. O tamanho vem do Size do Sprite Renderer, não da escala, e o collider usa o mesmo Size.
Auto Tiling desligado

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.

  1. Crie um objeto vazio Drone em Position = (−3, 0, 0). Em Tag, selecione Player; essa tag será usada na pontuação.
  2. Arraste T_Drone para dentro de Drone, renomeie para Visual, deixe a posição local em (0, 0, 0) e Order in Layer = 2.
  3. No Drone, adicione Rigidbody2D com os valores da tabela abaixo.
  4. Adicione CircleCollider2D com Radius = 0.36 e Is Trigger desmarcado.
PropriedadeValorMotivo
Body TypeDynamico corpo responde à gravidade, forças e colisões
Gravity Scale1.5queda perceptível sem ser imediatamente punitiva
Collision DetectionContinuousreduz a chance de atravessar colliders finos em alta velocidade
Constraints → Freeze Rotation Zmarcadoimpede 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
  1. 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 um EventSystem na raiz da cena, fora do Canvas.
  2. Crie um texto UI → Text - TextMeshPro como filho do Canvas. Nomeie TextoPontuacao, escreva 0, tamanho 64, alinhamento central, âncora superior central, Pos = (0, −55) e Size = (220, 90).
  3. 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).
  4. 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 #140F24 com Alpha 217.
  5. 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.
  6. Crie um UI → Image chamado PainelFimDeJogo, centralizado, com Size = (500, 280) e cor #140F24 com Alpha 240.
  7. Dentro do painel, crie TextoFimDeJogo (Fim de jogo, tamanho 46, Pos = (0, 80)) e TextoDica (Espaço ou botão sul do controle também reinicia, tamanho 20, Pos = (0, 30), Size = (460, 40)).
  8. Crie UI → Button - TextMeshPro como filho do painel: BotaoReiniciar, Size = (320, 66), Pos = (0, −80), cor #FFCC40, texto Reiniciar em tamanho 28.
  9. Selecione o EventSystem. Se o Inspector oferecer a troca para Input System UI Input Module, aceite.
  10. Desative PainelFimDeJogo na Hierarchy.

Crie um objeto vazio ControladorJogo na raiz da cena e, em Assets/Aula11/Scripts, o script ControladorJogo.cs:

📥 Baixe o arquivo: ControladorJogo.cs

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 v0v_0, o ganho de velocidade é Δv=5,5−v0\Delta v = 5{,}5 - v_0. 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:

∑Δv=g T,g=9,81×1,5≈14,7 unidades/s2.\sum \Delta v = g \, T, \qquad g = 9{,}81 \times 1{,}5 \approx 14{,}7 \text{ unidades/s}^2 .

Entre dois portais passam T=2,4T = 2{,}4 s, e a gravidade retira 14,7×2,4≈35,314{,}7 \times 2{,}4 \approx 35{,}3 unidades/s. Quem aperta uma vez por arco, no fim da queda, pega o drone descendo a −5,5 e ganha Δv=11\Delta v = 11: 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

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 pp compondo três matrizes,

M=T(p) S(sx,1) T(−p),M = T(p)\, S(s_x, 1)\, T(-p),

para que pp 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

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:

  1. crie um Action Map chamado Jogador;
  2. crie a ação Impulsionar e escolha Action Type = Button;
  3. adicione o binding <Keyboard>/space (Space [Keyboard]);
  4. adicione <Mouse>/leftButton (Left Button [Mouse]) como segundo binding;
  5. adicione <Gamepad>/buttonSouth (Button South [Gamepad]) como terceiro binding;
  6. 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

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 Controles para Controles;
  • o objeto ControladorJogo para 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.

Janela do Unity Editor com a cena Aula11. Na Hierarchy aparecem Main Camera, Global Light 2D, Cenario, ControladorJogo, Drone, GeradorDePortais, Interface e EventSystem. A janela Game mostra o fundo espacial, o drone, o placar 0, a barra da bateria cheia no canto superior esquerdo e o aviso para começar. O Inspector mostra o Drone com Transform, Rigidbody 2D, Circle Collider 2D, Bateria (Capacidade 15, Recarga Por Portal 5) e Controle Drone.
A cena completa no Editor. Repare na Hierarchy: `EventSystem` fica na raiz, e `Main Camera` e `Global Light 2D` vêm do template Universal 2D.

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.unitypackage traz 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
  1. Crie um objeto vazio P_Portal com 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. Adicione Rigidbody2D com Body Type = Kinematic.
  2. Arraste T_Portal para dentro de P_Portal e renomeie para ObstaculoSuperior. No Sprite Renderer: Draw Mode = Sliced, Size = (1.5, 4.5), Order in Layer = 1. Posição local (0, 4, 0).
  3. Adicione BoxCollider2D com Size = (1.5, 4.5), Is Trigger e Auto Tiling desmarcados.
  4. Duplique o obstáculo, renomeie para ObstaculoInferior e mude a posição local para (0, −4, 0).
  5. Crie ZonaPontuacao como filho vazio de P_Portal, em posição local (0.9, 0, 0), e adicione BoxCollider2D com 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.

À esquerda, esquema de um portal com obstáculo superior e inferior separados por uma passagem; uma faixa tracejada estreita, à direita do eixo do portal, representa a zona de pontuação. À direita, hierarquia com P_Portal, que tem Rigidbody2D Kinematic e MovimentoPortal, como pai de ObstaculoSuperior, ObstaculoInferior e ZonaPontuacao.
Anatomia do prefab composto. O movimento pertence ao pai; aparência, colisão sólida e detecção de pontuação ficam distribuídas entre os filhos.

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çãoO que a física fazEvento recebido
collider sólidoimpede a interpenetração e calcula contatoOnCollisionEnter2D
collider com Is Triggerpermite a passagem e apenas informa a sobreposiçãoOnTriggerEnter2D

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.

Comparação lado a lado. Um corpo circular é bloqueado por um collider sólido e gera OnCollisionEnter2D; em seguida atravessa um collider tracejado configurado como trigger e gera OnTriggerEnter2D.
Collider sólido e trigger usam regiões geométricas semelhantes, mas produzem respostas físicas diferentes.

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 (cx,cy)(c_x, c_y) e meias-dimensões (hx,hy)(h_x, h_y). Duas AABBs se sobrepõem quando

∣cx,1−cx,2∣≤hx,1+hx,2e∣cy,1−cy,2∣≤hy,1+hy,2.|c_{x,1} - c_{x,2}| \le h_{x,1} + h_{x,2} \quad\text{e}\quad |c_{y,1} - c_{y,2}| \le h_{y,1} + h_{y,2}.

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.unitypackage traz o projeto neste ponto (acrescenta o prefab P_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

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.unitypackage traz o projeto neste ponto (acrescenta MovimentoPortal ao 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

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 GeradorDePortais e 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.unitypackage traz o projeto neste ponto (acrescenta o GeradorDePortais; 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

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

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:

PropriedadeValorMotivo
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 Triggermarcadoo drone precisa atravessar a linha
BoxCollider2D → Size(0.2, 12)altura maior que a pista para qualquer altura sorteada do portal (±1,2)
ScriptZonaBaterialiga 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.unitypackage traz o projeto neste ponto (acrescenta ZonaPontuacao e o filho ZonaBateria ao 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.

  1. Abra File → Build Profiles.
  2. Na Scene List, adicione Aula11.unity (Add Open Scenes).
  3. Selecione BotaoReiniciar.
  4. Em Button → On Click(), pressione +.
  5. Arraste o objeto ControladorJogo para o campo do evento.
  6. 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.unitypackage traz 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 desejadaOnde alterarPor quê
drone subir maisintensidadeImpulso em ControleDroneé uma regra do movimento do jogador
portais andarem mais rápidovelocidade em MovimentoPortalpertence a cada obstáculo
portais surgirem mais vezesintervalo em GeradorPortaisé uma regra de geração
aumentar a pontuaçãoControladorJogoele é o dono desse estado
mudar o custo do impulso ou a recargacapacidade e recargaPorPortal em Bateriaa carga é um recurso do drone
decidir quando a bateria passa a gastarposição da ZonaBateria no prefabé uma interação espacial, como a pontuação
mostrar a cargaIndicadorBateriaa interface lê o estado, não o altera
reconhecer uma travessiaZonaPontuacaoé a interação espacial que dispara a regra
mostrar a derrotaControladorJogoa interface responde à mudança de estado
trocar a arteSprite Renderer do Visual e dos obstáculosaparê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.

Um asset P_Portal é ligado por três setas Instantiate a três instâncias independentes. A instância B recebe Destroy e é removida, enquanto uma seta de retorno indica que o asset continua disponível.
Ciclo de vida das instâncias. `Destroy` remove um objeto da cena sem apagar o asset de prefab nem as outras instâncias.

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​

SintomaVerificação mais provável
Drone não respondeação não atribuída ou habilitada; janela Game sem foco; referência do controlador vazia
Drone para de responder no meio da partidabateria vazia (confira a barra); só um portal atravessado a recarrega
Barra encolhe para o centroPivot X do Preenchimento diferente de 0
Barra não muda ou só mostra cheia e vaziareferências do IndicadorBateria vazias; Fracao sem o (float) (divisão inteira)
Barra cinza a partida toda; a bateria nunca gastaZonaBateria 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 toqueLigada começando verdadeira; Consumir sem o teste if (!Ligada)
Bateria não recarregafalta a linha do Recarregar em ZonaPontuacao; Bateria fora do objeto que tem o collider do drone
Drone sobe de forma incontrolávelimpulso alto demais; velocidade vertical não zerada antes do impulso
Drone atravessa o portalcomponente 2D ausente; Is Trigger marcado no obstáculo; layer sem colisão
Drone bate no meio da passagemcollider maior que a arte: Auto Tiling marcado ou Size do collider ≠ Size do sprite (ligue os Gizmos)
Borda do portal ou dos limites esticadaescala 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çasprite 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 borradaFilter Mode = Bilinear; compressão ativada; PPU diferente de 18
Portal se desmonta ao moverMovimentoPortal anexado a um filho, não ao pai
Nenhum portal apareceprefabPortal não atribuído; intervalo inválido; partida já encerrada ou não iniciada
Algumas passagens são impossíveislimites verticais ignoram o tamanho da passagem e do drone
Pontuação nunca mudatrigger desmarcado; tag Player ausente; script no objeto errado
Um portal soma mais de um pontofalta da proteção pontoRegistrado
Jogo continua após a colisãocomponente não consulta PartidaAtiva
Botão não reiniciaevento não ligado; cena ausente da Scene List do Build Profile
Espaço não reiniciaSolicitarReinicio não chamado em AoImpulsionar; tempo menor que esperaReinicio
Textos aparecem como quadrados ou vaziosTMP Essential Resources não importados
Ajustes “somem” ao pararvalores foram alterados durante o Play Mode
Tutorial antigo gera InvalidOperationExceptioncó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:

  1. aumente e diminua a intensidade do impulso;
  2. ajuste a gravidade do Rigidbody2D;
  3. varie a velocidade dos portais;
  4. varie o intervalo de geração;
  5. mude o tamanho da passagem no prefab (posições Y dos obstáculos);
  6. 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?
  7. mova a ZonaBateria do 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 em Awake e, em FixedUpdate, passe a MovePosition um Y igual ao inicial mais amplitude * Mathf.Sin(frequencia * tempo), somando Time.fixedDeltaTime a tempo a 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 um AreaEffector2D com 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 CircleCollider2D em modo trigger e um script que, em OnTriggerEnter2D, devolve cargas à bateria de quem tem a tag Player e chama Destroy(gameObject). Acrescente à Bateria um método que some uma quantidade qualquer, com o mesmo Mathf.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.cs vai no filho Visual e tem três TODO: converter corpo.linearVelocity.y num ângulo com Mathf.Clamp (6° por unidade/s, entre −60° e 30°), suavizar com Mathf.MoveTowardsAngle — que trata a volta de 360°, ao contrário de MoveTowards — e aplicar o resultado em transform.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 pontoGlobal 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 coneSpot 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 1/(kc+kld+kqd2)1/(k_c + k_l d + k_q d^2)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:

PropriedadeValorMotivo
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#FFEDBFbranco quente, distinto do céu roxo
Intensity0começa apagado; o script o acende
RadiusInner 0.5, Outer 8alcança os portais que se aproximam
Inner / Outer Spot Angle40 / 80cone com penumbra
Falloff Strength0.5queda suave até o raio externo
Shadowsdesmarcadonã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

SetorEscuro.cs (esqueleto)
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.

Drone Gate num setor escuro: o céu, o planeta, as montanhas e os portais aparecem bem mais apagados; um cone de luz branca quente sai da frente do drone e ilumina a passagem entre os portais; o placar mostra 7 e a barra da bateria, no canto superior esquerdo, está curta e vermelha.
Um setor escuro (placar 7). A Global Light 2D caiu para 0,2 e o farol, uma Spot Light 2D filha do `Visual`, ilumina a passagem à frente. A barra vermelha indica bateria abaixo de um quarto (2 de 10 cargas). Ao fundo, as camadas do *parallax* da Atividade 3 também escurecem.

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​

  1. Na Aula 08, GL_SPOT_CUTOFF = 25 produz um cone de que abertura total? Que Outer Spot Angle daria o mesmo cone na Unity?
  2. Troque o material do Fundo para Sprite-Unlit-Default e entre num setor escuro. O que acontece e por quê?
  3. O Farol é filho do Visual, não do Drone. 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,

intervalo=distaˆncia entre portaisvelocidade.\text{intervalo} = \frac{\text{distância entre portais}}{\text{velocidade}}.

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:

MovimentoPortal.cs (acréscimo)
/// <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:

GeradorPortais.cs (substituições)
[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:

GeradorPortais.cs (acréscimo)
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​

  1. 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.
  2. Por que DefinirVelocidade altera a instância e não o asset de prefab? O que mudaria se o gerador alterasse o campo no próprio prefabPortal?
  3. Flappy Bird não acelera: a dificuldade é constante do primeiro ao último cano. Cite uma vantagem de cada escolha para o jogador.
  4. 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 x′=d x/zx' = d\,x/z. Se um objeto à profundidade zz se desloca Δx\Delta x no mundo, a imagem dele se desloca

Δx′=dz Δx,\Delta x' = \frac{d}{z}\,\Delta x ,

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): x′x' não depende de zz, 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 f=d/zf = d/z entre 0 e 1 e anda a ff vezes a velocidade dos portais. f=1f = 1 é a profundidade dos portais; f=0f = 0 é o infinito, que não se move (Figura 10a).

CamadaSpriteFator ffProfundidade equivalente z=d/fz = d/fOrder in Layer
CamadaCeuT_Ceu0,110 d10\,d−12
CamadaPlanetaT_Planeta0,254 d4\,d−11
CamadaMontanhasT_Montanhas0,52 d2\,d−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 −1,5L-1{,}5L a 1,5L1{,}5L, com L=320/18≈17,78L = 320/18 \approx 17{,}78 unidades. A janela da câmera vai de −8,89 a 8,89, ou seja, exatamente ±L/2\pm L/2. Depois de a camada andar LL para a esquerda, sua borda direita chega a 1,5L−L=L/21{,}5L - L = L/2: a borda da câmera. Nesse instante, o script a devolve LL para a direita. Como a imagem se repete a cada LL, 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.

Dois painéis. À esquerda, vista de cima de uma câmera perspectiva: três camadas nas profundidades d, 2d e 4d se deslocam o mesmo Δx no mundo, e os raios até o olho cortam a tela em segmentos de comprimento Δx, Δx/2 e Δx/4; rótulos indicam os fatores 1, 0,5 e 0,25, e o céu a 10d com fator 0,1. À direita, uma camada formada por três imagens lado a lado e, abaixo, a mesma camada deslocada uma imagem para a esquerda; um retângulo verde marca a janela da câmera, de −L/2 a L/2, ainda coberta na segunda posição, e uma seta indica a volta de L para a direita.
(a) Na perspectiva, o mesmo deslocamento no mundo aparece na tela multiplicado por $d/z$: esse é o fator de cada camada. (b) Uma camada **Tiled** com três imagens de largura cobre a câmera ortográfica até ter andado uma imagem inteira; a volta de $L$ troca uma cópia pela seguinte sem mudar nada na tela.

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

ParallaxCamada.cs (esqueleto)
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. O size do 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 a transform.position com Vector3.left.
  • TODO 3. Compare transform.position.x com xInicial - larguraImagem e, quando alcançar, some Vector3.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 Fundo desativado 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​

  1. Pela tabela, as montanhas estão a 2d2d. Que fator teria uma camada de nuvens a 1,25 d1{,}25\,d, pouco atrás dos portais? E uma camada à frente dos portais, a 0,5 d0{,}5\,d — o fator poderia passar de 1? O que o [Range(0f, 1f)] impede?
  2. 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 ±L/2\pm L/2 da câmera.
  3. 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?
  4. Por que as camadas podem mudar o Transform em Update, e os portais precisam de MovePosition em FixedUpdate?

Exercícios (checkpoints)​

Questões dissertativas​

Q

O drone realmente avança pelo mundo? Explique o movimento relativo usado no jogo e duas vantagens dessa decisão.

Q

Por que o drone usa Rigidbody2D dinâmico e impulso, enquanto o portal usa Rigidbody2D cinemático e MovePosition?

Q

Compare o obstáculo e a zona de pontuação: ambos têm Collider2D, mas por que somente um deles deve ser trigger?

Q

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.

Q

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?

Q

Por que o ControladorJogo protege RegistrarDerrota e AdicionarPonto com testes de PartidaAtiva e PartidaIniciada?

Q

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​

Quiz9 questões

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)​

Aprofundamento (opcionais)​