Matheus Cunha
Matheus Cunha16/09/2023 21:19
Compartilhe

Javascript na pática: Construindo o Snake Game

  • #Lógica de Programação
  • #JavaScript

Tabela de Conteúdos:

  • Visão Geral
  • O projeto
  • O que você pode aprender
  • Prática

Visão geral

Não tem nada mais chato do que ver teoria sem prática. Esta publicação é para trazer um pouco de conhecimento atrelado à prática no JavaScript, tentando explorar minuciosamente alguns parâmetros que estamos aplicando em um projeto.

O projeto

O projeto que usaremos como prática é o famosos “jogo da cobrinha” ou Snake Game. O jogo dispensa apresentações, por isso vou deixar apenas esta imagem do seu funcionamento:

image

O que você vai aprender

  • Criação e manipulação do elemento Canva.
  • Aplicação de lógica do JavaScript em um jogo
  • Eventos

Prática

Certo, vamos dividir a prática em 4 grandes etapas para ficar mais fácil abordá-la: 1 — criar o campo; 2 — Criar a cobra e a maçã; 3— adicionar regras do jogo.

Dica: Você pode realizar todo o projeto no CodePen

1 — Criar o campo.

A criação do campo é bem tranquila. No html vamos utilizar o elemento “Canva” com a definição de width e height em 400px para darmos vida a ele na tela (Este tamanho de 400px que estamos definindo agora é importante para dividirmos posteriormente)Teremos algo assim:

<canva id="board" width="400px" height="400px"></canva>

Para os colegas que ainda não o utilizaram, o <canva> é um elemento que cria na tela uma área “bitmizada” que permite fazer a manipulação desta área com o JavaScript.

image

Foto de um campo bitmap

Como demonstrado na imagem, conseguimos “desenhar” em campos pré-definidos.

Agora vamos para o JavaScript para iniciarmos a pintura do campo. Para delimitar o campo precisamos definir 4 variáveis no arquivo JavaScript:

1 — board: Nesta variável vamos usar o método “getElementById()”.

let board = getElementById('board'); //Aqui criamos a referência do elemento que está na tela

2 — getContext(): Vamos utilizar este método para preparar o canva para receber as nossas alterações em um contexto de 2 dimensões. Esta é uma estrutura básica do próprio canva.

var context = board.getContext("2d");

3 — fieldQuantity: Esta constante será responsável por dizer em quantos campos teremos dentro do nosso canva.

const fieldQuantity = 20;

4 — fieldLength: Esta constante será responsável por definir o tamanho de cada campo em px.

const fieldLength = 20;

Repare que, tanto a quantidade de campos e o tamanho de campos são iguais. A lógica aqui é simples, no eixo X temos a distância de 400px e vamos dividir em 20 partes(quantity) de 20px(length). O mesmo para o eixo Y.

Aí você, leitor, que é um sabedor das ciências ocultas e matemáticas, deve ter se perguntado “se em um plano qualquer de 2 dimensões dividido da forma como dividimos, deveríamos ter 400 campos de 20 pixel na tela e não 20 campos, onde entra esta informação?”. E aqui, senhores, está a ação do getContext(“2d”), que interpreta os seus dados e replica para as duas dimensões.

Certo, agora vamos dar cor ao nosso quadro. Neste caso, vamos pegar o elemento projetado em duas dimensões, e utilizar o método “fillRect(xInicial, yInicial, width, height)” para preenchê-lo como uma figura retangular; e o método “fillStyle” para definir a cor do elemento.

context.fillStyle = "black";
context.fillRect(0, 0, board.width, board.height);
// x = 0 e y = 0 informa que a insersão da cor deve começar nos pontos (0,0) e ir até width e height do board.
// importante ressaltar que os pontos 0,0 ficarão no topo do canva.
// Então olhe para ele como um plano cartesiano onde o eixo x,y ficam no canto superior a esquerda da tela, onde começao canva.

Após completar este etapa, você deve ser capaz de ver no seu navegador o board com fundo preto:

image

Board do game até o momento. Um retângulo de fundo preto

2 — Criar a cobra e a maçã

Apesar de separar esta etapa da criação das regras do jogo, no desenvolvimento dela, vamos precisar pensar e interpretar algumas regras para decidir qual será o nosso próximo passo. Se fizéssemos o oposto, faríamos um exercício abstrato demais para ter um resultado apenas no final.

A primeira regra que precisamos pensar aqui, para este grame, é a taxa de atualização da tela. Veja que a partir do momento que criamos a tela, ela é um monolito estático, como um quadro. Como nos desenhos antigos e nos jogos de minigame, para dar a sensação de movimento, precisamos apagar a tela e redesenhar a tela atualizada. Então, sempre que a cobra (que vamos chamar de Cruela) andar, a tela precisa atualizar a informação para mostrar onde a Cruela está atualmente.

