昨天 Day18 我們已經準備好 isGameStart
參數來幫助我們做今天的重新開始按鈕。
首先,我們需要先來製作一個按鈕,這個按鈕我想要直接蓋在遊戲地圖的正中間,讓他很顯眼,因為我希望遊戲一開始的時候,或是需要重新開始遊戲的時候,他可以很清楚的引導玩家去按這個按鈕。
先給大家看一下我想要的樣子
這邊是我實作的部分程式碼
containers/SnakeGame/index.js
<!-- 開始按鈕 -->
{
<div className="snake-game__panel">
<div className="snake-game__score">
<span>Score: </span>
</div>
<button
className="snake-game__start-game-btn"
>
Start
</button>
</div>
}
<!-- 遊戲地圖主畫面 -->
<div className="snake-game__map-wrapper">
{
blocks.map((rows) => (
rows.map((block) => (
<div
key={block.get('id')}
className={updateGameView(snake, block, food)}
>
</div>
))
))
}
</div>
首先我有一個 div tag 來當作我的 wrapper,如上程式碼,這個 div tag 跟遊戲地圖是放在 DOM 的同一層。這個 wrapper 我給他一個名為 snake-game__panel 的 class,然後這個 snake-game__panel 是透明的,我希望他完整的且剛好地蓋在遊戲地圖 snake-game__map-wrapper 上面。所以他的長寬我希望是跟遊戲地圖是一樣的,然後為了讓他蓋在上面
,我給他 position: absolute; 的 css 樣式。
這邊需要對 CSS 的 position 屬性有點瞭解。absolute 元素的定位是在他所處上層容器的相對位置。所以為了讓 snake-game__panel 可以順利蓋在 snake-game__map-wrapper 上面,我把他們的 DOM 放在同一層,也就是說他們會有共同的 parent element tag ,然後我希望 snake-game__panel 可以對這個 parent element tag 做相對位置的定位,所以需要把 parent element tag 設定為 position: relative; 變成一個可定位 tag 。
我之所以會想要設計一個 snake-game__panel 的 wrapper 來把 button 包住,是因為我希望開始按鈕也可以對這個 snake-game__panel 做定位,讓我很容易地用 FlexBox 來把按鈕放到中間。
所以,跟前面一樣的道理,放置好 snake-game__panel 之後,我把 snake-game__panel 宣告為 dispaly: flex; 成為 FlexBox 的外容器,為了讓內元件,也就是開始按鈕左右置中,上下也置中,所以我在外容器設定內部元件的排列方式,用 justify-content: center; 設定左右置中,用 align-items: center; 設定上下置中。然後再來給這個按鈕加上一些樣式,相關的 CSS 如下:
containers/SnakeGame/Styled.js
.snake-game__panel {
width: ${GAME_WRAPPER_SIZE}px;
height: ${GAME_WRAPPER_SIZE}px;
@media only screen and (max-width: 600px) {
width: calc(100vw - 20px);
height: calc(100vw - 20px);
}
position: absolute;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.snake-game__start-game-btn {
width: 100px;
height: 40px;
background: black;
border: 2px solid white;
color: white;
border-radius: 20px;
font-size: 1.2em;
cursor: pointer;
outline: none;
&:hover {
color: black;
background: white;
transition: all 0.3s;
}
}
做到這邊,應該就可以看到上圖的結果,完整的程式碼可以參考我的 github。
有了開始遊戲按鈕之後,我們要來實作他的功能,在這個按鈕 onClick 的時候,我們需要觸發一個函數,這邊我命名為 handleOnGameStartClick
containers/SnakeGame/index.js
handleOnGameStartClick = () => {
const {
handleOnSetSnakeMoving,
handleOnSetGameStart,
} = this.props;
handleOnSetGameStart();
handleOnSetSnakeMoving();
}
這個函數要做的事情很單純,就是發送一個 action 到 reducer 去,然後把我們昨天 Day18 有先準備好的 isGameStart 布林值參數,設定為 true
containers/SnakeGame/reducer.js
case SET_SNAKE_GAME_START: {
return initialState
.set('isGameStart', true);
}
另外,由於我們已經可以從開始按鈕來觸發遊戲開始,所以之前我們在 componentDidMount 讓遊戲初始進行的 setInterval() 也可以在這個時候移除(這邊註解掉的地方就是我要移除的部分)
containers/SnakeGame/index.js
componentDidMount() {
// const {
// snake,
// handleOnSetSnakeMoving,
// } = this.props;
document.addEventListener('keydown', this.handleOnKeyDown);
// gameInterval = setInterval(() => {
// handleOnSetSnakeMoving()
// }, snake.get('speed'));
}
這樣我們開始按鈕的功能基本上就搞定啦!
接下來我們就是要用這個 isGameStart 的參數來控制我們遊戲的流程。
首先,我希望我們按完開始遊戲按鈕之後,畫面上的開始遊戲按鈕就可以消失了,然後當需要重新開始遊戲的時候,再自動的出現。不然遊戲如果開始之後,按鈕還一直在那邊,真的很礙事,所以透過 isGameStart 這個參數,我們可以這樣做
containers/SnakeGame/index.js
{
!isGameStart &&
<div className="snake-game__panel">
<div className="snake-game__score">
<span>Score: </span>
<span>{score}</span>
</div>
<button
className="snake-game__start-game-btn"
onClick={this.handleOnGameStartClick}
>
Start
</button>
</div>
}
上面的意思是就是,遊戲開始之後,按鈕消失,遊戲結束的時候,按鈕會出現,就是這麼直白。
接下來我要用 isGameStart 這個參數來控制蛇是不是會運動。當遊戲還沒開始的時候,或是遊戲結束的時候,蛇不應該再動來動去。除此之外,也不應該再繼續調用 setInterval() 裡面的方法,否則會變成蛇已經不動了,但是其實背後還一直狂調用 setInterval() 裡面的方法,在我們這邊的例子,就是狂發沒有作用的 action 到 reducer 去,感覺還蠻消耗電腦運算資源的。
所以在我們之前執行 setInterval() 的生命週期函數 componentDidUpdate 裡面,我們要把 isGameStart 放進去,讓遊戲停止的時候,也可以透過 clearInterval() 清除 setInterval() ,部分程式碼參考如下:
containers/SnakeGame/index.js
componentDidUpdate(prevProps, prevState) {
const {
isSpeedModified,
} = prevProps;
const {
snake,
isGameStart,
handleOnSetSnakeMoving,
handleOnSetSpeedModified,
} = this.props;
if (isSpeedModified) { // to udpate speed
handleOnSetSpeedModified(false);
clearInterval(gameInterval);
gameInterval = setInterval(() => {
if (isGameStart) {
handleOnSetSnakeMoving()
}
}, snake.get('speed'));
}
if (!isGameStart) {
clearInterval(gameInterval);
handleOnSetSpeedModified(true);
}
}
最後,我們要來把分數顯示在畫面上。我們設計一個參數叫做 score ,用來記錄分數,當蛇吃到一個食物的時候,score 就加一。當按下開始按鈕的時候,分數需要歸零重新計算。
containers/SnakeGame/reducer.js
// update score
.updateIn(['score'], (score) => {
if (isEatFood) {
return score + 1;
}
return score;
})
最後我們的來展示一下今天的成果: