前端实现贪吃蛇功能

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ与SpringAMQP

MQ&#xff0c;中文是消息队列&#xff08;MessageQueue&#xff09;&#xff0c;字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。&#xff08;经纪人&#xff01;&#xff09; 1.RabbitMQ介绍 微服务间通讯有同步和异步两种方式 同步&#xff08;通信&#xff0…

实现SERVLET应用程序

实现SERVLET应用程序 Smart Software 的开发人员希望开发一个Web应用程序,使用servlet显示保存在表中的雇员信息。该应用程序需要有用户界面,用户可在该用户界面中指定要查看雇员数据的雇员ID。该界面还应显示网站被访问的次数。 解决方案 要解决上述问题,需要执行以下任务…

PWM之舵机

舵机又称直流电机&#xff0c;如下图 本节承接上节&#xff0c;具体的PWM技术已经在上一节讲的很详细了&#xff0c;本节就不再讲了&#xff0c;那么我们的重点就放在直流电机的工作原理上了。 一、工作原理 我们研究直流电机&#xff0c;主要式研究直流电机旋转速度的调节&a…

2024年最热门的网络安全自学学习方法,一网打尽!

学好网络安全是一个复杂而又迫切的需求&#xff0c;特别是在当今数字化时代。对于零基础的人来说&#xff0c;建议按照以下步骤和原则学习网络安全&#xff0c;以建立坚实的基础。 第一步&#xff1a;了解网络安全的基础概念 在学习网络安全之前&#xff0c;首先需要了解一些…

keep-alive组件缓存

keep-alive组件缓存 从a跳b&#xff0c;a已经销毁&#xff0c;b重新渲染&#xff1b;b跳a&#xff0c;b销毁a重新渲染 源组件销毁&#xff0c;目标组件渲染 组件缓存&#xff1a;组件实例等相关&#xff08; 包括vnode&#xff09;存储起来 重新渲染指的是&#xff1a;把视图重…

街机模拟游戏逆向工程(HACKROM)教程:[14]68K汇编-标志寄存器

在M68K中&#xff0c;有许多条件分支指令&#xff0c;和jmp指令一样也会修改PC达到程序跳转或分支的目的&#xff0c;不过这些会根据一些情况或状态来选择是否跳转。而在M68K中&#xff0c;有一个特别的寄存器来标记这些情况。 CCR(状态标志寄存器) CCR寄存器是用来保存一些对…

回调地狱与解决方案

什么是回调地狱&#xff1f; 简单理解就是回调函数嵌套回调 示例&#xff1a; setTimeout(() > {console.log(1);setTimeout(() > {console.log(2);setTimeout(() > {console.log(3);}, 1000);}, 2000)}, 3000)如上代码所示&#xff0c;回调函数嵌套回调&#xff0c;就…

Rustdesk自建服务搭建好了,打开Win10 下客户端下面状态一直正在接入网络,无法成功连接服务器

环境: Rustdesk1.2.3 自建服务器 有域名地址 问题描述: Rustdesk自建服务搭建好了,打开Win10 下客户端下面状态一直正在接入网络,无法成功连接服务器 解决方案: RustDesk是一款免费的远程桌面软件,它允许用户通过互联网远程连接和控制其他计算机。它是用Rust编程语…

专题篇|国芯科技系列化布局车载DSP芯片,满足不同层次车载音频产品的需求

随着高端DSP芯片产品CCD5001的亮相&#xff0c;国芯科技也在积极布局未来的DSP系列芯片群。通过深入研究不同车型音频处理需求&#xff0c;对比国外DSP产品综合性能和成本&#xff0c;国芯科技未来将推出全新DSP芯片家族&#xff0c;包括已经推出的高端产品CCD5001&#xff0c;…

VRRP协议

VRRP概述 虚拟路由冗余协议VRRP(Virtual Router Redundancy Protocol)通过把几台路由设备联合组成一台虚拟的路由设备,将虚拟路由设备的IP地址作为用户的默认网关实现与外部网络通信。当网关设备发生故障时,VRRP机制能够选举新的网关设备承担数据流量,从而保障网络的可靠…

AI大模型开发架构设计(1)——LLM大模型Agent剖析和应用案例实战

