Desenvolvimento Unity

Como desenvolver o jogo da cobra na Unity 2D

Prefácio

Snake é um jogo arcade criado na década de 1970. Ele foi portado para quase todos os sistemas existentes, até mesmo em telefones Nokia antigos e clássicos! Como a maioria dos jogos de arcade, ainda é fácil e muito divertido desenvolver seu próprio jogo Snake. Neste tutorial, explicaremos como fazer um clone simples, mas funcional, do Snake.

Requisitos

Conhecimento

Este tutorial não requer nenhuma habilidade especial, exceto algum conhecimento sobre os fundamentos do Unity, Mesmo que você ainda não conheça esses conceitos, porém recomendamos que você aprimore seu conhecimento dos assuntos mencionados antes de tentar este tutorial, veja no canal DFILITTO.

Versão da unidade

Nosso tutorial do Snake usará o Unity 2018.4 LTS . Versões mais recentes, como 2019.2, também devem funcionar; no entanto, para evitar qualquer confusão, é sempre recomendável manter a versão do Unity para a qual o tutorial foi escrito.

Protip: Nunca atualize para qualquer versão do Unity que tenha um sufixo ‘a’ ou ‘b’. Essas versões são alfa e beta, respectivamente, e na maioria das vezes as coisas quebram! Resolver problemas de coisas quebradas não é divertido e causa frustração que você deseja evitar ao iniciar o desenvolvimento de jogos.

Configuração do projeto

Vamos começar! Supondo que você esteja usando uma versão recente do Unity Hub, inicializaremos a interface do Hub e selecionaremos New e, em seguida, preencheremos a caixa de diálogo de acordo:

Vamos chamá-lo de Snake , selecione qualquer local como C:\GameDev , selecione 2D na seção Template e clique em Create :

Se selecionarmos a Câmera Principal na Hierarquia então podemos definir a Cor de Fundo para preto, ajustar o Tamanho e a Posição como mostrado na imagem a seguir:

Nota: O tamanho é basicamente o fator de zoom.

Adicionando Bordas

Usaremos uma imagem de linha branca horizontal e uma vertical para nossas bordas:

Assim que os tivermos na pasta Assets do nosso projeto, podemos selecioná-los na área do projeto do Unity :

Depois podemos alterar suas configurações de importação no Inspetor para que apareçam no tamanho certo e com a aparência certa:

Nota: Pixels Per Unit é a razão entre um pixel na imagem e uma unidade no mundo. O Snake terá um tamanho de 1×1 pixel, que deve ser 1 unidade no mundo do jogo. É por isso que usaremos um valor de Pixels Per Unit de 1 para todas as nossas texturas.

Agora podemos arrastar cada imagem de borda para a cena duas vezes e posicioná-las de forma que formem uma borda retangular:

Nota: a imagem da borda horizontal é usada para as bordas superior e inferior. A imagem da borda vertical é usada para as bordas esquerda e direita.

Vamos renomear as bordas para BorderTop , BorderBottom , BorderLeft e BorderRight . Selecionaremos um após o outro na Hierarquia e então pressionaremos F2 ou clicaremos com o botão direito e selecionaremos Renomear . Aqui está o resultado:

Se você tiver problemas para posicionar as bordas, use os seguintes valores XYZ para cada uma.
– BorderTop: X 0 , Y 25 , Z 0
– BorderBottom: X , Y -25 , Z 0
– BorderLeft: X -34.5 , Y 0 , Z 0
– BorderRight: X 34.5 , Y 0 , Z 0
– Nota: certifique-se Z está sempre definido como 0. Como estamos trabalhando com 2D, não precisamos usar o eixo Z, pois isso causará um comportamento indesejado.

No momento as bordas são apenas imagens no jogo. Podem parecer fronteiras, mas ainda não fazem parte do mundo físico. Se quisermos que o Snake colida com as bordas, teremos que adicionar Colliders a elas. Podemos fazer isso selecionando primeiro todas as fronteiras na Hierarquia :

