Tutorial Snake 3D usando Unity - Parte 6

Tutorial Snake 3D usando Unity - Parte 6

Iai galerinha, tudo tranquilo com vocês? Espero que o recesso de ano novo tenha sido maravilhoso e tenha dado tempo para que vocês possam ter estudado sobre Game Dev. Voltando ao nosso belíssimo tutorial, já havia comentado nas últimas postagens que estamos entrando no nosso divisor de águas, e hoje será o nosso ultimato. Fica por dentro do que iremos construir hoje:

Tasks

  1. Explicar os conceitos principais de lista encadeada simples;
  2. Fazer as partes novas da cobra seguirem o movimento;
  3. Ajeitar posição da nova parte da cobra.

Lista encadeada simples

Basicamente, lista encadeada é uma estrutura de dados que usamos na programação para representar dados que possuem linearidade, e neste caso, por ser uma lista simples só nos importamos com a ordem do próximo elemento da lista.

Uma forma simples de entender isso é explicando com o exemplo dos números. Se eu perguntar qual o sucessor do número 32, você com certeza vai pensar no 33, porque já sabe que o próximo elemento é ele visto que você soma mais um e temos este número natural. Do mesmo jeito temos o seu antecessor, que seria o 31. Veja que você compreende uma lista encadeada dupla apenas por assimilar à algo do cotidiano, sabendo que um elemento tem uma referência para o próximo e uma para o anterior. Na lista encadeada simples, guardamos a referência apenas para o próximo por motivos de 'simplicidade'.

Lista Encadeada Simples - MundoJS
Imagem da internet

Esta é uma das estruturas de dados mais simples que podemos aprender. O estudo de algoritmos segue nesta linha para facilitar a compreensão de problemas mais complexos. Na programação também é comum vermos estruturas de dados como: fila, pilha, lista circular, árvore, grafos, entre diversos outros.

Sem mais delongas, vou te apresentar uma versão de como nosso código pode fazer uso dessa estrutura. Iremos usá-la para controlar as partes da cobra. Veja que essa imagem quase representa uma cobra; se você pensar que os elementos "Data" são os objetos que vamos usar para representar a parte do corpo a ser adicionada e que o "Next" é a referência para a próxima parte do corpo que a cobra vai ter.

using UnityEngine;

public class SnakePiece: MonoBehaviour {
    
    GameObject snakepiece;
    SnakePiece next;

    public void MoveOnto(Vector3 position)
    {
        Vector3 nextPos = this.transform.position;
        this.transform.position = position;
        next?.MoveOnto(nextPos);
    }

    public void SetNext(SnakePiece next)
    {
        this.next = next;
    }
}

Este script vai ser inserido no nosso prefab "Snake Part" e também na cabeça da cobra. E agora, precisamos fazer uma alteração no comportamento de movimento da cobra. Esse pequeno trecho de código aqui que se encontra lá no SnakeComponent:

SnakePiece head;

void Start()
{
    tickController = FindObjectOfType<TickController>();
    tickController.onTick += OnTick;
    head = GetComponent<SnakePiece>();
}

void OnTick()
{
    // TO DO: perform action
    Vector3 dir = transform.position + direction * stepOffset;
    head.MoveOnto(dir);
}

Agora, a gente ta garantindo que o nosso SnakeComponent, que atua como um controlador geral da cobra, tenha uma referência para a cabeça da cobra (que por sinal é ela mesma) e a partir da cabeça, ela chama uma função de movimentar para a posição final que queremos, e dentro então da função de movimentar temos:

  • next?.MoveOnto(this.transform.position); - um trecho que verifica se existe uma próxima parte de cobra e só então chama a função de movimento com a posição atual da peça que chamou essa função.
  • this.transform.position = position; - um trecho que movimenta a parte da cobra atual para a próxima posição final que ela deve estar.

O movimento ainda não está completo. Ainda precisamos de mais alguns ajustes para que fique perfeitinho. Agora vamos alterar a posição em que uma nova parte da cobra é posta ao ser criado e aproveitar para definir o próximo elemento da nossa estrutura de dados que vai dizer qual é a próxima parte da cobra. Junto com isso vamos manter nossa hierarquia limpa e organizada para que nossa estrutura de projeto não seja um Frankenstein gigante. Para isso, tudo que vamos precisar fazer agora é alterar este pequeno trecho de código no SnakeComponent:

[SerializeField] Transform snakepartParent;

