iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
1

有了地圖之後,我們就可以開始讓蛇在上面爬了,今天我們的目標是畫出蛇的頭,並且可以操控他在地圖上跑來跑去。

新增蛇的物件

首先我們先來新增一個蛇的物件

const snake = {
   headPosition: {
       x: 0,
       y: 0,
   },
   body: [],
   maxLength: 2,
   direction: {
       x: 1,
       y: 0,
   },
   speed: SNAKE_INITIAL_SPEED,
};

參數的詳細說明可以參考 Day12 - 貪吃蛇篇:蛇的原理及資料結構規劃

在地圖上畫出蛇的頭

snake 物件當中的 headPosition 是用來記錄蛇頭位置的物件,根據 headPosition 的 xy 位置,我們可以把蛇的頭畫在相對應的座標方格上。

containers/SnakeGame/index.js 裡面,我們昨天使用迭代的方式把 30x30 的地圖方格一個一個畫出來,所以如果方格的座標位置跟蛇的頭的位置是一樣的,我就把那一格的顏色樣式給定白色,其餘的方格給他黑色

<div className="snake-game__map-wrapper">
    {
        blocks.map((rows) => (
            rows.map((block) => (
                <div
                    key={block.get('id')}
                    className={updateGameView(snake, block)}
                >
                </div>
            ))
        ))
    }
</div>

所以這邊方格的 className ,我用一個函數來給予值,這個函數 udpateGameView 有兩個輸入,一個是當下的方格物件,物件裡面當然也包含方格的 xy 位置,另一個參數是 snake ,裡面有頭的位置 headPosition ,所以在這個函數裡面我來一一比對,就可以在地圖上每個格子給予相對應的樣式
containers/SnakeGame/index.js

const updateGameView = (snake, block) => {
   //draw snake head
   if (snake.getIn(['headPosition', 'x']) === block.get('x') &&
       snake.getIn(['headPosition', 'y']) === block.get('y')) {
       return 'snake-game__map-block-item snake-game__draw-snake-body';
   }
   return 'snake-game__map-block-item';
};

containers/SnakeGame/Styled.js

.snake-game__map-block-item {
    border: 1px solid black;
    box-sizing: border-box;
}
.snake-game__draw-snake-body {
    background: white;
    transition: all 0.1s;
}

到這邊我們就可以順利的把蛇的頭畫在格子上,因為我們預設頭的位置在 { x: 0, y: 0 } 的位置,所以到目前為止的成果會如下圖所示
draw-head

讓頭動起來

由於紀錄蛇的頭部位置的參數是 headPosition ,為了讓頭部動起來,我們只需要不斷的去改變 headPosition 的 xy 位置就可以了。

我的方法是透過 setInterval() 這個方法,setInterval() 可以按照指定的週期(毫秒)來調用函數,這邊的週期就是蛇的移動速度。然後記得要在不需要的時候透過 clearInterval() 取消由 setInterval() 设置的 timeout。
MDN web doc - setInterval()
Window clearInterval() Method

containers/SnakeGame/index.js

componentDidMount() {
    const {
        snake,
        handleOnSetSnakeMoving,
    } = this.props;
    gameInterval = setInterval(() => {
        handleOnSetSnakeMoving()
    }, snake.get('speed'));
}

componentWillUnmount() {
    clearInterval(gameInterval);
}
const mapDispatchToProps = dispatch => ({
   handleOnSetSnakeMoving: () => dispatch(setSnakeMoving()),
});

由於我們是使用 react + redux 的架構,所以這邊我會在每個週期發一個 action 到 reducer 去更新 headPosition 的參數,headPosition 的更新方式,是每次的移動,會加上一個方向的向量,這邊用 direction 來表示,direction 預設值是 { x: 1, y: 0 } ,也就是往 x 軸正向移動。

containers/SnakeGame/reducer.js