Neste momento as fronteiras são apenas imagens, não são realmente fronteiras. A cobra poderia passar por eles porque ainda não fazem parte do mundo da física. Vamos dar uma olhada no Inspetor e selecionar Add Component -> Physics 2D -> Box Collider 2D . E como temos todas as bordas selecionadas agora, isso adicionará um Box Collider 2D a cada uma delas:

E isso é tudo. Acabamos de criar as bordas do nosso jogo sem escrever uma única linha de código, graças a este poderoso mecanismo de jogo.

Criando a Pré-fabricada de Alimentos

Não queremos que nossa cobra fique com fome, então vamos gerar comida aleatoriamente no jogo. Como de costume começaremos com uma imagem de comida, no nosso caso um pixel colorido:

  • food.png
    Obs: clique com o botão direito no link, selecione Salvar como… e salve na pasta Assets do projeto.

Usaremos as seguintes configurações de importação para a imagem de comida:

Tudo bem, vamos arrastar a imagem da comida para dentro da Cena para criar um GameObject :

A Cobra deverá receber algum tipo de informação sempre que colidir com um alimento. Isso significa que a comida tem que fazer parte do mundo da física, o que pode ser feito com um Collider.

Um GameObject sem Collider é apenas uma coisa visual, não faz parte do mundo da física. Um GameObject com Collider faz parte do mundo da física, assim como uma parede. Isso fará com que outras coisas colidam com ele e acionará o evento OnCollisionEnter2D . Um GameObject com um Collider que tenha Is Trigger marcado não fará com que outras coisas colidam com ele, mas ainda assim acionará o evento OnTriggerEnter2D .

A Cobra deve ser notificada quando passar pela comida, mas não deve colidir com ela como se a comida fosse uma parede. Então vamos selecionar o alimento na Hierarquia e depois escolher Add Component -> Physics 2D -> Box Collider 2D no Inspector e habilitar Is Trigger :

Ok, agora não queremos que a comida esteja na cena desde o início. Em vez disso, queremos um Prefab para que possamos instanciá -lo sempre que precisarmos. Para criar um Prefab , tudo o que precisamos fazer é renomear o alimento para FoodPrefab e arrastá-lo da Cena para a Área do Projeto :

Agora podemos excluir o FoodPrefab GameObject da Hierarchy , porque não queremos que ele esteja no mundo do jogo ainda.

Gerando Comida

Vamos gerar novos alimentos em alguma posição aleatória a cada poucos segundos. Este tipo de comportamento pode ser implementado com um Script, então vamos criar um Script SpawnFood . O Script deve estar na Cena o tempo todo, então vamos mantê-lo simples e adicioná-lo à Câmera Principal (porque ele estará na Cena o tempo todo também) . Vamos selecionar a Câmera Principal na Hierarquia e depois clicar em Add Component -> New Script , nomeá-la como SpawnFood e selecionar CSharp para a linguagem:

Depois clicaremos duas vezes nele na Área do Projeto para abri-lo no editor de scripts (geralmente Visual Studio):

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {

    // Use this for initialization
    void Start () {
   
    }
   
    // Update is called once per frame
    void Update () {
   
    }
}

Não precisaremos da função Update , então vamos removê-la:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {

    // Use this for initialization
    void Start () {
   
    }
}

Nosso Script precisa saber onde está o alimento Prefab. Adicionaremos uma variável pública do tipo GameObject :

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;
   
    // Use this for initialization
    void Start () {
   
    }
}

A comida deve ser gerada dentro das fronteiras, não fora. Então precisaremos também de uma variável para cada uma das bordas para que nosso Script conheça suas posições:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;

    // Borders
    public Transform borderTop;
    public Transform borderBottom;
    public Transform borderLeft;
    public Transform borderRight;

    // Use this for initialization
    void Start () {
   
    }
}

Nota: eles já são do tipo Transform então não precisamos escrever borderTop.transform.position o tempo todo. Em vez disso, poderemos acessar suas posições como borderTop.position .

Vamos criar a função Spawn que gera um pedaço de comida dentro das fronteiras. Primeiramente escolheremos a posição do eixo X em algum lugar aleatoriamente entre as bordas esquerda e direita. Em seguida, escolheremos a posição y aleatoriamente entre as bordas superior e inferior. Depois iremos instanciar o food Prefab nessa posição:

