大家都玩过贪吃蛇小游戏,控制一条蛇去吃食物,然后蛇在吃到食物后会变大。本篇博客将会实现贪吃蛇小游戏的功能。
1.实现效果
2.整体布局
/**
* 游戏区域样式
*/
const gameBoardStyle = {
gridTemplateColumns: `repeat(${width}, 1fr)`,
gridTemplateRows: `repeat(${height}, 1fr)`,
display: 'grid',
border: '1px solid #000',
width: '500px',
height: '500px',
backgroundColor: '#488cfa'
};
/**
* 小蛇样式
*/
const snakeBodyStyle = (segment) => ({
gridRowStart: segment.y,
gridColumnStart: segment.x,
backgroundColor: 'green'
})
/**
* 食物样式
*/
const foodStyle = {
gridRowStart: food.current.y,
gridColumnStart: food.current.x,
backgroundColor: 'red'
}
<div className={'snake-game'}>
<div className={'game-board'} style={gameBoardStyle}>
{/*蛇身体*/}
{snake.map((segment, idx) =>
<div key={idx} className={'snake-body'} style={snakeBodyStyle(segment)}/>
)
}
{/*食物*/}
<div className={'food'} style={foodStyle}></div>
</div>
</div>
采用grid 布局,整个游戏区域划分为width*height个小块,小蛇身体的每一部分对应一小块,食物对应一小块。
3.技术实现
a.数据结构
小蛇的数据结构是个坐标数组,snake[0]是蛇头,snake[snake.length-1]是蛇尾巴。snake[i].x表示第i块位置的x坐标,snake[i].y表示第i块位置的y坐标。
食物的数据结构是坐标。
游戏区域是一个width*height的虚拟空间。
b.场景
一、小蛇如何移动,以及移动方式
1. 通过设置监听键盘的上下左右事件,来触发小蛇的移动。
2. 通过定时器实现小蛇沿着当前方向移动
// 移动方向,上下左右
const directions = [[0, -1], [0, 1], [-1, 0], [1, 0]];
// 当前移动方向
const [currentDirection, setCurrentDirection] = useState(3);
// 小蛇移动
function move() {
const direction = directions[currentDirection];
// 更新上一次蛇尾巴
lastTail.current = {x: snake[snake.length - 1].x, y: snake[snake.length - 1].y};
const head = snake[0];
// 移动小蛇,将数组后移动
for (let i = snake.length - 1; i > 0; i--) {
snake[i].x = snake[i - 1].x;
snake[i].y = snake[i - 1].y;
}
// 更新蛇头
head.x += direction[0];
head.y += direction[1];
// 触发渲染
setSnake([...snake]);
}
const [click, setClick] = useState(0)
// 设置键盘监听函数
useEffect(() => {
document.addEventListener('keydown', function (event) {
const key = event.key;
if (key === 'ArrowUp') {
// 监听到了向上箭头键的按下操作
setCurrentDirection(0)
setClick((c)=>c+1);
} else if (key === 'ArrowDown') {
// 监听到了向下箭头键的按下操作
setCurrentDirection(1)
setClick((c)=>c+1);
} else if (key === 'ArrowLeft') {
// 监听到了向左箭头键的按下操作
setCurrentDirection(2)
setClick((c)=>c+1);
} else if (key === 'ArrowRight') {
// 监听到了向右箭头键的按下操作
setCurrentDirection(3)
setClick((c)=>c+1);
}
});
}, [])
/**
* 设定定时器,每1s向当前方向移动小蛇
* 如果敲键盘,或者吃到食物需要更新定时器
* tips: 吃到食物更新是因为定时器晚执行可能会有并发问题
*/
useEffect(() => {
console.log(click)
move()
const timer = setInterval(() => {
move();
}, 1000);
return () => {
clearInterval(timer);
};
}, [click, snake.length]);
二、游戏结束判断
1.游戏成功判断,若无发生成新的食物,则游戏成功
2.游戏失败判断,若小蛇出边界或者小蛇撞到自己,则游戏失败。
// 每次渲染后,判断小蛇状态
useEffect(() => {
// 判断小蛇撞出边界
if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) {
console.log('游戏失败')
alert('出界,游戏失败');
reset();
return;
}
// 判断小蛇撞到自己
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
console.log('游戏失败')
console.log('snake:' + JSON.stringify(snake))
alert('撞到自己了,游戏失败');
reset();
return;
}
}
})
三、食物生成以及吃食物操作
1.食物需要在区域内随机生成,并且不能生成在小蛇身体上,若无地方生成,则游戏通关。
2.吃食物操作会增长小蛇的长度,在小蛇的尾巴添加一截,需要存储前一个路径的尾巴位置。
// 随机生成食物
function generateFood(snake) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
// 如果蛇长等于宽高,说明蛇占满了整个区域,已成功
if (snake.length === width * height) {
return null;
}
// 判断食物是否在蛇身上
for (let node of snake) {
if (node.x === x && node.y === y) {
// 重新生成食物,
return generateFood(snake);
}
}
return {x, y};
}
// 蛇尾巴
const lastTail = useRef(null);
// 每次渲染后,判断小蛇状态
useEffect(() => {
const head = snake[0];
// 小蛇吃到食物
if (head.x === food.current.x && head.y === food.current.y) {
console.log('eat food!')
// 添加上次蛇尾巴
let nTail = {...lastTail.current};
snake.push(nTail);
lastTail.current = nTail;
// 重新生成食物
food.current = generateFood(snake);
if (food.current === null) {
console.log('恭喜已通过')
alert('恭喜已经通关');
reset();
return;
}
// 发起渲染
console.log('newsnake:' + JSON.stringify(snake))
setSnake([...snake]);
return;
}
});
c.整体代码
const {useState, useRef, useEffect} = require("react");
const Snake = ({width, height}) => {
// 移动方向,上下左右
const directions = [[0, -1], [0, 1], [-1, 0], [1, 0]];
// 当前移动方向
const [currentDirection, setCurrentDirection] = useState(3);
// 初始小蛇
const initialSnake = [{
x: 0, // pos x
y: 0, // pos y
}];
// 蛇身体
const [snake, setSnake] = useState(initialSnake);
// 食物
const food = useRef(null);
// 初始化食物
if (food.current === null) {
food.current = generateFood(snake);
}
// 随机生成食物
function generateFood(snake) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
// 如果蛇长等于宽高,说明蛇占满了整个区域,已成功
if (snake.length === width * height) {
return null;
}
// 判断食物是否在蛇身上
for (let node of snake) {
if (node.x === x && node.y === y) {
// 重新生成食物,
return generateFood(snake);
}
}
return {x, y};
}
// 蛇尾巴
const lastTail = useRef(null);
// 小蛇移动
function move() {
const direction = directions[currentDirection];
// 更新蛇尾巴
lastTail.current = {x: snake[snake.length - 1].x, y: snake[snake.length - 1].y};
const head = snake[0];
for (let i = snake.length - 1; i > 0; i--) {
snake[i].x = snake[i - 1].x;
snake[i].y = snake[i - 1].y;
}
head.x += direction[0];
head.y += direction[1];
setSnake([...snake]);
}
// 游戏结束后重置
function reset() {
setSnake([...initialSnake]);
setCurrentDirection(3);
lastTail.current = null;
}
// 判断是否游戏结束
useEffect(() => {
const head = snake[0];
// 判断小蛇撞出边界
if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) {
console.log('游戏失败')
alert('出界,游戏失败');
reset();
return;
}
// 判断小蛇撞到自己
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
console.log('游戏失败')
console.log('snake:' + JSON.stringify(snake))
alert('撞到自己了,游戏失败');
reset();
return;
}
}
})
// 判断是否吃到食物
useEffect(()=>{
const head = snake[0];
// 小蛇吃到食物
if (head.x === food.current.x && head.y === food.current.y) {
console.log('eat food!')
// 添加上次蛇尾巴
let nTail = {...lastTail.current};
snake.push(nTail);
lastTail.current = nTail;
// 重新生成食物
food.current = generateFood(snake);
if (food.current === null) {
console.log('恭喜已通过')
alert('恭喜已经通关');
reset();
return;
}
// 发起渲染
console.log('newsnake:' + JSON.stringify(snake))
setSnake([...snake]);
return;
}
})
const [click, setClick] = useState(0)
// 设置键盘监听函数
useEffect(() => {
document.addEventListener('keydown', function (event) {
const key = event.key;
if (key === 'ArrowUp') {
// 监听到了向上箭头键的按下操作
setCurrentDirection(0)
setClick((c)=>c+1);
} else if (key === 'ArrowDown') {
// 监听到了向下箭头键的按下操作
setCurrentDirection(1)
setClick((c)=>c+1);
} else if (key === 'ArrowLeft') {
// 监听到了向左箭头键的按下操作
setCurrentDirection(2)
setClick((c)=>c+1);
} else if (key === 'ArrowRight') {
// 监听到了向右箭头键的按下操作
setCurrentDirection(3)
setClick((c)=>c+1);
}
});
}, [])
/**
* 设定定时器,每1s向当前方向移动小蛇
* 如果敲键盘,或者吃到食物需要更新定时器
* tips: 吃到食物,由于定时器晚执行,可能会用老的state覆盖
*/
useEffect(() => {
console.log(click)
move()
const timer = setInterval(() => {
move();
}, 1000);
return () => {
clearInterval(timer);
};
}, [click, snake.length]);
/**
* 游戏区域样式
*/
const gameBoardStyle = {
gridTemplateColumns: `repeat(${width}, 1fr)`,
gridTemplateRows: `repeat(${height}, 1fr)`,
display: 'grid',
border: '1px solid #000',
width: '500px',
height: '500px',
backgroundColor: '#488cfa'
};
/**
* 小蛇样式
*/
const snakeBodyStyle = (segment) => ({
gridRowStart: segment.y,
gridColumnStart: segment.x,
backgroundColor: 'green'
})
/**
* 食物样式
*/
const foodStyle = {
gridRowStart: food.current.y,
gridColumnStart: food.current.x,
backgroundColor: 'red'
}
// 小蛇组成
return (
<>
<div className={'snake-game'}>
<div className={'game-board'} style={gameBoardStyle}>
{/*蛇身体*/}
{snake.map((segment, idx) =>
<div key={idx} className={'snake-body'} style={snakeBodyStyle(segment)}/>
)
}
{/*食物*/}
<div className={'food'}
style={foodStyle}>
</div>
</div>
</div>
</>
)
}
export default Snake