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:
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.
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:
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:
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:
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.
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
}
}
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.
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:
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