前端实现贪吃蛇功能

        大家都玩过贪吃蛇小游戏,控制一条蛇去吃食物,然后蛇在吃到食物后会变大。本篇博客将会实现贪吃蛇小游戏的功能。

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 xy: 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/638090.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

新买电脑配置不低却卡顿?

目录 前言&#xff1a; 电脑卡顿的原因 Windows 10必做的系统优化 禁用 IP Helper 关闭系统通知 机械硬盘开启优化驱动器功能 开启存储感知 前言&#xff1a; 新买的电脑配置不低&#xff0c;但却卡顿甚至程序不反应&#xff0c;这是怎么回事儿&#xff1f; 其实并不…

代码随想录day32 贪心算法训练

122.买卖股票的最佳时机 II 题目 给定一个数组&#xff0c;它的第 i 个元素是一支给定股票第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;。 注意&#xff1a;你不能同时参与多笔交易&a…

Java中泛型的详细介绍

引言&#xff1a; Java语言中的泛型是一种强大的特性&#xff0c;它允许我们在编写代码时指定类、接口和方法的参数类型。通过使用泛型&#xff0c;我们可以提高代码的重用性、可读性和安全性。在本博客中&#xff0c;我们将详细介绍Java中泛型的知识。 一、泛型的基本概念 泛型…

《Linux C编程实战》笔记:Linux信号介绍

信号是一种软件中断&#xff0c;它提供了处理一种异步事件的方法&#xff0c;也是进程惟一的异步通信方式。在Linux系统中&#xff0c;根据POSIX标准扩展的信号机制&#xff0c;不仅可以用来通知某进程发生了什么事&#xff0c;还可以给进程传递数据。 信号的来源 信号的来源…

信号处理专题设计-基于边缘检测的数字图像分类识别

目录 一、实验目的 二、实验要求 三、实验原理 1.卷积神经网络&#xff08;CNN&#xff09;模型 2.边缘检测 3.形态学操作 4.鲁棒性 四、实验过程 1.数据预处理 2. 网络的构建 3.模型的训练 4.边缘检测和形态学操作相关代码 5.模型训练结果 6.关键信息的保存 五、实验测试与评估…

【LeetCode】150. 逆波兰表达式求值(中等)——代码随想录算法训练营Day11

题目链接&#xff1a;150. 逆波兰表达式求值 题目描述 给你一个字符串数组 tokens &#xff0c;表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。 注意&#xff1a; 有效的算符为 、-、* 和 / 。每个操作数&#xff08;运算…

竞赛保研 机器视觉人体跌倒检测系统 - opencv python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 机器视觉人体跌倒检测系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&…

【蓝桥杯日记】复盘第一篇——顺序结构

&#x1f680;前言 本期是一篇关于顺序结构的题目的复盘,通过复盘基础知识&#xff0c;进而把基础知识学习牢固&#xff01;通过例题而进行复习基础知识。 &#x1f6a9;目录 前言 1.字符三角形 分析&#xff1a; 知识点&#xff1a; 代码如下 2. 字母转换 题目分析: 知…

C语言·预处理详解

1. 预定义符号 C语言设置了一些预定义符号&#xff0c;可以直接使用&#xff0c;预定义符号也是在预处理期间处理的 __FILE__ 进行编译的源文件 __LINE__ 文件当前的行号 __DATE__ 文件被编译的日期 __TIME__ 文件被编译的时间 __STDC__ 如果编译器遵循ANSI C&#xff0c;…

SpringCloud Aliba-Sentinel【下篇】-从入门到学废【6】

&#x1f929;java小课堂&#x1f929; &#x1f32d;&#x1f32d;&#x1f32d; 和 equals 的区别是什么&#xff1f;&#x1f979;&#x1f979;&#x1f979; 对于基本类型&#xff0c; 比较的是值&#xff1b;对于引用类型&#xff0c;比较的是地址&#xff1b;equals不能…

【现代密码学基础】详解完美安全与不可区分安全

