使用原生JS和CSS构建浏览器游戏:从零打造国旗猜谜游戏

本文详细介绍了如何使用纯原生JavaScript和CSS构建一个完整的浏览器游戏,包含数据结构处理、DOM操作、CSS动画、模块化编程和游戏逻辑实现,无需任何外部库或框架。

使用原生JS和CSS构建浏览器游戏

构思

我们将构建一个国旗猜谜游戏。玩家会看到一个国旗图案和一个多项选择式的答案列表。

第一步:基础结构

首先需要准备国家和对应国旗的列表。幸运的是,我们可以使用表情符号来显示国旗,这意味着我们不需要自己寻找或创建它们。我已经以JSON格式准备好了这些数据。

最简单的界面将显示一个国旗表情符号和五个按钮:

1
/* 使用网格居中所有元素,并使用相对尺寸确保在不同屏幕尺寸上良好显示 */

项目文件结构如下:

1
2
3
4
5
6
7
8
9
step1.html
step2.html // 等等
js/
  data.json
  // 更多js文件
helpers/
  // 第三步会用到
css/
i/

第二步:简单原型

首先获取data.json文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)
});

启动游戏逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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;
  })
}

答案检查逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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');
  }
}

Fisher-Yates洗牌算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;
}

第三步:类结构

使用模块化保持代码结构:

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

在js/step3.js中导入辅助函数:

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

创建Game类封装游戏逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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');
    }
  }
}

第四步:计分和游戏结束界面

更新Game构造函数处理多轮游戏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Game {
  constructor(countries, numTurns = 3) {
    this.numTurns = numTurns;
    // DOM元素引用
    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'),
    }
  }
}

添加计分和游戏结束逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
start() {
  this.countries = shuffle([...this.masterCountries]);
  this.score = 0;
  this.turn = 0;
  this.updateScore();
  this.showCountries();
}

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

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

CSS样式:

1
2
3
4
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; }

第五步:添加动画效果

使用CSS关键帧动画:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.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); }
}

动画事件监听:

1
2
3
4
5
6
7
8
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');
  }
});

第六步:最终完善

添加开始界面:

1
2
3
4
5
6
<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>

评分系统:

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

星星动画效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
button::before, button::after { 
  content: ' '; 
  background: url(../i/star.svg); 
  height: 32px; width: 32px; 
  position: absolute; 
  opacity: 0; 
}

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

总结与扩展想法

在不到200行(带有详细注释)的JavaScript代码中,我们完成了一个功能完整、移动友好的游戏,而且没有任何依赖或库!

可以添加的改进功能:

  • 为正确和错误答案添加基本音效
  • 使用Web Workers使游戏可离线使用
  • 在localStorage中存储游戏次数、总体评分等统计数据并显示
  • 添加分享分数和在社交媒体上挑战朋友的功能
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计