Criando um jogo de Arqueiro em Javascript
O “Jogo do Arqueiro” é um jogo de física onde o objetivo é atirar flechas para acertar alvos, utilizando a biblioteca Matter.js para simular a física realista.
Conceitos Fundamentais
O Matter.js é uma biblioteca JavaScript que permite criar simulações físicas realistas em 2D. Com ela você pode:
Criar corpos com propriedades físicas (peso, atrito, elasticidade)
Simular colisões e interações entre objetos
Desenvolver jogos e experiências interativas
Exemplo Prático: Arqueiro Épico
Aqui está o jogo que usaremos como referência:
Passo 1: Configurando o ambiente
Antes de começarmos a programar, certifique-se de ter um ambiente de desenvolvimento configurado:
Um editor de código (VS Code)
- Um navegador (Google Chrome)
Arquivo de template
Passo 2: Classe PlayerArcher() (arquivo acher.js)
A classe PlayerArcher
controla o arqueiro do jogo, incluindo sua física, aparência e movimentos.
- Criando as propriedades do Arqueiro
class PlayerArcher {
constructor(x, y, width, height) {
// 1. Configurações Físicas
const options = {
isStatic: true // Define que o corpo não será afetado por forças físicas
}
// 2. Criação do Corpo Físico
this.body = Matter.Bodies.rectangle(x, y, width, height, options);
// 3. Propriedades do Arqueiro
this.width = width; // Armazena a largura do arqueiro
this.height = height; // Armazena a altura do arqueiro
this.collapse = false; // Flag para estado de colisão/destruição
this.image = loadImage("./assets/playerArcher.png"); // Carrega o sprite
// 4. Adição ao Mundo Físico
World.add(world, this.body);
// 5. Configuração Angular Inicial
Matter.Body.setAngle(this.body, -90); // Aponta para cima (-90 graus)
}
isStatic: true
: Torna o corpo imóvelMatter.Bodies.rectangle()
: Cria um corpo retangular no Matter.jswidth/height
: Armazenam as dimensões para renderização precisacollapse
: Flag booleana para controle de estado (útil para game over)image
: Carrega o arquivo PNG do assets (deve estar pré-carregado)World.add()
: Insere o corpo na simulação física globalsetAngle(-90)
: Rotaciona o corpo para apontar verticalmente para cima
- Criando o sistema de Controle
// 6. Controle de Movimento (Para Baixo)
if (keyIsDown(DOWN_ARROW) && this.body.angle < -85 ) {
this.body.angle += 1; // Incrementa o ângulo
Matter.Body.setAngle(this.body, this.body.angle); // Aplica a mudança
}
// 7. Controle de Movimento (Para Cima)
if (keyIsDown(UP_ARROW) && this.body.angle > -103) {
this.body.angle -=1; // Decrementa o ângulo
Matter.Body.setAngle(this.body, this.body.angle); // Aplica a mudança
}
Limites angulares (-73° e -103°) criam um arco de movimento de 30°
keyIsDown()
verifica pressionamento contínuo das teclasIncrementos/decrementos de 1° garantem movimento suave
setAngle()
aplica as mudanças ao corpo físico
- Renderização Gráfica
display() {
// 6. Controle de Movimento (Para Baixo)
if (keyIsDown(DOWN_ARROW) && this.body.angle < -85 ) {
this.body.angle += 1; // Incrementa o ângulo
Matter.Body.setAngle(this.body, this.body.angle); // Aplica a mudança
}
// 7. Controle de Movimento (Para Cima)
if (keyIsDown(UP_ARROW) && this.body.angle > -103) {
this.body.angle -=1; // Decrementa o ângulo
Matter.Body.setAngle(this.body, this.body.angle); // Aplica a mudança
}
// 8. Transformações Gráficas
push(); // Salva o estado atual do sistema de coordenadas
translate(this.body.position.x, this.body.position.y); // Move origem para a posição do corpo
rotate(this.body.angle); // Rotaciona o sistema de coordenadas
imageMode(CENTER); // Define o ponto de ancoragem da imagem como centro
image(this.image, 0, 0, this.width, this.height); // Desenha a imagem
pop(); // Restaura o estado anterior do sistema de coordenadas
}
}
push()/pop()
: Isolam transformações gráficastranslate()
: Move a origem para a posição do arqueirorotate()
: Aplica a rotação física ao sistema de coordenadasimageMode(CENTER)
: Faz a imagem girar em torno do seu centroimage()
: Desenha o sprite com dimensões armazenadas
Passo 3: Classe PlayerArrow() (arquivo PlayerArrow.js)
Vamos criar a flecha que o arqueiro irá atirar. A flecha começa estática (parada) e, quando disparada, ganha velocidade e segue uma trajetória física realista.
- Criando as propriedades da Flecha
class PlayerArrow {
constructor(x, y, width, height, archerAngle) {
var options = {
isStatic: true,
density: 0.1
};
this.width = width;
this.height = height;
this.body = Bodies.rectangle(x, y, this.width, this.height, options);
this.image = loadImage("./assets/arrow.png");
this.archerAngle = archerAngle;
this.velocity = 0;
World.add(world, this.body);
}
isStatic: true
: Torna o corpo imóveldensity: 0.1
define uma densidade baixa para um movimento mais suave.Matter.Bodies.rectangle()
: Cria um corpo retangular no Matter.jswidth/height
: Armazenam as dimensões para renderização precisathis.archerAngle = archerAngle :
Armazena o ângulo do arqueiro no momento do disparothis.velocity = 0:
Inicializa a velocidade da flecha como zero (será alterada ao disparar)image
: Carrega o arquivo PNG do assets (deve estar pré-carregado)World.add()
: Insere o corpo na simulação física global
- Método para disparar a flecha
shoot(archerAngle) {
// Ajusta ângulo para sistema Matter.js
archerAngle += 90;
// Cria vetor de velocidade (radianos)
this.velocity = p5.Vector.fromAngle(archerAngle * (3.14 / 180))
// Define força do disparo
this.velocity.mult(0.5);
// Aplica velocidade ao corpo físico
Matter.Body.setVelocity(this.body, {
x: degrees(this.velocity.x),
y: degrees(this.velocity.y)
});
// Libera flecha para simulação física
Matter.Body.setStatic(this.body, false);
}
archerAngle += 90
Ajusta o ângulo em +90 graus para corrigir a orientação padrão do Matter.jsp5.Vector.fromAngle()
converte o ângulo de graus para radianos e cria vetor de velocidadevelocity.mult(0.5)
define a força do disparo (quanto maior, mais rápida a flecha).Matter.Body.setStatic(this.body, false)
permite que a flecha se mova.Matter.Body.setVelocity()
aplica a velocidade ao corpo físico.
- Criando ângulo temporário
// Calcula ângulo temporário da flecha
var tmpAngle;
// Se flecha parada (sem velocidade vertical)
if (this.body.velocity.y === 0) {
// Usa ângulo do arco + 90° (posição padrão)
tmpAngle = this.anguloarco + 90;
} else {
// Se em movimento: calcula ângulo baseado na velocidade (transforma em graus)
tmpAngle = Math.atan(this.body.velocity.y / this.body.velocity.x) * (180 / 3.14);
}
// Aplica ângulo calculado ao corpo físico
Matter.Body.setAngle(this.body, tmpAngle);
this.body.velocity.y === 0
: Detecta flecha paradathis.anguloarco + 90
: Posição de repouso alinhada com o arcoMath.atan():
Calcula ângulo de trajetória* (180/3.14):
Converte radianos para graussetAngle():
Atualiza rotação física da flecha
- Renderização Gráfica
// Método para desenhar a flecha na tela
display() {
// Calcula ângulo temporário da flecha
var tmpAngle;
// Se flecha parada (sem velocidade vertical)
if (this.body.velocity.y === 0) {
// Usa ângulo do arco + 90° (posição padrão)
tmpAngle = this.anguloarco + 90;
} else {
// Se em movimento: calcula ângulo baseado na velocidade (transforma em graus)
tmpAngle = Math.atan(this.body.velocity.y / this.body.velocity.x) * (180 / 3.14);
}
// Aplica ângulo calculado ao corpo físico
Matter.Body.setAngle(this.body, tmpAngle);
push(); // Salva as configurações atuais do canvas
translate(this.body.position.x, this.body.position.y); // Move o sistema de coordenadas para a posição da flecha
rotate(this.body.angle); // Rotaciona conforme a direção do movimento
imageMode(CENTER);
image(this.image, 0, 0, this.width, this.height); // Desenha a flecha
pop(); // Restaura as configurações anteriores
}
}
push()/pop()
: Isolam transformações gráficastranslate()
: Move a origem para a posição do arqueirorotate()
: Aplica a rotação física ao sistema de coordenadasimageMode(CENTER)
: Faz a imagem girar em torno do seu centroimage()
: Desenha o sprite com dimensões armazenadas
Passo 4: Classe Board() (arquivo board.js)
Vamos criar o quadro, que será o alvo do jogador. O quadro é estático (parado) em todo momento do jogo.
- Criando as propriedades do Quadro
class Board {
constructor(x, y, width, height) {
var options = {
isStatic: true
};
this.body = Bodies.rectangle(x, y, width, height, options);
this.width = width;
this.height = height;
this.image = loadImage("./assets/board.png");
World.add(world, this.body);
}
isStatic: true
: Torna o corpo imóvelMatter.Bodies.rectangle()
: Cria um corpo retangular no Matter.jswidth/height
: Armazenam as dimensões para renderização precisaimage
: Carrega o arquivo PNG do assets (deve estar pré-carregado)World.add()
: Insere o corpo na simulação física global
- Renderização Gráfica
display() {
push();
imageMode(CENTER);
image(this.image, this.body.position.x, this.body.position.y, this.width, this.height);
pop();
}
}
push()/pop()
: Isolam transformações gráficasimageMode(CENTER)
: Faz a imagem girar em torno do seu centroimage()
: Desenha o sprite com dimensões armazenadas
Passo 5: Declaração de variáveis
Aqui estamos declarando as variáveis que serão usadas para armazenar os módulos do Matter.js e os corpos físicos.
// Módulos do Matter.js
const Engine = Matter.Engine;
const World = Matter.World;
const Bodies = Matter.Bodies;
const Constraint = Matter.Constraint;
// Variáveis do jogo
var engine, world;
var canvas;
var player, playerBase, playerArcher;
var board1, board2;
var arrow;
var playerArrows = [];
Engine
: O “cérebro” que calcula todas as interações físicasWorld
: O “container” onde todos os objetos físicos existemBody
: Permite manipular objetos após a criação (ex.: girar paredes)Constraint
: Permite criar restrições e conexões entre corpos físicos
Passo 6: Função preload()
A função preload() é usada para carregar imagens e animações antes do jogo iniciar.
loadImage("./assets/background.png")
: Carrega a imagem de fundo do cenárioloadImage("./assets/base.png")
: Carrega a imagem da base do personagemloadImage("./assets/player.png")
: Carrega a imagem do personagem arqueiro
function preload() {
backgroundImg = loadImage("./assets/background.png");
baseimage = loadImage("./assets/base.png");
playerimage = loadImage("./assets/player.png");
}
Passo 7: Função setup()
A função setup()
é executada apenas uma vez e configura o ambiente do jogo.
createCanvas(windowWidth, windowHeight)
: Cria uma tela que ocupa toda a janela do navegador
function setup(){
createCanvas(windowWidth, windowHeight)
- Agora criamos o motor de física:
// Cria o motor de física
engine = Engine.create();
world = engine.world;
Engine.create()
: Inicializa o motor de físicaengine.world
: Acessa o mundo físico criado
- E criamos os corpos estáticos:
var options = {
isStatic: true
};
playerBase = Bodies.rectangle(200, 350, 180, 150, options);
World.add(world, playerBase);
player = Bodies.rectangle(250, playerBase.position.y - 160, 50, 180, options);
World.add(world,player)
isStatic: true
: Faz o corpo ficar imóvelBodies.rectangle()
: Cria um retagulo físicoWorld.add()
: Adiciona o corpo ao mundo físico
- Criando os quadros:
board1 = new Board(width - 300, 330, 50, 200);
board2 = new Board(width - 550, height - 300, 50, 200);
Board(x, y, w, h)
: Cria o quadro com posição (x
,y
) e dimensões (w
,h
)
- Criando o playerAcher:
playerArcher = new PlayerArcher(
340,
playerBase.position.y - 112,
120,
120
);
PlayerAcher(x, y, w, h)
: Cria o braço do player com posição (x
,y
) e dimensões (w
,h
)
- E por fim configuramos os modos de desenho:
angleMode(DEGREES);
}
angleMode(DEGREES)
: Configura o ângulo para graus
Passo 8: Função draw()
A função draw() é chamada continuamente para atualizar e renderizar:
background(backgroundImg)
: Define a imagem de fundo do jogoEngine.update()
: Calcula todas as interações físicas a cada frame
function draw() {
// Define o fundo usando a imagem carregada
background(backgroundImg);
// Atualiza o motor de física
Engine.update(engine);
- Agora, renderizando o playerAcher na tela:
playerArcher.display();
- Renderizando os quadros na tela:
board1.display();
board2.display();
- Renderizando os corpos para aparecem na tela:
image(baseimage,playerBase.position.x,playerBase.position.y,180,150)
image(playerimage,player.position.x,player.position.y,50,180)
// Título
fill("#FFFF");
textAlign("center");
textSize(40);
text("ARQUEIRO ÉPICO", width / 2, 100);
image():
Renderiza imagens na tela usando posições físicas e dimensõesfill
: Define cor do texto (branco)textAlign
: Alinhamento centralizadotextSize
: Tamanho da fonte (40px)text
: Exibe texto
- Mas também, renderizando as flechas na tela:
for (var i = 0; i < playerArrows.length; i++) {
if (playerArrows[i] !== undefined) {
playerArrows[i].display();
}
}
}
for (var i = 0; i < playerArrows.length; i++):
Loop através de todas as flechas disparadasif (playerArrows[i] !== undefined):
Verifica se a flecha atual existeplayerArrows[i].display():
Exibe cada flecha ativa na tela
Passo 9: Função keyReleased()
Agora que já temos a flecha (PlayerArrow
) pronta, vamos fazer com que o jogador possa atirá-la pressionando a tecla ESPAÇO. Para isso, usaremos a função keyReleased()
do p5.js, que detecta quando uma tecla é liberada após ser pressionada.
function keyReleased() {
if (keyCode === 32) {
if (playerArrows.length) {
var angle = playerArcher.body.angle;
playerArrows[playerArrows.length - 1].shoot(angle);
}
}
}
keyCode === 32:
detecta exclusivamente a tecla ESPAÇOplayerArrows.length:
Verifica se existem flechas no arrayvar angle = playerArcher.body.angle:
Obtém o ângulo atual do arqueiroplayerArrows[playerArrows.length - 1].shoot(angle):
Dispara a última flecha criada com o ângulo atual
Passo 10: Função keyPressed()
Vamos fazer com que o jogador possa atirá-la pressionando a tecla ESPAÇO. Para isso, usaremos a função keyPressed()
do p5.js, que detecta quando uma tecla é pressionada.
function keyPressed() {
if (keyCode === 32) {
var angle = playerArcher.body.angle;
var arrow = new PlayerArrow(playerArcher.body.position.x, playerArcher.body.position.y, 100, 10, angle);
Matter.Body.setAngle(arrow.body, angle);
playerArrows.push(arrow);
}
}
keyCode === 32:
detecta exclusivamente a tecla ESPAÇOvar angle = playerArcher.body.angle:
Obtém o ângulo atual do arqueirovar arrow = new PlayerArrow(x, y, 100, 10, angle):
Cria uma nova flecha na posição do arqueiroMatter.Body.setAngle(arrow.body, angle):
Ajusta o ângulo do corpo físico da flechaplayerArrows.push(arrow):
Adiciona a nova flecha ao array de flechas