昨天我們做了下列這些功能
今天我們要把流程串起來
首先,記得在 Day27 開始遊戲的時候,我們按下開始遊戲按鈕,會把 isGameStart 參數從 false 變成 true。
接著,開始遊戲的同時,我們要播放一個答對的音效來告訴玩家遊戲開始了,然後就會馬上開始播放題目,播放題目的期間玩家縱使隨便按,也無法答題,等到題目播放完之後,再開放給玩家答題,下面是我們點擊 start 開始遊戲按鈕之後,會調用的函式 handleOnGameStart
src/containers/MemoryBlocks/index.js
handleOnGameStart = () => {
const {
levelData,
blocks,
isCorrect,
handleSetInit,
handleSetIsPlaying,
} = this.props;
handleSetInit(); // 把 isGameStart 參數從 false 變成 true
handleSetIsPlaying(true); // 把 isPlaying 參數設為 true,表示題目播放中,不能亂按
playSoundEffect(SOUND_EFFECT.correct); // 播放答對音效,表示遊戲開始
flashAllBlocks(blocks, isCorrect); // 播放答對音效的同時,所有方塊要亮起來
setTimeout(() => {
const finishedTime = playLevelSound(levelData, blocks); // 音效播完間隔2秒後開始播放題目
setTimeout(() => {
handleSetIsPlaying(false); // 播放完題目之後,把 isPlaying 參數設為 false,玩家可以答題
}, finishedTime);
}, 2000);
}
我們剛剛有說,當題目以及音效播放時,把 isPlaying 參數設為 true ,表示玩家操作無效,等音樂和音效都播放完了,再把isPlaying 參數設為 false。所以,在 Day26 - 按下去有聲音之事件處理 的時候,我們要在按下方格所調用的函數 handleOnBlockClick 做一個判斷
src/containers/MemoryBlocks/index.js
handleOnBlockClick = (event) => {
const {
isPlaying,
blocks,
handleUpdateAnswer,
} = this.props;
if (isPlaying) { // 如果題目或是音效正在播放,則按下去無效,直接 return
return;
}
const blockId = event.target.getAttribute('data-id'); // 取得 block id
const audioObject = blocks.getIn([blockId, 'audio'])(); // 用 block id 取得對應的 Audio Object
handleUpdateAnswer(parseInt(blockId, 10)); // 送出答案並記錄在 answer 陣列參數裡
audioObject.currentTime = 0; // 播放歸零
audioObject.play(); // 播放音訊
}
這樣的話,就可以防止玩家在音效還在播放的時候亂按而出錯了。
接著,我們昨天有做答對以及答錯的判斷,如果有任何一個音答錯,那就會播放答錯的音效,然後全部的方塊會閃紅色。那如果全部都答對,會播放答對的音效,全部的方塊也會亮起來,但是是亮方塊原本的顏色,表示過關。詳細實作方式及流程如下
src/containers/MemoryBlocks/index.js
componentDidUpdate(prevProps, prevState) {
const {
blocks,
levelData,
chance,
isComplete,
isCorrect,
isPlaying,
handleUpdateIsComplete,
handleUpdateIsCorrect,
handleSetGameRestart,
handleSetIsPlaying,
} = this.props;
if (isPlaying) {
return;
}
if (isComplete) { // 如果這一題全部都答對
handleSetIsPlaying(true); // 音效開始播放,isPlaying 設為 true
handleUpdateIsComplete(false); // 進到下一題前,要重設 isComplete 參數
setTimeout(() => {
playSoundEffect(SOUND_EFFECT.correct); // 播放答對音效
flashAllBlocks(blocks, isCorrect); // 全部方塊閃答對的燈
}, 500);
setTimeout(() => {
const finishedTime = playLevelSound(levelData, blocks); // 播放下一題的題目
setTimeout(() => {
handleSetIsPlaying(false); // 播完題目之後,isPlaying 設為 false,玩家可以開始答題
}, finishedTime);
}, 3000);
} else if (!isCorrect) { // 如果有任何一個音答錯
handleSetIsPlaying(true); // 音效開始播放,isPlaying 設為 true
clearAllTimeouts(); // 清除目前所有的 setTimeout
handleUpdateIsCorrect(true); // 重設 isCorrect參數
setTimeout(() => {
playSoundEffect(SOUND_EFFECT.wrong); // 播放答錯音效
flashAllBlocks(blocks, isCorrect); // 全部方塊閃答錯的燈
}, 500);
setTimeout(() => {
const finishedTime = playLevelSound(levelData, blocks); // 重新播放這一題的題目
setTimeout(() => {
handleSetIsPlaying(false); // 播完題目之後,isPlaying 設為 false,玩家可以開始答題
}, finishedTime);
}, 3000);
}
}
補充說明一下,我們播放音樂的時候,是透過 setTimeout() 這個非同步
的方法來幫我們實現,但是有時候我們需要清除目前所有正在進行的音效播放,例如答錯要重新答題的時候,所以這邊我客製了一個 clearAllTimeouts() 的方法來幫助我們清除目前所有的
setTimeout()。
這邊說明一下原理,每當我們執行一次 setTimeout() 的時候,都會回傳一個整數形態的 id,由於 id 會隨著 setTimeout() 被調用的增加而增加,所以我們就呼叫一次沒有做事情的 setTimeout() 取得目前最大的 id,然後把小於這個 id 的所有動作都清除
src/containers/MemoryBlocks/utils.js
export const clearAllTimeouts = () => {
// clear "all" timeouts
const biggestTimeoutId = window.setTimeout(function () { }, 1);
for (let i = 1; i <= biggestTimeoutId; i++) {
clearTimeout(i);
}
};
到這邊,我們就已經完成了答對以及答錯流程了。
接下來,我們要來時做我們的進度條,當題目越來越長的時候,我們有時候會忘記到底我們已經答對了幾個音,或是還要答對幾個音才能到下一題。所以我這邊希望能夠給玩家一個提示。
我們先新增一個 <Progress />
元件,元件裡面有兩個輸入當作 props,一個是題目,一個是回答。在畫面部分,這一題有幾個音,那 <Progress />
裡面就需要出現幾個小黑豆,那我們答對幾題,就讓其中幾個小黑豆亮起來,其實就只有這樣而已。
為了達到上面這個目標,我的做法是,因為 answer 這個陣列裡面只會存回答正確的答案,然後每一個答案的 index 會對應到 levelData 這個陣列的 index,所以這兩個陣列的關係是 answer 陣列永遠都是 levelData 的子集合,而且有順序性,也可以說 answer 陣列永遠都被包含於 levelData。所以說,answer 陣列裡面就是需要亮起來的那幾個小黑豆,也就是會變成小亮豆。因此我的做法是,跑一次 levelData 的迴圈,其中每一個元素的 index 若小於 answer 的大小,就表示是已經回答的正確答案,就讓它亮起來,其他的則保持小黑豆。
程式碼如下
src/containers/MemoryBlocks/components/Progress/index.js
const Progress = ({ levelData, answer }) => {
const progressNode = Array.from(Array(levelData.size), (value, index) => ({
id: index,
className: (index < answer.size) ? 'progress__node progress__node-active' : 'progress__node',
}));
return (
<StyledProgress>
{
progressNode.map((node) => (
<div
key={node.id}
className={node.className}
/>
))
}
</StyledProgress>
);
};
接下來我們要實作重新開始的按鈕及重播音樂按鈕,跟開始遊戲按鈕一樣,首先我們需要一個按鈕。這個按鈕在 Day23 頁面佈局的時候,我們已經給為這兩個按鈕留了在右下角的位置。
src/containers/MemoryBlocks/index.js
{
isGameStart &&
<div className="memory-blocks__group-btn-wrapper">
<button
className="memory-blocks__hint-btn memory-blocks__font-music"
onClick={this.handleOnReplaySound}
>
重播
</button>
<button
className="memory-blocks__restart-btn"
onClick={this.handleOnGameRestart}
>
Restart
</button>
</div>
}
然後因為我不希望在遊戲開始之前就讓玩家去按這兩個按鈕,所以我希望在按下開始遊戲之後, isGameStart 這個參數被設為 true 了,這兩個按鈕才會出現。
這兩個按鈕我用一個 div tag 當作外容器來包住,目的是為了幫助裡面這兩個按鈕定位,所以我把外容器宣告成 display: flex; 並且讓他靠右對齊,然後橫向排列。
src/containers/MemoryBlocks/Styled.js
.memory-blocks__group-btn-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.memory-blocks__hint-btn {
margin-right: 10px;
${buttonMixin()}
}
.memory-blocks__restart-btn {
${buttonMixin()}
}
}
然後可以注意到,這兩個按鈕我是套用跟開始按鈕一樣的 buttonMixin() 樣式,所以樣式會跟開始按鈕一致,都是圓圓像膠囊一樣的按鈕。
接下來,我們先來實作重新開始遊戲按鈕的功能,一樣,我讓這個按鈕 onClick 的時候調用一個名為 handleOnGameRestart() 的函數,這個函數會執行我們重新開始遊戲所需要的所有動作。
src/containers/MemoryBlocks/index.js
handleOnGameRestart = () => {
const {
handleSetGameRestart,
} = this.props;
clearAllTimeouts(); // 暫停並清除目前所有的 setTimeout
handleSetGameRestart(); // 重設所有參數並重新開始遊戲
}
透過 handleSetGameRestart() 發一個 action 到 reducer 之後,我們 reducer 所要做的事情如下
src/containers/MemoryBlocks/reducer.js
case SET_RESTART_GAME: {
return initialState
.set('isGameStart', false)
.set('levelData', fromJS(generateLevelData(DEFAULT_LEVEL, DEFAULT_SIDE_LENGTH)));
}
這邊做兩件事,一個當然是把 isGameStart 這個參數設為 false,另一件事情,就是我們要產生新的題目。
再來,我們要實作重播題目音樂的功能,我們希望重播按鈕按下去之後,可以重播那一題的題目。跟重新開始遊戲一樣,我們按下重播按鈕的時候,調用了一個函式,這個函數我命名為 handleOnReplaySound() ,在這個函式裡,我們所要做的事情如下
src/containers/MemoryBlocks/index.js
handleOnReplaySound = () => {
const {
levelData,
blocks,
chance,
handleSetReplaySound,
handleSetIsPlaying,
} = this.props;
if (!chance) {
return;
}
clearAllTimeouts(); // 重播遊戲之前,先清空目前所有可能在播放的音樂以及任何setTimeout的動作
handleSetIsPlaying(true); // 音效開始播放,isPlaying 設為 true
setTimeout(() => {
const finishedTime = playLevelSound(levelData, blocks); // 開始重播題目音樂
setTimeout(() => {
handleSetIsPlaying(false); // 重播完之後,把 isPlaying 設為 true,使用者可以開始動作
}, finishedTime);
}, 500);
}
這樣我們就可以順利重播題目啦!