// Spawn one piece of food
void Spawn() {
    // x position between left & right border
    int x = (int)Random.Range(borderLeft.position.x,
                              borderRight.position.x);

    // y position between top & bottom border
    int y = (int)Random.Range(borderBottom.position.y,
                              borderTop.position.y);

    // Instantiate the food at (x, y)
    Instantiate(foodPrefab,
                new Vector2(x, y),
                Quaternion.identity); // default rotation
}

Nota: xey são arredondados via (int) para garantir que a comida seja sempre gerada em uma posição como 1, 2) mas nunca em algo como (1.234, 2.74565) .

Agora vamos garantir que nosso script chame a função Spawn a cada poucos segundos. Podemos fazer isso usando InvokeRepeating :

// Use this for initialization
void Start () {
    // Spawn food every 4 seconds, starting in 3
    InvokeRepeating("Spawn", 3, 4);
}

Nota: Existem outras maneiras de executar uma função de repetição, mas esta é a maneira mais fácil de fazer isso.

Aqui está nosso roteiro completo:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;

    // Borders
    public Transform borderTop;
    public Transform borderBottom;
    public Transform borderLeft;
    public Transform borderRight;

    // Use this for initialization
    void Start () {
        // Spawn food every 4 seconds, starting in 3
        InvokeRepeating("Spawn", 3, 4);
    }

    // Spawn one piece of food
    void Spawn() {
        // x position between left & right border
        int x = (int)Random.Range(borderLeft.position.x,
                                  borderRight.position.x);

        // y position between top & bottom border
        int y = (int)Random.Range(borderBottom.position.y,
                                  borderTop.position.y);

        // Instantiate the food at (x, y)
        Instantiate(foodPrefab,
                    new Vector2(x, y),
                    Quaternion.identity); // default rotation
    }
}

Se salvarmos o Script e selecionarmos a Câmera Principal então poderemos ver que todas as nossas variáveis ​​públicas agora são mostradas no Inspetor. Eles ainda são None , então vamos arrastar FoodPrefab da Project Area para a variável Food Prefab e as Borders da Hierarchy para seus slots correspondentes:

Nota: podemos arrastar algo para um slot de variável de Script literalmente arrastando-o com o mouse da Hierarquia ou Área do Projeto para os slots que podem ser vistos na imagem acima.

Tudo bem, agora é hora de apertar Play e aguardar alguns segundos. Deveríamos ser capazes de ver alguns novos alimentos surgindo entre as fronteiras:

Criando a cobra

Vamos terminar a parte principal do nosso jogo: a Cobra. Como de costume, começaremos desenhando a imagem da cobra, que é apenas uma textura de 1 x 1 pixel:

  • Snake.png
    Nota: clique com o botão direito no link, selecione Salvar como… e salve-o na pasta Assets do projeto.

Usaremos as seguintes configurações de importação para isso:

Agora podemos arrastar a imagem da cobra para o meio da cena:

Até agora é apenas a cabeça da Cobra, então vamos renomeá-la para Cabeça para manter as coisas limpas:

A cobra deve fazer parte do mundo da física, o que significa que precisamos adicionar um Collider a ela novamente, então vamos selecionar Add Component -> Physics 2D -> Box Collider 2D :

Nota: o Collider tem o tamanho (0,7, 0,7) em vez de (1, 1) para não colidir com outras partes da cobra que estão próximas a ele. Queremos simplesmente dar algum espaço ao Unity.

Agora a cobra também deve se movimentar. Como regra geral, tudo no mundo da física que deveria se mover precisa de um corpo rígido . Um corpo rígido cuida de coisas como gravidade, velocidade e forças de movimento. Podemos adicionar um selecionando Add Component -> Physics 2D -> Rigidbody 2D . Usaremos as seguintes configurações para isso:

Notas:

  • A escala de gravidade do Rigidbody é 0 porque não queremos que a cobra caia na parte inferior da tela o tempo todo.
  • A opção Is Kinematic desativa o comportamento físico do Rigidbody, para que ele não reaja à gravidade ou colisões. Só precisamos saber se a cobra colidiu com alguma coisa. Não precisamos da física do Unity para movimentar as coisas em caso de colisões. Mais informações: Rigidbody2D Unity Docs .

