You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/br/6-space-game/3-moving-elements-around/README.md

25 KiB

Construindo um Jogo Espacial Parte 3: Adicionando Movimento

Pense nos seus jogos favoritos o que os torna cativantes não são apenas os gráficos bonitos, mas a forma como tudo se move e responde às suas ações. No momento, seu jogo espacial é como uma pintura bonita, mas estamos prestes a adicionar movimento que o trará à vida.

Quando os engenheiros da NASA programaram o computador de orientação para as missões Apollo, enfrentaram um desafio semelhante: como fazer uma espaçonave responder aos comandos do piloto enquanto mantém correções automáticas de curso? Os princípios que aprenderemos hoje ecoam esses mesmos conceitos gerenciar o movimento controlado pelo jogador junto com comportamentos automáticos do sistema.

Nesta lição, você aprenderá como fazer as naves espaciais deslizarem pela tela, responderem aos comandos do jogador e criarem padrões de movimento suaves. Vamos dividir tudo em conceitos gerenciáveis que se complementam naturalmente.

Ao final, os jogadores poderão pilotar sua nave heroica pela tela enquanto as naves inimigas patrulham acima. Mais importante, você entenderá os princípios fundamentais que alimentam os sistemas de movimento dos jogos.

Quiz Pré-Aula

Quiz pré-aula

Entendendo o Movimento em Jogos

Os jogos ganham vida quando as coisas começam a se mover, e existem basicamente duas maneiras de isso acontecer:

  • Movimento controlado pelo jogador: Quando você pressiona uma tecla ou clica com o mouse, algo se move. Essa é a conexão direta entre você e o mundo do jogo.
  • Movimento automático: Quando o próprio jogo decide mover as coisas como aquelas naves inimigas que precisam patrulhar a tela, independentemente do que você esteja fazendo.

Fazer objetos se moverem na tela de um computador é mais simples do que você imagina. Lembra-se das coordenadas x e y da aula de matemática? É exatamente isso que estamos usando aqui. Quando Galileu rastreou as luas de Júpiter em 1610, ele estava essencialmente fazendo a mesma coisa traçando posições ao longo do tempo para entender os padrões de movimento.

Mover coisas na tela é como criar uma animação de flipbook você precisa seguir esses três passos simples:

  1. Atualizar a posição Alterar onde seu objeto deve estar (talvez movê-lo 5 pixels para a direita)
  2. Apagar o quadro antigo Limpar a tela para que você não veja rastros fantasmagóricos por toda parte
  3. Desenhar o novo quadro Colocar seu objeto em sua nova posição

Faça isso rápido o suficiente e pronto! Você terá um movimento suave que parece natural para os jogadores.

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);

O que este código faz:

  • Atualiza a coordenada x do herói em 5 pixels para movê-lo horizontalmente
  • Limpa toda a área do canvas para remover o quadro anterior
  • Preenche o canvas com uma cor de fundo preta
  • Redesenha a imagem do herói em sua nova posição

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.

Lidando com eventos de teclado

É aqui que conectamos a entrada do jogador à ação do jogo. Quando alguém aperta a barra de espaço para disparar um laser ou pressiona uma tecla de seta para desviar de um asteroide, seu jogo precisa detectar e responder a essa entrada.

Os eventos de teclado acontecem no nível da janela, o que significa que toda a janela do navegador está ouvindo essas teclas pressionadas. Cliques do mouse, por outro lado, podem ser vinculados a elementos específicos (como clicar em um botão). Para nosso jogo espacial, focaremos nos controles de teclado, já que é isso que dá aos jogadores aquela sensação clássica de arcade.

Isso me lembra como os operadores de telégrafo no século XIX tinham que traduzir entradas de código Morse em mensagens significativas estamos fazendo algo semelhante, traduzindo teclas pressionadas em comandos de jogo.

Para lidar com 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 ocorrido.

Aqui está um exemplo:

window.addEventListener('keyup', (evt) => {
  // evt.key = string representation of the key
  if (evt.key === 'ArrowUp') {
    // do something
  }
});

Desmembrando o que acontece aqui:

  • Escuta eventos de teclado em toda a janela
  • Captura o objeto do evento que contém informações sobre qual tecla foi pressionada
  • Verifica se a tecla pressionada corresponde a uma tecla específica (neste caso, a seta para cima)
  • Executa o código quando a condição é atendida

