iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 20
4
Modern Web

以經典小遊戲為主題之ReactJS應用練習系列 第 20

Day20 - 貪吃蛇篇:虛擬方向鍵及暫停遊戲

前情提要及動機

Day11 的功能構想中我們有提到,雖然到昨天 Day19 整個貪吃蛇遊戲已經很完整了,但是有一點美中不足的地方,就是透過手機開啟這個遊戲的時候,會發現竟然沒有方向鍵可以按!

過去在貪吃蛇流行的非智慧型手機年代,直接有方向鍵可以操作,但是現在手機已經進化到只剩下一個螢幕了,所以雖然一開始很高興的把遊戲做成可以適應手機螢幕大小,雖然適應是適應了,但是卻只能按下開始遊戲,然後就無法操作了,狀況非常的囧。而且目前時下大部分的人都使用手機來瀏覽網頁,所以如果過年回家之後,在小朋友面前炫耀自己做了貪吃蛇,卻發現沒有方向鍵可以玩,就會當場從天堂掉到地獄,非常的淒慘。

所以為了要避免這種尷尬的狀況,讓我們從地獄返回天堂,我們最後要做的一件事情就是希望在螢幕上手刻出一個虛擬的上下左右鍵,順便在今天也加上可以暫停遊戲的功能,方便我們玩到一半突然想上廁所的時候不會很煩惱。

虛擬方向鍵

首先,我們先新增一個命名為 <VirtualKeyboard /> 的元件
containers/SnakeGame/components/VirtualKeyboard/index.js

import {
    ARROW_UP,
    ARROW_DOWN,
    ARROW_LEFT,
    ARROW_RIGHT,
} from 'containers/SnakeGame/constants';

const VirtualKeyboard = ({ handleOnClick }) => (
    <StyledVirtualKeyboard>
        <div>
            <div data-code={ARROW_UP}>上</div>
        </div>
        <div className="virtual-keyboard__wrapper-bottom">
            <div data-code={ARROW_LEFT}>左</div>
            <div data-code={ARROW_DOWN}>下</div>
            <div data-code={ARROW_RIGHT}>右</div>
        </div>
    </StyledVirtualKeyboard>
);

再來我們在元件裡面新增四個分別為上下左右的 div tag ,因為我們希望 ArrowUp 按鈕是同一個 row 層,另外 ArrowLeft, ArrowDown, ArrowRight 是同一 row 層,這兩層分層又分上下。所以為了達成這樣的排列,我把同一層的按鈕用一個 div tag 包起來,方便我做排版,如上面程式碼。

下圖是上面程式碼的結果
keyboard-draft

上面示意圖當中我們給了文字的上下左右方便大家辨識,但是這邊我想要用箭頭圖案來代替文字,一方面比較美觀,另一方面比較直覺。圖案的部分我想直接使用 Font Awesome 上面的icon。

Font Awesome

做了一些樣式調整之後,如下圖:
keyboard-awesome-font

有了上下左右操作鍵的外觀之後,接下來要做他的功能。
記得我們之前在 Day15 做鍵盤操作上下左右功能的時候,我們拿到的 event.code 分別是 ArrowUp, ArrowLeft, ArrowDown, ArrowRight ,為了可以重複使用我們之前寫過的程式,我分別給每一個按鈕資料屬性(data-* attribute),上箭頭就給他 data-code={’ArrowUp’} ,下箭頭就給他 data-code={‘ArrowDown’} ,依此類推給他相對應的屬性。

再來我在每一個箭頭 onClick 的時候,透過傳入 <VirtualKeyboard /> 的函數來取的相對應的資料屬性

<VirtualKeyboard handleOnClick={this.handleOnVirtualKeyboardClick} />

拿到資料屬性之後,就跟前面 Day15 透過鍵盤操作上下左右功能時一樣的方法,來改變 snake 的 direction 方向屬性

containers/SnakeGame/index.js

handleOnVirtualKeyboardClick = (event) => {
    const {
        handleOnSetSnakeDirection,
    } = this.props;
    const code = event.currentTarget.getAttribute('data-code');
    handleOnSetSnakeDirection(code);
}