A cobra final consistirá de muitos pequenos elementos. Sempre haverá a Cabeça na frente e depois haverá vários elementos da Cauda como aqui:

ooooox

A única diferença entre os elementos Cauda e a Cabeça é que a cabeça pensa todo. Adicionaremos um Script a ele mais tarde.

Vamos arrastar a cabeça da cobra da hierarquia para a ProjectArea para criar um Prefab e depois nomeá-la como TailPrefab para que possamos carregá-la sempre que a cobra crescer:


Nota: algumas versões do Unity renomearão automaticamente o GameObject na hierarquia também, então certifique-se de que aquela na Hierarquia ainda é denominado Cabeça .

Tudo bem, vamos selecionar o Snake Head na Hierarquia novamente e clicar em Add Component -> New Script , nomeá-lo Snake e selecionar CSharp como idioma:

Podemos abrir o Script clicando duas vezes nele na Área do Projeto :

using UnityEngine;
using System.Collections;

public class Snake : MonoBehaviour {

    // Use this for initialization
    void Start () {
   
    }
   
    // Update is called once per frame
    void Update () {
   
    }
}

Vamos modificar a parte superior do Script para incluir algumas funcionalidades da Lista que precisaremos mais tarde:

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

public class Snake : MonoBehaviour {

    // Use this for initialization
    void Start () {
   
    }
   
    // Update is called once per frame
    void Update () {
   
    }
}

A Cobra deve sempre mover exatamente uma unidade em qualquer direção que desejar. Agora, se permitíssemos que ele se movesse em todas as chamadas de atualização , seria muito rápido. Em vez disso, permitiremos apenas movimentos a cada 300 milissegundos usando a função InvokeRepeating do Unity . É como se criássemos nosso próprio método Update que só é chamado a cada 300 ms em vez de cada quadro:

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

public class Snake : MonoBehaviour {

    // Use this for initialization
    void Start () {
        // Move the Snake every 300ms
        InvokeRepeating("Move", 0.3f, 0.3f);    
    }
   
    // Update is called once per frame
    void Update () {
   
    }
   
    void Move() {
        // Do Movement Stuff..
    }
}

A Cobra deve estar sempre se movendo em alguma direção, nunca deve ficar parada. Então, vamos definir uma variável de direção e usá-la para mover a cobra na função Move :

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

public class Snake : MonoBehaviour {
    // Current Movement Direction
    // (by default it moves to the right)
    Vector2 dir = Vector2.right;

    // Use this for initialization
    void Start () {
        // Move the Snake every 300ms
        InvokeRepeating("Move", 0.3f, 0.3f);    
    }
   
    // Update is called once per frame
    void Update () {
   
    }
   
    void Move() {
        // Move head into new direction
        transform.Translate(dir);
    }
}

Nota: transform.Translate significa ‘adicionar este vetor à minha posição’ .

A variável de direção é do tipo Vector2 , o que significa que possui um valor no eixo X e no eixo Y. A imagem a seguir mostra diferentes direções para um Vector2:

Se apertarmos play já podemos ver a Cobra se movendo para a direita:

O usuário deverá ser capaz de alterar a direção do movimento pressionando uma das teclas de seta. Agora poderíamos apenas verificar o pressionamento de teclas em nossa função Mover , mas isso tornaria o jogo lento, porque só detectaríamos o pressionamento de teclas a cada 300 ms. Em vez disso, usaremos a função Update para detectar o pressionamento de teclas o tempo todo:

// Update is called once per Frame
void Update() {
    // Move in a new Direction?
    if (Input.GetKey(KeyCode.RightArrow))
        dir = Vector2.right;
    else if (Input.GetKey(KeyCode.DownArrow))
        dir = -Vector2.up;    // '-up' means 'down'
    else if (Input.GetKey(KeyCode.LeftArrow))
        dir = -Vector2.right; // '-right' means 'left'
    else if (Input.GetKey(KeyCode.UpArrow))
        dir = Vector2.up;
}

