一、前言
作者第一次写掘金文章,有很多不足之点,请多多包涵。
这是一个贪吃蛇游戏。用到的技术栈有:html、css、js,没有使用其他框架或者插件。
二、实现的功能
-
按钮三种情况:
点击开始按钮,实现游戏开始,贪吃蛇开始移动,若键盘的方向键没被触发,则蛇默认向右移动。
点击暂停按钮,实现游戏暂停,贪吃蛇静止状态。
点击重新按钮,实现游戏重新开始。
-
蛇的情况:
若蛇头碰到墙,则游戏结束并提示用户,用户点确定键后重新渲染页面
若蛇头碰到蛇身,则游戏结束并提示用户,用户点确定键后重新渲染页面
若蛇头碰到食物,积分将会加1,蛇长度会加1,食物会消失并重新再渲染一个到页面
三、运行效果
四、思路及代码
tip: 完整代码可直接运行,在最后面!莫急
1. html的编写
html
<body>
<div id="btn">
<button class="start">开始</button>
<button class="stop">暂停</button>
<button class="reset">重新</button>
</div>
<p class="fraction">积分: <span class="integral">0</span></p>
<!-- 网格 -->
<div id="grid"></div>
</body>
2. css的样式
新建index.css文件,并用link导入index.html的head标签内。
具体样式:body和子元素的居中,按钮的外观
css
body {
width: 800px;
display: flex;
flex-direction: column;
margin: 60px auto;
align-items: center;
}
/* 按钮样式 */
button {
width: 150px;
display: inline-block;
padding: 15px 25px;
font-size: 24px;
cursor: pointer;
text-align: center;
text-decoration: none;
outline: none;
color: #fff;
background-color: #4CAF50;
border: none;
border-radius: 15px;
box-shadow: 0 9px #999;
margin: 0 5px 30px 0;
}
button:hover {
background-color: #3e8e41
}
button:active {
background-color: #3e8e41;
box-shadow: 0 5px #666;
transform: translateY(4px);
}
/* 积分 */
.fraction {
font-size: 30px;
}
3. 初始化地图
首先创建一个map.js,表示地图,并放入到index.html的body尾部。
思路:设置地图(网格)的行列数,以及封装一个渲染网格到页面的函数init。
函数:根据行列数去渲染多个div。再分别设置每行和每列的类名,css则编写类名对应的样式,然后将列渲染到行,行则渲染到grid标签内。
此外:①列在创建时还添加id名,是为了方便后期修改数据。②网格四周为墙,因此在css对四周的格子添加border边框即可。
map.js
// 设置行列数
let rows = 15, cols = 15
let gird = document.getElementById('grid')
// 说明:初始化网格
function init() {
// 创建行
for(let i = 0; i < rows; i++) {
let row = document.createElement('div')
row.className = 'row'
// 创建列
for(let j = 0; j < cols; j++) {
let col = document.createElement('div')
col.className = 'col'
// 后期修改数据需访问id
col.id = 'col-' + i + '-' + j
row.appendChild(col)
}
// 渲染到页面
gird.appendChild(row)
}
}
index.css
/* 行 */
.row {
display: flex;
}
.col {
width: 30px;
height: 30px;
border: 1px solid #3c3c3c;
}
/* 外围:墙 */
.row:first-child {
border-top: 5px solid #000;
}
.row:last-child {
border-bottom: 5px solid #000;
}
.col:first-child {
border-left: 5px solid #000;
}
.col:last-child {
border-right: 5px solid #000;
}
由于是初始化的函数,没被调用则不会执行。则需再创index.js;放在所有js标签的结尾,负责管理所有js代码。
index.js
// 初始化网格
init()
网格渲染完毕~
4. 创建食物
首先创建一个food.js,表示食物,并放到map.js下。
思路:设置食物的初始化位置,以对象格式存储行列及数值,即 let food = {x: 2, y: 2}, 接着封装一个食物随机的位置的函数randomFood。
randomFood函数:用随机数去定义行列数的随机位置范围,再次赋给食物的行和列,最后将食物渲染到页面。(可封装为一个函数renderFood)其中:还需设置食物位置不能出现蛇本身,具体后文会补充。
renderFood函数:根据当前食物的行列值,去找该值对应的div的id(这就是上文为什么要给div设置id的原因),找到后给其赋类名为food, 同时不能丢失原来的网格的类名col。最后到css给该类名设置背景颜色即是食物。
food.js
// 食物初始值
let food = {x: 2, y: 2}
// 说明:随机食物的变化
// 食物初始值
let food = {x: 2, y: 2}
// 说明:随机食物的变化
function randomFood() {
// 只能出现在网格内的随机位置
let x = Math.floor(Math.random() * rows)
let y = Math.floor(Math.random() * cols)
// 再次更新位置
food = {x: x, y: y}
// 渲染食物到页面
renderFood()
}
// 说明:渲染食物到页面
function renderFood() {
// 根据当前的行列值,找到该值对应的div的id
let div = document.getElementById('col-' + food.x + '-' + food.y)
// 它即为食物,同时不能丢失原来的列名
div.className = 'col food'
}
index.css
.food {
background-color: #dd2323; // 红色
}
效果则需在index.js内的其他代码下,调用该函数
index.js
// 随机食物的位置
randomFood()
食物渲染完毕~
5. 创建蛇
首先创建一个snake.js,表示蛇,并放到food.js下。
思路:设置蛇的初始位置,以数组内为对象格式存储行列及数值,即 let snake = [{x: 8, y: 6}, {x: 8, y: 5}, {x: 8, y: 4}], 接着封装一个渲染蛇到页面的函数renderSnake。
renderSnake函数:根据当前蛇长度,去找该值对应的div的id,找到后给其赋类名为snake, 同时不能丢失原来的网格的类名col。最后到css给该类名设置背景颜色即是蛇。
snake.js
// 蛇的初始位置
let snake = [{x: 8, y: 6}, {x: 8, y: 5}, {x: 8, y: 4}];
// 说明:渲染食物到页面
function renderSnake() {
for(let i = 0; i < snake.length; i++) {
// 根据当前的行列值,找到该值对应的div的id
let div = document.getElementById('col-' + snake[i].x + '-' + snake[i].y)
// 找到后即赋类名为蛇,同时不能丢失原来的网格类名
div.className = 'col snake'
}
}
index.css
.snake {
background-color: #3c3c;
}
效果则需在index.js内的其他代码下,调用该函数
index.js
// 蛇的位置
renderSnake()
蛇渲染完毕~
接下来是关于游戏开始出现的各种情况啦!!!
6. 根据蛇头的方向去走动
返回到snake.js文件,继续封装函数。
思路:先定义蛇头方向为右,再去封装一个根据蛇头方向去走动的函数updateSnake。
updateSnake函数:先获取蛇数组的蛇头坐标,存进一个对象,根据蛇头方向去改变对象的x或y坐标,再将新的蛇头加到原数组前面,以及移除蛇尾。
说直接点:蛇根据方向去走动,走前一步即把该步更换为蛇头,蛇尾则删除一节。
snake.js
// 蛇头位置
let direction = 'right'
function updateSnake() {
// 获取蛇头的坐标
let head = { x: snake[0].x , y: snake[0].y}
// 根据蛇头的方向去改变坐标位置
switch(direction) {
case "up":
head.x--; // 向上移动一格
break;
case "down":// 向下移动一格
head.x++;
break;
case "left":// 向左移动一格
head.y--;
break;
case "right":// 向右移动一格
head.y++;
break;
}
// 把新蛇头添加原数组前面
snake.unshift(head)
// 移除蛇尾
snake.pop()
}
测试:先用定时器执行蛇,是否按蛇头方向走动。
index.js
setInterval(function(){
renderSnake() // 更新蛇的位置
updateSnake() // 根据蛇头的方向去走动
},1000)
测试后会发现有小bug,蛇会一直变长,是因为蛇在更新前没有清掉原来的蛇,因此会一直叠加。解决办法:先注释该定时器,再看第 7
7. 清空原来的蛇
返回到snake.js文件,继续封装函数。
思路:根据蛇的长度,去获得对应的div的id,将其格子去掉snake的类名,最后封装为一个函数clearSnake。
snake.js
// 说明:蛇走动时,清掉原来的蛇
function clearSnake() {
for (let i = 0; i < snake.length; i++) {
// 获取原来的蛇的位置
let div = document.getElementById('col-' + snake[i].x + '-' + snake[i].y)
// 清掉该蛇,相当于不写类名snake,同时不能丢失网格的类名col
div.className = 'col'
}
}
测试:继续用定时器执行蛇,成功运行。
index.js
setInterval(function(){
clearSnake()
updateSnake() // 根据蛇头的方向去走动
renderSnake() // 更新蛇的位置
},1000)
等蛇头走到墙,会发现控制台报错,是因为还没设置游戏机制:蛇头撞到墙,游戏到此结束。
8.蛇头撞到墙
起因:蛇头撞到墙,说明已超出网格的范围,没有网格(div)给蛇占位了,此时蛇要更新位置会找不到该元素,返回null,而null被赋类名,因此会报错。或者根据定时器得出报错信息: Cannot set properties of null (setting 'className')看是第几行有问题。
思路:在snake.js的更新蛇位置的函数内,做一个判断,若蛇获得的位置是null,即是撞墙了,那么就返回false出去,不是的话就继续赋类名。而index.js则对该函数做一层判断,将它的返回值存在一个变量,若变量为flase,表示撞墙,则游戏结束。(若是测试的话则假设清除定时器和提示用户)
snake.js
// 判断是否撞墙了
if(div !== null) {
// 找到后即赋类名为蛇,同时不能丢失原来的网格类名
div.className = 'col snake'
}else {
return false
}
测试:继续用定时器执行蛇。
index.js
let gameOver = false
let flag
let timer = setInterval(function(){
clearSnake()
updateSnake() // 根据蛇头的方向去走动
flag = renderSnake() // 更新蛇的位置
if(flag === false) {
clearInterval(timer)
alert('你输了,游戏结束')
}
在此会发现蛇没办法控制上下左右,一直向右走,因此需要借助方向键去控制蛇。看第9。
9.以方向键去控制蛇头的值
思路:index.js监听键盘事件,触发则调用snake.js的方向键改变蛇头方向的函数keydownHandler并传键值。 而snake.js封装一个函数,根据键值去修改蛇头的方向值。(方向值:38=>上、39=>右、40=>下、37=>左)
index.js
// 监听键盘事件
document.addEventListener('keydown', function(e) {
keydownHandler(e.keyCode)
})
snake.js
// 以方向键去控制蛇头的值
function keydownHandler(val) {
switch(val) {
case 38: // 上
direction = "up";
break;
case 40: // 下
direction = "down";
break;
case 37: // 左
direction = "left";
break;
case 39: // 右
direction = "right";
break;
}
}
此时看运行效果,蛇可以根据用户的方向键开始移动了,那下一步就是看蛇是否吃到食物了。
10.蛇吃到食物
若蛇移动的位置是食物的位置,表示吃到食物,那么蛇长度加1,原食物消失,重新渲染食物,积分要加1。
总思路:(在snake.js的updateSnake的函数里)根据蛇头的方向去走动时,在移除蛇尾时做一层判断,若蛇头的位置和食物的位置一致,则表示吃到食物了,不移除蛇尾,积分要做加1的操作,若位置不一致则移除蛇尾。
积分思路:新建integral.js文件,放到snake.js和DOM元素结束之间都可。内部定义变量初值为0,以及一个函数,每调用一次,变量就会加1,并渲染到DOM元素上。
integral.js
let integral = document.querySelector('.integral')
let sum = 0
// 每调用一次,变量就会加1,并渲染到DOM元素上
function add() {
sum++
integral.innerText = sum
}
snake.js
// 判断是否吃到食物
if (head.x == food.x && head.y == food.y) {
// 积分要加1的操作
add()
// 吃到则重新渲染食物且不移除蛇尾
randomFood()
}
else {
snake.pop() // 移除蛇尾
}
游戏内部功能基本完成,接下来是游戏过程了。
11. 游戏过程
先把index.js原来的定时器删除掉,再去创建一个process.js,表示进程,并放到snake.js下。
思路:index.js声明全局变量来判断游戏是否正在进行状态,接着封装一个函数到process内,若是进行时,则执行清空蛇原来的位置、更新蛇的位置、更新食物位置的变化。若执行完毕需判断蛇头的情况,更新蛇的位置的返回值若为false,则提示用户并做一步退出游戏操作的处理,反之用延时器1秒后继续以上操作。(这就是为什么要在更新蛇的位置的函数内接收一个false返回值的原因了)
游戏退出操作(函数):①游戏状态应该为true,表示游戏已经结束了。②提示用户失败的原因。③重新刷新页面,让用户再玩一次。
index.js
// 游戏是否结束
let gameOver = false
process.js
// 游戏循环
function gameLoop() {
// 若进行时
if(!gameOver) {
// 清空蛇原来的位置
clearSnake()
// 更新蛇的位置
updateSnake()
// 渲染蛇的位置
let flag = renderSnake()
// 执行完毕后,判断蛇的位置的情况
if(flag === false) {
failure()
}else {
setTimeout(gameLoop, 600)
}
}
}
// 说明:游戏结束
function failure() {
// 游戏状态
gameOver = true
// 提示用户已失败
alert('游戏结束')
// 重新刷新页面再玩一局
location.reload()
}
由于函数没被调用,想要看效果则需在index.js调用
index.js
gameLoop()
测试完毕后记得注释或删除该行!
12. 三大按钮
总思路:process内获取三大按钮的DOM元素和封装三个函数,而index.js根据点击不同的按钮会进入不同的函数。
process.js
let btn = document.getElementById('btn')
let start = btn.querySelector('.start')
let stop = btn.querySelector('.stop')
let reset = btn.querySelector('.reset')
// 说明:点击开始按钮执行的函数
function startHandler() {
}
// 说明:点击暂停按钮执行的函数
function stopHandler() {
}
// 说明:点击重新按钮执行的函数
function resetHandler() {
}
index.js
// 注意'='后面已经是一个函数了,无需加()
// 4-1.游戏开始
start.onclick = startHandler
// 4-2.游戏暂停
stop.onclick = stopHandler
// 4-3.游戏重新
reset.onclick = resetHandler
开始按钮的函数
思路:游戏状态为正在进行时->游戏过程(测试的函数)->设置开始按钮是禁用的->暂停按钮是可点击的。
process.js
// 说明:点击开始按钮执行的函数
function startHandler() {
// 状态:正在进行时
gameOver = false
// 游戏过程
gameLoop()
// 开始按钮禁用
start.disabled = true
// 暂停按钮开启
stop.disabled = false
}
可直接去页面运行效果
暂停按钮的函数
思路:游戏状态假设为结束状态->设置开始按钮可点击->暂停按钮是禁用的。
process.js
// 说明:点击暂停按钮执行的函数
function stopHandler() {
// 游戏假设为结束状态
gameOver = true
// 开始按钮可点击
start.disabled = false
// 暂停按钮禁用
stop.disabled = true
}
可直接去页面运行效果
重新按钮的函数
思路:调用js内置函数的刷新页面函数,相当于按f5。
process.js
// 说明:点击重新按钮执行的函数
function resetHandler() {
// 刷新页面
location.reload()
}
可直接去页面运行效果
13. 细节:食物不能出现在蛇身上
思路:渲染食物的函数,当食物获取随机位置的行列数后,进一步用while不断去调用是否在蛇身上的函数,重新获取行列数,直到找到一个不在蛇身体上的坐标为止。
是否在蛇身上的函数:接收外部行列参数,遍历蛇本身,根据蛇每一部分去判断是否和行列一致,是则表示食物在蛇身上,返回true,循环结束都发现不一致,表示不是在蛇身上,则返回false。
food.js
// 说明:判断食物的位置是否出现会出现在蛇的每个身上
function isSnakeBody(x, y) {
for(let i = 0; i < snake.length; i++) {
if(x === snake[i].x && y === snake[i].y) {
return true // 说明是在蛇身上了
}
}
return false // 说明不是在蛇身上了
}
// randomFood()函数内,获取随机位置后的补充代码
// 不能在蛇身体生成食物
// 不断尝试生成新的坐标,直到找到一个不在蛇身体上的坐标为止
while(isSnakeBody(x,y)) {
x = Math.floor(Math.random() * rows)
y = Math.floor(Math.random() * cols)
}
14. 细节:蛇头不能碰到蛇身
思路:snake.js封装一个函数isSnakeHitSelf,判断蛇头是否会等于每一个蛇身,是的话返回true,不是则返回false。再到process.js的游戏循环函数,找出关于判断蛇位置的情况,再添加一个判断条件,调用isSnakeHitSelf,若返回true则表示碰到自己,那么需结束游戏。
snake.js
// 说明:判断蛇头是否会等于每一个蛇身
function isSnakeHitSelf() {
for(let i = 1; i < snake.length; i++) { // 蛇头的下一个开始算起
if(snake[0].x === snake[i].x && snake[0].y === snake[i].y){
return true
}
}
// 蛇头不会碰到自己
return false
}
process.js
// gameLoop函数内
else if(isSnakeHitSelf()) { // 蛇头是否碰到蛇身
failure()
}
15. 细节:暂停按钮的禁用
思路:游戏还没有开始,不让用户点击暂停按钮,在获取暂停按钮元素后,直接将它的状态设为禁用。
process.js
// ... 获取DOM元素
stop.disabled = true
16. 细节:提示用户失败原因
思路:在游戏正在进行时,利用一个字符串变量,在蛇撞墙或碰到自己时,给变量赋不同的文字,再传入游戏结束函数。游戏结束函数则修改弹出信息为形参值。
process.js
// gameLoop函数内的修改
// 提示词
let point = ''
if(flag === false) { // 是否撞墙
point = '蛇头撞到墙了,游戏结束'
failure(point)
}
else if(isSnakeHitSelf()) { // 蛇头是否碰到蛇身
point('蛇头碰到蛇身,游戏结束')
failure(point)
}
// failure函数内的修改
function failure(point) { // 接收参数
alert(point) // 修改传入的值
}
17. 细节:两个刷新页面的代码
在process.js函数内,会发现有两个刷新页面的代码:location.reload(),是游戏结束和重新按钮的,因此在游戏结束后可直接调用重新按钮的函数。
process.js
// 说明:游戏结束
function failure(point) {
// 重新刷新页面再玩一局
resetHandler()
}
18.拓展
本案例没有清除食物的函数,因为在蛇吃到食物后,调用食物更新位置,也就是食物原来的位置被覆盖掉,重新渲染新的位置。若有需要的可参考以下代码。
js
// 说明:清除食物位置标记
function clearFood() {
// 根据当前蛇节的坐标获取对应的格子元素
let cellElement = document.getElementById("col-" + food.x + "-" + food.y);
cellElement.className = "col";
}
五、源代码
1.gitee官网直接搜:@mini25的demo合集的原生js的snake文件 2.点击链接跳转到我的仓库 原生js · mini25/demo合集 - 码云 - 开源中国 (gitee.com)
六、致谢
感谢读者能看到这里,如果你对文章中的某些内容感到困惑或不理解,可以在评论区提问,我会尽力为你解答。此外,你也可以分享自己的观点和看法,与其他读者进行交流和讨论。
七、总结
第一次写文章的经历是一次富有挑战性和收获的旅程!!!