14 KiB
Construindo um Jogo Espacial Parte 3: Adicionando Movimento
Quiz Pré-Aula
Jogos não são muito divertidos até que você tenha alienígenas se movendo pela tela! Neste jogo, utilizaremos dois tipos de movimentos:
- Movimento por teclado/mouse: quando o usuário interage com o teclado ou mouse para mover um objeto na tela.
- Movimento induzido pelo jogo: quando o jogo move um objeto em um determinado intervalo de tempo.
Então, como movemos coisas na tela? Tudo se resume a coordenadas cartesianas: mudamos a localização (x, y) do objeto e, em seguida, redesenhamos a tela.
Normalmente, você precisa dos seguintes passos para realizar o movimento na tela:
- Definir uma nova localização para um objeto; isso é necessário para que o objeto pareça ter se movido.
- Limpar a tela, a tela precisa ser limpa entre os desenhos. Podemos limpá-la desenhando um retângulo preenchido com uma cor de fundo.
- Redesenhar o objeto na nova localização. Fazendo isso, finalmente conseguimos mover o objeto de um local para outro.
Veja como isso pode parecer no código:
//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
✅ Você consegue pensar em um motivo pelo qual redesenhar seu herói várias vezes por segundo pode gerar custos de desempenho? Leia sobre alternativas para este padrão.
Manipulando eventos de teclado
Você manipula eventos anexando eventos específicos ao código. Eventos de teclado são acionados em toda a janela, enquanto eventos de mouse, como um click
, podem ser conectados a um elemento específico. Usaremos eventos de teclado ao longo deste projeto.
Para manipular um evento, você precisa usar o método addEventListener()
da janela e fornecer dois parâmetros de entrada. O primeiro parâmetro é o nome do evento, por exemplo, keyup
. O segundo parâmetro é a função que deve ser invocada como resultado do evento.
Aqui está um exemplo:
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
Para eventos de teclado, há duas propriedades no evento que você pode usar para ver qual tecla foi pressionada:
key
, que é uma representação em string da tecla pressionada, por exemplo,ArrowUp
.keyCode
, que é uma representação numérica, por exemplo,37
, que corresponde aArrowLeft
.
✅ A manipulação de eventos de teclado é útil fora do desenvolvimento de jogos. Em que outros usos você consegue pensar para essa técnica?
Teclas especiais: um alerta
Existem algumas teclas especiais que afetam a janela. Isso significa que, se você estiver ouvindo um evento keyup
e usar essas teclas especiais para mover seu herói, isso também realizará o deslocamento horizontal. Por esse motivo, você pode querer desativar esse comportamento padrão do navegador ao construir seu jogo. Você precisará de um código como este:
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
O código acima garantirá que as teclas de seta e a barra de espaço tenham seu comportamento padrão desativado. O mecanismo de desativação ocorre quando chamamos e.preventDefault()
.
Movimento induzido pelo jogo
Podemos fazer as coisas se moverem sozinhas usando temporizadores, como as funções setTimeout()
ou setInterval()
, que atualizam a localização do objeto a cada intervalo de tempo. Veja como isso pode parecer:
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
O loop do jogo
O loop do jogo é um conceito que é essencialmente uma função invocada em intervalos regulares. É chamado de loop do jogo porque tudo o que deve ser visível para o usuário é desenhado dentro do loop. O loop do jogo utiliza todos os objetos do jogo que fazem parte dele, desenhando todos eles, a menos que, por algum motivo, não devam mais fazer parte do jogo. Por exemplo, se um objeto for um inimigo atingido por um laser e explodir, ele não faz mais parte do loop atual do jogo (você aprenderá mais sobre isso em lições subsequentes).
Veja como um loop de jogo pode ser tipicamente expresso em código:
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
O loop acima é invocado a cada 200
milissegundos para redesenhar o canvas. Você pode escolher o melhor intervalo que faça sentido para o seu jogo.
Continuando o Jogo Espacial
Você pegará o código existente e o expandirá. Comece com o código que você completou na parte I ou use o código em Parte II - inicial.
- Movendo o herói: você adicionará código para garantir que pode mover o herói usando as teclas de seta.
- Mover inimigos: você também precisará adicionar código para garantir que os inimigos se movam de cima para baixo em uma determinada taxa.
Passos recomendados
Localize os arquivos que foram criados para você na subpasta your-work
. Ela deve conter o seguinte:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Você inicia seu projeto na pasta your_work
digitando:
cd your-work
npm start
O comando acima iniciará um servidor HTTP no endereço http://localhost:5000
. Abra um navegador e insira esse endereço. No momento, ele deve renderizar o herói e todos os inimigos; nada está se movendo - ainda!
Adicione código
-
Adicione objetos dedicados para
hero
,enemy
egame object
, eles devem ter propriedadesx
ey
. (Lembre-se da seção sobre Herança ou composição).DICA:
game object
deve ser o que possuix
ey
e a capacidade de se desenhar em um canvas.dica: comece adicionando uma nova classe GameObject com seu construtor delineado como abaixo, e depois desenhe-a no canvas:
class GameObject { constructor(x, y) { this.x = x; this.y = y; this.dead = false; this.type = ""; this.width = 0; this.height = 0; this.img = undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } }
Agora, estenda este GameObject para criar o Hero e o Enemy.
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } }
-
Adicione manipuladores de eventos de teclado para lidar com a navegação por teclas (mover o herói para cima/baixo/esquerda/direita).
LEMBRE-SE: é um sistema cartesiano, o canto superior esquerdo é
0,0
. Também lembre-se de adicionar código para interromper o comportamento padrão.dica: crie sua função onKeyDown e anexe-a à janela:
let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown);
Verifique o console do navegador neste ponto e observe as teclas sendo registradas.
-
Implemente o Padrão Pub/Sub, isso manterá seu código limpo enquanto você segue as próximas partes.
Para fazer esta última parte, você pode:
-
Adicionar um ouvinte de eventos na janela:
window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } });
-
Criar uma classe EventEmitter para publicar e assinar mensagens:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach((l) => l(message, payload)); } } }
-
Adicionar constantes e configurar o EventEmitter:
const Messages = { KEY_EVENT_UP: "KEY_EVENT_UP", KEY_EVENT_DOWN: "KEY_EVENT_DOWN", KEY_EVENT_LEFT: "KEY_EVENT_LEFT", KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter();
-
Inicializar o jogo
function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -=5 ; }) eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); }
-
-
Configurar o loop do jogo
Refatore a função window.onload para inicializar o jogo e configurar um loop de jogo em um bom intervalo. Você também adicionará um feixe de laser:
window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) };
-
Adicione código para mover inimigos em um determinado intervalo.
Refatore a função
createEnemies()
para criar os inimigos e empurrá-los para a nova classe gameObjects:function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } }
e adicione uma função
createHero()
para fazer um processo semelhante para o herói.function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); }
e, finalmente, adicione uma função
drawGameObjects()
para começar a desenhar:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
Seus inimigos devem começar a avançar em direção à sua nave espacial herói!
🚀 Desafio
Como você pode ver, seu código pode se transformar em um 'código espaguete' quando você começa a adicionar funções, variáveis e classes. Como você pode organizar melhor seu código para que ele seja mais legível? Esboce um sistema para organizar seu código, mesmo que ele ainda resida em um único arquivo.
Quiz Pós-Aula
Revisão e Autoestudo
Embora estejamos escrevendo nosso jogo sem usar frameworks, existem muitos frameworks baseados em JavaScript para desenvolvimento de jogos com canvas. Reserve um tempo para fazer algumas leituras sobre eles.
Tarefa
Aviso Legal:
Este documento foi traduzido utilizando o serviço de tradução por IA Co-op Translator. Embora nos esforcemos para garantir a precisão, esteja ciente de que traduções automatizadas podem conter erros ou imprecisões. O documento original em seu idioma nativo deve ser considerado a fonte autoritativa. Para informações críticas, recomenda-se a tradução profissional realizada por humanos. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações equivocadas decorrentes do uso desta tradução.