Nota: se você não tem certeza por que usamos Update e Move em vez de apenas Update ou apenas Move , sinta-se à vontade para colocar o código de Move em Update ou o código de Update em Move , então você verá que a cobra se move rapidamente ou que as teclas pressionadas são detectadas apenas raramente.

Se pressionarmos play, agora podemos mover a Cobra com as setas do teclado:

A cauda da cobra

Vamos pensar em como funcionará a cauda da Cobra. Em primeiro lugar, vamos supor que temos uma cobra com uma cabeça e três elementos de cauda:

ooox

Agora, assim que a cabeça se mover para a direita, o óbvio seria mover cada elemento da cauda para onde estava o elemento da cauda anterior, assim:

step 1: ooox   // snake didn't move yet
step 2: ooo x  // head moved to the right
step 3: oo ox  // first tail follows
step 4: o oox  // second tail follows
step 5:  ooox  // third tail follows

Isso funcionaria, mas também seria um código complicado. Vamos usar um pequeno truque para facilitar nossas vidas. Em vez de mover um elemento final após o outro, simplesmente moveremos o último elemento final para a lacuna, como aqui:

step 1: ooox   // snake didn't move yet
step 2: ooo x  // head moved to the right
step 3:  ooox  // last tail element moved into the gap

Agora, isso parece um algoritmo fácil. Em cada chamada de movimento, tudo o que precisamos fazer é mover o último elemento da cauda para onde estava a cabeça antes.

A princípio precisaremos de algum tipo de estrutura de dados para controlar todos os elementos finais que adicionaremos mais tarde:

// Keep Track of Tail
List<Transform> tail = new List<Transform>();

Nota: é muito importante adicionar ‘ using System.Collections.Generic; ‘ e ‘ usando System.Linq; ‘ para o topo para que as listas funcionem.

Vamos ao código que pega o último elemento final, remove-o da parte de trás e coloca-o na lacuna mencionada acima:

void Move() {
    // Save current position (gap will be here)
    Vector2 v = transform.position;

    // Move head into new direction (now there is a gap)
    transform.Translate(dir);

    // Do we have a Tail?
    if (tail.Count > 0) {
        // Move last Tail Element to where the Head was
        tail.Last().position = v;

        // Add to front of list, remove from the back
        tail.Insert(0, tail.Last());
        tail.RemoveAt(tail.Count-1);
    }
}

Nota: Traduzir significa simplesmente ‘adicionar este vetor à minha posição’. Depois verificamos se há alguma coisa na lista final, nesse caso alteramos a posição do último elemento final para a posição gap (onde estava a cabeça antes). Também temos que manter a ordem da nossa lista, daí as chamadas Insert e RemoveAt no final. Eles garantem que o último elemento final agora também seja o primeiro elemento da lista.

E essa foi a única parte um pouco complicada do nosso tutorial do Unity 2D Snake. Agora estamos quase terminando.

Alimentando a cobra

Usaremos a função OnTriggerEnter2D para receber informações de colisão (que acontecerá sempre que a cobra entrar em comida ou em uma borda) .

Sempre que ele encontrar comida, usaremos exatamente a mesma mecânica que usamos para nosso movimento acima, exceto que desta vez, em vez de remover o último elemento da cauda e movê-lo para a lacuna, iremos apenas instanciar um novo elemento na lacuna:

ooo x  // gap
oooox  // gap filled with new element

É importante entender que não deixaremos a Cobra mais longa imediatamente após ela comer alguma coisa. Assim como pressionamos as teclas de seta, esperaremos até que ele realmente se mova. Portanto precisaremos de uma nova variável que definiremos como verdadeira sempre que a Cobra comer alguma coisa:

// Did the snake eat something?
bool ate = false;

Também precisaremos de uma variável pública que nos permitirá atribuir o TailPrefab posteriormente:

// Did the snake eat something?
bool ate = false;

// Tail Prefab
public GameObject tailPrefab;

Nota: as duas variáveis ​​são definidas no topo do nosso script Snake.

Agora vamos para a função OnTriggerEnter2D . Este será direto novamente. Descobriremos se a Cobra colidiu com a comida; nesse caso, definimos a variável ate como verdadeira e destruímos a comida. Se não colidiu com a comida, então colidiu consigo mesmo ou com uma borda:

