前端实现贪吃蛇功能

news2024/12/27 1:56:06

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

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

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

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

相关文章

Hive-SQL语法大全

Hive SQL 语法大全 基于语法描述说明 CREATE DATABASE [IF NOT EXISTS] db_name [LOCATION] path; SELECT expr, ... FROM tbl ORDER BY col_name [ASC | DESC] (A | B | C)如上语法&#xff0c;在语法描述中出现&#xff1a; []&#xff0c;表示可选&#xff0c;如上[LOCATI…

【51单片机】矩阵按键

0、前言 参考&#xff1a;普中 51 单片机开发攻略 1、硬件 2、软件 main.c #include <reg52.h> #include <intrins.h> #include "delayms.h"typedef unsigned int u16; //对数据类型进行声明定义 typedef unsigned char u8; #define GPIO_KEY P1 #d…

【操作系统和计网从入门到深入】(五)软硬链接和动静态库

前言 这个专栏其实是博主在复习操作系统和计算机网络时候的笔记&#xff0c;所以如果是博主比较熟悉的知识点&#xff0c;博主可能就直接跳过了&#xff0c;但是所有重要的知识点&#xff0c;在这个专栏里面都会提到&#xff01;而且我也一定会保证这个专栏知识点的完整性&…

使用RSVP进行带宽预留

使用RSVP进行带宽预留 服务质量&#xff08;QoS&#xff09;工具包括 标记&#xff0c; 管制和 整形 机制&#xff0c;主要是区分服务&#xff08;DiffServ&#xff09;工具。DiffServ机制可提供带宽保证&#xff08;在各种刚性级别上&#xff09;&#xff0c;但是它们都不提供…

Three.js 学习笔记之模型(学习中1.20更新) | 组 - 模型 - 几何体 - 材质

文章目录 模型 几何体 材质层级模型组- THREE.Group递归遍历模型树结构object3D.traverse() 模型点模型Points - 用于显示点线模型Line | LineLoop | LineSegments网格模型mesh - 三角形网格模型独有的属性与方法 几何体BufferGeometry缓冲类型几何体BufferGeometry - 基类创…

【Android】app中阻塞的looper为什么可以响应touch事件

这里&#xff0c;我们考虑一个问题&#xff0c;Android中的应用是一个looper线程&#xff0c;没有任务时就阻塞着&#xff0c;其他线程通过handler调用等方式向主线程looper发送任务&#xff0c; 如果点击应用上的按钮&#xff0c;应用是怎么及时响应的呢&#xff0c; 是专门启…

JS-WebAPIs- Window对象(五)

• BOM(浏览器对象模型) BOM(Browser Object Model ) 是浏览器对象模型 window对象是一个全局对象&#xff0c;也可以说是JavaScript中的顶级对象像document、alert()、console.log()这些都是window的属性&#xff0c;基本BOM的属性和方法都是window的。所有通过var定义在全局…

提升 Go 编程:Go 1.22 中有哪些新特性?

深入了解 Go 1.22 版本及其语言增强功能 随着 Go 编程语言的不断发展&#xff0c;即将发布的版本 Go 1.22 承诺带来各种新功能、改进和变化。预计于 2024 年 2 月发布的 Go 1.22 引入了对语言、工具、运行时、编译器和核心库的增强。让我们深入了解一些在这个即将发布的版本中开…

python基础语法(下)—— python中的元组、集合、字典、类与异常的处理

文章目录 8.python中的元组8.1元组的初始化8.2元组的解包8.3元组的比较运算8.4else 9.python中的集合9.1集合的初始化9.2集合的常用操作9.3使用for循环遍历集合 10.python中的字典10.1字典的初始化10.2字典的常见操作10.3使用for循环遍历字典 11.python中的类11.1类的定义11.2类…

二进制部署高可用k8s集群V1.20.11版本

文章目录 一、操作系统初始化配置&#xff08;所有节点均执行&#xff09;1、关闭防火墙2、关闭selinux3、关闭swap4、根据规划修改主机名5、在master节点上添加host6、将桥接的IPv4流量传递到iptables的链7、时间同步 二、部署Etcd集群1、准备cfssl证书生成工具2、生成Etcd证书…

