iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 19
1

昨天 Day18 我們已經準備好 isGameStart 參數來幫助我們做今天的重新開始按鈕
首先,我們需要先來製作一個按鈕,這個按鈕我想要直接蓋在遊戲地圖的正中間,讓他很顯眼,因為我希望遊戲一開始的時候,或是需要重新開始遊戲的時候,他可以很清楚的引導玩家去按這個按鈕。

先給大家看一下我想要的樣子
start-button

按鈕位置定位

這邊是我實作的部分程式碼
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 。

學習 CSS 版面配置 - 關於 position 屬性

我之所以會想要設計一個 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;
})

今日成果展示

最後我們的來展示一下今天的成果:
snakeGame__score


參考程式碼

Snake - Github


上一篇
Day18 - 貪吃蛇篇:吃到自己會死
下一篇
Day20 - 貪吃蛇篇:虛擬方向鍵及暫停遊戲
系列文
以經典小遊戲為主題之ReactJS應用練習30

尚未有邦友留言

立即登入留言