Para eventos de tecla, há duas propriedades no evento que você pode usar para ver qual tecla foi pressionada:

  • key - esta é uma representação em string da tecla pressionada, por exemplo, 'ArrowUp'
  • keyCode - esta é uma representação numérica, por exemplo, 37, que corresponde a ArrowLeft

Manipulação de eventos de tecla é útil fora do desenvolvimento de jogos. Quais outros usos você consegue pensar para essa técnica?

Teclas especiais: atenção!

Algumas teclas têm comportamentos embutidos no navegador que podem interferir no seu jogo. As teclas de seta rolam a página e a barra de espaço faz a página descer comportamentos que você não quer quando alguém está tentando pilotar sua nave espacial.

Podemos evitar esses comportamentos padrão e deixar nosso jogo lidar com a entrada. Isso é semelhante a como os primeiros programadores de computadores tinham que substituir interrupções do sistema para criar comportamentos personalizados estamos apenas fazendo isso no nível do navegador. Veja como:

const 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);

Entendendo este código de prevenção:

  • Verifica códigos de tecla específicos que podem causar comportamentos indesejados no navegador
  • Previne a ação padrão do navegador para as teclas de seta e barra de espaço
  • Permite que outras teclas funcionem normalmente
  • Usa e.preventDefault() para interromper o comportamento embutido do navegador

Movimento induzido pelo jogo

Agora vamos falar sobre objetos que se movem sem a entrada do jogador. Pense em naves inimigas cruzando a tela, balas voando em linha reta ou nuvens flutuando ao fundo. Esse movimento autônomo faz com que o mundo do jogo pareça vivo, mesmo quando ninguém está tocando nos controles.

Usamos temporizadores embutidos do JavaScript para atualizar posições em intervalos regulares. Esse conceito é semelhante ao funcionamento de relógios de pêndulo um mecanismo regular que aciona ações consistentes e cronometradas. Veja como isso pode ser simples:

const id = setInterval(() => {
  // Move the enemy on the y axis
  enemy.y += 10;
}, 100);

O que este código de movimento faz:

  • Cria um temporizador que é executado a cada 100 milissegundos
  • Atualiza a coordenada y do inimigo em 10 pixels a cada vez
  • Armazena o ID do intervalo para que possamos pará-lo mais tarde, se necessário
  • Move o inimigo para baixo na tela automaticamente

O loop do jogo

Aqui está o conceito que une tudo o loop do jogo. Se o seu jogo fosse um filme, o loop do jogo seria o projetor de filmes, exibindo quadro após quadro tão rápido que tudo parece se mover suavemente.

Todo jogo tem um desses loops rodando nos bastidores. É uma função que atualiza todos os objetos do jogo, redesenha a tela e repete esse processo continuamente. Isso mantém o controle do seu herói, todos os inimigos, quaisquer lasers voando por aí todo o estado do jogo.

Esse conceito me lembra como os primeiros animadores de filmes, como Walt Disney, tinham que redesenhar os personagens quadro a quadro para criar a ilusão de movimento. Estamos fazendo a mesma coisa, só que com código em vez de lápis.

Aqui está como um loop de jogo geralmente se parece, expresso em código:

const 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();
  }
  gameLoop();
}, 200);

Entendendo a estrutura do loop do jogo:

  • Limpa todo o canvas para remover o quadro anterior
  • Preenche o fundo com uma cor sólida
  • Desenha todos os objetos do jogo em suas posições atuais
  • Repete esse processo a cada 200 milissegundos para criar uma animação suave
  • Gerencia a taxa de quadros controlando o tempo do intervalo

Continuando o Jogo Espacial

Agora vamos adicionar movimento à cena estática que você construiu anteriormente. Vamos transformá-la de uma captura de tela em uma experiência interativa. Vamos trabalhar nisso passo a passo para garantir que cada peça se construa sobre a anterior.

Pegue o código de onde paramos na lição anterior (ou comece com o código na pasta Parte II - inicial se precisar começar do zero).

Aqui está o que estamos construindo hoje:

  • Controles do herói: As teclas de seta irão pilotar sua nave espacial pela tela
  • Movimento dos inimigos: As naves alienígenas começarão seu avanço

Vamos começar a implementar esses recursos.

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 que este comando faz:

  • Navega até o diretório do seu projeto
  • Inicia um servidor HTTP no endereço http://localhost:5000
  • Serve os arquivos do seu jogo para que você possa testá-los em um navegador

