用 JavaScript 寫一個卡片小遊戲
小遊戲使用了HTML5,CSS3和JavaScript的基本的技術。
將討論資料屬性、定位、透視、轉換、flexbox、事件處理、超時和三元組。
你不需要在程式設計方面有太多的知識和經驗就能看懂,不過還是需要知道HTML,CSS和JS都是什麼。
專案結構
先在終端中建立專案檔案:
mkdir memory-game
cd memory-game
touch index.html styles.css
scripts.js mkdir img
HTML
初始化頁面模版並連結 css 檔案 js 檔案.
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Memory Game</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>
這個遊戲有 12 張卡片。 每張卡片中都包含一個名為
.memory-card
的容器
div
,它包含兩個img元素。 一個代表卡片的正面
front-face
,另一個個代表背面
back-face
。
<div> <img src="img/react.svg" alt="React"> <img src="img/js-badge.svg" alt="Memory Card"> </div>
這組卡片將被包裝在一個 section 容器元素中。 最終程式碼如下:
<!-- index.html --> <section> <div> <img src="img/react.svg" alt="React"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/react.svg" alt="React"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/angular.svg" alt="Angular"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/angular.svg" alt="Angular"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/ember.svg" alt="Ember"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/ember.svg" alt="Ember"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/vue.svg" alt="Vue"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/vue.svg" alt="Vue"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/backbone.svg" alt="Backbone"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/backbone.svg" alt="Backbone"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/aurelia.svg" alt="Aurelia"> <img src="img/js-badge.svg" alt="Memory Card"> </div> <div> <img src="img/aurelia.svg" alt="Aurelia"> <img src="img/js-badge.svg" alt="Memory Card"> </div> </section>
CSS
我們將使用一個簡單但非常有用的配置,把它應用於所有專案:
/* styles.css */ * { padding: 0; margin: 0; box-sizing: border-box; }
box-sizing: border-box 屬效能使元素充滿整個邊框,所以我們就可以不用做一些數學計算了。
把 display:flex 設定給 body ,並且把 margin:auto應用到到 .memory-game 容器,這樣可以使它將垂直水平居中。
.memory-game 是一個彈性容器,在預設情況下,裡面的元素會縮小寬度來適應這個容器。透過把 flex-wrap 的值設定為 wrap,會根據彈性元素的大小進行自適應。
/* styles.css */ body { height: 100vh; display: flex; background: #060AB2; } .memory-game { width: 640px; height: 640px; margin: auto; display: flex; flex-wrap: wrap; }
每個卡片的 width 和 height 都是用 CSS 的 calc()函式進行計算的。 下面我們需要製作一個三行四列的介面,並且把 width 設定為 25%, height 設定為 33.333% ,還要再減去 10px 留足邊距.
為了定位 .memory-card 子元素,還要新增屬性 position: relative ,這樣我們就可以相對它進行子元素的絕對定位。
把 front-face and back-face 的position屬性都設定為 absolute ,這樣就可以從原始位置移除元素,並使它們堆疊在一起。
這時頁面模版看上去應該是這樣:
我們還需要新增一個點選效果。 每次元素被點選時都會觸發 :active 偽類,它引發一個 0.2秒的過渡:
翻轉卡片
要在單擊時翻轉卡片,需要把一個 flip 類新增到元素。 為此,讓我們用 document.querySelectorAll 選擇所有 memory-card 元素,然後使用 forEach 遍歷它們並附加一個事件監聽器。 每當卡片被點選時,都會觸發 flipCard 函式,其中 this 代表被單擊的卡片。 該函式訪問元素的 classList 並切換到 flip 類:
// scripts.js const cards = document.querySelectorAll('.memory-card'); function flipCard() { this.classList.toggle('flip'); } cards.forEach(card => card.addEventListener('click', flipCard));
CSS 中的 flip 類會把卡片旋轉 180deg:
.memory-card.flip { transform: rotateY(180deg); }
為了產生3D翻轉效果,還需要將 perspective 屬性新增到 .memory-game。 這個屬性用來設定物件與使用者在 z 軸上的距離。 值越小,透視效果越強。 為了能達得最佳的效果,把它設定為 1000px:
.memory-game { width: 640px; height: 640px; margin: auto; display: flex; flex-wrap: wrap; + perspective: 1000px; }
接下來對 .memory-card 元素新增 transform-style:preserve-3d屬性,這樣就把卡片置於在父節點中建立的3D空間中,而不是將其平鋪在 z = 0 的平面上(transform-style)。
.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; box-shadow: 1px 1px 1px rgba(0,0,0,.3); transform: scale(1); + transform-style: preserve-3d; }
再把 transition 屬性的值設定為 transform 就可以生成動態效果了
.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; box-shadow: 1px 1px 1px rgba(0,0,0,.3); transform: scale(1); transform-style: preserve-3d; + transition: transform .5s; }
現在我們得到了帶有 3D 翻轉效果的卡片, 不過為什麼卡片的另一面沒有出現? 由於絕對定位的原因,現在 .front-face 和 .back-face 都堆疊在了一起。 每個元素的 back face 都是它 front face 的映象。 屬性 backface-visibility 預設為 visible,因此當我們翻轉卡片時,得到的是背面的 JS 徽章。
為了顯示它背面的影像,讓我們在 .front-face 和 .back-face 中新增 backface-visibility:hidden
.front-face, .back-face { width: 100%; height: 100%; padding: 20px; position: absolute; border-radius: 5px; background: #1C7CCC; + backface-visibility: hidden; }
如果我們重新整理頁面並翻轉一張卡片,它就消失了!
由於我們將兩個影像都藏在了背面,所以另一面沒有任何東西。 所以接下來需要再把 .front-face 翻轉180度:
.front-face { transform: rotateY(180deg); }
效果出來了!
匹配卡片
完成翻轉卡片的功能之後,接下來處理匹配的邏輯。
當點選第一張卡片時,需要等待另一張被翻轉。 變數 hasFlippedCard 和 flippedCard 用來管理翻轉狀態。 如果沒有卡片翻轉,hasFlippedCard 的值為 true,flippedCard 被設定為點選的卡片。 讓我們切換到 toggle 方法:
const cards = document.querySelectorAll('.memory-card'); + let hasFlippedCard = false; + let firstCard, secondCard; function flipCard() { - this.classList.toggle('flip'); + this.classList.add('flip'); + if (!hasFlippedCard) { + hasFlippedCard = true; + firstCard = this; + } } cards.forEach(card => card.addEventListener('click', flipCard));
現在,當使用者點選第二張牌時,程式碼會進入 else 塊,我們將檢查它們是否匹配。為了做到這一點,需要能夠識別每一張卡片。
每當我們想要向HTML元素新增額外資訊時,就可以使用資料屬性。 透過使用以下語法: data- ,這裡的 可以是任何單詞,它將被插入到元素的 dataset 屬性中。 所以接下來為每張卡片新增一個 data-framework :
<section> + <div data-framework="react"> <img src="img/react.svg" alt="React"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="react"> <img src="img/react.svg" alt="React"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="angular"> <img src="img/angular.svg" alt="Angular"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="angular"> <img src="img/angular.svg" alt="Angular"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="ember"> <img src="img/ember.svg" alt="Ember"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="ember"> <img src="img/ember.svg" alt="Ember"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="vue"> <img src="img/vue.svg" alt="Vue"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="vue"> <img src="img/vue.svg" alt="Vue"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="backbone"> <img src="img/backbone.svg" alt="Backbone"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="backbone"> <img src="img/backbone.svg" alt="Backbone"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="aurelia"> <img src="img/aurelia.svg" alt="Aurelia"> <img src="img/js-badge.svg" alt="Memory Card"> </div> + <div data-framework="aurelia"> <img src="img/aurelia.svg" alt="Aurelia"> <img src="img/js-badge.svg" alt="Memory Card"> </div> </section>
這下就可以透過訪問兩個卡片的資料集來檢查匹配了。 下面將匹配邏輯提取到它自己的方法 checkForMatch(),並將 hasFlippedCard 設定為 false。 如果匹配的話,則呼叫 disableCards() 並分離兩個卡上的事件偵聽器,以防止再次翻轉。 否則 unflipCards() 會將兩張卡都恢復成超過 1500 毫秒的超時,從而刪除 .flip 類:
把程式碼組合起來:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let firstCard, secondCard; function flipCard() { this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; + return; + } + + secondCard = this; + hasFlippedCard = false; + + checkForMatch(); + } + + function checkForMatch() { + if (firstCard.dataset.framework === secondCard.dataset.framework) { + disableCards(); + return; + } + + unflipCards(); + } + + function disableCards() { + firstCard.removeEventListener('click', flipCard); + secondCard.removeEventListener('click', flipCard); + } + + function unflipCards() { + setTimeout(() => { + firstCard.classList.remove('flip'); + secondCard.classList.remove('flip'); + }, 1500); + } cards.forEach(card => card.addEventListener('click', flipCard));
更優雅的進行條件匹配的方法是用三元運算子,它由三部分組成: 第一部分是要判斷的條件, 如果條件符合就執行第二部分的程式碼,否則執行第三部分:
- if (firstCard.dataset.name === secondCard.dataset.name) { - disableCards(); - return; - } - - unflipCards(); + let isMatch = firstCard.dataset.name === secondCard.dataset.name; + isMatch ? disableCards() : unflipCards();
鎖定
現在已經完成了匹配邏輯,接著為了避免同時轉動兩組卡片,還需要鎖定它們,否則翻轉將會被失敗。
先宣告一個 lockBoard 變數。 當玩家點選第二張牌時,lockBoard將設定為true,條件 if (lockBoard) return; 在卡被隱藏或匹配之前會阻止其他卡片翻轉:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; + let lockBoard = false; let firstCard, secondCard; function flipCard() { + if (lockBoard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); } function unflipCards() { + lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); + lockBoard = false; }, 1500); } cards.forEach(card => card.addEventListener('click', flipCard));
點選同一個卡片
仍然是玩家可以在同一張卡上點選兩次的情況。 如果匹配條件判斷為 true,從該卡上刪除事件偵聽器。
為了防止這種情況,需要檢查當前點選的卡片是否等於firstCard,如果是肯定的則返回。
if (this === firstCard) return;
變數 firstCard 和 secondCard 需要在每一輪之後被重置,所以讓我們將它提取到一個新方法 resetBoard()中, 再其中寫上 hasFlippedCard = false; 和 lockBoard = false 。 es6 的解構賦值功能 [var1, var2] = ['value1', 'value2'] 允許我們把程式碼寫得超短:
function resetBoard() { [hasFlippedCard, lockBoard] = [false, false]; [firstCard, secondCard] = [null, null]; }
接著呼叫新方法 disableCards() 和 unflipCards():
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return; + if (this === firstCard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; - hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); + resetBoard(); } function unflipCards() { lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); - lockBoard = false; + resetBoard(); }, 1500); } + function resetBoard() { + [hasFlippedCard, lockBoard] = [false, false]; + [firstCard, secondCard] = [null, null]; + } cards.forEach(card => card.addEventListener('click', flipCard));
點選同一個卡片
仍然是玩家可以在同一張卡上點選兩次的情況。 如果匹配條件判斷為 true,從該卡上刪除事件偵聽器。
為了防止這種情況,需要檢查當前點選的卡片是否等於firstCard,如果是肯定的則返回。
if (this === firstCard) return;
變數 firstCard 和 secondCard 需要在每一輪之後被重置,所以讓我們將它提取到一個新方法 resetBoard()中, 再其中寫上 hasFlippedCard = false; 和 lockBoard = false 。 es6 的解構賦值功能 [var1, var2] = ['value1', 'value2'] 允許我們把程式碼寫得超短:
function resetBoard() { [hasFlippedCard, lockBoard] = [false, false]; [firstCard, secondCard] = [null, null]; }
接著呼叫新方法 disableCards() 和 unflipCards():
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return; + if (this === firstCard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; - hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); + resetBoard(); } function unflipCards() { lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); - lockBoard = false; + resetBoard(); }, 1500); } + function resetBoard() { + [hasFlippedCard, lockBoard] = [false, false]; + [firstCard, secondCard] = [null, null]; + } cards.forEach(card => card.addEventListener('click', flipCard));
洗牌
我們的遊戲看起來相當不錯,但是如果不能洗牌就沒有樂趣,所以現在處理這個功能。
當 display: flex 在容器上被宣告時,flex-items 會按照組和源的順序進行排序。 每個組由order屬性定義,該屬性包含正整數或負整數。 預設情況下,每個 flex-item 都將其 order 屬性設定為 0,這意味著它們都屬於同一個組,並將按源的順序排列。 如果有多個組,則首先按組升序順序排列。
遊戲中有12張牌,因此我們將迭代它們,生成 0 到 12 之間的隨機數並將其分配給 flex-item order 屬性:
function shuffle() { cards.forEach(card => { let ramdomPos = Math.floor(Math.random() * 12); card.style.order = ramdomPos; }); }
為了呼叫 shuffle 函式,讓它成為一個立即呼叫函式表示式(IIFE),這意味著它將在宣告後立即執行。 指令碼應如下所示:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return; if (this === firstCard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; lockBoard = true; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); resetBoard(); } function unflipCards() { setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); resetBoard(); }, 1500); } function resetBoard() { [hasFlippedCard, lockBoard] = [false, false]; [firstCard, secondCard] = [null, null]; } + (function shuffle() { + cards.forEach(card => { + let ramdomPos = Math.floor(Math.random() * 12); + card.style.order = ramdomPos; + }); + })(); cards.forEach(card => card.addEventListener('click', flipCard));
完成了!
自己是從事五年的前端工程師了,不少人私下問我,2019年前端該怎麼學啊,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想學習前端,卻又不知道怎麼開始學習的童鞋。
這裡推薦一下我的前端學習交流群:731771211,裡面都是學習前端的從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴。2019最新技術,與企業需求同步。好友都在裡面學習交流,每天都會有大牛定時講解前端技術!
點選:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901074/viewspace-2621331/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 用jQuery手寫一個小遊戲jQuery遊戲
- 用GDX寫個安卓小遊戲安卓遊戲
- 用 JavaScript 寫一個區塊鏈JavaScript區塊鏈
- 【譯】用JavaScript寫一個區塊鏈JavaScript區塊鏈
- 用 JavaScript 和 C3 實現一個轉盤小遊戲JavaScript遊戲
- 期末前端web大作業——用前端語言寫一個小遊戲前端Web遊戲
- 寫一個狼吃羊的小遊戲遊戲
- 用 JavaScript 寫一個超小型編譯器JavaScript編譯
- 從0開始用python寫一個命令列小遊戲(十)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(二)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(六)Python命令列遊戲
- ES6 手寫一個“辨色”小遊戲遊戲
- [ 邏輯鍛鍊] 用 JavaScript 做一個小遊戲 ——2048 (初級版)JavaScript遊戲
- [ 邏輯鍛鍊] 用 JavaScript 做一個小遊戲 ——2048 (詳解版)JavaScript遊戲
- 用Excel編寫小遊戲 (轉)Excel遊戲
- 一個小遊戲遊戲
- 無聊的週末用Java寫個掃雷小遊戲Java遊戲
- 不用正規表示式,用javascript從零寫一個模板引擎(一)JavaScript
- 用webpack寫個現代的JavaScript包WebJavaScript
- 我只是想用JavaScript寫一個方法...JavaScript
- 寫一個更好的Javascript DOM庫JavaScript
- 閒得無聊寫的一個貪吃蛇小遊戲~遊戲
- [譯] 用javascript實現一門程式語言-寫一個解析器JavaScript
- 用Python寫個魂鬥羅,另附30個Python小遊戲原始碼Python遊戲原始碼
- JavaScript-開發一個簡單的貪吃蛇小遊戲JavaScript遊戲
- Three.js系列: 寫一個第一/三人稱視角小遊戲JS遊戲
- Python寫個“點球大戰”小遊戲Python遊戲
- 用eclipes寫第一個HelloWorld
- 用 Node.js 寫微信小遊戲輔助Node.js遊戲
- 用C++語言寫遊戲——打怪小遊戲C++遊戲
- 編寫一個簡單的JavaScript模板引擎JavaScript
- 教你用200行程式碼寫一個偶像拼拼樂H5小遊戲(附原始碼)行程H5遊戲原始碼
- 用純 JavaScript 擼一個 MVC 程式JavaScriptMVC
- 用C語言編寫小遊戲——“井字棋”C語言遊戲
- [譯] 教程 — 用 C 寫一個 Shell
- [譯] 用 Rust 寫一個微服務Rust微服務
- 用 PHP 寫一個"程式語言"PHP
- 如何寫一個實用的 bind?