Desenvolvendo um jogo de quebra blocos em javascript

Programando um jogo clássico de arcade usando javascript e p5.js. O usuário deve quebrar os blocos utilizando uma bola ao mesmo tempo que evita que a bola saia pela parte inferior da tela

Postado em 16 dezembro 2022

Atualizado em 16 dezembro 2022



Introdução

O jogo de quebra-blocos (block breaker) é um jogo clássico e de fácil desenvolvimento. Este projeto aborda sobre conceitos extremamente básicos na programação de jogos, como por exemplo conflito e inversão de direção.

O usuário controla um bloco que fica localizado na parte inferior da tela, podendo apenas movimentar no sentido horizontal. O objetivo do jogador é quebrar os blocos na parte superior da tela com uma bola que fica em constante movimento, cuidando para que ela não escape dos limites da tela na parte de baixo.

Esse projeto mostra como desenvolver um jogo de quebra-blocos do modo mais simples possível. Nesse tutorial não terá tela de início, tela de fim e nem recursos gráficos para enfeitar. Serão apenas aplicados a lógico do jogo.

Ferramentas utilizadas

Esse projeto será desenvolvido em javascript e estará utilizando a biblioteca p5.js. O editor utilizado será o editor disponibilizado pela p5.js que também é utilizado no tutorial de jogo da serpente postado no nosso site.

Planejamento

O jogo será desenvolvido levando em consideração os seguintes valores:

Constante Valor
Altura da tela 500
Largura da tela 700
Altura de cada bloco 20
Largura de cada bloco 100
Diâmetro da bola 15

Estágios

Como dito anteriormente, o jogo não terá tela de início e nem tela de começo. Mesmo quando todos os blocos tiverem sido destruídos o jogo continua rodando até que a bola escape pela parte inferior da tela.

O primeiro e único estágio será simples, possuindo cinco blocos em quatro linhas. A imagem abaixo descreve melhor.

planejamento do jogo de quebra blocos mostrando as constantes

Ângulo da bola ao tocar em superfícies

Quando a bola bater nos limites da tela, ela será redirecionada para o lado oposto que ela seguia antes. Porém, caso a bola chegue no limite da tela na parte inferior da tela, ela não será redirecionada e o jogo irá parar de rodar.

trajeto que a bola toma ao entrar em contanto com os limites da tela

Essa lógica funciona da mesma forma quando a bola entra em colisão com algum bloco, seja do usuário ou seja um obstáculo. Porém, ao bater nas extremidades do bloco a bola volta pelo mesmo rumo que ela venho, como mostra a imagem abaixo:

comportamento da bola ao entrar em contato com a extremidade do bloco

Declarando as constantes

As constantes serão as variáveis com o valor fixo durante o jogo inteiro. O valor das constantes será igual aos valores mostrados na etapa de planejamento.

// Tamanho padrão da tela
const screenWidth = 700;
const screenHeight = 500;
// Tamanho padrão do bloco
const blockDefaultWidth = 100;
const blockDefaultHeight = 20;
// Tamanho padrão da bola
const defaultBallSize = 15;

As constantes acima são importantíssimas e servem de base para os cálculos que serão feitos durante o jogo.

Implementando o bloco

A classe “Block” será utilizada para o uso do usuário e dos obstáculos, possuindo apenas uma função com o objetivo de exibir o bloco na tela.

class Block {
  constructor(x,y, width, height, color = {
    red: 255,
    green: 255,
    blue: 255
  }) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color
  }
  
  draw() {
    fill(this.color.red, this.color.green, this.color.blue);
    rect(this.x, this.y, this.width, this.height);
  }
}

Como mostra no construtor da classe, a cor padrão do bloco será branca, a menos que sua cor seja definida.

Implementando a bola

Os atributos da bola são as coordenadas, diâmetro, velocidade, direção e ativação. A variável de ativação (isBoucing) é responsável por gerenciar se a bola está ou não em movimento.

Outra variável importantíssima é a de direção. A variável de direção é um objeto contendo o valor atual da direção horizontal e vertical da bola.

