本篇承接以兒子為素材的Web記憶遊戲,說明遊戲實作細節,包含以下部分
- 遊戲邏輯
- 翻卡片動畫效果
程式碼的部分,為了說明方便,會與實際程式中出現順序不同,但作用是一樣的,可能宣告在最外層,在某方法中賦值,之後在另一個方法中放至畫面上,說明時擺在一起看較容易理解。原始碼Open Source。
還沒看過上一篇的同學,可以先去看看~ 自製記憶遊戲實作細節(1/2)
遊戲邏輯
雖然記憶遊戲看似簡單,但是把所有考慮到的部分列出來也是不少,下面先列出流程判斷,接著才在程式碼中解釋每個變數的作用,這樣對照參考比較清楚
卡片有3種狀態,背面、正面、已配對(不顯示),卡片點擊事件觸發時,要檢查以下條件
- 卡片當下的狀態,背面=>翻到正面
- 是否已有正面的卡片
- 無=>翻到正面
- 有一張=>翻到正面後比對是否相同
- 相同=>卡片狀態變更為已配對,並隱藏起來,最後判斷是否全部配對完成
- 不同=>則兩張卡片都翻回背面
- 因為卡片有翻轉動畫
- 需等待翻轉動畫結束再判斷,不然使用者看不到圖片翻為正面就消失了
- 翻轉時需鎖定不觸發卡片點擊,避免造成多張卡片被翻開
- 時間倒數計時
- 記答案的時間=>時間到全部卡片翻到背面
- 遊戲的時間到=>挑戰失敗
//遊戲時使用的卡片數量
const CARD_NUMBER = 8;
//所有卡片圖片數量
const CARD_IMG_NUMBER = 20;
//完成配對的排組數
var doneCount = 0;
//時間秒數計算
var timeCounter = 0;
//遊戲開始前初值化
doneCount = 0;
timeCounter = 0;
//暫停卡片點擊判斷
var gamePause = false;
//是否已有一張正面卡片
var hasFlipped = false;
//掀開的第一張牌和第二張牌element的id
var cardOneId = -1;
var cardTwoId = -1;
開始遊戲時,設置定時器,更新時間顯示並判斷時間是否用完
function startPlay(){
currentStage = STAGE_PLAYING;
pauseFlip = false;
timeCounter = 0;
minute = Math.floor((PLAY_TIME - timeCounter)/60);
second = (PLAY_TIME - timeCounter) % 60;
if(second < 10){second = '0' + second;}
counterBoard.innerHTML = minute+':'+second;
interval = setInterval(function(){
if(!gamePause){
timeCounter++;
}
minute = Math.floor((PLAY_TIME - timeCounter)/60);
second = (PLAY_TIME - timeCounter) % 60;
if(second < 10){second = '0' + second;}
counterBoard.innerHTML = minute+':'+second;
//檢查時間是否結束
if(timeCounter >= PLAY_TIME){
clearInterval(interval);
sound_bg.pause();
sound_over.play();
gameOver();
}
}, 1000);
}
點擊事件是用全域監聽的方式,再判斷點擊的目標是否為我們所要的,並根據當前的階段(主選單、遊戲中…等)來判斷,以下只列出遊戲中的判斷
document.addEventListener('click',function(e){
switch(currentStage){
case STAGE_PLAYING:
//點擊到卡片的正面或反面
if(e.target &&
(e.target.classList.contains('front') ||
e.target.classList.contains('back'))){
if(!pauseFlip){
var card = e.target.parentNode.parentNode;
if(card.dataset['status'] == STATUS_BACK){
flip(card);
//若已經翻過一張,比對兩張是否相同
if(hasFlipped){
pauseFlip = true;
cardTwoId = card.id;
console.log('cardTwoId='+cardTwoId);
//比對相同,播放音效,卡片消失
if(document.querySelector('#'+cardOneId).dataset['id'] == document.querySelector('#'+cardTwoId).dataset['id']){
doneCount++;
sound_disappear.play();
card.dataset['status'] = STATUS_DONE;
document.querySelector('#'+cardOneId).dataset['status'] = STATUS_DONE;
pauseFlip = true;
//等待卡片動畫效果
setTimeout(function(){
card.classList.toggle('disappear');
document.querySelector('#'+cardOneId).classList.toggle('disappear');
pauseFlip = false;
//判斷是否所有卡片皆已配對,若是則遊戲結束
if(doneCount == CARD_NUMBER){
clearInterval(interval);
setTimeout(function(){
sound_bg.pause();
sound_complete.play();
gameComlete();
}, 300);
}
}, 300);
}else{
//比對不同張,翻回背面
setTimeout(function(){
flip(card);
flip(document.querySelector('#'+cardOneId));
}, 300);
}
hasFlipped = false;
setTimeout(function(){pauseFlip = false;}, 300);
}else{
cardOneId = card.id;
console.log('cardOneId='+cardOneId);
hasFlipped = true;
}
}
}
}
break;
}
});
翻卡片動畫效果
翻頁的效果是由CSS處理,再用方法動態切換class,首先用javascript動態插入卡片的HTML
for(var i=0;i<CARD_NUMBER*2;i++){
playground.innerHTML +=
'<div class="flip-container" id="card_'+i+'" data-id="'+cardList[i]+'" data-status="'+STATUS_FRONT+'">' +
'<div class="flipper">' +
'<div class="front" style="background-image: url(\'card_'+(cardImgList[cardList[i]])+'.png\');"></div>'+
'<div class="back cover"></div>'+
'</div>'+
'</div>';
}
container包含整個卡片,其中又有前面和後面兩個div,利用transform: rotateY(0deg)進行水平翻轉,藉由backface-visibility: hidden;,可讓翻轉過去的元素變為不可見,所以原理就是,預設前面未翻轉,後面已翻轉,故不可見,將前面跟後面兩個div同時水平翻轉,前面消失,後面則顯現出來,以下為CSS翻轉動畫相關定義,若覺得原始碼中包含其他代碼不好理解,可以參考這篇Create a CSS Flipping Animation,單純講解翻轉的原理,筆者也是參考他修改而成的
/* entire container, keeps perspective */
.flip-container {
display: inline-block;
perspective: 1000px;
margin: 1.4%;
}
/* flip the pane when hovered */
.flip-container.hover .flipper {
transform: rotateY(180deg);
}
.flip-container {
width: 22%;
height: 22%;
}
.flipper, .front, .back{
width: 100%;
height: 100%;
}
/* flip speed goes here */
.flipper {
transition: 0.3s;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
position: relative;
}
/* hide back of pane during swap */
.front, .back {
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
background-repeat: no-repeat;
background-size: cover;
}
/* front pane, placed above back */
.front {
z-index: 2;
/* for firefox 31 */
transform: rotateY(0deg);
background-color: #ffffff;
}
/* back, initially hidden pane */
.back {
transform: rotateY(180deg);
background-color: #dddddd;
background-image: url('cover.png');
}
再配合方法動態新增刪除hover屬性,配合transition: 0.3s;,自動補間產生0.3秒的動畫效果
function flip(e){
e.classList.toggle('hover');
if(e.classList.contains('hover')){
e.dataset['status'] = STATUS_BACK;
}else{
e.dataset['status'] = STATUS_FRONT;
}
}
實作細節到這裡較複雜的部份都做說明了,其他未說明的部分,都只是畫面切換,或者原理相同,就不一一贅述,若是有說明不清楚的地方,也歡迎提出問題,一起討論~
參考來源: