Fazendo um jogo baseado no navegador com JS e CSS – SitePoint de baunilha


Desenvolver para a Web hoje em dia pode parecer esmagador. Há uma escolha quase infinitamente rica de bibliotecas e estruturas para escolher.

Você provavelmente também precisará implementar uma etapa de construção, controle de versão e um pipeline de implantação. Tudo antes de você escrever uma única linha de código. Que tal uma sugestão divertida? Vamos dar um passo atrás e nos lembrar o quão sucinto e poderoso JavaScript e CSS podem ser, sem a necessidade de extras brilhantes.

Interessado? Venha comigo então, em uma jornada para fazer um jogo baseado no navegador usando apenas JS e CSS de baunilha.

A ideia

Estaremos construindo um jogo de adivinhação de bandeira. O jogador é apresentado com uma bandeira e uma lista de respostas no estilo de múltipla escolha.

Etapa 1. Estrutura básica

Primeiro, vamos precisar de uma lista de países e suas respectivas bandeiras. Felizmente, podemos aproveitar o poder dos emojis para exibir as bandeiras, o que significa que não precisamos adquirir ou, pior ainda, criá -los nós mesmos. Eu preparei isso em JSON FORM.

Na sua mais simples, a interface mostrará um emoji de bandeira e cinco botões:

Uma pitada de CSS usando a grade para centralizar tudo e tamanhos relativos, para que ele seja exibido bem da menor tela até o maior monitor.

Agora pegue uma cópia do nosso Shim inicianteestaremos construindo sobre isso o tempo todo
o tutorial.

A estrutura do arquivo para o nosso projeto é assim:


  step1.html
  step2.html 
  js/
    data.json
    
  helpers/
    
  css/
  i/

No final de cada seção, haverá um link para o nosso código em seu estado atual.

Etapa 2. Um protótipo simples

Vamos fazer rachaduras. Primeiro, precisamos pegar nosso data.json arquivo.


    async function loadCountries(file) {
      try {
        const response = await fetch(file);
        return await response.json();
      } catch (error) {
        throw new Error(error);
      }
    }

    
    
    loadCountries('./js/data.json')
    .then((data) => {
        startGame(data.countries)
    });

Agora que temos os dados, podemos iniciar o jogo. O código a seguir é generosamente comentado. Reserve alguns minutos para ler e controlar o que está acontecendo.


    function startGame(countries) {
      
      
      
      shuffle(countries);

      
      let answer = countries.shift();

      
      let selected = shuffle((answer, ...countries.slice(0, 4)));

      
      document.querySelector('h2.flag').innerText = answer.flag;
      
      document.querySelectorAll('.suggestions button')
          .forEach((button, index) => {
        const countryName = selected(index).name;
        button.innerText = countryName;
        
        
        button.dataset.correct = (countryName === answer.name);
        button.onclick = checkAnswer;
      })
    }

E alguma lógica para verificar a resposta:


    function checkAnswer(e) {
      const button = e.target;
      if (button.dataset.correct === 'true') {
        button.classList.add('correct');
        alert('Correct! Well done!');
      } else {
        button.classList.add('wrong');
        alert('Wrong answer try again');
      }
    }

Você provavelmente percebeu que nosso startGame A função chama uma função Shuffle. Aqui está uma simples implementação do algoritmo Fisher-Yates:


    
    
    function shuffle(array) {
      var m = array.length, t, i;

      
      while (m) {

        
        i = Math.floor(Math.random() * m--);

        
        t = array(m);
        array(m) = array(i);
        array(i) = t;
      }

      return array;

    }
Código desta etapa

Etapa 3. Um pouco de classe

Hora de um pouco de limpeza. Bibliotecas e estruturas modernas geralmente forçam certas convenções que ajudam a aplicar a estrutura a aplicativos. À medida que as coisas começam a crescer, isso faz sentido e ter todo o código em um arquivo logo fica confuso.

Vamos aproveitar o poder dos módulos para manter nosso código, errm, modular. Atualize seu arquivo HTML, substituindo o script embutido por isso:


  <script type="module" src="./js/step3.js">script>