O comando acima iniciará um servidor HTTP no endereço http://localhost:5000. Abra um navegador e insira esse endereço, agora ele deve renderizar o herói e todos os inimigos; nada está se movendo ainda!

Adicionar código

  1. Adicione objetos dedicados para hero, enemy e game object, eles devem ter propriedades x e y. (Lembre-se da parte sobre Herança ou composição).

    DICA game object deve ser aquele com x e y 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);
      }
    }
    

    Entendendo esta classe base:

    • Define propriedades comuns que todos os objetos do jogo compartilham (posição, tamanho, imagem)
    • Inclui uma bandeira dead para rastrear se o objeto deve ser removido
    • Fornece um método draw() que renderiza o objeto no canvas
    • Define valores padrão para todas as propriedades que as classes filhas podem substituir

    Agora, estenda este GameObject para criar o Hero e o Enemy:

    class Hero extends GameObject {
      constructor(x, y) {
        super(x, y);
        this.width = 98;
        this.height = 75;
        this.type = "Hero";
        this.speed = 5;
      }
    }
    
    class Enemy extends GameObject {
      constructor(x, y) {
        super(x, y);
        this.width = 98;
        this.height = 50;
        this.type = "Enemy";
        const id = setInterval(() => {
          if (this.y < canvas.height - this.height) {
            this.y += 5;
          } else {
            console.log('Stopped at', this.y);
            clearInterval(id);
          }
        }, 300);
      }
    }
    

    Conceitos-chave nestas classes:

    • Herdam de GameObject usando a palavra-chave extends
    • Chamam o construtor pai com super(x, y)
    • Definem dimensões e propriedades específicas para cada tipo de objeto
    • Implementam movimento automático para inimigos usando setInterval()
  2. Adicione manipuladores de eventos de tecla 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:

    const onKeyDown = function (e) {
      console.log(e.keyCode);
      // Add the code from the lesson above to stop default behavior
      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 que este manipulador de eventos faz:

    • Escuta eventos de tecla pressionada em toda a janela
    • Registra o código da tecla para ajudar você a depurar quais teclas estão sendo pressionadas
    • Previne o comportamento padrão do navegador para teclas de seta e barra de espaço
    • Permite que outras teclas funcionem normalmente

    Verifique o console do seu navegador neste ponto e observe as teclas sendo registradas.

  3. Implemente o Padrão Pub-Sub, isso manterá seu código limpo enquanto você segue as próximas partes.

    O padrão Publish-Subscribe ajuda a organizar seu código separando a detecção de eventos do tratamento de eventos. Isso torna seu código mais modular e fácil de manter.

    Para fazer esta última parte, você pode:

    1. 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);
        }
      });
      

    O que este sistema de eventos faz:

    • Detecta entrada de teclado e a converte em eventos personalizados do jogo
    • Separa a detecção de entrada da lógica do jogo
    • Facilita a alteração dos controles mais tarde sem afetar o código do jogo
    • Permite que vários sistemas respondam à mesma entrada
    1. Crie 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);
        }
      
      
    2. Adicione constantes e configure 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();
      

    Entendendo a configuração:

    • Define constantes de mensagem para evitar erros de digitação e facilitar refatorações
    • Declara variáveis para imagens, contexto do canvas e estado do jogo
    • Cria um emissor de eventos global para o sistema pub-sub
    • Inicializa um array para armazenar todos os objetos do jogo
    1. Inicialize 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;
        });
      
      
  4. Configure 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();
      const gameLoopId = setInterval(() => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "black";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        drawGameObjects(ctx);
      }, 100);
    };
    

    Entendendo a configuração do jogo:

    • Aguarda o carregamento completo da página antes de começar
    • Obtém o elemento canvas e seu contexto de renderização 2D
    • Carrega todos os recursos de imagem de forma assíncrona usando await
    • Inicia o loop do jogo rodando em intervalos de 100ms (10 FPS)
    • Limpa e redesenha toda a tela a cada quadro
  5. Adicione código para mover os 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);
        }
      }
    }
    

    O que a criação de inimigos faz:

    • Calcula posições para centralizar os inimigos na tela
  • Cria uma grade de inimigos usando loops aninhados
  • Atribui a imagem do inimigo a cada objeto inimigo
  • Adiciona cada inimigo ao array global de objetos do jogo

e adicione uma função createHero() para realizar um processo semelhante para o herói.

```javascript
function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}
```

