Projeto: um jogo simples
LidoNesta aula, você vai construir um jogo simples de navegador usando JavaScript, aplicando conceitos como loop de jogo, gerenciamento de estado, interação por teclado e renderização. O projeto será um jogo de "pegue o item" onde o jogador controla um personagem para coletar itens que aparecem aleatoriamente, com pontuação e dificuldade progressiva.
Nesta aula, vamos colocar em prática os conceitos fundamentais de desenvolvimento de jogos em JavaScript, construindo um jogo simples mas completo. Você aprenderá a estrutura de um loop de jogo, o gerenciamento de estado, a captura de entradas do teclado e a renderização em um canvas HTML5. Este projeto servirá como base para que você possa criar seus próprios jogos e entender como os jogos modernos funcionam internamente.
O jogo que vamos construir é um "pegue o item": um personagem controlado pelo jogador se move em um canvas, e itens (estrelas) aparecem em posições aleatórias. O objetivo é coletar o máximo de itens possível em 60 segundos, com a dificuldade aumentando conforme a pontuação cresce. Ao final, apresentaremos boas práticas e exercícios para fixar o conteúdo.
Loop de jogo
O loop de jogo é o coração de qualquer jogo: ele roda continuamente, atualizando o estado do jogo e renderizando a nova imagem a cada quadro (frame). Em JavaScript, usamos a função requestAnimationFrame para criar um loop suave e sincronizado com a taxa de atualização do monitor (geralmente 60 FPS).
O loop básico consiste em três etapas: atualizar o estado (posições, colisões, pontuação), renderizar (desenhar no canvas) e agendar o próximo quadro. A função requestAnimationFrame recebe uma função de callback que será chamada antes do próximo repaint. Dentro dessa função, chamamos novamente requestAnimationFrame para criar um loop infinito.
function gameLoop(timestamp) {
// Atualiza o estado do jogo
update(timestamp);
// Renderiza a cena
render();
// Agenda o próximo quadro
requestAnimationFrame(gameLoop);
}
// Inicia o loop
requestAnimationFrame(gameLoop);O parâmetro timestamp é o tempo em milissegundos desde o início da página. Podemos usar a diferença entre quadros (delta time) para garantir que o jogo rode na mesma velocidade em diferentes dispositivos, independentemente da taxa de atualização.
Estado
O estado do jogo é a representação de todos os dados que mudam durante o jogo: posição do jogador, posição dos itens, pontuação, tempo restante, etc. Manter o estado bem organizado é essencial para a manutenção e evolução do jogo. Usaremos um objeto gameState para centralizar essas informações.
Em nosso jogo, o estado inclui: o jogador (com posição x, y e tamanho), a lista de itens (cada um com posição e tamanho), a pontuação, o tempo restante e a dificuldade (que influencia a frequência de aparecimento dos itens). A inicialização do estado é feita em uma função initGame.
const gameState = {
player: { x: canvas.width / 2, y: canvas.height / 2, size: 30 },
items: [],
score: 0,
timeLeft: 60, // segundos
difficulty: 1,
gameOver: false
};
function initGame() {
gameState.player = { x: canvas.width / 2, y: canvas.height / 2, size: 30 };
gameState.items = [];
gameState.score = 0;
gameState.timeLeft = 60;
gameState.difficulty = 1;
gameState.gameOver = false;
spawnItem();
}Durante a atualização, modificamos o estado: movemos o jogador com base nas teclas pressionadas, decrementamos o tempo, verificamos colisões e geramos novos itens conforme a dificuldade. É importante que toda a lógica de mudança de estado esteja centralizada na função update.
Interação por teclado
Para capturar as teclas pressionadas, usamos eventos do DOM: keydown e keyup. Mantemos um objeto keys que armazena o estado de cada tecla (pressionada ou não). Quando uma tecla é pressionada, marcamos como true; quando solta, false. Isso permite que o movimento seja contínuo enquanto a tecla estiver pressionada.
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.code] = true;
});
document.addEventListener('keyup', (e) => {
keys[e.code] = false;
});Na função update, verificamos as teclas de seta (ou WASD) e movemos o jogador de acordo. A velocidade deve ser multiplicada pelo delta time para que o movimento seja independente da taxa de quadros.
const speed = 200; // pixels por segundo
if (keys['ArrowLeft'] || keys['KeyA']) {
gameState.player.x -= speed * deltaTime;
}
if (keys['ArrowRight'] || keys['KeyD']) {
gameState.player.x += speed * deltaTime;
}
if (keys['ArrowUp'] || keys['KeyW']) {
gameState.player.y -= speed * deltaTime;
}
if (keys['ArrowDown'] || keys['KeyS']) {
gameState.player.y += speed * deltaTime;
}Além disso, podemos usar o evento keydown para ações discretas, como reiniciar o jogo quando a tecla R for pressionada.
Renderização
A renderização é o processo de desenhar o estado atual do jogo no canvas. Usamos o contexto 2D (ctx) para desenhar formas, textos e imagens. No nosso caso, desenharemos o jogador como um círculo colorido, os itens como estrelas (ou círculos amarelos), a pontuação e o tempo restante no topo.
A função render primeiro limpa o canvas com clearRect, depois desenha todos os elementos. É importante desenhar em ordem: fundo, itens, jogador, HUD (informações).
function render() {
// Limpa o canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Desenha os itens
gameState.items.forEach(item => {
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(item.x, item.y, item.size, 0, Math.PI * 2);
ctx.fill();
});
// Desenha o jogador
ctx.fillStyle = '#00F';
ctx.beginPath();
ctx.arc(gameState.player.x, gameState.player.y, gameState.player.size, 0, Math.PI * 2);
ctx.fill();
// Desenha o HUD
ctx.fillStyle = '#FFF';
ctx.font = '20px Arial';
ctx.fillText('Pontos: ' + gameState.score, 10, 30);
ctx.fillText('Tempo: ' + Math.ceil(gameState.timeLeft), canvas.width - 150, 30);
}Para melhorar a aparência, podemos adicionar efeitos como sombras, gradientes ou imagens. Mas o essencial é que a renderização reflita fielmente o estado atual.
Conclusão e boas práticas
Neste projeto, você aprendeu a estruturar um jogo simples em JavaScript usando canvas. As boas práticas incluem: manter o estado centralizado, separar a lógica de atualização da renderização, usar delta time para consistência, e organizar o código em funções pequenas e reutilizáveis.
Além disso, é importante testar o jogo em diferentes navegadores e dispositivos, e considerar a acessibilidade (por exemplo, permitir pausa). Agora, vamos praticar com exercícios que reforçam os conceitos.
Referências
- MDN: CanvasRenderingContext2D
- MDN: requestAnimationFrame
- MDN: KeyboardEvent
- MDN: Canvas Tutorial
- W3Schools: HTML5 Game Tutorial
- MDN: Game Techniques
Exercícios
- Exercício 1: Implemente um sistema de vidas: o jogador começa com 3 vidas e perde uma ao tocar em um item perigoso (por exemplo, um item vermelho). O jogo termina quando as vidas chegarem a zero. Descreva as mudanças no estado e na lógica de colisão.✓ Resposta: Adicione
lives: 3ao estado inicial. Na detecção de colisão, se o item for perigoso (por exemplo,item.dangerous), decrementelivese remova o item. Seliveschegar a 0, definagameOver = true. Atualize o HUD para mostrar as vidas restantes. - Exercício 2: Modifique o jogo para que o jogador possa atirar projéteis (por exemplo, bolas de energia) ao pressionar a barra de espaço. Crie uma lista de projéteis no estado e atualize a renderização e a colisão com itens. Descreva a implementação.✓ Resposta: Adicione
projectiles: []ao estado. No eventokeydown, see.code === 'Space', crie um projétil na posição do jogador com uma velocidade vertical (por exemplo,vy: -300). Na funçãoupdate, mova os projéteis e verifique colisão com itens: se a distância entre o projétil e o item for menor que a soma dos raios, remova ambos e incremente a pontuação. Renderize os projéteis como círculos. - Exercício 3: Crie um sistema de pausa: quando o jogador pressionar a tecla
P, o jogo pausa/despausa. Implemente a lógica no loop de jogo para não atualizar o estado quando pausado, mas continuar renderizando (para mostrar a tela congelada).✓ Resposta: Adicionepaused: falseao estado. Nokeydown, see.code === 'KeyP', alternepaused. No loop de jogo, antes de chamarupdate, verifique se!gameState.paused. A renderização continua sempre. Exiba um texto "Pausado" na tela quando pausado. - Exercício 4: Implemente um aumento de dificuldade: a cada 10 pontos, reduza o intervalo de geração de itens e aumente a velocidade dos itens (se houver movimento). Descreva como você faria isso no estado e na atualização.✓ Resposta: Adicione
spawnInterval: 2000(ms) eitemSpeed: 0ao estado. Na funçãoupdate, quandoscoreaumentar, verifique sescore % 10 === 0e, se sim, reduzaspawnIntervalpor 100 (mínimo 500) e aumenteitemSpeedem 20. Use um temporizador para gerar itens com base emspawnInterval. Se os itens se moverem, atualize suas posições comitemSpeed. - Exercício 5: Adicione efeitos sonoros simples usando a Web Audio API: toque um som ao coletar um item e outro ao game over. Descreva os passos para integrar o áudio ao jogo.✓ Resposta: Crie um contexto de áudio (
new AudioContext()). Para cada efeito, crie um oscilador (OscillatorNode) e um ganho (GainNode). No momento da coleta, toque um tom curto (ex.: 880Hz por 0.1s). No game over, toque um tom grave (ex.: 220Hz por 0.5s). Lembre-se de conectar os nós e iniciar/parar o oscilador.