void Grow()
{
    // TODO
    // Debug.Log("I'm working");
    length += 1;
    // Debug.Log("Snake length " + length);
    
    // get snake part object reference
    GameObject snakepart = Instantiate(snakePartPrefab);
    
    // set the parent to clean up hierarchy
    snakepart.transform.SetParent(snakepartParent);
    
    // set initial position
    snakepart.transform.position = this.transform.position - direction * stepOffset;
    
    // get SnakePiece component and set next piece
    SnakePiece snakepartPiece = snakepart.GetComponent<SnakePiece>();
    head.next = snakepartPiece;
}

O trecho explicado fica dessa forma:

  • GameObject snakepart = Instantiate(snakePartPrefab); - este trecho 'cacheando' a referência do GameObject a ser criado e adicionado como nova parte da cobra em uma variável local*.
  • snakepart.transform.SetParent(snakepartParent); - este trecho está garantindo o novo parentesco com a variável snakepartParent que nós criamos anteriormentes.
  • snakepart.transform.position = this.transform.position - direction * stepOffset; - este trecho está fazendo com que a nova posição da parte da cobra recém-criada seja exatamente um passo atrás da posição da cobra atual.

*variável local: é uma variável que só existe dentro do escopo criado. Um escopo é definido por { } no C#.

Crie um novo transform vazio dentro do nosso objeto Cobra, eu chamei de "SnakePieceParent", e faça a assimilação da referência na variável do editor.

Gif 1 - Hierarquia de componentes

E o nosso movimento vai ficar assim:

Gif 2 - Movimento com nova estrutura de dados

Como podem ver a movimentação está clean do clean. Porém, ainda temos um probleminha ali no final que é a parte de adição de novas e novas partes da cobra. Podemos identificar dois erros:

  1. Ao adicionar uma nova parte, não pegamos a parte da cobra que foi adicionada por último;
  2. Adicionar uma segunda nova parte não posiciona ela no local adequado, visto que a posição tem referência apenas com relação à posição da cabeça da cobra.

Pra resolver o primeiro problema tudo que precisamos fazer é ter uma nova referência para a cauda da cobra. Assim como fizemos com a cabeça, vamos criar uma nova variável e "setar" ela. Deixei uma imagem explicando quais vão ser os passos para fazer uma adição no final da estrutura. Acontece que a lista encadeada tem algumas funções que são comuns de serem implementar, são elas:

  • Contagem de nós
  • Adição no final
  • Adição no começo
  • Remoção no começo
  • Remoção no final

E estas são apenas algumas dentre várias. Para este tutorial iremos implementar apenas adição no final sem mexer com a referência da cabeça.

Explicação - Etapa de adição da cauda

No passo (1) ao adicionar uma nova peça na lista o que fazemos é pegar a referência da cauda atual e apontar o próximo elemento dela para a nova referência, passando pra código seria algo tipo:

tail.SetNext(newPiece);

No passo (2) mudamos a referência da cauda para a referência da nova peça que foi adicionada, que nos daria algo parecido com:

tail = newPiece;


SnakePiece tail;

void Start()
{
    tickController = FindObjectOfType<TickController>();
    tickController.onTick += OnTick;
    head = GetComponent<SnakePiece>();
    tail = GetComponent<SnakePiece>();
}

void Grow()
{
    // TODO
    // Debug.Log("I'm working");
    length += 1;
    // Debug.Log("Snake length " + length);
    
    // get snake part object reference
    GameObject snakepart = Instantiate(snakePartPrefab);
    // set the parent to clean up hierarchy
    snakepart.transform.SetParent(snakepartParent);
    // set initial position
    snakepart.transform.position = this.transform.position - direction * stepOffset;
    // get SnakePiece component and set next piece
    SnakePiece snakepartPiece = snakepart.GetComponent<SnakePiece>();
    // set the current tail next to point to the new snake part
    tail.SetNext(snakepartPiece);
    // update the new last piece
    tail = snakepartPiece;
}

Conclusão

Até agora, temos uma nova estrutura implementada na nossa cobra e podemos mexer com o movimento dela garantindo que todas as outras partes da cobra vão seguir uma cadeia de movimento encadeadas dependendo sempre da sua parte anterior. Espero que este tutorial tenha sido um complemento forte para a lógica de programação de vocês. Venham discutir com a gente no nosso grupo do whatsapp e participar dos encontros no nosso servidor, estamos resolvendo questões de lógica diariamente pela manhã a partir das 9h BRT.