js已经学了大部分了,现在就利用我所学的js知识试试做贪吃蛇小游戏吧
以下部分相关图片以及思路笔记均出自渡一陈老师的视频
首先制作简单的静态页面,添加贪吃蛇移动的背景和相关图片,比如开始游戏等等
将各个功能均封装在函数中,利用主函数调用分函数会使结构更清晰
初始化游戏
初始化地图
地图坐标
//1.初始化地图
for(let i=0;i<tr;i++){
for(let j=0;j<td;j++){
gridData.push({
x:j,
y:i
})
}
}
绘制蛇
- 在游戏相关配置文件(js文件)中定义蛇的身体大小,以及蛇的相关位置信息(位置坐标,对应身体还是头部标志,身体元素相关元素标签)
- 在index.js文件中遍历snake的位置信息,若domContent(身体元素相关元素标签)为空,就添加元素标签,给标签设置定位,并设置每个元素的left和top为身体大小*x或者*y,并且判断身体元素是蛇头还是身体,是蛇头就添加蛇头的背景,是身体就添加背景颜色并设置圆角边框
- 判断是否第一次创建蛇后,在容器中append创建的元素
// 蛇的身体大小
let snakeBody=20
// 蛇相关的配置信息
let snake={
// 蛇的初始位置
snakePos:[//domContent为蛇的相应dom元素 flag根据身体还是蛇头分别渲染
{x:0,y:0,domContent:"",flag:'body'},
{x:1,y:0,domContent:"",flag:'body'},
{x:2,y:0,domContent:"",flag:'body'},
{x:3,y:0,domContent:"",flag:'head'}
]
}
/**
*绘制蛇的方法
* @param {*} snake
* */
function drawSnake(snake){
for(let i=0;i<snake.snakePos.length;i++){
if(!snake.snakePos[i].domContent){
// 如果进入此if,说明是第一次创建蛇
snake.snakePos[i].domContent=document.createElement("div")
snake.snakePos[i].domContent.style.position="absolute"
snake.snakePos[i].domContent.style.width=snakeBody+"px"
snake.snakePos[i].domContent.style.height=snakeBody+"px"
snake.snakePos[i].domContent.style.left=snake.snakePos[i].x*snakeBody+"px"
snake.snakePos[i].domContent.style.top=snake.snakePos[i].y*snakeBody+"px"
if(snake.snakePos[i].flag==='head'){
// 说明当前是蛇头
snake.snakePos[i].domContent.style.background=`url("../images/snake.png") center/contain no-repeat`
}else{
// 说明是蛇身
snake.snakePos[i].domContent.style.background="#9ddbb1"
snake.snakePos[i].domContent.style.borderRadius='50%'
}
}
// 需要将创建的DOM元素添加到container 容器上面
document.querySelector(".container").append(snake.snakePos[i].domContent)
}
}
添加几个蛇身后效果如图所示:
绘制食物
- 其过程和绘制蛇过程基本类似,先在配置文件中声明食物的相关配置信息,食物对象中不需要标志元素
- 要求食物不可出现在蛇头蛇身以及container外面区域,生成随机数在while中循环无限循环,直到坐标符合条件退出循环,利用一个变量判断是否需要退出循环
- 生成正确的坐标后,就创建一个新的变量,设置绝对定位,宽高,背景,最后append到container中,当然,要先判断food.domContent是否为空,为空才可创建,创建之后只是改变食物的位置,不会再继续创建元素,故设置食物的elft和top要在判断条件外执行
// 整个网格的行与列
const tr=30
const td=30// 食物相关的配置信息
let food={
x:0,y:0,domContent:""
}function drawFood(){
// 食物的坐标是随机的 但食物不可生成在蛇头 蛇身 以及container之外的区域
while(true){
// 构成一个死循环,直到生成符合要求的坐标才能退出该循环
let isRepeat=false //默认生成的坐标符合要求
// 随机生成一个坐标
food.x=Math.floor(Math.random()*tr)
food.y=Math.floor(Math.random()*td)
// 查看坐标是否符合要求(遍历蛇)
for(let i=0;i<snake.snakePos.length;i++){
if(snake.snakePos[i].x===food.x&&snake.snakePos[i].y===food.y){
// 进入此if 说明当前生成的食物坐标和蛇的位置冲突
isRepeat=true
break
}
}
if(!isRepeat){
break
}
}
// 整个while 循环跳出之后,食物的坐标一定是OK的
if(!food.domContent){
food.domContent=document.createElement("div")
food.domContent.style.width=snakeBody+"px"
food.domContent.style.height=snakeBody+"px"
food.domContent.style.position="absolute"
food.domContent.style.background=`url("../images/food.png") center/contain no-repeat`
document.querySelector('.container').append(food.domContent)
}
food.domContent.style.left=food.x*snakeBody+"px"
food.domContent.style.top=food.y*snakeBody+"px"
}
绑定事件
- 按下键盘的上下左右时,蛇会上下左右移动,此时要添加相关配置信息,明确新蛇头和旧蛇头之间的位置关系,在确定新的蛇头坐标时,会用一个对象和旧蛇头进行计算
- 绑定事件keydown
- 同时在蛇的相关配置信息中添加方向
// 要明确新的蛇头和旧的蛇头之间的位置关系
// 我们在确定新的蛇头坐标的时候,会那下面的对象和旧蛇头做一个计算
const directionNum={
left:{x:-1,y:0,flag:'left'},
right:{x:1,y:0,flag:'right'},
top:{x:0,y:-1,flag:'top'},
bottom:{x:0,y:1,flag:'bottom'}
}
// 蛇相关的配置信息
let snake={
// 蛇一开始移动的方向
direction:directionNum.right,//一开始向右边移动
// 蛇的初始位置
snakePos:[//domContent为蛇的相应dom元素 flag根据身体还是蛇头分别渲染
{x:0,y:0,domContent:"",flag:'body'},
{x:1,y:0,domContent:"",flag:'body'},
{x:2,y:0,domContent:"",flag:'body'},
{x:3,y:0,domContent:"",flag:'head'}
]
}
/**
* 绑定事件
*/
function bindEvent(){
document.addEventListener('keydown',function(e){
// console.log(e.key)
if(e.key==='ArrowUp'||e.key.toLocaleLowerCase()==='w'){
// 用户按的是上
snake.direction=directionNum.top
}
if(e.key==='ArrowDown'||e.key.toLocaleLowerCase()==='s'){
// 用户按的是下
snake.direction=directionNum.bottom
}
if(e.key==='ArrowLeft'||e.key.toLocaleLowerCase()==='a'){
// 用户按的是左
snake.direction=directionNum.left
}
if(e.key==='ArrowRight'||e.key.toLocaleLowerCase()==='d'){
// 用户按的是右
snake.direction=directionNum.right
}
snakeMove()
})
}
移动蛇
- 声明一个新蛇头,新蛇头对象中包含snakePos中的所有,移动时要将新蛇头添加至snake.snakePos中,将旧蛇头变为蛇身,即把flag变为body,并将其相关属性变为身体的属性,最后重新绘制蛇即可
- 注意,蛇头会随着方向的变化而变化,在drawSnack中添加switch,若方向为上,蛇头逆时针旋转90deg,其余以此类推
- 碰撞检测:声明一个判断对象,里面包含两个判断,一个为是否吃到食物,一个判断是否碰撞墙壁或者碰到蛇身
- 碰到墙壁即为新蛇头的位置坐标越界,碰到蛇身即为判断新蛇头是否和蛇身位置坐标冲突,冲突就说明碰到自己,吃到东西则为新蛇头的坐标和食物的坐标一致,吃到食物之后就重新生成食物的位置坐标
- 蛇整体移动的过程为:每次触发键盘事件,蛇头改变,旧蛇头变为蛇身,若吃到苹果,不删除最后一个蛇身,若没有吃到苹果,则删除最后一个蛇身,即为snake.snakePos[0].domContent。
/***
* 碰撞检测
*/
function isCollide(newHead){
let collideCheckInfo={
isCollide:false,//是否碰撞墙壁 蛇身
isEat:false//是否吃到食物
}
// 1.检测是否碰到墙壁
if(newHead.x<0||newHead.x>=td||newHead.y<0||newHead.y>=tr){
collideCheckInfo.isCollide=true
return collideCheckInfo
}
// 检测是是否碰到自己
for(let i=0;i<snake.snakePos.length;i++){
if(snake.snakePos[i].x===newHead.x&&snake.snakePos[i].y===newHead.y){
collideCheckInfo.isCollide=true
return collideCheckInfo
}
}
// 检测是否吃到东西
if(newHead.x===food.x&&newHead.y===food.y){
collideCheckInfo.isEat=true
score++
}
return collideCheckInfo
}
/**
* 蛇的移动方法
*/
function snakeMove(){
let oldHead=snake.snakePos[snake.snakePos.length-1]
// 根据方向计算出新的蛇头的坐标
let newHead={
domContent:"",
x:oldHead.x+snake.direction.x,
y:oldHead.y+snake.direction.y,
flag:'head'
}
// 接下来我们首先要做碰撞检测
// 看计算出来的蛇头有没有碰上食物
let collideCheckResult=isCollide(newHead)
if(collideCheckResult.isCollide){
//进入此if 说明撞墙了
window.confirm(`游戏结束,您当前的得分为${score}分,是否要重新开始游戏`)
alert("撞墙了")
}
// 将旧的头修改为身体
oldHead.flag='body'
oldHead.domContent.style.background="#9ddbb1"
oldHead.domContent.style.borderRadius="50%"
snake.snakePos.push(newHead)
// 判断是否吃到东西
if(collideCheckResult.isEat){
// 1.重新生成新的食物
drawFood()
}
else{
// 说明没有吃到食物
// 移除最后一个元素
document.querySelector(".container").removeChild(snake.snakePos[0].domContent)
snake.snakePos.shift();//删除第一个元素
}
drawSnake(snake)
}
- 当游戏开始时,要让蛇自动沿着当前方向移动,使用间歇函数定时器来决定蛇移动的速度
- 给用户选择当游戏结束时,是继续游戏还是结束游戏,继续游戏就初始化游戏,结束游戏就取消键盘事件,并停止计时器
- 游戏进行中若点击container区域就先暂停游戏,暂停游戏时,要用到事件委托,因为当用户点击确定重新开始游戏时,会将container里的内容重置,此时的绑定事件已经不存在了,通过父亲找到儿子进行相关操作可避免直接给儿子添加绑定事件的缺点,暂停游戏时记得清除定时器,开始时再打开
相关效果如下所示:
代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/index.css">
<title>Document</title>
</head>
<body>
<!-- 整个游戏按钮 -->
<div class="container">
<!-- 开始按钮 -->
<button class="startBtn"></button>
<!-- 暂停按钮 -->
<button class="pauseBtn"></button>
</div>
<script src="../js/config.js"></script>
<script src="../js/index.js"></script>
</body>
</html>
index.css
*{
/* 去除内外边距 */
margin: 0;
padding: 0;
}
/* 整体游戏容器样式 */
.container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 600px;
height: 600px;
background-color: #225675;
border: 20px solid #7dd9ff;
margin: 20px auto;
}
.container button {
border: none;
outline: none;
}
/* 开始按钮 */
.startBtn {
width: 200px;
height: 80px;
background: url(../images/startGame.png) center/contain no-repeat;
display: block;
}
/* 暂停按钮 */
.pauseBtn {
width: 70px;
height: 70px;
background: url(../images/start.png) center/contain no-repeat;
display: none;
}
index.js
/**
*绘制蛇的方法
* @param {*} snake
* */
function drawSnake(snake){
for(let i=0;i<snake.snakePos.length;i++){
if(!snake.snakePos[i].domContent){
// 如果进入此if,说明是第一次创建蛇
snake.snakePos[i].domContent=document.createElement("div")
snake.snakePos[i].domContent.style.position="absolute"
snake.snakePos[i].domContent.style.width=snakeBody+"px"
snake.snakePos[i].domContent.style.height=snakeBody+"px"
snake.snakePos[i].domContent.style.left=snake.snakePos[i].x*snakeBody+"px"
snake.snakePos[i].domContent.style.top=snake.snakePos[i].y*snakeBody+"px"
if(snake.snakePos[i].flag==='head'){
// 说明当前是蛇头
snake.snakePos[i].domContent.style.background=`url("../images/snake.png") center/contain no-repeat`
// 根据方向进行一个旋转
switch(snake.direction.flag){
case 'top':{
snake.snakePos[i].domContent.style.transform=`rotate(-90deg)`
break
}
case 'bottom':{
snake.snakePos[i].domContent.style.transform=`rotate(90deg)`
break
}
case 'left':{
snake.snakePos[i].domContent.style.transform=`rotate(-180deg)`
break
}
case 'right':{
snake.snakePos[i].domContent.style.transform=`rotate(0deg)`
break
}
}
}else{
// 说明是蛇身
snake.snakePos[i].domContent.style.background="#9ddbb1"
snake.snakePos[i].domContent.style.borderRadius='50%'
}
}
// 需要将创建的DOM元素添加到container 容器上面
document.querySelector(".container").append(snake.snakePos[i].domContent)
}
}
function drawFood(){
// 食物的坐标是随机的 但食物不可生成在蛇头 蛇身 以及container之外的区域
while(true){
// 构成一个死循环,直到生成符合要求的坐标才能退出该循环
let isRepeat=false //默认生成的坐标符合要求
// 随机生成一个坐标
food.x=Math.floor(Math.random()*tr)
food.y=Math.floor(Math.random()*td)
// 查看坐标是否符合要求(遍历蛇)
for(let i=0;i<snake.snakePos.length;i++){
if(snake.snakePos[i].x===food.x&&snake.snakePos[i].y===food.y){
// 进入此if 说明当前生成的食物坐标和蛇的位置冲突
isRepeat=true
break
}
}
if(!isRepeat){
break
}
}
// 整个while 循环跳出之后,食物的坐标一定是OK的
if(!food.domContent){
food.domContent=document.createElement("div")
food.domContent.style.width=snakeBody+"px"
food.domContent.style.height=snakeBody+"px"
food.domContent.style.position="absolute"
food.domContent.style.background=`url("../images/food.png") center/contain no-repeat`
document.querySelector('.container').append(food.domContent)
}
food.domContent.style.left=food.x*snakeBody+"px"
food.domContent.style.top=food.y*snakeBody+"px"
}
/***
* 碰撞检测
* @param {*} newHead 新计算出来的蛇头坐标
*/
function isCollide(newHead){
let collideCheckInfo={
isCollide:false,//是否碰撞墙壁 蛇身
isEat:false//是否吃到食物
}
// 1.检测是否碰到墙壁
if(newHead.x<0||newHead.x>=td||newHead.y<0||newHead.y>=tr){
collideCheckInfo.isCollide=true
return collideCheckInfo
}
// 检测是是否碰到自己
for(let i=0;i<snake.snakePos.length;i++){
if(snake.snakePos[i].x===newHead.x&&snake.snakePos[i].y===newHead.y){
collideCheckInfo.isCollide=true
return collideCheckInfo
}
}
// 检测是否吃到东西
if(newHead.x===food.x&&newHead.y===food.y){
collideCheckInfo.isEat=true
score++
}
return collideCheckInfo
}
function move(e){
console.log(e.key)
if((e.key==='ArrowUp'||e.key.toLocaleLowerCase()==='w')&&snake.direction.flag!=="bottom"){
// 用户按的是上
snake.direction=directionNum.top
}
if((e.key==='ArrowDown'||e.key.toLocaleLowerCase()==='s')&&snake.direction.flag!=="top"){
// 用户按的是下
snake.direction=directionNum.bottom
}
if((e.key==='ArrowLeft'||e.key.toLocaleLowerCase()==='a')&&snake.direction.flag!=="right"){
// 用户按的是左
snake.direction=directionNum.left
}
if((e.key==='ArrowRight'||e.key.toLocaleLowerCase()==='d')&&snake.direction.flag!=="left"){
// 用户按的是右
snake.direction=directionNum.right
}
snakeMove()
}
/**
* 蛇的移动方法
*/
function snakeMove(){
let oldHead=snake.snakePos[snake.snakePos.length-1]
// 根据方向计算出新的蛇头的坐标
let newHead={
domContent:"",
x:oldHead.x+snake.direction.x,
y:oldHead.y+snake.direction.y,
flag:'head'
}
// 接下来我们首先要做碰撞检测
// 看计算出来的蛇头有没有碰上食物
let collideCheckResult=isCollide(newHead)
if(collideCheckResult.isCollide){
//进入此if 说明撞墙了
if(window.confirm(`游戏结束,您当前的得分为${score}分,是否要重新开始游戏`)){
// 重新开始游戏
document.querySelector('.container').innerHTML=`
<!-- 开始按钮 -->
<button class="startBtn" style="display:none"></button>
<!-- 暂停按钮 -->
<button class="pauseBtn" style="display:none"></button>
`
score=0
console.log(score)
// gridData=[]
snake={
// 蛇一开始移动的方向
direction:directionNum.right,//一开始向右边移动
// 蛇的初始位置
snakePos:[//domContent为蛇的相应dom元素 flag根据身体还是蛇头分别渲染
{x:0,y:0,domContent:"",flag:'body'},
{x:1,y:0,domContent:"",flag:'body'},
{x:2,y:0,domContent:"",flag:'body'},
{x:3,y:0,domContent:"",flag:'head'}
]
}
food={
x:0,y:0,domContent:""
}
console.log("已初始化")
initGame()
return
// drawSnake(snake)
}else {
// 结束游戏
document.removeEventListener('keydown',move)
// console.log("取消")
clearInterval(timerStop)
return
}
// alert("撞墙了")
}
// 将旧的头修改为身体
oldHead.flag='body'
oldHead.domContent.style.background="#9ddbb1"
oldHead.domContent.style.borderRadius="50%"
snake.snakePos.push(newHead)
// 判断是否吃到东西
if(collideCheckResult.isEat){
// 1.重新生成新的食物
drawFood()
}
else{
// 说明没有吃到食物
// 移除最后一个元素
document.querySelector(".container").removeChild(snake.snakePos[0].domContent)
snake.snakePos.shift();//删除第一个元素
}
drawSnake(snake)
}
/**
* 初始化游戏方法
*/
function initGame(){
//1.初始化地图
for(let i=0;i<tr;i++){
for(let j=0;j<td;j++){
gridData.push({
x:j,
y:i
})
}
}
// 2.绘制蛇
drawSnake(snake)
// 3.绘制食物
drawFood()
}
function startGame(){
timerStop=setInterval(function(){
snakeMove()
},time)
}
/**
* 绑定事件
*/
function bindEvent(){
// 1.键盘事件
document.addEventListener('keydown',move)
// 2.计时器自动调用蛇移动的方法
startGame();
// 3.点击整个容器的时候,可以暂停和重新开始游戏
document.querySelector('.container').addEventListener('click',function(e){
// 通过事件委托的形式,判断用户点击的是container还是暂停按钮
if(e.target.className ==="container"){
document.querySelector('.pauseBtn').style.display='block'
clearInterval(timerStop)
}else {
document.querySelector('.pauseBtn').style.display='none'
startGame()
}
})
// 4.给暂停按钮绑定事件
// 点击子元素事件,冒泡后也会触发父元素点击事件
// document.querySelector('.pauseBtn').addEventListener('click',function(e){
// e.stopPropagation()
// })
}
/**
* 游戏的主方法
*/
function main(){
// 用户点击了开始游戏之后,再做后续工作
document.querySelector('.startBtn').addEventListener('click',function(e){
e.stopPropagation()
document.querySelector('.startBtn').style.display="none"
// 1.首先初始化游戏
initGame()
// 2.绑定事件
bindEvent()
})
}
main()
config.js
// 游戏相关配置文件
let gridData=[]//存储地图对象
// 整个网格的行与列
const tr=30
const td=30
// 蛇的身体大小
let snakeBody=20
// 要明确新的蛇头和旧的蛇头之间的位置关系
// 我们在确定新的蛇头坐标的时候,会那下面的对象和旧蛇头做一个计算
const directionNum={
left:{x:-1,y:0,flag:'left'},
right:{x:1,y:0,flag:'right'},
top:{x:0,y:-1,flag:'top'},
bottom:{x:0,y:1,flag:'bottom'}
}
// 蛇相关的配置信息
let snake={
// 蛇一开始移动的方向
direction:directionNum.right,//一开始向右边移动
// 蛇的初始位置
snakePos:[//domContent为蛇的相应dom元素 flag根据身体还是蛇头分别渲染
{x:0,y:0,domContent:"",flag:'body'},
{x:1,y:0,domContent:"",flag:'body'},
{x:2,y:0,domContent:"",flag:'body'},
{x:3,y:0,domContent:"",flag:'head'}
]
}
// 食物相关的配置信息
let food={
x:0,y:0,domContent:""
}
// 游戏分数
let score=0
// 停止计时器
let timerStop=null
// 计时器事件
let time=300