Fermer

mars 14, 2025

Faire un jeu basé sur un navigateur avec Vanilla JS et CSS –

Faire un jeu basé sur un navigateur avec Vanilla JS et CSS –


Développer pour le Web de nos jours peut sembler écrasant. Il y a un choix presque infiniment riche de bibliothèques et de cadres à choisir.

Vous devrez probablement également implémenter une étape de génération, un contrôle de version et un pipeline de déploiement. Tout avant d’avoir écrit une seule ligne de code. Et une suggestion amusante? Prenons un pas en recul et rappelons-nous à quel point le JavaScript moderne et le CSS modernes puissants peuvent être succincts, sans avoir besoin d’extras brillants.

Intéressé? Venez avec moi alors, dans un voyage pour faire un jeu basé sur un navigateur en utilisant uniquement Vanilla JS et CSS.

L’idée

Nous allons construire un jeu de devinettes de drapeau. Le joueur est présenté avec un drapeau et une liste de réponses de style à choix multiples.

Étape 1. Structure de base

Tout d’abord, nous allons avoir besoin d’une liste de pays et de leurs drapeaux respectifs. Heureusement, nous pouvons exploiter le pouvoir des emojis pour afficher les drapeaux, ce qui signifie que nous n’avons pas à nous procurer ou, pire encore, à les créer nous-mêmes. J’ai préparé ça dans Formulaire JSON.

À son plus simple, l’interface va montrer un emoji de drapeau et cinq boutons:

Une pincée de CSS utilisant la grille pour tout centrer et des tailles relatives afin qu’elle s’affiche bien du plus petit écran au plus grand moniteur.

Maintenant, prenez une copie de notre cale de démarragenous allons construire sur cela tout au long
Le tutoriel.

La structure de fichiers de notre projet ressemble à ceci:


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

À la fin de chaque section, il y aura un lien vers notre code dans son état actuel.

Étape 2. Un prototype simple

Allons craquer. Tout d’abord, nous devons saisir notre data.json déposer.


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

Maintenant que nous avons les données, nous pouvons commencer le jeu. Le code suivant est généreusement commenté. Prenez quelques minutes pour lire et comprendre ce qui se passe.


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

Et une logique pour vérifier la réponse:


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

Vous avez probablement remarqué que notre startGame La fonction appelle une fonction de remaniement. Voici une simple mise en œuvre de l’algorithme 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;

    }
Code de cette étape

Étape 3. Un peu de classe

Temps pour un peu de entretien ménager. Les bibliothèques et les cadres modernes obligent souvent certaines conventions qui aident à appliquer la structure aux applications. Au fur et à mesure que les choses commencent à croître, cela a du sens et que tout le code dans un seul fichier devient bientôt désordonné.

Tenons-en-tort la puissance des modules pour garder notre code, errm, modulaire. Mettez à jour votre fichier HTML, en remplaçant le script en ligne par ceci:


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

Maintenant, dans JS / Step3.js, nous pouvons charger nos aides:


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

Assurez-vous de déplacer les fonctions Shuffle et LoadCountries vers leurs fichiers respectifs.

Note: Idéalement, nous importerions également nos données.json en tant que module mais, malheureusement, Firefox ne prend pas en charge les affirmations d’importation.

Vous devrez également démarrer chaque fonction par défaut d’exportation. Par exemple:


  export default function shuffle(array) {
  ...

Nous résumerons également notre logique de jeu dans un cours de jeu. Cela permet de maintenir l’intégrité des données et rend le code plus sécurisé et maintenable. Prenez une minute pour lire les commentaires du code.


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');
    }
  }
}
Code de cette étape

Étape 4. Scoring et écran Gameover

Mettons à jour le constructeur de jeux pour gérer plusieurs tours:


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

Notre DOM devra être mis à jour afin que nous puissions gérer le jeu sur l’état, ajouter un bouton de relecture et afficher le score.


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

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

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

Nous cachons simplement le jeu sur la section jusqu’à ce qu’il soit nécessaire.