Pegando esta informação, vamos criar uma regra para atualizar a tela a cada unidade de tempo:

setInterval(updFrame, 1000 / 15);

function updFrame(){

}

Neste caso então, vamos chamar a cada 60 milisegundos uma função chamada Update Frame(updFrame) e vamos começar o jogo a partir dela.

a. A maçã

A maçã por ser mais fácil de criá-la, vamos começar por ela. Para a maçã, assim como para o board e para a Cruela posteriormente, sempre precisaremos tratá-los em 2 dimensões, então vamos definir um ponto inicial para a que ela apareça.

E para pintá-la usaremos os mesmos métodos que utilizamos anteriormente:

//........
let [appleX, appleY] = [10, 15]; //repare que, como nosso board vai até o ponto 20, se colocar que a maçã estará em um ponto maior ou igual ao ponto 20, ela estará fora do nosso tabuleiro.

function updFrame(){

context.fillStyle = "red";  //definimos a cor da maçã como vermelha  
  context.fillRect(
    appleX * fieldLength, //aqui definimos que a maçã existirá a partir do ponto X (10) até 20px depois desse ponto e antes do ponto 11.
    appleY * fieldLength, // o mesmo para o eixo y para formar um quadrado
    fieldLength, // aqui define a distância que será preenchida no eixo x
    fieldLength // aqui define a distância que será preenchida no eixo y
  );
}

A partir destas definições, teremos a seguinte tela:

image

Board do game mostrando a maçã

b. A cruela

Para fazer a Cruela, vamos pensar o conceito dela. Diferente da maçã, a cobra se moverá e o corpo dela seguirá o caminho feito. Precisamos pensar nesta regra, pois o movimento é importante e quase uma identidade do jogo. A partir desta lógica, precisamos criar uma cabeça, que ditará o movimento, o corpo dela e guardar o caminho / trilha que o movimento que foi executado para cada parte do corpo seguir esta trilha. Para isto criaremos 3 variáveis:

let [headX, headY] = [10 , 5]; //Assim como a maçã, vamos iniciar a Cruela em um ponto do mapa e será no ponto (10,5).
let snakeBody = 3; //Aqui estamos definindo o tamanho inicial da cobra
let snakeTrail = []; // E neste array iremos guardar a trilha que a Cruela fez. A ideia é que 
// No snakeTrail será uma array de objetos com as seguintes infos 
// snakeTrail = [ { x:10, y:5 }, { x:11, y:5 }, { x:11, y:4 }, { x:11, y:3 } ...]
// neste exemplo mostra mais ou menos uma representação de algo se movendo para o lado (eixo X) e para cima (eixo Y)

function updFrame(){
// .........

}

Para dar via ao corpo da cobra, o processo será diferente da maça. Vemos aqui que a forma como a cobra é apresentada no mapa, é o seu tamanho e como definimos, o seu tamanho inicial é 3. Ou seja, ela irá ocupar “campos” do mapa. Ao mesmo tempo, como estamos armazenando o caminho que a cobra fará dentro de uma array, podemos usar o maravilho método “array.map()” que irá pegar o caminho que a cobra faz e irá “pintar” o corpo da cobra em todos os campos por onde ela está passando.

!Importante: Todas as variáveis iniciais devem estar FORA da função update Frame, okay? O porquê… se você colocar dentro da função, toda vez que ele atualizar a página, a cobra voltará para o ponto inicial.

Faremos da seguinte forma:

function updFrame(){
// .........

context.fillStyle = "white";  
 
for (let a = 0; a < snakeTrail.length; a++) { //Não gosto de usar o i no loop for
  
context.fillRect(
  snakeTrail[a].x * fieldLength,
  snakeTrail[a].y * fieldLength,
  fieldLength,
  fieldLength
    );
}

// Aqui nós adicionamos adicionamos a cabeça da cobra no Trail, pois a cabeça será o primeiro movimento
snakeTrail.push({ x: headX, y: headY });
// A cabeça deve ser renderizada por ultimo, pois se ela for a primeira coisa
// renderizada, o resto do corpo será renderizado por cima da caveça e se tentarmos
// checar se a cobra bateu em algo, ela sempre acusará que está batendo no próprio corpo

}

Repare que a confecção é muito parecida com a anterior, o que difere é a necessidade de usar a iteração para acessar todo o caminho que a cobra fará, interagindo com os pontos x,y armazenados (estamos levando em consideração os objetos armazenados neste Array).

Antes de continuar, vamos pular algumas etapas e dar movimento ao nosso personagem Cruela, apenas para vê-la se mexer no board? depois disso vamos direto para as regras do jogo.

