有了地圖之後,我們就可以開始讓蛇在上面爬了,今天我們的目標是畫出蛇的頭,並且可以操控他在地圖上跑來跑去。
首先我們先來新增一個蛇的物件
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 } 的位置,所以到目前為止的成果會如下圖所示
由於紀錄蛇的頭部位置的參數是 headPosition ,為了讓頭部動起來,我們只需要不斷的去改變 headPosition 的 xy 位置就可以了。
我的方法是透過 setInterval()
這個方法,setInterval() 可以按照指定的週期(毫秒)來調用函數,這邊的週期就是蛇的移動速度。然後記得要在不需要的時候透過 clearInterval() 取消由 setInterval() 设置的 timeout。
MDN web doc - setInterval()
Window clearInterval() Method
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;
};
完成之後可以看到下面這個效果
今天最後一個步驟我們要來改變蛇的移動方向,如同前面所說,在 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 來自由操作蛇的移動方向啦!