snakeGame__virtual-keyboard

暫停按鈕

完成畫面上的方向鍵之後,最後一個步驟,我要來實作暫停按鈕。
為了讓遊戲可以判斷是否現在是暫停的狀態,還是繼續遊戲的狀態,我們要用一個布林值的參數,我命名為 isPause ,當 isPause 為 true 的時候,遊戲暫停,反之,遊戲繼續。
containers/SnakeGame/index.js

<div data-code={SPACE} onClick={this.handleOnVirtualKeyboardClick} <div
	data-code={SPACE}
	onClick={this.handleOnVirtualKeyboardClick}
	className="snake-game__pause-game-btn"
>
    {
        isPause ? '繼續' : '暫停'
    }
</div>

然後因為我希望除了畫面上有暫停按鈕,在鍵盤控制的時候,按下空白鍵也可以切換暫停和繼續,就像是上下左右鍵操作一樣,所以我這邊也使用空白鍵的 event.code 來作為判斷,所以空白鍵的 event.code 是 Space ,我也把畫面上暫停按鈕的資料屬性設為跟 event.code 一樣為 data-code={‘Space’}。

有了可以切換暫停功能的參數之後,我們就可以透過 isPause 來控制遊戲是否暫停,如下程式碼,遊戲暫停的時候,我們會用 clearInterval() 來讓 setInterval() 停止調用內部函數,當切換成繼續遊戲的時候,再重新執行 setInterval()。

containers/SnakeGame/index.js

componentDidUpdate(prevProps, prevState) {
    const {
        isSpeedModified,
    } = prevProps;
    const {
        snake,
        isGameStart,
        isPause,
        handleOnSetSnakeMoving,
        handleOnSetSpeedModified,
    } = this.props;
    if (isPause) {
        clearInterval(gameInterval);
    }
    if (isSpeedModified) { // to udpate speed
        handleOnSetSpeedModified(false);
        clearInterval(gameInterval);
        gameInterval = setInterval(() => {
            if (isGameStart && !isPause) {
                handleOnSetSnakeMoving()
            }
        }, snake.get('speed'));
    }
    if (!isGameStart) {
        clearInterval(gameInterval);
        handleOnSetSpeedModified(true);
    }
}

另一部分,我們希望在暫停的時候,上下左右控制鍵會失去功能,避免在暫停期間去改變了蛇的運動方向,導致繼續遊戲的時候,出現錯誤
containers/SnakeGame/reducer.js

case SET_SNAKE_DIRECTION: {
    if (!state.get('isGameStart')) {
        return state;
    }
    let isPause = state.get('isPause');
    let isSpeedModified = state.get('isSpeedModified');
    if (action.payload === 'Space') {
        isPause = !state.get('isPause');
        isSpeedModified = isPause;
    }
    if (!direction[action.payload] && !(action.payload === 'Space')) {
        return state;
    }

    return state.updateIn(['snake', 'direction'], (dir) => {
        if (action.payload === 'Space' || isPause) {
            return dir;
        }
        if (dir.get('x') * -1 === direction[action.payload].x &&
            dir.get('y') * -1 === direction[action.payload].y) {
            return dir;
        }
        return fromJS(direction[action.payload]);
    })
    .set('isPause', isPause)
    .set('isSpeedModified', isSpeedModified);
}

最終成品展示

到這邊,我們在 Day11 所希望做的所有貪吃蛇的功能就全部完成啦!下面是我們最終成果的演示:

snakeGame__final-demo

參考程式碼

Snake - Github


上一篇
Day19 - 貪吃蛇篇:重新開始按鈕
下一篇
Day21 - 記憶方塊篇:前言及功能構想
系列文
以經典小遊戲為主題之ReactJS應用練習30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Homura
iT邦高手 1 級 ‧ 2018-11-04 10:33:02

虛擬方向鍵的想法真不錯
我怎都沒想到/images/emoticon/emoticon06.gif

0
傑瑞林
iT邦新手 5 級 ‧ 2018-11-04 12:18:02

有這個之後我在手機上玩真的是從地獄到天堂了

我要留言

立即登入留言