function snakeGameReducer(state = initialState, action) {
   switch (action.type) {
       case SET_SNAKE_MOVING: {
           const direction = state.getIn(['snake', 'direction']);
           return state
               .updateIn(['snake', 'headPosition'], (headPosition) =>
                   headPosition
                       .set('x', updatePosition(headPosition.get('x') + direction.get('x')))
                       .set('y', updatePosition(headPosition.get('y') + direction.get('y')))
               );
       }

       default:
           return state;
   }
}

然後根據我們 Day11 所訂下的規則,我們希望這個遊戲沒有牆壁的限制,也就是可以讓蛇左進右出,右進左出,依此類推,所以當蛇的頭一直往右移動,x 的值大於格子數的時候,就會歸零,製造出從左邊出來的效果,所以我們在更新位置的時候,加入下面這個判斷

const updatePosition = (position) => {
   if (position > GAME_WIDTH - 1) {
       return 0;
   } else if (position < 0) {
       return GAME_WIDTH;
   }
   return position;
};

完成之後可以看到下面這個效果
snakeGame__head-moving

事件處理 - 改變蛇的移動方向

今天最後一個步驟我們要來改變蛇的移動方向,如同前面所說,在 snake 這個物件當中記錄了蛇移動的當下方向 direction ,direction 預設值是 { x: 1, y: 0 } ,也就是往 x 軸正向移動。如果我們要改變蛇的方向,就是想辦法在我們按下方向鍵的時候改變這個值就好了。

因為我們希望監聽鍵盤是否被按下方向鍵,這邊我使用 addEventListener 來監聽鍵盤事件

document.addEventListener('keydown', this.handleOnKeyDown);

記得程式結束或不需要的時候要移除

document.removeEventListener('keydown', this.handleOnKeyDown);

所以當我監聽到鍵盤按下去的事件時,我要執行 handleOnKeyDown 這個函數

handleOnKeyDown = (event) => {
    const {
        handleOnSetSnakeDirection,
    } = this.props;
    handleOnSetSnakeDirection(event.code);
}

這個函數很簡單,就是把監聽到的 event.code 發一個 action 到 reducer 去,鍵盤方向鍵的 event.code 分別是 ArrowUp, ArrowDown, ArrowLeft, ArrowRight。就如同字面上的意思一樣,是上下左右,所以當接收到鍵盤事件,而且是上下左右的時候,我們就會指定相對應的方向給 snake 的 direction 物件。

containers/SnakeGame/constants.js

export const ARROW_UP = 'ArrowUp';
export const ARROW_DOWN = 'ArrowDown';
export const ARROW_LEFT = 'ArrowLeft';
export const ARROW_RIGHT = 'ArrowRight';

記得我們在 Day14 - 貪吃蛇篇:畫出主畫面地圖 當中有提到,x 軸方向往右是正向, y 軸方向往下是正向, 根據這個座標系,我們可以把 event.code 轉換成對應的向量,如下所示:
containers/SnakeGame/reducer.js

const direction = {};
direction[ARROW_UP] = { x: 0, y: -1 };
direction[ARROW_DOWN] = { x: 0, y: 1 };
direction[ARROW_LEFT] = { x: -1, y: 0 };
direction[ARROW_RIGHT] = { x: 1, y: 0 };

case SET_SNAKE_DIRECTION: {
    if (!direction[action.payload]) {
        return state;
    }

    return state.updateIn(['snake', 'direction'], (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_SNAKE_DIRECTION 裡面,因為遊戲當蛇不能往反方向走,所以這邊我多設一個條件,讓反方向的時候,不會改變方向,反方向就是 xy 方向都乘上 -1。

今日成果展示

完成之後展示一下今天的成果,我們已經可以透過鍵盤按下時(keydown)所抓到的 event.code 來自由操作蛇的移動方向啦!
snakeGame__head-set-direction

參考程式碼

Snake - Github


上一篇
Day14 - 貪吃蛇篇:畫出主畫面地圖
下一篇
Day16 - 貪吃蛇篇:加入蛇的身體
系列文
以經典小遊戲為主題之ReactJS應用練習30

尚未有邦友留言

立即登入留言