Day 30 - Make a Whack A Mole Game with Vanilla JS

前言

JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用课程,课程主打 No FrameworksNo CompilersNo LibrariesNo Boilerplate 在30天的30部教学影片里,建立30个JavaScript的有趣小东西。

另外,Wes Bos 也很无私地在 Github 上公开了所有 JS 30 课程的程序码,有兴趣的话可以去 fork 或下载。


本日目标

最後一天要实作的内容是"网页版打地鼠"。(下面是实际网页效果的gif图)


解析程序码

HTML 部分

<h1>是页面的标题,旁边的<span class="score">是打地鼠游戏的记分板。

<button>在滑鼠点击时,会呼叫startGame()方法,开始打地鼠游戏。

<div class="game">是打地鼠游戏的主体,内部有<dvi class="hole">共计六个洞,每个洞(.hole)都有一只初始被隐藏的地鼠(<div class="mole">)。

<h1>Whack-a-mole! <span class="score">0</span></h1>
<button onClick="startGame()">Start!</button>

<div class="game">
    <div class="hole hole1">
      <div class="mole"></div>
    </div>
    <div class="hole hole2">
      <div class="mole"></div>
    </div>
    <div class="hole hole3">
      <div class="mole"></div>
    </div>
    <div class="hole hole4">
      <div class="mole"></div>
    </div>
    <div class="hole hole5">
      <div class="mole"></div>
    </div>
    <div class="hole hole6">
      <div class="mole"></div>
    </div>
</div>

CSS 部分

初始每一只地鼠(.mole)都是采用绝对定位(position:absolute),然後将top指定为100%,把地鼠(.mole)隐藏起来。

在游戏过程中,我们会为地洞(.hole)添加.up这个 CSS class 选择器,让地鼠探头出来给我们打 XD。

.mole {
  /*上略...*/
  position: absolute;
  top: 100%;
  /*下略...*/
}

.hole.up .mole {
  top: 0;
}

JS 部分

宣告常数holes取得所有的地洞(.hole),资料型态是 NodeList。

宣告常数scoreBoard取得页面中显示的分数(.score)。

宣告常数mole取得所有的地鼠(.mole),资料型态是 NodeList。

const holes = document.querySelectorAll('.hole'); //NodeList
const scoreBoard = document.querySelector('.score');
const moles = document.querySelectorAll('.mole');

撰写ranTime()帮我们决定地鼠出现的持续时间并给定参数minmax作为出现持续时间范围的最小、最大值。

下面用Math.random()随机产生一个介於0~1的数字,然後把它乘上(max-min)再加上min,最後用Math.round()四舍五入得到随机的出现持续时间。

function randTime(min,max){
    return Math.round(Math.random() * (max-min) + min);
}

randomHole()帮我们随机选择地鼠出现的洞,为避免接连选到两次一样的洞,所以另外宣告变数lastHole,来帮我们记住上一次出现的洞。

在方法里,首先要传入所有洞穴(holes,NodeList),接着一样用Math.random()随机产生0~1的数字并乘上holes的长度後,呼叫Math.floor()无条件舍去小数点,取得一个随机的index放入常数idx中。

然後,宣告的常数hole就可以用这个idx,随机取得holes中的一个洞穴。

为避免选到和上次一样的洞穴,利用条件判断hole是不是跟lastHole相同,如果相同就递回呼叫randomHole(),直至选到不同的洞为止。

在方法的最後,把这次的结果放到lastHole中,之後回传被随机选到的hole

let lastHole;

function randomHole(holes){
    const idx = Math.floor(Math.random() * holes.length);
    const hole = holes[idx]
    if(hole === lastHole){
      console.log('You got the same hole.');
      return randomHole(holes);
    }
    lastHole = hole;
    return hole;
}

宣告变数timeup作为游戏是否已经结束的flag

peep()是让地鼠从洞穴探头出来的关键!!!

peep()的最一开始,宣告常数time并取得由randTime(200,1000)随机产生的地鼠持续出现时间,接着宣告常数hole取得由randomHole(holes)随机挑出的地洞。

随机挑出的hole会被添加.up这个 class,目的是让躲在洞中的地鼠探出头来。

如果超过地鼠持续出现的时间,地鼠就应该要重新回到洞中。所以这边使用setTimeout(),在经过地鼠出现持续时间time後,移除hole上的.up,让地鼠顺利回家。在游戏未结束(timeup = false)且前一只地鼠已经回家的状态下,我们会重新呼叫peep(),让下一只地鼠探头出来。

let timeup = false;

function peep(){
    const time = randTime(200,1000);
    const hole = randomHole(holes);
    hole.classList.add('up');
    setTimeout(()=>{
      hole.classList.remove('up');
      if(!timeup) peep();
    },time);
}

宣告变数score给定初始值0,用来帮我们算分数。

startGame()可以让我们在点击页面上的button後,立即开始打地鼠游戏。

在方法的一开始,先把记分板(scoreBoard)归零,然後把代表结束游戏与否的timeup设成flase,同时也把计算的分数(score)归零。

上面都做完後,呼叫peep()让地鼠开始探头出来给我们打,接着利用setTimeout()订定打地鼠游戏的时间限制,这边设定游戏时间为15秒(15000毫秒),15秒後把timeup设为true并提示使用者游戏结束。

let score = 0;

function startGame(){
    scoreBoard.textContent = 0;
    timeup = false;
    score = 0;
    peep();
    setTimeout(()=>{
      timeup = true;
      alert("时间到,游戏结束!!!");
    } , 15000);
}

最後一部分要来处理的是"当地鼠被点击到,页面上的得分要加1,然後地鼠要缩回洞中"。

我们可以为每一只地鼠(mole)注册click event listenerbonk()作为event handler

bonk()里,首先判断点击是不是"人为"的,如果是使用者点击触发,则e.isTrusted会回传true,而如果是用像是script之类的去触发click event,则会回传false并直接停止往下执行方法。

接着,在每一次点击成功後,把分数(score)加1并移除加到地鼠(mole)上的.up,让地鼠回到洞中,之後更新页面上的得分(scoreBoard)就完成了。

function bonk(e){
    if(!e.isTrusted) return; //cheater
    score++;
    this.classList.remove('up');
    scoreBoard.textContent = score;
}

moles.forEach(mole => mole.addEventListener('click',bonk));
补充资料:

Math.random()
Math.round()
Math.floor()
setTimeout()
Element.classList
Node.textContent
Event.isTrusted

范例网页请点此

完整程序码请点此

30天的漫长挑战终於结束啦~ /images/emoticon/emoticon29.gif (ps. 有空应该会再发一篇参赛心得吧! XD)


<<:  Day 30 - 铁人赛完赛心得

>>:  [Day 17] Sass - Parent Selector

[Day15] 建立订单交易API_8

本节将进行完整的虚拟订单请求发送 def get_order(shop_no, need_pay, ...

【Day10】模组化及引用模组

模组 在一个 .V 档案里面,可以有很多个 module,但是 Top Module 只会有一个,所...

Day.14 「基础打稳了,就能走得更长久~」 —— JavaScript 基础运算子

学习任何东西,都要把基础学的扎实,基础稳了,遇到问题就能迎刃而解。 而学习程序语言的基础就是数学逻...

【Day17】Git 版本控制 - 多人协作 Fork(2)

在上一篇笔记中已经提到 Fork 的功能以及使用办法了,那本篇就来实际发个 Pull request...

Material UI in React [Day 22] Data Display (part 2) 分隔线 & 列表

今天会接续昨天未讲解的部分往下... Divider 其实他就是分隔线而已,跟 hr tag 是差不...