本篇文章带你从零实现一个前端网页贪吃蛇小游戏(js,h5,css)
游戏开源地址:GHW666666/tanchishe
废话不多说我们先看效果图
整体布局大概可以分为两部分左边的按钮盒子与右边的地图盒子
step1
千万不要忽略把每个盒子的内外边距设置成0
*{
margin: 0;
padding: 0;
}
下面我们来一一讲述开发小细节
左布局思路
.btbox{
top: 20px;
left: 100px;
width: 200px;
height: 500px;
position: fixed;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.bt{
height: 60px;
width: 150px;
background-color: orange;
color: black;
font-size: large;
border-radius: 20px;
background: url(./bt2.png) no-repeat;
}
左边的布局比较简单一个div盒子标签装三个按钮盒子,三个按钮利用弹性布局设置主轴为列,盒子在父容器中沿主轴平均分配空间于是我们看到了这个效果
设置好左边的btbox后我们来设置map盒子
右布局思路
map样式很重要,至于为什么,我们来思考这几个问题
1.map类的盒子里有什么
很清楚,答案是蛇和食物
我们先来解决蛇的问题(问题2-4)
2.如何让蛇和食物创建的时候固定在这个map盒子里
楼主的方法比较简单,各类样式和类如下,首先我们定义map样式,然后再定义map样式的层叠样式.head 和 .food 类的 <div> 元素会“继承” .map div 的 width,height,position,和 border-radius 属性,然后应用它们自己的特定样式(如 background 和 right)
然后在Food类与Snake类中创建一个this.map属性利用构造函数把地图样式.map传给这个属性,这样我们后面使用appendChild函数便能实现在creatElement函数时保证创建的food与sanke标签一定在.map盒子内部,我们来解释解释这个函数
this.map.appendChild(this.food): 将新创建的 <div> 元素(即 this.food)作为子元素添加到 this.map 所引用的元素中。这意味着 this.food 将会出现在 this.map 的内部。
这样我们的food与snake都是div标签而且一定创建在 <div class="map"></div>盒子的内部,
我们来看看串代码
ok创建确实能创建food与snake,但很重要很重点的一个问题,
3.你如何保证你的蛇(div)每一个部位是拼接在一起的 就是连成这样
我们知道创建蛇(div)的时候会创建成这样
<div class="map">
<div class="food"></div>
<div class="head"></div>
<div class="head"></div>
<div class="head"></div>
</div>
但是div是块级元素,按照文档流会占据父类map的整行,所以在不设置map样式的position属性时,蛇会创建成这样
一个蛇身体(div)占据一行所以我们把.map样式与.map div样式脱离文档流设置fixd属性与absoulte属性
这是这个小游戏很重要的小细节
.map{
width: 1300px;
height: 600px;
position: fixed;
bottom: 0px;
right: 30px;
top: 10%;
box-shadow: 0px 0px 50px black;
}
.map div{
width: 20px;
height: 20px;
/* position: absolute; */
border-radius: 50%;
background:-webkit-repeating-radial-gradient(yellow,orange) ;
background: url("body.png");
}
div.head{
background: -webkit-repeating-radial-gradient(orange,yellow);
background: url("body.png") ;
}
div.food{
right: 0px;
background: -webkit-repeating-radial-gradient(rgb(35, 244, 3),white);
background: url(food.png);
}
现在想清楚了蛇正常的创建,各位大佬我们来想想,
4.如何让蛇一格一格的在map里移动
其实也很简单,map样式的宽和高一定要设置成 .map div样式的整数倍,小编这里设置的1300600像素的地图,而蛇的head与食物food就是 .map div标签的width与height 2020像素,这样我们才能保证食物与蛇在地图中按照一个75*30的格子进行移动
这里先简单回答后面我们再详细算算位置坐标
5.如何让食物在地图里随机移动
这个我们我们来详细解析Food类
Food类
ok,虽然上图暴露了Food类与Snake类,但还是大佬们还是思考一下,Food类包含什么属性
首先Food类肯定要有map属性来接收map盒子的大小范围,再用x,y坐标定位food的位置,其次蛇在吃到食物时一定要改变当前的食物的坐标,重新创建一个新的食物,这样随机foodMoov位置函数也确定,这样Food类便出来了
有了设计思路但是我们仔细想想foodMove如何实现呢
class Food{
constructor(select){
this.map=document.querySelector(select)
this.food=document.createElement("div")
this.map.appendChild(this.food)
this.food.className="food"
this.x=0
this.y=0
this.foodMove()
}
foodMove(){
const w=this.map.clientWidth/20
const h=this.map.clientHeight/20
let n1=Math.floor(Math.random()*w)
let n2=Math.floor(Math.random()*h)
this.x=n1*20
this.y=n2*20
this.food.style.left=this.x+"px"
this.food.style.top=this.y+"px"
}
}
我们来解释一下这段foodMove代码
-
获取父容器尺寸:
const w=this.map.clientWidth/20:这行代码计算了父容器this.map的宽度(clientWidth),然后将其除以20。这个计算结果的目的是将父容器的宽度划分为20等份,每份的宽度存储在变量w中。const h=this.map.clientHeight/20:类似地,这行代码计算了父容器的高度(clientHeight),并将其划分为20等份,每份的高度存储在变量h中。
-
生成随机位置:
let n1=Math.floor(Math.random()*w):通过Math.random()生成一个0到1之间的随机数,然后乘以w(每份的宽度),最后使用Math.floor()向下取整,得到一个介于0到w-1之间的随机整数n1。这个整数代表food元素应该移动到的列数(假设父容器被划分成了20列)。let n2=Math.floor(Math.random()*h):类似地,这行代码生成一个介于0到h-1之间的随机整数n2,代表food元素应该移动到的行数。
-
计算新位置:
this.x=n1*20:由于父容器被划分成了20等份,每份的宽度是w,因此food元素的新X坐标(水平位置)是其列数n1乘以每份的宽度(20像素)。this.y=n2*20:类似地,food元素的新Y坐标(垂直位置)是其行数n2乘以每份的高度(20像素)。
-
设置新位置:
this.food.style.left=this.x+"px":将food元素的left样式属性设置为计算出的新X坐标,以像素为单位。this.food.style.top=this.y+"px":将food元素的top样式属性设置为计算出的新Y坐标,以像素为单位。
其实这里有个小细节,如果大家没有获取父容器的尺寸,直接用this.x=Math.floor(Math.random()*this.map.clientWidth)会导致很严重的错误,就是因为Math.random()函数生成的是0-1的随机浮点数,这就导致了我们生成的食物坐标出现这种情况--this.x=Math.floor(0.11x1300)=143px ,很明显这个坐标不符合20的倍数这个情况,这样我们的蛇永远不会吃到食物
Food类只要解决foodMove移动的坐标这个难题就行了,难的其实是蛇类
Snake类
Snake是最复杂的一个类,我们来先看代码
class Snake{
constructor(select){
this.map=document.querySelector(select)
this.direction="right"
this.snakelist=[]
this.createsanke()
this.move()
}
createhead(){
const head=this.snakelist[0]
console.log(head)
const pos={x:0,y:0}
if(head){
switch(this.direction){
case "right":
pos.x=head.offsetLeft+20
pos.y=head.offsetTop
break;
case "left":
pos.x=head.offsetLeft-20
pos.y=head.offsetTop
break;
case "top":
pos.x=head.offsetLeft
pos.y=head.offsetTop-20
break;
case "bottom":
pos.x=head.offsetLeft
pos.y=head.offsetTop+20
break;
}
head.className="body"
}
const div=document.createElement("div")
div.className="head"
this.snakelist.unshift(div)
div.style.left=pos.x+"px"
div.style.top=pos.y+"px"
this.map.appendChild(div)
}
createsanke(){
for(let i=0;i<4;i++){
this.createhead()
}
}
move(){
const body=this.snakelist.pop()
body.remove()
this.createhead()
}
isEat(foodx,foody){
const head=this.snakelist[0]
head.x=head.offsetLeft
head.y=head.offsetTop
if(head.x===foodx&&head.y===foody){
return true
}else{
return false
}
}
isdie(){
const head=this.snakelist[0]
const w=this.map.clientWidth/20
const h=this.map.clientHeight/20
if(head.offsetLeft<0||head.offsetLeft>=w*20||head.offsetTop<0||head.offsetTop>=h*20){
return true
}else{
return false
}
}
}
构造函数 constructor(select)
- 接收一个选择器字符串
select,用于选取游戏区域(地图)的DOM元素,并将其存储在实例的map属性中。 - 初始化蛇的移动方向为向右(
"right")。 - 初始化一个空数组
snakelist,用于存储蛇的身体各部分(<div>元素)。 - 调用
createhead方法创建蛇的头部,并重复调用4次来初始化一条由4个部分组成的蛇。 - 调用
move方法使蛇开始移动。
方法 createhead()
- 用于创建蛇的头部(或新的身体部分,当蛇吃下食物时)。
- 如果蛇已经有头部(即
snakelist不为空),则根据当前移动方向计算新头部的位置。 - 创建一个新的
<div>元素作为新的头部,设置其类名为"head",并更新snakelist数组,将新头部添加到数组开头。 - 将新头部添加到游戏区域(
this.map)中,并根据计算出的位置设置其样式。
方法 createsanke()
- 循环调用
createhead方法4次,以初始化一条由4个部分组成的蛇。
方法 move()
- 从
snakelist数组中移除蛇的尾部(即数组的最后一个元素),并从DOM中删除对应的元素。 - 调用
createhead方法在蛇的头部前面创建一个新的头部,使蛇向前移动。
方法 isEat(foodx, foody)
- 接收食物的位置坐标(
foodx,foody)。 - 获取蛇的头部元素,并获取其当前位置。
- 判断蛇的头部是否与食物重合,如果重合则返回
true,表示蛇吃到了食物;否则返回false。
方法 isdie()
- 检查蛇是否撞到了游戏区域的边界。
- 获取蛇的头部元素和游戏区域的宽度、高度(划分为20等份后的值)。
- 判断蛇的头部位置是否超出了游戏区域的边界,如果超出则返回
true,表示蛇死亡;否则返回false。
各位大佬我们其实最主要讨论这几个问题
1.蛇什么时候需要调用创建蛇的函数--creathead()
- creathead函数内部实现值得细细琢磨
其实仔细想想,蛇就两次需要新创建 第一是吃到食物 第二是初始化一条小蛇,还有一处是改变蛇的方向(这个我们到Game类讨论)
知道了这两处,我们先来说说吃到食物时如何创建蛇 来先看代码
我们先来看看这串数组的作用--snakelist[]这个数组是储存蛇的每一个部位,包括蛇头与蛇身(div),我们先说结论,每次创建新蛇的部位都会把这个部位插入这个数组的开头-snakelist[0],而且我们只创建蛇头,使用每次创建蛇头会先把蛇头的class改为body,然后把老蛇头,body往数组后面推,再通过方向判断新蛇头应该插入的像素点,所以蛇在屏幕上的蛇头和蛇尾就是这个数组的开头与数组末尾。我们来看这张图
我们有了snakelist[0]便能更好的操作屏幕上的蛇的移动与创建
为什么一定要把蛇头放在snakelist[0]呢,而不是push到末尾,大佬们可以细细思考这个问题
第二种便是初始化一条小蛇,原理也很简单,你想创造初始化多长的蛇就调用几次 creathead函数就行了
2.如何让蛇在屏幕上有视觉上的移动
视觉上的移动,大佬们想想,如果把蛇数组snakelist最后一个dom元素删掉,再重新创建一个蛇头,这样在屏幕中是不是会出现这样的画面--蛇尾减少一个div,蛇头增加一个div,如果一直执行,这样是不是就成功造成了视觉上的移动
3.如何通过上下左右改变蛇的方向
这要用到我们上述的snake.direction这个属性,大佬们想想,我们是不是能设置一个监听函数,在按下开始按钮后,监听键盘事件,再用switch case来改变蛇的方向是不是就能实现了,我们来看代码
这时有人问为什么改变了directio属性蛇就会按照你想要的方向走呢,不急,我们看到Gme类就能解决这个问题了
4.判断isdie与isEat时一定是蛇头撞墙或者吃到食物,那么如何保证拿到的一定是蛇头(div)坐标,而不是蛇其他部位
其这就是我们上个问题的一部分原因,我们只要拿到蛇头的位置就是snakelist[0]--一定是蛇头的位置这样我们就能准确判断蛇头位置与食物和蛇头与墙的位置是否重合从而判断是isEat或者isdie
最难的蛇类解决了,我们只要把上述的各种dom结合game类便能决定游戏进程了
game类
我们先来看Game类是如何决定游戏进程的
<div class="btbox">
<button class="bt" id="start">开始</button>
<button class="bt" id="pause">暂停</button>
<button class="bt" id="restart">结束</button>
<audio class="music" src="music.mp4"></audio>
</div>
<div class="map">
</div>
<script src="food.js"></script>
<script src="snake.js"></script>
<script src="game.js"></script>
<script>
const game = new Game(".map")
const start = document.getElementById("start")
const pause = document.getElementById("pause")
const restart = document.getElementById("restart")
const music = document.querySelector(".music")
start.onclick = function(){
game.start()
music.play()
}
pause.onclick = function(){
game.pause()
music.pause()
}
restart.onclick = function(){
game.restart()
music.pause()
}
document.onkeydown = function(e) {
switch(e.keyCode){
case 37:game.change("left")
break;
case 38:game.change("top")
break;
case 39:game.change("right")
break;
case 40:game.change("bottom")
break;
}
}
</script>
-
- 当大佬们访问这个网页时,浏览器会加载HTML文档。这个文档包含了一个游戏区域(
<div class="map"></div>),以及三个按钮(开始、暂停、结束)和一个音频元素。
- 当大佬们访问这个网页时,浏览器会加载HTML文档。这个文档包含了一个游戏区域(
-
JavaScript文件加载:
- 在HTML文档的底部,通过
<script>标签引入了三个外部JavaScript文件:food.js、snake.js、game.js。
- 在HTML文档的底部,通过
-
实例化游戏对象:
- 在加载完外部JavaScript文件后,脚本会创建一个
Game类的实例,并将游戏区域(.map)作为参数传递给这个实例。这允许游戏逻辑能够访问和操作游戏区域。
- 在加载完外部JavaScript文件后,脚本会创建一个
当我们加载完js文件后,实例化了game,把map样式的div标签传给了game.map属性,随后在game类里面初始化了food与snake这,用三个变量start,restart,pause绑定了三个按钮
-
事件监听器设置:
- 接下来,脚本为开始、暂停、结束按钮添加了点击事件监听器。当大佬们点击这些按钮时,会触发相应的事件处理函数,这些函数会调用
Game实例上的start、pause、restart方法,并相应地播放或暂停背景音乐。
- 接下来,脚本为开始、暂停、结束按钮添加了点击事件监听器。当大佬们点击这些按钮时,会触发相应的事件处理函数,这些函数会调用
当按下按钮后,我们设置了监听函数,当键盘敲下上下左右时,监听函数会依据对应的阿思可码值去改变蛇的方向
-
键盘事件监听器设置:
- 脚本还设置了一个全局的键盘事件监听器,用于监听用户的键盘输入。当用户按下键盘上的箭头键时,会根据按下的键(通过
e.keyCode判断)调用Game实例上的change方法,改变蛇的移动方向。
- 脚本还设置了一个全局的键盘事件监听器,用于监听用户的键盘输入。当用户按下键盘上的箭头键时,会根据按下的键(通过
-
游戏启动:
- 当大佬们点击“开始”按钮时,会触发
start按钮的点击事件处理函数。这个函数会调用Game实例的start方法,启动游戏。此时,游戏区域中可能会出现蛇和食物,游戏逻辑会开始运行,蛇会根据大佬们的输入或预设的逻辑移动。
- 当大佬们点击“开始”按钮时,会触发
-
游戏进行:
- 在游戏进行过程中,大佬们可以通过键盘控制蛇的移动方向。游戏逻辑会不断更新蛇的位置,检查蛇是否吃到了食物、是否撞到了自己或游戏区域的边界等,并根据这些情况更新游戏状态。
-
游戏暂停与结束:
- 大佬们可以通过点击“暂停”按钮来暂停游戏,此时游戏逻辑会停止运行,但游戏状态会被保留。点击“结束”按钮会触发游戏的重启逻辑,游戏会回到初始状态,背景音乐也会被暂停。
class Game{
constructor(select){
this.map=document.querySelector(select)
this.food=new Food(select)
this.snake=new Snake(select)
this.timer=0
}
start(){
this.timer=setInterval(()=>{
this.snake.move()
if(this.snake.isEat(this.food.x,this.food.y)){
this.snake.createhead()
this.food.foodMove()
}
if(this.snake.isdie()){
clearInterval(this.timer)
}
},100)
}
pause(){
clearInterval(this.timer)
}
restart(){
window.location.reload()
}
change(type){
this.snake.direction=type
}
}
其实看完Game类,我们也能发现这个类的作用其实是继集成了Food与Snake类以及决定了游戏的进程
我们重点来看看为什么改变了蛇这个direction后就能改变蛇的走向呢
首先监听到键盘的改变方位命令后,我们调用了Game类这个函数改变方向
随后Game中snake属性的direction属性跟着改变,但我们设置的定时器this.timer还在执行
我们看到执行的方法存在snake.move()这个函数,我们重新回到move函数
move函数原理是把蛇尾删掉重新创建一个蛇头,所以我们回到执行creathead函数
我们改变方向的时候,蛇头已经存在所以我们会进入方向的判断,我们按照相应的方向创造pos{x,y} 坐标的值
并且把蛇头head-snakelist[0]的class改为body,这样我们是不是不仅把老蛇头改为body,而且在对应老蛇头的坐标上创造了新蛇头的坐标
大佬们现在是不是更加感受到把蛇头放在snakelist[0]的妙处了
本游戏素材均由ai绘制
最后大佬们我们最后看看效果图(不得不承认ai的绘画技术越来越强大了)