void OnTriggerEnter2D(Collider2D coll) {
    // Food?
    if (coll.name.StartsWith("FoodPrefab")) {
        // Get longer in next Move call
        ate = true;
       
        // Remove the Food
        Destroy(coll.gameObject);
    }
    // Collided with Tail or Border
    else {
        // ToDo 'You lose' screen
    }
}

Nota: usamos coll.name.StartsWith porque o alimento é chamado de ‘FoodPrefab(Clone)’ após instanciá-lo. A maneira mais elegante de descobrir se col é food ou não seria usando um Tag , mas para simplificar usaremos comparação de strings neste Tutorial.

Tudo bem, vamos modificar nossa função Move para que a Snake fique mais longa sempre que ate for verdadeiro:

void Move() {
    // Save current position (gap will be here)
    Vector2 v = transform.position;

    // Move head into new direction (now there is a gap)
    transform.Translate(dir);

    // Ate something? Then insert new Element into gap
    if (ate) {
        // Load Prefab into the world
        GameObject g =(GameObject)Instantiate(tailPrefab,
                                              v,
                                              Quaternion.identity);

        // Keep track of it in our tail list
        tail.Insert(0, g.transform);

        // Reset the flag
        ate = false;
    }
    // Do we have a Tail?
    else if (tail.Count > 0) {
        // Move last Tail Element to where the Head was
        tail.Last().position = v;

        // Add to front of list, remove from the back
        tail.Insert(0, tail.Last());
        tail.RemoveAt(tail.Count-1);
    }
}

Nota: tudo o que fizemos foi verificar se ate é verdadeiro e, em seguida, instanciar o pré-fabricado final na posição v com a rotação padrão ( Quaternion.identity ). Depois, nós o adicionamos à lista final e redefinimos o sinalizador ate . O resto do código já estava lá.

Agora podemos selecionar o Snake Head na Hierarchy , dar uma olhada no Inspector e arrastar o TailPrefab da Project Area para o Script:

Se pressionarmos Play , agora podemos jogar uma bela partida de Snake:

Nota: Como não há condição de fim de jogo para colidir com sua própria cauda ou bater em uma parede, você descobrirá que passará pelos referidos objetos. Isso não é um bug e cabe ao leitor implementar a referida condição.

Resumo

Snake é um jogo incrível para um tutorial do Unity. Há muito valor em entender como fazer jogos com pixels exatos como esse e como adicionar movimento com InvokeRepeating . Mais uma vez vimos como os jogos 2D são incrivelmente fáceis com os robustos recursos 2D do Unity.

E ai, esta gostando da área de games? Então que tal se tornar um desenvolvedor de jogos completo com o curso Desenvolvedor de Jogos 2D e 3D. Neste curso você irá aprender na prática tudo sobre Game Design, Criação de artes para jogos, Lógica de Programação, Programação Orienta a Objetos e é claro, criar seus próprios jogos utilizando a Engine Unity.

FONTE: Esse texto foi retirado do site https://noobtuts.com/unity,

Super Dicas

Venha conhecer os nossos cursos da Hotmart Clube e Udemy.

Se inscreva em nosso canal, compartilhe as matérias que gostar com os seus colegas, e participe da nossa comunidade no Telegram.

Aproveite também e venha fazer parte do nosso clube de vantagens e ter acesso exclusivo a vídeos, tutoriais, cursos e muito mais. Clique no link para se tornar um membro do dfilitto – clube de vantagens e ter acesso a todos os benefícios do nosso clube.

Matheus Correia Augusto da Silva

Me chamo Matheus Correia, tenho 21 anos e curso graduação em Marketing, faculdade na qual termino em junho, em breve será lançado um livro no qual faço parte.
Faço estágio na empresa Micro2000 e faço parte do marketing de criação de imagens para redes sociais.

Assinar blog por e-mail

Digite seu endereço de e-mail para assinar este blog e receber notificações de novas publicações por e-mail.

Junte-se a 2.252 outros assinantes

Anúncios

Aprenda a criar seus jogos com os melhores desenvolvedores de jogos

Advertisement

Quer aprender a programar?