Agora, no js/step3.js, podemos carregar nossos ajudantes:


  import loadCountries from "./helpers/loadCountries.js";
  import shuffle from "./helpers/shuffle.js";

Certifique -se de mover as funções Shuffle e LoadCountries para seus respectivos arquivos.

Observação: Idealmente, também importaríamos nosso Data.json como um módulo, mas, infelizmente, o Firefox não suporta asserções de importação.

Você também precisará iniciar cada função com o padrão de exportação. Por exemplo:


  export default function shuffle(array) {
  ...

Também encapsularemos nossa lógica de jogo em uma aula de jogo. Isso ajuda a manter a integridade dos dados e torna o código mais seguro e sustentável. Reserve um minuto para ler os comentários do código.


loadCountries('js/data.json')
  .then((data) => {
    const countries = data.countries;
    const game = new Game(countries);
    game.start();
  });

class Game {
  constructor(countries) {
    
    
    this.masterCountries = countries;
    
    this.DOM = {
      flag: document.querySelector('h2.flag'),
      answerButtons: document.querySelectorAll('.suggestions button')
    }

    
    this.DOM.answerButtons.forEach((button) => {
      button.onclick = (e) => {
        this.checkAnswer(e.target);
      }
    })

  }

  start() {

    
    
    
    
    this.countries = shuffle((...this.masterCountries));
    
    
    const answer = this.countries.shift();
    
    const selected = shuffle((answer, ...this.countries.slice(0, 4)));


    
    this.DOM.flag.innerText = answer.flag;
    
    selected.forEach((country, index) => {
      const button = this.DOM.answerButtons(index);
      
      button.classList.remove('correct', 'wrong');
      button.innerText = country.name;
      button.dataset.correct = country.name === answer.name;
    });
  }

  checkAnswer(button) {
    const correct = button.dataset.correct === 'true';

    if (correct) {
      button.classList.add('correct');
      alert('Correct! Well done!');
      this.start();
    } else {
      button.classList.add('wrong');
      alert('Wrong answer try again');
    }
  }
}
Código desta etapa

Etapa 4. Pontuação e uma tela de jogo

Vamos atualizar o construtor do jogo para lidar com várias rodadas:


class Game {
  constructor(countries, numTurns = 3) {
    // number of turns in a game
    this.numTurns = numTurns;
    ...

Nosso DOM precisará ser atualizado para que possamos lidar com o jogo no estado, adicionar um botão de repetição e exibir a pontuação.


    <main>
      <div class="score">0div>

      <section class="play">
      ...
      section>

      <section class="gameover hide">
       <h2>Game Overh2>
        <p>You scored:
          <span class="result">
          span>
        p>
        <button class="replay">Play againbutton>
      section>
    main>

Apenas escondemos o jogo pela seção até que seja necessário.

Agora, adicione referências a esses novos elementos DOM em nosso construtor de jogos:


    this.DOM = {
      score: document.querySelector('.score'),
      play: document.querySelector('.play'),
      gameover: document.querySelector('.gameover'),
      result: document.querySelector('.result'),
      flag: document.querySelector('h2.flag'),
      answerButtons: document.querySelectorAll('.suggestions button'),
      replayButtons: document.querySelectorAll('button.replay'),
    }

Também arrumamos nosso método de início do jogo, movendo a lógica para exibir os países para um método separado. Isso ajudará a manter as coisas limpas e gerenciáveis.



  start() {
    this.countries = shuffle((...this.masterCountries));
    this.score = 0;
    this.turn = 0;
    this.updateScore();
    this.showCountries();
  }

  showCountries() {
    // get our answer
    const answer = this.countries.shift();
    // pick 4 more countries, merge our answer and shuffle
    const selected = shuffle((answer, ...this.countries.slice(0, 4)));

    // update the DOM, starting with the flag
    this.DOM.flag.innerText = answer.flag;
    // update each button with a country name
    selected.forEach((country, index) => {
      const button = this.DOM.answerButtons(index);
      // remove any classes from previous turn
      button.classList.remove('correct', 'wrong');
      button.innerText = country.name;
      button.dataset.correct = country.name === answer.name;
    });

  }

  nextTurn() {
    const wrongAnswers = document.querySelectorAll('button.wrong')
          .length;
    this.turn += 1;
    if (wrongAnswers === 0) {
      this.score += 1;
      this.updateScore();
    }

    if (this.turn === this.numTurns) {
      this.gameOver();
    } else {
      this.showCountries();
    }
  }

  updateScore() {
    this.DOM.score.innerText = this.score;
  }

  gameOver() {
    this.DOM.play.classList.add('hide');
    this.DOM.gameover.classList.remove('hide');
    this.DOM.result.innerText = `${this.score} out of ${this.numTurns}`;
  }

Na parte inferior do método do construtor do jogo, nós iremos
Ouça os cliques no (s) botão (s) de repetição. No
Evento de um clique, reiniciamos chamando o método de início.


    this.DOM.replayButtons.forEach((button) => {
      button.onclick = (e) => {
        this.start();
      }
    });

Por fim, vamos adicionar uma pitada de estilo aos botões, posicionar a pontuação e
Adicione nossa classe .hide para alternar o jogo conforme necessário.


button.correct { background: darkgreen; color: #fff; }
button.wrong { background: darkred; color: #fff; }

.score { position: absolute; top: 1rem; left: 50%; font-size: 2rem; }
.hide { display: none; }

Progresso! Agora temos um jogo muito simples.
É um pouco sem graça, no entanto. Vamos abordar isso
na próxima etapa.

Código desta etapa

Etapa 5. Traga o bling!

As animações do CSS são uma maneira muito simples e sucinta para
traga elementos estáticos e interfaces à vida.

KeyFrames
Permita -nos definir os quadros -chave de uma sequência de animação com a mudança
Propriedades do CSS. Considere isso para deslizar nossa lista de países dentro e fora da tela:


.slide-off { animation: 0.75s slide-off ease-out forwards; animation-delay: 1s;}
.slide-on { animation: 0.75s slide-on ease-in; }

@keyframes slide-off {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0; transform: translateX(50vw); }
}
@keyframes slide-on {
  from { opacity: 0; transform: translateX(-50vw); }
  to { opacity: 1; transform: translateX(0); }
}

Podemos aplicar o efeito deslizante ao iniciar o jogo …


  start() {
    // reset dom elements
    this.DOM.gameover.classList.add('hide');
    this.DOM.play.classList.remove('hide');
    this.DOM.play.classList.add('slide-on');
    ...
  }

… E no método próximo


  nextTurn() {
    ...
    if (this.turn === this.numTurns) {
      this.gameOver();
    } else {
      this.DOM.play.classList.remove('slide-on');
      this.DOM.play.classList.add('slide-off');
    }
  }

Também precisamos ligar para o método NextTurn depois de verificar a resposta. Atualize o método Checkanswer para conseguir isso:


  checkAnswer(button) {
    const correct = button.dataset.correct === 'true';

    if (correct) {
      button.classList.add('correct');
      this.nextTurn();
    } else {
      button.classList.add('wrong');
    }
  }

Depois que a animação deslizante terminar, precisamos deslizá-la novamente e atualizar a lista de países. Poderíamos definir um tempo limite, com base no comprimento da animação, e o desempenho dessa lógica. Felizmente, existe uma maneira mais fácil de usar o evento AnimationEnd:


    // listen to animation end events
    // in the case of .slide-on, we change the card,
    // then move it back on screen
    this.DOM.play.addEventListener('animationend', (e) => {
      const targetClass = e.target.classList;
      if (targetClass.contains('slide-off')) {
        this.showCountries();
        targetClass.remove('slide-off', 'no-delay');
        targetClass.add('slide-on');
      }
    });

Código desta etapa

Etapa 6. Toques finais

Não seria bom adicionar uma tela de título? Dessa forma, o usuário recebe um pouco de contexto e não é jogado diretamente no jogo.

Nossa marcação ficará assim:


      
      <div class="score hide">0div>

      <section class="intro fade-in">
       <h1>
          Guess the flag
      h1>
       <p class="guess">🌍p>
      <p>How many can you recognize?p>
      <button class="replay">Startbutton>
      section>


      
      <section class="play hide">
      ...

Vamos conectar a tela de introdução ao jogo.
Precisamos adicionar uma referência a ele nos elementos DOM:


    
    this.DOM = {
      intro: document.querySelector('.intro'),
      ....

Em seguida, basta esconder ao iniciar o jogo:


  start() {
    
    this.DOM.intro.classList.add('hide');
    
    this.DOM.score.classList.remove('hide');
    ...

Além disso, não se esqueça de adicionar o novo estilo:


section.intro p { margin-bottom: 2rem; }
section.intro p.guess { font-size: 8rem; }
.fade-in { opacity: 0; animation: 1s fade-in ease-out forwards; }
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

Agora não seria bom fornecer ao jogador uma classificação com base na pontuação deles também? Isso é super fácil de implementar. Como pode ser visto, no método de jogo atualizado:


    const ratings = ('💩','🤣','😴','🤪','👎','😓','😅','😃','🤓','🔥','⭐');
    const percentage = (this.score / this.numTurns) * 100;
    
    const rating = Math.ceil(percentage / ratings.length);

    this.DOM.play.classList.add('hide');
    this.DOM.gameover.classList.remove('hide');
    
    this.DOM.gameover.classList.add('fade-in');
    this.DOM.result.innerHTML = `
      ${this.score} out of ${this.numTurns}
      
      Your rating: ${this.ratings(rating)}
      `;
  }

Um toque final final; Uma boa animação quando o jogador adivinha corretamente. Podemos voltar mais uma vez para as animações do CSS para alcançar esse efeito.




button::before { content: ' '; background: url(../i/star.svg); height: 32px; width: 32px; position: absolute; bottom: -2rem; left: -1rem; opacity: 0; }
button::after {  content: ' '; background: url(../i/star.svg); height: 32px; width: 32px; position: absolute; bottom: -2rem; right: -2rem; opacity: 0; }

button { position: relative; }

button.correct::before { animation: sparkle .5s ease-out forwards; }
button.correct::after { animation: sparkle2 .75s ease-out forwards; }

@keyframes sparkle {
  from { opacity: 0; bottom: -2rem; scale: 0.5 }
  to { opacity: 0.5; bottom: 1rem; scale: 0.8; left: -2rem; transform: rotate(90deg); }
}

@keyframes sparkle2 {
  from { opacity: 0; bottom: -2rem; scale: 0.2}
  to { opacity: 0.7; bottom: -1rem; scale: 1; right: -3rem; transform: rotate(-45deg); }
}

Usamos o :: antes e :: depois pseudo elementos para Anexe a imagem de fundo (Star.svg) Mas mantenha -o oculto através da configuração da opacidade para 0. Ele é ativado invocando a animação Sparkle quando o botão tiver o nome da classe correto. Lembre -se, já aplicamos esta classe ao botão quando a resposta correta for selecionada.

Código desta etapa

Encerrar e algumas idéias extras

Em menos de 200 linhas de JavaScript (comentado liberalmente), temos um totalmente
jogo de trabalho e amigável para dispositivos móveis. E nem uma única dependência ou biblioteca à vista!

Obviamente, existem inúmeras características e melhorias que poderíamos adicionar ao nosso jogo.
Se você gosta de um desafio aqui estão algumas idéias:

  • Adicionar efeitos sonoros básicos Para respostas corretas e incorretas.
  • Visite o jogo offline usando trabalhadores da web
  • Armazenar estatísticas como o número de peças, classificações gerais no localStorage e exibição
  • Adicione uma maneira de compartilhar sua pontuação e desafiar os amigos nas mídias sociais.



Source link