新闻详情

新闻详情

首页 / 资讯中心 / 详情

手把手用 JavaScript 事件和 setTimeout 做出可玩的贪吃蛇

发布时间:2026/9/19 3:04:35来源:尧图网络
手把手用 JavaScript 事件和 setTimeout 做出可玩的贪吃蛇
手把手用 JavaScript 事件和 setTimeout 做出可玩的贪吃蛇【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum这个贪吃蛇项目出自 The Odin Project 开放课程仓库cu/curriculum里《Web Development 101》的实战作业 project_events_effects.md用纯 JavaScript 实现方向键控制、setTimeout驱动回合、吃食物变长、撞墙或撞到自己就结束。别被游戏吓到它的本质只有三件事一个网格、一串坐标、一个反复调用的函数。动手之前先把三个问题记下来结尾会逐条回答蛇为什么必须建成一串坐标对而不是一个点循环为什么用setTimeout而不是while格子加了边框后为什么整排会挤到下一行去动手前三件事数据建模、状态归属与回合节奏原任务 Setup 第 3 条要求先想清楚需要哪些对象和函数——这一步看起来虚实际决定了后面 90% 的写法。把这三项定下来再敲代码数据对象40×40 的网格用二维数组蛇用一个对象其中body是坐标对的数组数组的数组初始[[20,20]]。理由是移动 头部插入 尾部弹出增长 只插不弹自碰撞 查数组里有没有新头全都免费得到。状态归属状态全部放在纯 JavaScript 对象里grid、snake、foodDOM 只是每次回合重新生成的投影。理由数据只有一份就不会出现数据和页面对不上这类灵异问题。回合节奏keydown只负责改snake.direction这一个字段从不移动蛇移动统一由setTimeout触发的回合完成。理由输入和推进彻底解耦监听器出错不会让蛇瞬移。现在你手上有了图纸剩下的只是按顺序填代码。第 1 关静态棋盘——40×40 网格与 render()目标一个 40×40 的格子棋盘画在页面上中心 [20,20] 处有一格绿色的蛇头。对应原任务 Setup 第 2、4、5、6 条空白 HTML 文档、空格填满的网格、把网格变成 div 的render()、页面加载时跑一次渲染。!DOCTYPE html html head meta charsetutf-8 style #grid { width: 800px; } .cell { width: 20px; height: 20px; /* 20px 是本文自定的格子尺寸 */ border: 1px solid #ccc; /* 边框只为看得清格子 */ display: inline-block; } .snake { background-color: green; } .food { background-color: red; } /style /head body div idgrid/div script srcsnake.js/script /body /html这是整个项目的骨架对应 Setup 第 2 条空白 HTML 文档。#grid { width: 800px }是刻意的40 格 × 20px 800px一行恰好摆 40 格排错没处藏box-sizing的展开放在本关末尾的坑里。const SIZE 40; // 源文档要求 40×40 的棋盘 const grid Array.from({ length: SIZE }, () Array(SIZE).fill( )); function render() { const container document.getElementById(grid); container.innerHTML ; // 每次全量重绘前先清场 for (let r 0; r SIZE; r) { for (let c 0; c SIZE; c) { const cell document.createElement(div); cell.className cell; if (grid[r][c] O) cell.classList.add(snake); else if (grid[r][c] F) cell.classList.add(food); container.appendChild(cell); } } }这一段同时覆盖 Setup 第 4 条网格对象和第 5 条render()函数。grid[r][c]的第一个下标是行纵向第二个是列横向后面的move()、碰撞判定都按这个约定走改约定要全篇同步改。设计理由render()每回合推倒重来、重建全部 1600 个 div看起来浪费但对 40×40 的规模毫无压力换来的是数据是唯一事实——不需要维护哪个 div 对应哪一格的映射表也不会漏更新某个格子。易踩的坑原任务第 5 条特意提醒边框会增加盒子的尺寸。默认盒模型下元素的占位宽度是width border20px 加左右各 1px 边框就是 22px——40 格 × 22px 880px塞不进 800px 的容器第 37 格之后整排被挤到下一行棋盘从此花掉。每个元素都是带 padding、border、margin 的矩形盒子边框也算在占位宽度里两种修法任选其一给格子加box-sizing: border-box让 20px 里包含边框或把#grid的宽度改到 880px 让出空间。仓库的 the_box_model.md 把这套盒模型讲得很细卡住了就翻它。第 2 关让蛇动起来——键盘监听与 setTimeout 循环目标方向键改方向setTimeout驱动的游戏循环每回合让蛇走一格蛇头越界立即结束。对应 Bring it to Life 第 1、2、3、4 条。① 键盘事件只改状态不动蛇document.addEventListener(keydown, (e) { switch (e.key) { case ArrowUp: snake.direction u; break; case ArrowDown: snake.direction d; break; case ArrowLeft: snake.direction l; break; case ArrowRight: snake.direction r; break; } console.log(e.key); });这一步对应 Bring it to Life 第 1 条。原任务建议当时用 jQuery 的keypress()现在用原生keydown更合适keydown在按键按下的瞬间触发且方向键必有值keypress已被废弃且只对产生字符的键可靠。末尾那行console.log()不是多此一举——原任务明确建议用它确认监听确实生效。② 回合循环与 move()蛇开始走let speed 100; // 每回合间隔 100ms本文自定的起点参数 let gameOver false; function gameLoop() { move(); render(); if (gameOver) { alert(撞墙了游戏结束); return; } setTimeout(gameLoop, speed); } function move() { const head snake.body[0].slice(); // 先复制见下方设计理由 if (snake.direction u) head[0]--; if (snake.direction d) head[0]; if (snake.direction l) head[1]--; if (snake.direction r) head[1]; snake.body.unshift(head); // 新头入队 if (!eatFood(head)) snake.body.pop(); // 没吃到就弹出旧尾第 3 关实现 updateGridMarks(); // 用蛇身/食物重写 grid 字符第 3 关实现 if (head[0] 0 || head[0] SIZE || head[1] 0 || head[1] SIZE) gameOver true; } function updateGridMarks() { // 让 grid 字符与 snake/food 状态保持一致 for (let r 0; r SIZE; r) for (let c 0; c SIZE; c) grid[r][c] ; snake.body.forEach(([r, c]) { grid[r][c] O; }); grid[food.position[0]][food.position[1]] F; } window.addEventListener(load, gameLoop);这一段串起 Bring it to Life 第 2 条setTimeout循环、第 3 条每回合move()走一格和第 4 条越界结束。gameLoop的骨架就是执行一回合并调度下一回合第 9 条 Play 靠它兜底——只要不return循环就永远转下去。设计理由三个选择各有一层不这么写会怎样。其一setTimeout而不是while后者会占死主线程页面卡死、按键没机会被处理而setTimeout每回合执行完就把控制权交还浏览器等下一次调度再来。其二setTimeout而不是setInterval间隔在每次调度时才读取以后想让蛇越吃越快改一个speed变量即可生效setInterval的间隔则被冻结在建定时器那一刻。其三head body[0].slice()先复制再改数组存的是引用若直接改snake.body[0]蛇身第一个元素被悄悄改写整条蛇的历史坐标全乱。易踩的坑把按键监听和移动逻辑写在一起是最常见的翻车姿势——在keydown回调里直接挪蛇蛇会跟着手指瞬移回合制节奏彻底消失。监听器只写snake.direction ...一行其余的交给循环。第 3 关食物、增长与撞到自己目标随机放食物、蛇头抵达就长一节、蛇头撞到自己结束。对应 Bring it to Life 第 5、6、7、8、9 条做完就是一个完整能玩的项目。① 食物对象与吃食物逻辑function rand() { return Math.floor(Math.random() * SIZE); } const food { position: [rand(), rand()], place() { this.position [rand(), rand() ]; } }; function eatFood(head) { if (head[0] food.position[0] head[1] food.position[1]) { food.place(); return true; // 真 本回合不弹尾蛇自然长一节 } return false; }这一步对应第 5 条食物对象 随机放置和第 6 条吃食物下一回合长一格。长一节不需要任何显式代码move()里eatFood返回真时跳过pop()头部已经插入、尾部原封不动蛇就变长了一格——这正是坐标对数组建模兑现的地方。② 自碰撞判定别冤枉了蛇尾function hitsSelf(head) { for (let i 0; i snake.body.length - 1; i) { const seg snake.body[i]; if (head[0] seg[0] head[1] seg[1]) return true; } return false; } // 在 gameLoop() 里紧跟撞墙检查之后加一句 // if (hitsSelf(head) true) { ... } // 需把新头传出或把判定挪进 move() 末尾这一步对应第 8 条撞到自己则结束。判定要放在pop()之前做同时循环上限写成length - 1蛇尾这一格在本回合结束时会弹掉蛇头回踩蛇尾其实是合法走法把最后一段算进判据会在原地转圈时误判死亡。原任务第 7 条要求move()支持多节蛇身——上面第 2 关的move()本来就没按单点写这里零改动兼容。加分项源文档 The Fun Stuff 给了三个延伸其中每吃一个食物略微加速最能体现setTimeout的调度优势——因为下一回合的间隔在调度那一刻才取值在eatFood()里加两行就够score snake.body.length; // The Fun Stuff 第 2 条蛇越长每份食物分值越高 speed Math.max(50, speed - 5); // The Fun Stuff 第 3 条加速50ms 是下限易踩的坑食物随机落点可能正好压在蛇身上下一回合蛇白长一格还不见食物。place()里加个落在snake.body中就重掷的循环即可这是原任务没写、你自己跑两局就会碰到的。设计复盘三个决策的取舍决策点本文的写法如果换一种会怎样蛇的状态body 坐标对数组网格字符只是给render()看的快照只存一个头部坐标蛇永远一格长增长和自碰撞全都无从写起循环驱动setTimeout链式调度setInterval想加速只能拆掉重建定时器while(true)主线程卡死键盘事件一个都收不到状态与 DOM数据在纯 JS 对象render()每回合全量重建直接改存量 div 的 class省事一时但每格对应哪个 div 得自己记账漏改一格就是 bug三行代码换掉任何一个如果那栏的写法后面几关的难度都会翻倍——这就是 Setup 第 3 条先想清楚再动手的分量。自测能答上来才算真懂如果把keydown监听从document挪到某个格子 div 上方向键控制为什么会失灵蛇头回踩即将弹掉的蛇尾为什么不算死亡你的判定里哪一行在负责这件事按住一个方向键不放蛇会怎样抖动用记录上一次已生效的方向能怎么修食物随机落点可能落在蛇身上place()怎么写才能避免snake.body用[row, col]约定若有人中途改成[col, row]哪几处会先出事游戏结束后setTimeout链条停了按 R 重新开始需要重置哪些状态才能安全地重新启动循环相关仓库文档按你下一步可能卡住的位置排列archive/old_lessons/javascript/events_effects.md事件绑定、冒泡、委托与解绑的学习目标方向键监听的理论底座。archive/old_lessons/javascript/event_driven.md回调函数与异步思维解释setTimeout(gameLoop, speed)背后发生什么。foundations/javascript_basics/DOM_manipulation_and_events.mdcreateElement、appendChild、addEventListener的现代原生写法。foundations/html_css/css_foundations/the_box_model.mdpadding、border、margin如何改变占位尺寸网格错位的根源。git/foundations_git/git_basics.mdSetup 第 1 条要求先建仓库提交与推送流程都在这里。archive/old_lessons/javascript/jq_basics.md原任务时代的 jQuery 写法对照看能理解从$(document).keypress()到原生keydown的迁移。回到开头的三个问题蛇用坐标对数组而不是单点是因为增长、移动、自碰撞在数组模型下分别退化成不弹尾、头插尾删、查包含一个单点连第二节身子在哪都回答不了。循环选setTimeout是因为它每回合结束才调度下一回合间隔参数随时可变、结束即停链setInterval的间隔冻结、while的线程占死都做不到。格子换行是因为盒模型下边框计入占位宽度20px 的格子加 1px 边框就是 22px一行 40 格装不下——加一句box-sizing: border-box就安静了。三个问题落定项目推上仓库这关就算通了。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