文章目录 LLM大模型Agent剖析和应用案例实战1 从 LLM 大模型到智能体演进技术语言模型是什么&#xff1f;大语音模型是什么&#xff1f;大语言模型日新月异LLM大模型存在局限性LLM Agent来势凶凶LLM Agent增长迅猛LLM Agent是什么&#xff1f; 2 LLM Agent 架构深度剖析规划能力…

1.10马原,总复习PART2

马克思主义鲜明特征 革命性&#xff0c;科学性&#xff0c;实践性 人民性&#xff0c;发展性 革命性、科学性&#xff0c;实践性&#xff0c;人民性&#xff0c;发展性 革命性&#xff0c;科学性&#xff0c;实践性&#xff0c;人 革命性&#xff0c;发展性&#xff0c…

架构篇05-复杂度来源:高可用

文章目录 计算高可用存储高可用高可用状态决策小结 今天&#xff0c;我们聊聊复杂度的第二个来源高可用。 参考维基百科&#xff0c;先来看看高可用的定义。 系统无中断地执行其功能的能力&#xff0c;代表系统的可用性程度&#xff0c;是进行系统设计时的准则之一。 这个定义…

数学建模实战Matlab绘图

二维曲线、散点图 绘图命令&#xff1a;plot(x,y,’line specifiers’,’PropertyName’,PropertyValue) 例子&#xff1a;绘图表示年收入与年份的关系 ‘--r*’:--设置线型&#xff1b;r:设置颜色为红色&#xff1b;*节点型号 ‘linewidth’&#xff1a;设置线宽&#xff1…

使用cmake进行完成开发实践

根据这个UML图进行cmake的实践 首先按照使用vscode在wsl2中配置clangd环境-CSDN博客的内容先创建出cmake项目。 之后在项目目录中创建include和src目录。 根据UML图&#xff0c;首先要完成Gun类的实现。分别在include&#xff0c;src目录下创建头文件和源文件&#xff0c;写入…

迭代器模式介绍

目录 一、迭代器模式介绍 1.1 迭代器模式定义 1.2 迭代器模式原理 1.2.1 迭代器模式类图 1.2.2 模式角色说明 1.2.3 示例代码 二、迭代模式的应用 2.1 需求说明 2.2 需求实现 2.2.1 抽象迭代类 2.2.2 抽象集合类 2.2.3 主题类 2.2.4 具体迭代类 2.2.5 具体集合类 …

android使用相机 intent.resolveActivity returns null

问题 笔者使用java进行android开发&#xff0c;启动相机时 intent.resolveActivity returns null takePictureIntent.resolveActivity(getPackageManager()) null详细问题 笔者使用如下代码启动相机 // 启动相机SuppressLint("LongLogTag")private void dispatc…

基于华为MRS3.2.0实时Flink消费Kafka落盘至HDFS的Hive外部表的调度方案

文章目录 1 Kafka1.1 Kerberos安全模式的认证与环境准备1.2 创建一个测试主题1.3 消费主题的接收测试 2 Flink1.1 Kerberos安全模式的认证与环境准备1.2 Flink任务的开发 3 HDFS与Hive3.1 Shell脚本的编写思路3.2 脚本测试方法 4 DolphinScheduler 该需求为实时接收对手Topic&a…

yum下载源,vim使用

文章目录 yum本地配置lzrsz命令行互传scp(远程拷贝)vim yum本地配置 [rootiZf8z3j2ckkap6ypn717msZ ~]# pwd /root [rootiZf8z3j2ckkap6ypn717msZ ~]# ls /etc/yum.repos.d CentOS-Base.repo epel.repo //本地配置源yum会根据/etc/yum.repo.d路径下的配置文件来构成自己的下载…

一个简单的ETCD GUI工具

使用ETCD没有好用的GUI工具&#xff0c;随手用c#写了一个&#xff0c; 做得好玩的一个ETCD GUI工具&#xff0c;后面加上CLI 工具&#xff0c;类似于 redis Cli工具一样&#xff0c;简化在 Linux下面的操作&#xff0c;不知道有没有必要&#xff0c; git 地址如下&#xff0c;…