class Ball {
  constructor(x, y, diameter = defaultBallSize) {
    this.x = x;
    this.y = y;
    this.diameter = diameter;
    this.isBouncing = false;
    this.speed = 5;
    this.direction = {
      x: -1,
      y: -1
    };
  }
}

A lógica da direção da bola é a seguinte:

Coordenada Valor Efeito
x -1 Movimento horizontal para a esquerda
x 1 Movimento horizontal para a direita
y -1 Movimento vertical para cima
y 1 Movimento vertical para baixo

Ao todo a bola possui três funções declaradas:

  • Função de exibição
  • Função de movimento
  • Função de redirecionamento

Função de exibição

A função de exibição da bola é extremamente simples.

draw() {
    // Faz com que a cor da bola seja constatemente branca
    fill(255, 255, 255);
    // Desenha a bola na tela
    circle(this.x, this.y, this.diameter);
    return this.bounce();
}

Diferentemente de outras funções de exibição, a bola retorna um valor boolean informando se a bola colidiu com os limites da tela.

Função de movimento

A função de movimento também é bastante simples e é invocada na função de exibição da bola. Enquanto a bola estiver ativada com o atributo “isBouncing”, ela estará em movimento. Caso contrário apenas um boolean com valor “true” será retornado.

bounce() {
    // Checa se a bola está em movimento
    if (this.isBoucing) {
      // Movimento a bola baseando-se na direção atual
      this.y += this.speed * this.direction.y;
      this.x += this.speed * this.direction.x;
      
      return this.rebounce();
    }
    
    return true;
}

É possível observar acima que a movimentação da bola é bastante dependente da direção atual da bola.

Função de redirecionamento

Responsável por redirecionar a bola quando ela colide com os limites da tela. A função de redirecionamento também checa se a bola saiu dos limites pelo lado inferior da tela.

rebounce() {
    // Limite da tela na parte superior
    if (this.y - (this.diameter / 2) <= 0) {
      // Muda a direção da bola para baixo
      this.direction.y = 1;
    }
    
    // Limite da tela na parte inferior
    if (this.y + (this.diameter / 2) > screenHeight) {
      return false;
    }
    
    // Limite da tela nos lados esquerdo e direito
    if (this.x - (this.diameter / 2) <= 0 ||
        this.x + (this.diameter / 2) >= screenWidth) {
      this.direction.x *= -1;
    }
    
    return true;
}

Ao retornar o valor boolean “false”, a função avisa se a bola saiu dos limites que causam o fim do jogo.

Declarando a função de inicialização

Na função “setup” do p5.js definimos valores que só precisam ser declarados uma vez durante o jogo. Nesse jogo queremos inicializar as seguintes variáveis:

  • Tela de exibição
  • Bloco do usuário
  • Bola
  • Blocos para quebrar

A declaração da tela de exibição será pulada por ser uma função padrão do ps5.js.

Bloco do usuário

O bloco do usuário deverá começar aparecendo no centro da tela no sentido horizontal e no fundo da parte inferior da tela no sentido vertical.

setup() {
    let coordenadaX = (screenWidth / 2) - (blockDefaultWidth / 2);
    let coordenadaY = screenHeight - blockDefaultHeight;
    playerBlock = new Block(coordenadaX, coordenadaY, blockDefaultWidth, blockDefaultHeight);
}

O valor da coordenada x deve resultar no resultado abaixo:
x=700/2100/2x=35050x=300 x = 700 / 2 - 100 / 2 \\ x = 350 - 50 \\ x = 300

Se a largura do bloco tem um valor de 100, isso significa que a extremidade da direita do bloco irá ficar localizada na coordenada x com valor de 400.

Bola

A bola deve ficar situada encima do bloco do usuário de modo centralizado.

var balls = [];
...
setup() {
    let ball = new Ball(playerBlock.x + (playerBlock.width / 2), playerBlock.y - (defaultBallSize / 2));
    balls.push(ball);
}

Como mostra no jogo da serpente, o tamanho da bola é medido apenas por um ponto, o centro do círculo.

O valor da coordenada x da bola é igual ao valor da coordenada x do bloco do usuário somado com a metade da largura do mesmo.

Blocos para quebrar