B(l)utter:一款针对Flutter移动端应用程序的逆向工程分析工具

关于B(l)utter B(l)utter是一款针对Flutter移动端应用程序的逆向工程分析工具&#xff0c;当前版本的B(l)utter仅支持Android libapp.so&#xff08;ARM64&#xff09;&#xff0c;可以帮助广大研究人员对基于Flutter开发的移动端应用程序进行逆向工程分析。 环境搭建 该应用…

C++参悟:正则表达式库regex

正则表达式库regex 一、概述二、快速上手Demo1. 查找字符串2. 匹配字符串3. 替换字符串 三、类关系梳理1. 主类1. basic_regex 2. 算法1. regex_match2. regex_search3. regex_replace 3. 迭代器4. 异常5. 特征6. 常量1. syntax_option_type2. match_flag_type3. error_type 一…

6.3.5编辑视频

6.3.5编辑视频 除了上面的功能外&#xff0c;Camtasia4还能进行简单的视频编辑工作&#xff0c;如媒体的剪辑、连接、画中画等。 下面我们就利用Camtasia4的强大功能来实现一个画中画效果&#xff0c;在具体操作之前&#xff0c;需要准备好两个视频文件&#xff0c;一个作为主…

C++:练习:类的一些简单例题(学习编程的前期需要大量的读和写基本C++代码)

2014年1月21日 内容整理自《程序设计教程&#xff1a;用C语言编程 第三版》 陈家骏 郑滔 --------------------------------------------------------------------------------------------------------------------------------- 例题1&#xff1a;用类来实现“栈”这一数…

(二十三)Kubernetes系列之prometheus+grafana安装

1.kube-prometheus安装 1.1 下载 访问地址&#xff1a;https://github.com/prometheus-operator/kube-prometheus/releases 点击Source code (tar.gz)进行下载&#xff0c;并上传到k8s服务器master节点 1.2解压 tar zxvf kube-prometheus-0.11.0.tar.gz 1.3进入kube-prome…

2024年黑龙江事业单位招聘报名详细流程图解,千万不要错过报名哦

⭐报名入口&#xff1a;入黑龙江省事业单位公开招聘服务平台 ✅报名时间&#xff1a;2024年1月22日9:00-1月26日17:00 ✅笔试联考&#xff1a;2024年3月30日 报名流程如下&#xff0c;具体可查看笔记详细图解 第一步&#xff0c;注册及登录&#xff0c;新用户注册新账号。 第二…

【数据结构】单链表的基本操作 (C语言版)

目录 一、单链表 1、单链表的定义&#xff1a; 2、单链表的优缺点&#xff1a; 二、单链表的基本操作算法&#xff08;C语言&#xff09; 1、宏定义 2、创建结构体 3、初始化 4、插入 4、求长度 5、清空 6、销毁 7、取值 8、查找 9、删除 10、头插法创建单链表…

Centos使用Docker搭建自己的Gitlab(社区版和设置汉化、修改密码、设置SSH秘钥、添加拉取命令端口号)

根据我的经验 部署Gitlab&#xff08;社区版&#xff09; 至少需要2核4g的服务器 带宽3~4M 1. 在自己电脑上安装终端&#xff1a;宝塔ssl终端 或者 FinalShell&#xff0c;根据喜好安装即可 http://www.hostbuf.com/t/988.html http://www.hostbuf.com/downloads/finalshell_w…

xshell配置隧道转移规则

钢铁知识库&#xff0c;一个学习python爬虫、数据分析的知识库。人生苦短&#xff0c;快用python。 xshell是什么 通俗点说就是一款强大ssh远程软件&#xff0c;可以方便运维人员对服务器进行管理操作&#xff0c;功能很多朋友们自行探索&#xff0c;今天只聊其中一个功能点那…

中仕教育:研究生毕业可以考选调生吗?

选调生的报考条件之一是应届生&#xff0c;研究生毕业也属于应届生&#xff0c;所以是可以报考的。 选调生不同学历的年龄限制&#xff1a; 1.应届本科生&#xff1a;年龄在25岁以内 2.应届研究生&#xff1a;年龄在30岁以内 3.应届博士生&#xff1a;年龄在35岁以内 研究…