Faça a atividade no: GoogleDocs / PDF

Engenharia Insper / Laboratório de Realidade Virtual e Jogos Digitais

Breakout (parte 1) [h.a7d3ebsuguue]

Breakout é um jogo criado pela Atari em 1976. Nele o objetivo é rebater uma bola na tela e ir destruindo blocos na parte superior sem deixar a bola cair. Uma curiosidade desse jogo é que Steve Jobs e Steve Wozniak participaram da construção dos dispositivos e programação.

       

Fontes: wikipedia e virtualatari.org

Objetivos de Aprendizagem [h.eh514ufq2evy]


Requisitos (Documento de Requisitos) [h.9tvvt7klinli]

Visão Geral [h.nwwq5na89jnk]

        Jogo de tela única, em que o jogador deve controlar uma raquete para rebater uma bola, que ao colidir com blocos presentes na cena devem ser quebrados.

No final do segundo Handout seu jogo deverá estar assim: https://rvinsper.itch.io/projeto-00-atari-breakout

Gameplay [h.g3mnfux5lefw]

Raquete [h.sy5r5e56nw4n]

        Elemento retangular controlado pelo jogador. Fica na parte inferior da tela, e se desloca somente para esquerda e direita. Não deve sair da tela.

        Controles:

Bola [h.s00qpgfq58ax]

        Elemento circular. Deverá iniciar o jogo acima da raquete no centro.

        Deve ter colisão elástica com os limites esquerdo, direito e superior da tela, bem como com a raquete e blocos.

        Ao colidir com limite inferior da tela deverá subtrair um ponto de vida e reiniciar sua posição.

Blocos [h.u118om4rpyev]

        Elementos retangulares, devem ser inicializados em posições específicas da tela.

        Ao colidir com a bola devem ser destruídos.

Condição de Vitória [h.v6k0w3530ilk]

        Quando todos os blocos da tela forem destruídos.

Condição de Derrota [h.q71u498ayc0]

        Quando os pontos de vida chegarem a 0.

Arte [h.n46cpafx77h4]

        Sprites de cores sólidas (pelo momento entenda sprites como os objetos 2D do jogo que em geral possuem alguma textura).

Interface do Usuário (UI - User Interface) [h.2k5xfbjl7obn]

        Apenas textual.

        Número de vidas do jogador no canto superior direito.

        Pontuação no canto superior esquerdo.

Fluxo de Telas [h.kawcqnnia5io]