Os blocos que o usuário deve quebrar usando a bola devem estar situados na parte superior da tela. Quando a bola entrar em contato com o bloco, o bloco desaparecerá e a bola será redirecionada para o lado contrário. Caso a bola entre em contato com umas das extremidades do bloco ela irá ser redirecionada pelo mesmo rumo que venho.

Para facilitar o mapeamento das coordenadas de cada bloco, utilizaremos um arranjo (array) multidimensional contendo zeros e uns. Os zeros representam ausência e os uns representam a presença de blocos.

var stageOne = [
  [0,0,0,0,0,0,0],
  [0,1,1,1,1,1,0],
  [0,1,1,1,1,1,0],
  [0,1,1,1,1,1,0],
  [0,1,1,1,1,1,0]
];

Levando em consideração as constantes de altura e comprimento da tela que são respectivamente 500 e 700, podemos confirmar que cabem 7 blocos no sentido horizontal. Essa conclusão vem do cálculo do comprimento da tela pelo comprimento do bloco que é 100.

Em seguida, ainda na função “setup” declaramos as instâncias de cada bloco e armazenamos em um array.

var blocks = [];
...
for (let i = 0; i < stageOne.length; i++) {
    for (let j = 0; j <= stageOne.length; j++) {
      if (stageOne[i][j] == 1) {
        let x = j * blockDefaultWidth;
        let y = i * blockDefaultHeight;

        let block = new Block(x, y, blockDefaultWidth, blockDefaultHeight, greenColor);
        blocks.push(block);
     }
   }
}

Pelo fato da variável “stageOne” ser multidimensional, temos uma repetição dentro da outra, como mostra no código acima. A ordem da criação dos blocos pode ser representada com a tabela abaixo:

a b c d e f g
X X X X X X X
X 01 02 03 04 05 X
X 06 07 08 09 10 X
X 11 12 13 14 15 X
X 16 17 18 19 20 X

A letra “X” representa os espaços ausentes de blocos.

Implementando o conteúdo da função “draw”

O conteúdo declarado dentro da função “draw” é repetido infinitamente até que o usuário interrompa fechando a aba ou o navegador.

O bloco do usuário pode ser exibido com uma única linha de código:

playerBlock.draw();

Lembrando que a instância do “playerBlock” foi inicializada na função de inicialização. Outras instâncias que foram inicializadas e serão utilizada nessa função são:

  1. A bola
  2. Os blocos para quebrar

Programando a bola

Enquanto a existir uma bola visível na tela, o jogo irá ter continuidade e o usuário poderá mover o seu bloco. Caso contrário, o jogo para e deve ser reinicializado.

Checando a visibilidade da bola

Pensando de uma forma mais ao longo prazo, como possibilidade de múltiplas bolas visíveis na tela, as instâncias de cada bola será armazenada em um array. Porém, nesse projeto só uma bola estará presente.

for(let i = 0; i < balls.length; i++) {
    let isBallStillShowing = balls[i].draw();
    if (! isBallStillShowing) {
      balls.splice(i, 1);
      continue;
    }
    ...
}

O código acima checa se a bola ainda está visível na tela. Caso a bola esteja fora da visibilidade da tela, ela será eliminada do array.

Controles do usuário e movimentação da bola

O usuário pode apenas se movimentar para esquerda e direita. Porém, a movimentação do bloco só pode ser dentro da visibilidade da tela.

...
if (keyIsDown(LEFT_ARROW) && (playerBlock.x >= 0)) {
     playerBlock.x -= 5;
     if (! balls[i].isBoucing) {
        balls[i].x -= 5;
     }
}

if (keyIsDown(RIGHT_ARROW) && (playerBlock.x + playerBlock.width <= screenWidth)) {
     playerBlock.x += 5;
     if (! balls[i].isBoucing) {
        balls[i].x += 5;
     }
}

No começo do jogo o usuário tem que pressionar a tecla espaço para que a bola comece a se movimentar. Caso ela não esteja em movimento e o usuário mova o bloco, a bola irá acompanhar o bloco do usuário.

...
if (keyIsDown(32) && !balls[i].isBoucing) {
  balls[i].isBoucing = true;
}

Ao apertar a tecla espaço que tem um valor de 32 como mostra no código acima, a bola começa a entrar em movimento.