手搭固定翼无人机六自由度模型实战指南 2026/9/19 6:17:22

手搭固定翼无人机六自由度模型实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI行业三级分化格局与中小企业生存策略 2026/9/19 6:17:22

AI行业三级分化格局与中小企业生存策略

1. 行业现状:AI企业的三级分化格局2023年成为AI技术发展的分水岭,行业呈现出明显的马太效应。根据最新市场调研数据显示,头部大厂平均单季度AI研发投入超过50亿元,而中小型AI企业融资总额同比下降37%。这种资源分配的不均衡正在重…

阅读更多 →
VS2015安装与C++环境配置:从下载到Qt+TCP实战 2026/9/19 6:17:22

VS2015安装与C++环境配置:从下载到Qt+TCP实战

1. 为什么2024年还有人折腾VS2015这套老环境你可能觉得奇怪,现在Visual Studio 2022都出了好几年了,VS2026的注册码都有人在搜,为什么还要回头去装VS2015?答案其实很实在:老项目维护、特定编译器版本兼容、教学环境统一…

阅读更多 →
RTCP旋转矩阵原理与五轴数控实时补偿实战 2026/9/19 6:17:22

RTCP旋转矩阵原理与五轴数控实时补偿实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
抖音小游戏必须用IL2CPP:原理、陷阱与运行时约束解析 2026/9/19 6:17:22

抖音小游戏必须用IL2CPP:原理、陷阱与运行时约束解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
SpringBoot+Vue旅行指南系统毕业设计实战 2026/9/19 6:14:22

SpringBoot+Vue旅行指南系统毕业设计实战

1. 项目概述作为一名有10年Java开发经验的程序员,我想分享一个非常适合作为毕业设计的项目——基于SpringBoot的旅行指南攻略游记系统。这个项目不仅涵盖了现代Web开发的完整技术栈,还包含了丰富的业务场景,能够全面锻炼学生的开发能力。这个…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