|
3 weeks ago | |
---|---|---|
.. | ||
README.md | 3 weeks ago | |
assignment.md | 4 weeks ago |
README.md
Construir um Jogo Espacial Parte 3: Adicionando Movimento
Questionário Pré-Aula
Os jogos não são muito divertidos até que tenhamos alienígenas a movimentar-se no ecrã! Neste jogo, vamos utilizar dois tipos de movimentos:
- Movimento por teclado/rato: quando o utilizador interage com o teclado ou rato para mover um objeto no ecrã.
- Movimento induzido pelo jogo: quando o jogo move um objeto em intervalos de tempo definidos.
Então, como movemos coisas no ecrã? Tudo se resume a coordenadas cartesianas: alteramos a localização (x, y) do objeto e depois redesenhamos o ecrã.
Normalmente, são necessários os seguintes passos para realizar movimento no ecrã:
- Definir uma nova localização para um objeto; isto é necessário para que o objeto pareça ter-se movido.
- Limpar o ecrã, o ecrã precisa de ser limpo entre os desenhos. Podemos limpá-lo desenhando um retângulo preenchido com uma cor de fundo.
- Redesenhar o objeto na nova localização. Ao fazer isto, conseguimos finalmente mover o objeto de uma localização para outra.
Aqui está como isso pode parecer em 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);
✅ Consegues pensar numa razão pela qual redesenhar o teu herói várias vezes por segundo pode gerar custos de desempenho? Lê sobre alternativas a este padrão.
Lidar com eventos de teclado
Lidamos com eventos ao associar eventos específicos ao código. Os eventos de teclado são acionados em toda a janela, enquanto eventos de rato, como um click
, podem ser conectados ao clique num elemento específico. Vamos usar eventos de teclado ao longo deste projeto.
Para lidar com um evento, precisas de usar o método addEventListener()
da janela e fornecer-lhe 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, existem duas propriedades no evento que podes usar para ver qual tecla foi pressionada:
key
, esta é uma representação em texto da tecla pressionada, por exemplo,ArrowUp
.keyCode
, esta é uma representação numérica, por exemplo,37
, que corresponde aArrowLeft
.
✅ Manipulação de eventos de teclado é útil fora do desenvolvimento de jogos. Que outros usos consegues imaginar para esta técnica?
Teclas especiais: um alerta
Existem algumas teclas especiais que afetam a janela. Isso significa que, se estiveres a ouvir um evento keyup
e usares essas teclas especiais para mover o teu herói, também será realizado o scroll horizontal. Por essa razão, talvez queiras desativar este comportamento padrão do navegador enquanto desenvolves o teu jogo. Precisarás de 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 tecla de espaço tenham o seu comportamento padrão desativado. O mecanismo de desativação ocorre quando chamamos e.preventDefault()
.
Movimento induzido pelo jogo
Podemos fazer com que as coisas se movam sozinhas usando temporizadores como as funções setTimeout()
ou setInterval()
que atualizam a localização do objeto em cada intervalo de tempo. Aqui está 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 utilizador é desenhado dentro do loop. O loop do jogo utiliza todos os objetos do jogo que fazem parte do jogo, 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 que foi atingido por um laser e explodiu, ele já não faz parte do loop atual do jogo (vais aprender mais sobre isso em lições subsequentes).
Aqui está como um loop do jogo pode tipicamente parecer, 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. Tens a capacidade de escolher o melhor intervalo que faz sentido para o teu jogo.
Continuando o Jogo Espacial
Vais pegar no código existente e expandi-lo. Podes começar com o código que completaste durante a parte I ou usar o código em Parte II - inicial.
- Mover o herói: vais adicionar código para garantir que podes mover o herói usando as teclas de seta.
- Mover inimigos: também precisarás de adicionar código para garantir que os inimigos se movam de cima para baixo a uma determinada velocidade.
Passos recomendados
Localiza os ficheiros que foram criados para ti na subpasta your-work
. Deve conter o seguinte:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Começa o teu projeto na pasta your_work
digitando:
cd your-work
npm start
O comando acima iniciará um servidor HTTP no endereço http://localhost:5000
. Abre um navegador e insere esse endereço; neste momento, deve renderizar o herói e todos os inimigos; nada está a mover-se - ainda!
Adicionar código
-
Adicionar objetos dedicados para
hero
,enemy
egame object
, eles devem ter propriedadesx
ey
. (Lembra-te da parte sobre Herança ou composição).DICA:
game object
deve ser aquele comx
ey
e a capacidade de se desenhar num canvas.dica: começa por adicionar uma nova classe GameObject com o seu construtor delineado como abaixo, e depois desenha-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, estende 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) } }
-
Adicionar manipuladores de eventos de teclado para lidar com a navegação por teclas (mover o herói para cima/baixo/esquerda/direita).
LEMBRA-TE: é um sistema cartesiano, o canto superior esquerdo é
0,0
. Também lembra-te de adicionar código para parar o comportamento padrão.dica: cria a tua função onKeyDown e associa-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);
Verifica o console do navegador neste ponto e observa as teclas a serem registadas.
-
Implementar o Padrão Pub/Sub, isto manterá o teu código limpo enquanto segues as partes restantes.
Para fazer esta última parte, podes:
-
Adicionar um listener 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 subscrever 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
Refatora a função window.onload para inicializar o jogo e configurar um loop do jogo num intervalo adequado. Também vais 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) };
-
Adicionar código para mover inimigos num determinado intervalo
Refatora a função
createEnemies()
para criar os inimigos e adicioná-los à 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 adiciona uma função
createHero()
para realizar 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, adiciona uma função
drawGameObjects()
para começar o desenho:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
Os teus inimigos devem começar a avançar na direção da tua nave espacial!
🚀 Desafio
Como podes ver, o teu código pode transformar-se em 'código espaguete' quando começas a adicionar funções, variáveis e classes. Como podes organizar melhor o teu código para que seja mais legível? Esboça um sistema para organizar o teu código, mesmo que ainda esteja num único ficheiro.
Questionário Pós-Aula
Revisão & Autoestudo
Embora estejamos a escrever o nosso jogo sem usar frameworks, existem muitos frameworks baseados em JavaScript para desenvolvimento de jogos com canvas. Dedica algum tempo a ler 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 automáticas podem conter erros ou imprecisões. O documento original no seu idioma nativo deve ser considerado a fonte autoritativa. Para informações críticas, recomenda-se uma tradução profissional realizada por humanos. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações incorretas resultantes do uso desta tradução.