Condição de conflito com a bola e o bloco do usuário

Ao entrar em contato com o bloco do usuário a direção vertical da bola muda sem excessões.

// Collision
if (balls[i].y + (balls[i].diameter / 2) >= playerBlock.y &&
      balls[i].x + (balls[i].diameter / 2) >= playerBlock.x &&
      balls[i].x - (balls[i].diameter / 2) <= playerBlock.x + playerBlock.width) {
      balls[i].direction.y = -1;
      
      balls[i].direction = calculateDirectionBouncing(playerBlock, balls[i]);
}

Entretanto, caso a bola acabe entrando em contato com uma das extremidades do bloco do usuário, ela irá voltar pelo mesmo caminho que venho. A função responsável por calcular a direção da bola em colisão com a extremidade é a “calculateDirectionBouncing”.

function calculateDirectionBouncing(block, ball) {
  let blockRightSide = block.width * (9 / 10);
  let blockLeftSide = block.width * (1 / 10);
  let direction = ball.direction;

  if (ball.x > blockRightSide + block.x) {
    direction.x = 1;
  }

  if (ball.x < blockLeftSide + block.x) {
    direction.x = -1;
  }
  
  return direction;
}

O bloco será divido em 10 pequenos pedaços. O primeiro bloco e último bloco da esquerda ou da direita serão considerados as extremidades. Ao entrar em contato com uma dessas extremidades o valor da direção horizontal é forçado para uma determinada direção.

localização e tamanho das extremidades de um bloco

Programando os blocos para quebrar

Uma vez que o mapeamento já foi feito na fase de inicialização, só precisamos exibir na função “draw”. Aproveitando o loop para exibir os blocos, checamos por colisões com a bola.

var i = blocks.length;
while (i--) {
    blocks[i].draw();
    
    for(let j = 0; j < balls.length; j++) {
       if (balls[j].x + (balls[j].diameter / 2) >= blocks[i].x &&
          balls[j].x - (balls[j].diameter / 2) <= blocks[i].x + blocks[i].width &&
          balls[j].y + (balls[j].diameter / 2) >= blocks[i].y &&
          balls[j].y - (balls[j].diameter / 2) <= blocks[i].y + blocks[i].height) {
          
          balls[j].direction = calculateDirectionBouncing(blocks[i], balls[j]);
          balls[j].direction.y *= -1;
          // Remove bloco colidido
          blocks.splice(i, 1);
      }
   }
}

O motivo para usarmos “while” invés do “for” é pelo fato de que estamos removendo os blocos do loop quando detectamos uma colisão. Se usássemos “for”, isso poderia trazer comportamentos inesperados dos blocos.

Uma vez que chegamos até aqui, o jogo ja estará pronto.

jogo concluído

O código do jogo pode ser acessado por esse link. O jogo ainda precisa de muitas melhorias, porém está satisfatório.

O que aprendemos com o jogo de quebra-blocos?

O jogo de quebra-blocos mostra como é importante o planejamento do jogo antes de seu desenvolvimento. Ao deixarmos a altura dos blocos inferior ao diâmetro da bola, teremos comportamentos inesperados como múltiplos blocos atingidos com uma única colisão. Esse tipo de comportamento pode passar despercebido na fase de planejamento, porém deverá ser revelado na fase de testes. Escrever um código flexível e ajustável usando cálculos de matemática torna a modificação de valores como constantes mais conveniente.

Um exemplo a não seguir nesse projeto é o mapa dos blocos armazenados na variável “stageOne”. Caso modificarmos constantes como o tamanho da tela ou comprimento do bloco, anomalias podem acontecer, como por exemplo, blocos não centralizados e no pior dos casos blocos escapando dos limites de visibilidade da tela.

Postagens mais vistas

Os 5 principais componentes do computador

Os 5 principais componentes do computador são a unidade de controle, unidade aritmética e lógica, memória, dispositivo de entrada e dispositivo de saída.

Portas TCP e UDP

A porta é um número de 16 bits que é adicionado no final do endereço IP, insinuando qual aplicativo está vinculado e atuando nessa porta.

LAN

Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.

Retornar aos projetos