O que a criação do herói faz:

  • Posiciona o herói no centro inferior da tela
  • Atribui a imagem do herói ao objeto herói
  • Adiciona o herói ao array de objetos do jogo para renderização

e, por fim, adicione uma função drawGameObjects() para iniciar a renderização:

```javascript
function drawGameObjects(ctx) {
  gameObjects.forEach(go => go.draw(ctx));
}
```

Entendendo a função de renderização:

  • Itera por todos os objetos do jogo no array
  • Chama o método draw() em cada objeto
  • Passa o contexto do canvas para que os objetos possam se renderizar

Seus inimigos devem começar a avançar contra sua nave espacial!
}
}
```

and add a `createHero()` function to do a similar process for the hero.

```javascript
function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}
```

e, por fim, adicione uma função drawGameObjects() para iniciar a renderização:

```javascript
function drawGameObjects(ctx) {
  gameObjects.forEach(go => go.draw(ctx));
}
```

Seus inimigos devem começar a avançar contra sua nave espacial!


Desafio do Agente GitHub Copilot 🚀

Aqui está um desafio que vai melhorar o acabamento do seu jogo: adicionar limites e controles suaves. Atualmente, seu herói pode sair da tela, e o movimento pode parecer travado.

Sua missão: Faça sua nave espacial parecer mais realista implementando limites na tela e movimentos fluidos. Isso é semelhante ao modo como os sistemas de controle de voo da NASA evitam que espaçonaves excedam parâmetros operacionais seguros.

O que você deve construir: Crie um sistema que mantenha sua nave espacial herói na tela e faça os controles parecerem suaves. Quando os jogadores mantêm pressionada uma tecla de seta, a nave deve deslizar continuamente em vez de se mover em passos discretos. Considere adicionar um feedback visual quando a nave atingir os limites da tela talvez um efeito sutil para indicar o limite da área de jogo.

Saiba mais sobre modo agente aqui.

🚀 Desafio

A organização do código se torna cada vez mais importante à medida que os projetos crescem. Você pode ter notado que seu arquivo está ficando cheio de funções, variáveis e classes misturadas. Isso me lembra como os engenheiros que organizaram o código da missão Apollo tiveram que criar sistemas claros e fáceis de manter para que várias equipes pudessem trabalhar simultaneamente.

Sua missão:
Pense como um arquiteto de software. Como você organizaria seu código para que, daqui a seis meses, você (ou um colega de equipe) pudesse entender o que está acontecendo? Mesmo que tudo permaneça em um único arquivo por enquanto, você pode criar uma organização melhor:

  • Agrupando funções relacionadas com cabeçalhos de comentários claros
  • Separando responsabilidades - mantenha a lógica do jogo separada da renderização
  • Usando convenções de nomenclatura consistentes para variáveis e funções
  • Criando módulos ou namespaces para organizar diferentes aspectos do seu jogo
  • Adicionando documentação que explique o propósito de cada seção principal

Perguntas para reflexão:

  • Quais partes do seu código são mais difíceis de entender quando você volta a elas?
  • Como você poderia organizar seu código para torná-lo mais fácil para outra pessoa contribuir?
  • O que aconteceria se você quisesse adicionar novos recursos, como power-ups ou diferentes tipos de inimigos?

Quiz Pós-Aula

Quiz pós-aula

Revisão & Autoestudo

Estamos construindo tudo do zero, o que é fantástico para aprender, mas aqui vai um pequeno segredo existem alguns frameworks incríveis de JavaScript que podem lidar com grande parte do trabalho pesado para você. Assim que você se sentir confortável com os fundamentos que abordamos, vale a pena explorar o que está disponível.

Pense nos frameworks como ter uma caixa de ferramentas bem equipada, em vez de fazer cada ferramenta à mão. Eles podem resolver muitos desses desafios de organização de código que mencionamos, além de oferecer recursos que levariam semanas para você desenvolver sozinho.

Coisas que valem a pena explorar:

  • Como os motores de jogos organizam o código você ficará impressionado com os padrões inteligentes que eles usam
  • Truques de desempenho para fazer jogos em canvas rodarem de forma super suave
  • Recursos modernos do JavaScript que podem tornar seu código mais limpo e fácil de manter
  • Diferentes abordagens para gerenciar objetos do jogo e suas relações

Tarefa

Comente seu código


Aviso Legal:
Este documento foi traduzido usando 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 humana. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações incorretas decorrentes do uso desta tradução.