Maintenant, ajoutez des références à ces nouveaux éléments DOM dans notre constructeur de jeux:


    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'),
    }

Nous allons également ranger notre méthode de démarrage de jeu, en déplaçant la logique pour afficher les pays vers une méthode distincte. Cela aidera à garder les choses propres et gérables.



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

    // 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}`;
  }

Au bas de la méthode du constructeur de jeux, nous allons
Écoutez les clics sur les bouton (s) de relecture. Dans le
Événement d’un clic, nous redémarrons en appelant la méthode de démarrage.


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

Enfin, ajoutons un pincement de style aux boutons, positionnons la partition et
Ajoutez notre classe. Hide pour basculer le jeu au besoin.


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

Progrès! Nous avons maintenant un jeu très simple.
C’est un peu fade, cependant. Adressez-nous cela
à l’étape suivante.

Code de cette étape

Étape 5. Apportez le bling!

Les animations CSS sont un moyen très simple et succinct de
Donnez vie aux éléments et interfaces statiques.

Images clés
Permettez-nous de définir les images clés d’une séquence d’animation avec le changement
Propriétés CSS. Considérez ceci pour faire glisser notre liste de pays sur et hors écran:


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

Nous pouvons appliquer l’effet coulissant lors du démarrage du jeu…


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

… Et dans la méthode NextTurn


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

Nous devons également appeler la méthode NextTurn une fois que nous aurons vérifié la réponse. Mettez à jour la méthode Checkanswer pour y parvenir:


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

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

Une fois l’animation de diapositive terminée, nous devons le ramener et mettre à jour la liste des pays. Nous pourrions définir un délai d’attente, basé sur la longueur d’animation, et les effectuer cette logique. Heureusement, il existe un moyen plus facile d’utiliser l’événement 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');
      }
    });

Code de cette étape

Étape 6. touches finales

Ne serait-il pas agréable d’ajouter un écran de titre? De cette façon, l’utilisateur reçoit un peu de contexte et non jeté directement dans le jeu.

Notre balisage ressemblera à ceci:


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

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


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

Ajoutons l’écran d’intro dans le jeu.
Nous devrons y ajouter une référence dans les éléments DOM:


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

Ensuite, cachez-le simplement lorsque vous démarrez le jeu:


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

N’oubliez pas non plus d’ajouter le nouveau style:


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

Maintenant, ne serait-il pas bien de fournir au joueur une note en fonction de son score aussi? C’est super facile à mettre en œuvre. Comme on peut le voir, dans la méthode Gameover mise à jour:


    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]}
      `;
  }

Une dernière touche finale; Une belle animation lorsque le joueur devine correctement. Nous pouvons nous tourner une fois de plus en animations CSS pour réaliser cet effet.




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

Nous utilisons le :: avant et :: après pseudo attacher l’image d’arrière-plan (star.svg) Mais gardez-le caché en définissant l’opacité sur 0. Il est ensuite activé en invoquant l’animation Sparkle lorsque le bouton a le nom de classe correct. N’oubliez pas que nous appliquons déjà cette classe sur le bouton lorsque la bonne réponse est sélectionnée.

Code de cette étape

Reploche et quelques idées supplémentaires

Dans moins de 200 lignes de javascript (généreusement commenté), nous avons entièrement
Jeu de travail et adapté aux mobiles. Et pas une seule dépendance ou une bibliothèque en vue!

Bien sûr, il y a des fonctionnalités et des améliorations infinies que nous pourrions ajouter à notre jeu.
Si vous avez envie d’un défi, voici quelques idées:

  • Ajouter des effets sonores de base pour des réponses correctes et incorrectes.
  • Rendre le jeu disponible hors ligne à l’aide de travailleurs Web
  • Stocker des statistiques telles que le nombre de pièces, les notes globales dans LocalStorage et l’affichage
  • Ajoutez un moyen de partager votre score et de défier des amis sur les réseaux sociaux.




Source link