c. movimentação

Para criar a movimentação temos 2 variáveis para levar em consideração e uma lógica. Como vimos anteriormente, nós inserimos a Cruela no board dando um “push” das suas coordenadas no plano cartesiano, Agora o que temos que fazer é andar e dar o push de todo o caminho feito dentro do “snakeTrail”.

As variáveis que preciamos computar agora são o botão digitado, que ditará o movimento, e a velocidade que nossa amiga andará, que naturalmente será 1 bloco por vez.

const velocity = 1; // Aqui dizemos a velocidade que o a cobra se moverá quando estiver em movimento
var [velX, velY] = [0,0] // esta variável nos contará a velocidade atual da cobra, inicialmente em 0

function updFrame(){
// ........  

// Atentamos aqui que se a velocidade x,y é da cobra, temos que atrelar a ela. Vamos ligar estas variáveis da seguinte forma

headX += velX;
headY +=  velY;
// assim estamos dizendo que headX = headX + velX
// então se headX está na posição 10 e a velocidade X for 1, o próximo local que headX estará é o 11
// assim movimentamos a cobra em um ponto no eixo X.

//Neste caso, esta atribuição deve ser feita antes da iteração, pois se fizemos depois,
// o programa primeiro irá checar se ela pode andar e depois ela irá andar
// mas sempre ficará travada. Desta forma garantimos que ela dará o primeiro
// passo e depois checará se ela colidiu com algo.

context.fillStyle = "white";    
for (let a = 0; a < snakeTrail.length; a++){
// .......
}

Neste caso, é importante que as variáveis de velocidade sejam declaradas no escopo global com var, ou fora da função updFrame, pois precisaremos acessá-la de outra função que irá verificar a tecla digitada.

Para fazer esta verificação, usaremos uma função que irá checar o botão apertado e, por meio do switch irá colocar, definir a velocidade X ou Y da cobra.

document.addEventListener("keydown", keyPress); 
//Este event listener irá escutar o botão apertado e executará a função keyPress


const velocity = 1;
var velX = 0;
var velY = 0;

function updFrame(){
// ........
headX += velX;
headY += velY;
}

function keyPress(event){
  //Se não souber o código do botão do seu teclado, ou quiser configurar o seu teclado à vontade
  //utilize este console.log para verificar o código dos botões do seu teclado.
  console.log(event.keyCode);

  //A lógica é bem simples no eixo X, imagine o plano cartesiano onde o 
  //eixo X conforme anda para  direita ele aumenta e quando anda 
  // e qando anda para a esquerda ele se aproxima de 0 
  switch (event.keyCode) {
    case 37: //Para esquerda
      velx = -velocity;
      vely = 0;
      break;
    case 38: //Para cima
      velx = 0;
      vely = -velocity;
      break;
    case 39: //Para direita
      velx = velocity;
      vely = 0;
      break;
    case 40: //Para baixo
      velx = 0;
      vely = velocity;
      break;
  }
}


A lógica do eixo X e Y são bem parecidas, a única nuance, como foi dito anteriormente, é que no caso do canva é como se fosse um plano cartesiano de cabeça para baixo então por isso ir para cima e para baixo é a lógica contrária do que vemos o X.

Neste momento ao apertar as teclas do seu teclado a Cruela começará a se movimentar pelo mapa. No entanto ela ficará deste jeito:

image

A cobra se movimentou sem parar e saiu da tela

Aqui vemos dois grandes problemas. O primeiro, a cobra saiu da tela, a segunda é que todos os movimentos estão sendo inseridos no array e o caminho da cobra não se atualiza. O que podemos fazer aqui é Verificar se a cobra está maior do que o corpo dela permite, e atualizar o seu tamanho removendo do array a entrada mais antiga. Ou seja, se a ultima entrada fica no final, removemos a entrada que está no índice 0.

function updFrame(){
//.......
while (snakeTrail.length > snakeBody) {
   snakeTrail.shift();
 }
}

Após fazer este movimento, verá que o array que contem o caminho da cobra (snakeTrail) e elimina as informações que seja desnecessárias, mostrando apenas as necessárias. Talvez você veja que a tela em si não mudou, o Cruela continua riscando todo o board e não vejo a diferença, se isto acontecer, provavelmente você terá que voltar lá nas primeiras linhas digitadas, recortar a “pintura” do board, e colá-lo dentro da função updateFrame.

function updFrame(){
context.fillStyle = "black";
context.fillRect(0, 0, board.width, board.height);
//.......... o restante da função deverá estar abaixo
}

Agora a Cruela estará andando andando livremente pelo board como você desejar.

image

A cobra andando corretamente.

3 — Regras do jogo

Neste jogo simples e bobo, vamos adicionar apenas três regras básicas: a. Ao passar por cima da maçã, a cobra deverá aumentar o seu tamanho e uma nova maçã deverá ser gerada em outro lugar do board; b. Caso a cobra bata em si mesma o jogo volta ao ponto inicial (game over); c. Queremos que quando a cobra chegue até a borda do mapa ela surja na borda oposta.

Estas são as regras de negócio do nosso jogo e, se você chegou até aqui, pode sofrer mais um pouquinho para terminarmos o game.

Para as regras a seguir, vamos, basicamente, usar o if para verificar se a cabeça da cobra tocou na maça, em si mesma, ou na borda e fará uma ação diferente em cada caso.

if(headX == PontoX de algo && headY == Ponto Y do mesmo algo){
}

a. regra da maçã de ouro

Seguindo esta nossa lógica anterior, vamos checar se a cobra tocou na maçã:

function updFrame(){
// ..........
if(headX == appleX && headY == appleY){ //aqui checamos a colisão dos objetos
  snakeBody++ // adicionamos + 1 ponto no tamanho da cobra

  appleX = Math.floor(Math.random() * fieldQuantity)
  appleY = Math.floor(Math.random() * fieldQuantity)
  
  //Aqui geramos um número aleatório para a maçã e ela será renderizada em outra parte do board, 
  //respeitando o tamanho a quantidade de campos que tem no board
  // para que ela não seja gerada fora do mapa ou apenas em um lugar do mapa

 }
}

image

Jogo com a regra da maçã adicionada

b. regra da colisão

Nesta regra faremos a mesma checagem. No entanto, vamos precisar fazer a checagem no mesmo local que fazemos a iteração do caminho feito pela cobra. Isto se faz necessário, porque cada parte da cobra preenche um campo dentro do board, então ela precia checar se a cabeça da cobra colidiu com cada um desses campos. Teremos o seguinte código:

for (let a = 0; a < snakeTrail.length; a++) {
    //Não gosto de usar o i no loop for
    context.fillRect(
      snakeTrail[a].x * fieldLength,
      snakeTrail[a].y * fieldLength,
      fieldLength,
      fieldLength
    );

  if( snakeTrail[a].x == headX && snakeTrail[a].y == headY){
    velX = 0;
    velY = 0;
    snakeBody = 3;
  }
}

Assim você já garante que a cobra pare e resete quando colidir consigo mesma. Agora vamos para a última regra:

c. regra da borda infinita

Aqui nós teremos que interagir com o board, tendo em mente aquele plano cartesiano que falamos desde o inicio da nossa criação.

image

Olhando aqui, nós podemo refletir o seguinte. Nós queremos verificar se a cabeça da cobra passou da borda esquerda, significa que headX ficou menor que 1. E para aparecer do outro lado, o que faremos? Sabemos que o board tem 20 campos (fieldQuantity), então nós queremos que ele surja no campo 20 — 1 (fieldQuantity — 1 )que é o último campo antes da borda.

Vamos usar esta lógica para todos os quatro campos:

if (headX < 0) {
   headX = fieldQuantity -1;
 }
 if (headX > fieldQuantity -1) {
   headX = 0 ;
 }
 if (headY < 0) {
   headY = fieldQuantity - 1;
 }
 if (headY > fieldQuantity -1 )  {
   headY = 0;
 }

E assim nosso código está completo e funcional de um Snake Game. Um ajuste legal que eu recomentdo é ir no código e fazer esta alteração:

//..........
for (let a = 0; a < snakeTrail.length; a++) { //Não gosto de usar o i no loop for
  
context.fillRect(
  snakeTrail[a].x * fieldLength,
  snakeTrail[a].y * fieldLength,
  fieldLength -1,
  fieldLength -1 //esta alteração irá tirar 1 px da pintura e dará o efeito de contorno em volta dos campos que a cobra ocupa
    );
//........

E o resultado final é este:

image

Parabéns! Vá lá, mostre para os seus amigos o que você fez usando algumas poucas linhas de código.

Se gostou deste tutorial, me siga no GitHub e no LinkedIn, dê uma olhada no repositório (link no início do artigo) e faça você mesmo. Não deixe nunca de praticar.

Ainda há muito o que melhorar e se voce gostar do desafio, continue daqui. Pegue este artigo e faça um estudo incrementando este jogo. Crie um bloco de pontuação, uma mensagem de GameOver personalizada, deixe a sua criatividade rolar.

Artigo escrito por mim originalmente no Medium : https://medium.com/@tirianx/javascript-na-p%C3%A1tica-construindo-o-snake-game-a772f7353eac

Compartilhe
Comentários (1)
HELTON ANDRADE
HELTON ANDRADE - 17/09/2023 09:17

muito bom, bem didático