Texto alternativo gerado por máquina: Fim de Jogo Jogo
(Vitöria,'Derrota) Pause Menu


[h.jin4n4t35tjp]

[h.u4ghplw9vhxt]

 Roteiro [h.et8omtdmpcn9]

01 - Criando o projeto [h.x8t4edohhhsf]

Neste curso estaremos usando a ferramenta de desenvolvimento de jogos (Game Engine) Unity, assim a primeira etapa é ter o Unity instalado. A Unity lança novas versões da ferramenta de edição continuamente e os projetos são relativamente presos às versões que foram construídos, assim o Unity conta com o UnityHub que é um aplicativo que permite criar e gerenciar seus projetos em função das várias versões do Unity existentes.

Assim que abrimos o UnityHub, podemos clicar em New para criar nosso novo projeto. Isso irá abrir uma nova janela com alguns templates que podemos selecionar. Estes templates mudam apenas as configurações padrão que são aplicadas na inicialização de nosso projeto. Para esse projeto iremos utilizar a Template 2D.

Assim selecione a opção 2D e crie um nome para seu projeto em Project Name. Selecione um local diferente no seu disco para criar seu projeto em Location caso necessário. Agora basta criar em CREATE e o Unity irá abrir o editor com seu novo projeto e as configurações necessárias carregadas.

02 - Criando a raquete. [h.4al6b0dykogl]

Para criar seu novo jogo Breakout, você pode utilizar as sprites padrões da Unity. Vamos começar pela raquete então, e para ela vamos usar uma sprite. Para isso clique no menu nas opções Assets->Create->Sprites->Square.

Assim que ganhar experiência você verá que consegue o mesmo conjunto de opções, com o botão direito em algum espaço da janela Project e depois selecionando Create->Sprites->Square

 C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image002.jpg

Isso irá criar um objeto sprite padrão da unity em seu projeto.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image003.png

 

Procure ser sempre organizado com os vários objetos nos seu projeto, isso facilitará muito quando seu projeto estiver maior (e quando os professores forem fazer as avaliações). Então crie uma pasta no seu projeto e chame ela de Sprites, para isso você pode clicar com o botão direito na janela project e selecionar Create > Folder, depois arraste o sprite recém-criado para ela.

Falando em organização, quando se criam novos Assets eles vêm com nomes padronizados e que logo você pode se perder, assim é bom renomear para algo que você reconheça depois. Assim selecione o asset “Square” e renomeie ele para “Raquete”, para ficar mais fácil de lembrar.

Agora adicione esse sprite recém criado na sua cena. Para isso, você pode arrastá-lo diretamente para a cena, ou ainda para a janela da Hierarquia.

 

Fazendo isso, o Unity irá criar um GameObject, que é o objeto que irá gerenciar a asset sprite. (Isso é algo que pode ser confuso no começo, pois o Unity tem muitos tipos de objetos, mas o objeto Raquete que é visível na janela de Projeto, não é o mesmo do Projeto.  Selecionando esse objeto podemos verificar seus atributos no Inspector.

Perceba que no Inspector, temos diversas secções, cada uma dessas corresponde a um componente, e todo objeto na Unity tem pelo menos um Transform (ou Rect Transform se for UI). Veja que o asset Raquete está na opção Sprite do componente Sprite Renderer. (para maiores detalhes veja o material de apoio sobe Components e GameObjects)

Procure o componente Transform (ele costuma ser o primeiro). E aplique a escala de 0.5 em y e 2 em x, finalmente defina a posição como (0, -4, 0).

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image006.png

 

 Veja como o objeto vai se alterando e posicionando na tela do editor do Unity.

Agora iremos adicionar o controle a nossa raquete, para isso precisamos criar um script. Vamos primeiro criar uma pasta “Scripts” (se desejar chame de “_Scripts”, dessa forma fica mais fácil de achar nossos códigos pois assim a pasta estará sempre no começo da nossa aba Project, essa é uma técnica muito usada). Novamente vá com o botão direito do mouse na janela de projeto e selecione Create->Folder.

 

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image007.png

 

Agora podemos criar um novo script nesta pasta, assim com o botão direito do mouse selecione Create->C# Script e defina o nome para MovimentoRaquete.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image008.png

 

Dê um duplo clique no nome do script para abrir uma IDE e assim poder editar o programa. Aqui você consegue ver como configurar para o editor de sua preferência

Assim que abrir o Script você verá algo como a seguir

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovimentoRaquete : MonoBehaviour
{
        
// Start is called before the first frame update
        
void Start()
    {

    }

   
// Update is called once per frame
   
void Update()
    {

    }
}

A Unity trabalha com  alguns métodos padrões que serão utilizados ao longo de todo desenvolvimento, para maiores detalhes veja essa documentação.

 

Neste momento utilizaremos a chamada para um método Update, que é executado cada vez que um novo quadro (frame) é desenhado e exibido. Utilizaremos também o Input Manager da Unity para tratar dos eventos de interação com teclado e mouse (ele normalmente já vem pré-configurado, para verificar isso, na Unity vá em Edit->Project Settings -> Input Manager).

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image010.jpg

Em nosso jogo vamos utilizar apenas o Eixo horizontal e iremos deslocar nossa raquete no eixo x. Atualize seu código da seguinte forma:

void Update()
{
        
float inputX = Input.GetAxis("Horizontal");

        transform.position +=
new Vector3(inputX, 0, 0);
}

Por fim, precisamos adicionar esse script como um componente do nosso GameObject Raquete. Assim selecione pela janela de hierarquia o objeto Raquete e na janela do Inspector procure o botão Add Component. Despois, na caixa de busca procure pelo nome do script que acabamos de criar.

Podemos ainda arrastar direto o script para o Inspector de nosso GameObject.

Clique no botão “play” para testar seu jogo:

Texto alternativo gerado por máquina:

        AVISO IMPORTANTE, NO MODO PLAY QUALQUER ALTERAÇÃO SERÁ PERDIDA QUANDO SAIR DO MODO.

Muito provavelmente ficou muito rápido e difícil de controlar, isso acontece pois a Update está sendo executada a cada frame, no momento em que escrevo esse handout isso acontece 5508 vezes por segundo.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image014.png

Para corrigir isso vamos multiplicar o novo vetor pelo tempo decorrido (em segundos) entre os quadros, esse valor é encontrado em Time.deltaTime. Dessa forma deixamos a velocidade independente do quão rápido ou lento é seu computador ou console, pois essa propriedade sempre calcula o tempo entre os quadros.

transform.position += new Vector3(inputX, 0, 0) * Time.deltaTime;

Muito provavelmente está muito devagar agora, vamos então criar um atributo, velocidade. Adicione no seu script uma nova variável.

public class MovimentoRaquete : MonoBehaviour
{

      public float velocidade;
        
// Start is called before the first frame update
    ...

Quando definimos um atributo público na Unity podemos controlá-lo pelo Inspector.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image017.png

Podemos ainda usar a anotação range para que ele seja um slider no Inspector.

public class MovimentoRaquete : MonoBehaviour
{
     [
Range(1, 15)]
     
public float velocidade = 5.0f;
        
// Start is called before the first frame update
    ...

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image019.jpg

Por fim multiplicamos tudo pela velocidade. Não esqueça de definir a velocidade que você quer no Inspector!

transform.position += new Vector3(inputX, 0, 0) * Time.deltaTime * velocidade;

[h.rzc9a44anb9f]

Sempre teste o que está fazendo, assim veja no Play se tudo está funcionando.

03 - Criando a Bola [h.c5lrk337rnmq]

Vamos agora criar a bola, para isso crie um sprite circle, e adicione em nossa cena.

Assim, relembrando. Vá em Assets->Create->Sprites->Circle. Depois mova essa nova sprite criada para a janela de hierarquia e um GameObject para a sprite será criada.

Ao fazer isso você deverá ver uma sprite circular na sua cena.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image021.jpg

 

Vamos escalonar para que nossa bola fique proporcional à nossa raquete, use uma escala de (0.3, 0.3, 1), você pode deixar na posição (0, -3.5, 0).

Texto alternativo gerado por máquina: Transform 0.3 Y -3.5 YO 3
Position Rotation Scale X

 

Vamos criar um novo script para controlar nossa bola, chame-o de MovimentoBola.

 

A primeira coisa a se fazer quando o jogo iniciar é calcular uma direção aleatória para nossa bola, queremos que essa direção ainda seja “para cima” então serão dois números, um para x, e outro para y. Vamos utilizar a medida normalizada também para que não haja diferença na velocidade da bola entre execuções.

public class MovimentoBola : MonoBehaviour

{
        [
Range(1, 15)]
        
public float velocidade = 5.0f;

        
private Vector3 direcao;

        
void Start()

      {
                
float dirX = Random.Range(-5.0f, 5.0f);
                
float dirY = Random.Range(1.0f, 5.0f);

                direcao =
new Vector3(dirX, dirY).normalized;
        }
// ...

Por fim, utilizamos essa direção no método Update().

void Update()
{
        transform.position += direcao * Time.deltaTime * velocidade;
}

Finalmente, adicione este script ao seu GameObject da bola.

 

O próximo passo é fazer com que nossa bola ricocheteie nas paredes e não saia da tela. Existem diversas formas de se implementar isso, aqui será apresentada uma solução simples, mas que funciona de forma adequada.

 

Iremos utilizar a função da câmera WorldToViewportPoint que tem como retorno uma posição normalizada entre (0, 0) e (1, 1) relativa à câmera. Assim fica fácil definir se estamos dentro ou fora da área de visão da câmera.

void Update()
{
        transform.position += direcao * Time.deltaTime * velocidade;

        Vector2 posicaoViewport = Camera.main.WorldToViewportP
oint(transform.position);

        
if( posicaoViewport.x < 0 || posicaoViewport.x > 1 )
        {
                direcao =
new Vector3(-direcao.x, direcao.y);
        }
        
if( posicaoViewport.y < 0 || posicaoViewport.y > 1 )
        {
                direcao =
new Vector3(direcao.x, -direcao.y);
        }

}

Com isso temos a bola contida dentro da área mostrada pela câmera. Inicie seu jogo no botão Play para testar se tudo funciona corretamente. Essa estratégia pode apresentar alguns problemas da bola ficar presa nos cantos, mas vamos deixar para você melhorar o algoritmo.

 

Temos agora que fazer a bola colidir com a raquete, para isso vamos usar estruturas de dados e rotinas especializadas do próprio Unity para fazer o cálculo de colisão diretamente. Assim precisaremos adicionar aos objetos criados, componentes de colisores para que o cálculo possa ser feito.

Adicione um Circle Collider 2D na Bola, e um Box Collider 2D na raquete. Para isso utilize o botão Add Component que se encontra na parte inferior da janela do Inspector. Para que o cálculo seja realizado no Unity também é necessário adicionar um RigidyBody2D em um dos objetos. Assim adicione o componente RigidyBody2D na bola.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image026.png 

 

O Unity possui duas formas principais de sistema de colisão. Uma é usando toda uma lógica de colisão física diretamente, você consegue usar esse recurso através do método OnCollisionEnter2D(), porém para esse projeto vamos fazer os cálculos diretamente no nosso código, assim vamos identificar quando o disparo do evento de contato acontece e então fazemos os cálculos, para isso usamos o método OnTriggerEnter2D().

Assim, após adicionar o RigidBody2D na bola, troque o Body Type dela para Kinematic (significa que nós vamos fazer o cálculo da posição dele). Como vamos querer detectar quando houver o contato, vamos precisar também que os disparos dos eventos sejam lançados, assim clique no Is Trigger.

 

Feito isso precisaremos tratar esse contato, para isso precisaremos criar o método “OnTriggerEnter2D” no Script MovimentoBola. Ao fazer isso o Unity saberá que quando houver uma colisão, esse método deverá ser invocado.

   void OnTriggerEnter2D(Collider2D col)
  {
     
float dirX = Random.Range(-5.0f, 5.0f);
     
float dirY = Random.Range(1.0f, 5.0f);

      direcao =
new Vector3(dirX, dirY).normalized;
  }

 Com isso nossa bola, agora rebate com nossa raquete. Teste agora clicando no Play.

04 - Blocos [h.kmc2vf6mpvp]

Agora que já temos nossa bola e raquete funcionando, vamos fazer os Blocos para serem quebrados. Para isso faremos primeiramente 1 bloco, e depois iremos instanciá-lo (criar versões dele) via código em scripts C#.

Crie mais um sprite quadrado, e adicione em sua cena. Como na explicação da raquete iremos ajustá-lo para que tenha o tamanho que queremos.

 

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image029.png

 

Adicione um box Collider 2D.

Agora iremos criar um script para controlar o comportamento dos tijolos. Você pode chamar esse script de Bloco.

 

Por enquanto vamos apenas destruí-lo quando a bola encostar nele. 

public class Bloco : MonoBehaviour
{
        
private void OnTriggerEnter2D(Collider2D collision)
        {
                Destroy(gameObject);
        }
}

Associe agora esse script no objeto Tijolo.

Agora precisamos corrigir algumas coisas no código da bola, para que nosso jogo funcione. Precisamos fazer com que a bola saiba que está colidindo com a raquete e nesse caso faça uma reflexão simples.

Vamos usar Tags para reconhecer o jogador, e os tijolos. Para isso, no inspector, vá em tags, e clique em AddTag

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image031.png

Vamos criar a tag Tijolo, perceba que a tag player já existe, então vamos utilizá-la para a raquete.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image032.png

 

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image033.png

Criada a Tag, selecione novamente o tijolo, e selecione a tag recém criada.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image034.png

 

Selecione a raquete e aplique a tag Player

 

Abra o código da bola para fazer os ajustes. Como agora já temos como saber se colidimos com um tijolo ou com o jogador utilizando a função do gameObject compare tag para fazer essa análise.

private void OnTriggerEnter2D(Collider2D col)
{
        
if(col.gameObject.CompareTag("Player"))
        {
                
float dirX = Random.Range(-5.0f, 5.0f);
                
float dirY = Random.Range(1.0f, 5.0f);

                direcao =
new Vector3(dirX, dirY).normalized;
        }
        
else if(col.gameObject.CompareTag("Tijolo"))
        {
                direcao =
new Vector3(direcao.x, -direcao.y);
        }
}

 

Agora já temos nosso bloco funcionando. Vamos então criar nossa parede de tijolos. A primeira coisa é transformar nosso tijolo em um Prefab, assim já o carregamos com os scripts e componentes necessários.

Crie uma pasta “Prefabs” no seu projeto e arraste nosso tijolo da hierarquia para esta pasta.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image036.png

Feito isso podemos deletar o objeto original da hierarquia, pois iremos instanciar vários blocos por código e se você usar o bloco que está na hierarquia poderá ter problemas no futuro.

 

Crie agora na Hierarquia um “EmptyObject” que irá gerar nossa parede. Renomeie para BlocoPool.

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image037.png

 

E agora crie um Script “BlocoSpawner” para que possamos instanciar nossos tijolos e criar nossa parede.

Crie um atributo público para inserirmos o prefab que acabamos de criar, e adicione esse script ao BlocoPool

public class BlocoSpawner : MonoBehaviour
{
        
public GameObject Bloco;
}

 

Agora arraste nosso prefab para o campo que apareceu no Inspector

C:\ADA15AC5\118A8B7A-DB49-4B44-8116-B104FAA58A55_arquivos\image039.png

 

Assim que nosso jogo iniciar iremos criar nossa parede, então precisamos escrever a rotina de criação dela na função Start()

void Start()
{
    for(int i = 0; i < 12; i++)
    {
        for(int j = 0; j < 4; j++){
           Vector3 posicao = new Vector3(-9 + 1.55f * i, 4 - 0.55f * j);
           Instantiate(Bloco, posicao, Quaternion.identity, transform);
          }
    }
}

Feito isso temos a base de nosso jogo funcionando. No próximo tutorial veremos como fazer a Interface de Usuário, e também como implementar o Game Loop.


[h.kt9vxj72ikto]

Questões de Estudo [h.6s2wfg57dtm7]

  1. Quais são os tipos possíveis de colisões na Unity?
  2. Por que usamos um Vector3 para calcular as posições se estamos em um projeto 2D?
  3. Qual a função do Quaternion.identity quando instanciamos os blocos?
  4. O que é um Component? E um MonoBehaviour?
  5. Qual a diferença entre Input.GetAxis(“Vertical”) e Input.GetKey(“down”)

Leitura Complementar [h.ddmy7bedq0u7]