目录 一. 介绍 二. 不可区分性试验 三. 不可区分性与完美安全 四. 例题 五. 小结 一. 介绍 敌手完美不可区分&#xff0c;英文写做perfect adversarial indistinguishability&#xff0c;其中adversarial经常被省略不写&#xff0c;在密码学的论文中经常被简称为IND安全。…

ICLR 2024 时间序列相关最新论文汇总,涉及transformer、GNN、大模型等热门领域

ICLR&#xff08;International Conference on Learning Representations&#xff09;&#xff0c;国际公认的深度学习顶会之一&#xff0c;与AAAI、CVPR、ACL和NIPS等老牌学术会议齐名&#xff0c;由图灵奖巨头Yoshua Bengio和Yann LeCun牵头举办&#xff0c;在人工智能、统计…

UI -- Vue3

Vue3 模板 Vue3 UI package.json同级components/slButton/element.vue <template><div class"dddiv"><span>测试321</span></div> </template><script setup>defineOptions({name: "sl-button" }); </scr…

Spring | Srping AOP (AOP简介、动态代理、基于“代理类”的AOP实现)

目录: 1.Spring AOP简介1.1 AOP简介1.2 AOP术语 2.动态代理2.1 JDK动态代理2.2 CGLIB代理 3.基于“代理类”的AOP实现3.1 Spring的通知类型3.2 ProxyFactoryBean ( 可通知.xml配置文件完成aop功能 ) 1.Spring AOP简介 1.1 AOP简介 Spring的AOP模块&#xff0c;是Spring框架体系…

SpringMVC获取参数与页面跳转

获取参数 第一种 直接当成方法的参数&#xff0c;需要与前台的name一致 相当于Request.getAttribute("username") Controller 第二种 使用对象接收 页面的name也要和对象的字段一致 创建一个对应的实体类 Controller 将参数更换为User对象就行 SpringMVC获取到…

P2P DMA并不是所有场景都会有性能提升

P2P (Peer-to-Peer) DMA技术理论上可以带来性能提升&#xff0c;特别是在特定的工作负载和场景下。例如&#xff0c;当两个高速设备&#xff08;如GPU与NVMe SSD&#xff09;需要频繁进行大量数据交换时&#xff0c;通过P2P DMA&#xff0c;数据可以直接在设备间传输&#xff0…

结构体内存对齐(面试重点)

结构体内存对齐 1. 结构体类型的声明1.1 结构体的概念1.1.1 结构的声明1.1.2 结构体变量的创建和初始化 1.2 结构的特殊声明1.3 结构的自引用 2. 结构体内存对齐2.1 对齐规则2.1.1 练习1:2.1.2 练习2:2.1.3 练习3:2.1.4 练习4: 2.2 offsetof宏的使用2.3 为什么存在内存对齐?2.…

electron + selenium报错: Server terminated early with status 1

解决办法&#xff1a; 这种错误一般是浏览器创建的某方法致命错误导致的&#xff0c;查看一下实例化driver的地方有哪些配置&#xff0c;着重看日志、用执行信息存储一类的配置&#xff0c;我的问题是日志文件夹改过了但没有创建 // 浏览器参数设置 const customArguments [-…

Mac book air 重新安装系统验证显示 untrusted_cert_title

环境&#xff1a; Mac Book Air macOS Sierra 问题描述&#xff1a; Mac book air 重新安装系统验证显示 untrusted_cert_title 解决方案&#xff1a; 1.终端输入命令行输入 date 会看到一个非常旧的日期 2.更改日期为当前时间 使用以下命令来设置日期和时间&#xff1a…

react和vue的区别

一、核心思想不同 Vue的核心思想是尽可能的降低前端开发的门槛&#xff0c;是一个灵活易用的渐进式双向绑定的MVVM框架。 React的核心思想是声明式渲染和组件化、单向数据流&#xff0c;React既不属于MVC也不属于MVVM架构。 如何理解React的单向数据流&#xff1f; React的单…