0.项目展示
目录
一,贪吃蛇游戏
介绍:这里采用模块化编程,即外联的方式进行编程,通过对类的拆分,单独用一个js文件表示每个类, 而不是所有部分都写在HTML中,Game类作为中介类。
1. 初始化及引入Game类
这里先简单的引入这个类,并进行测试
2.页面的初始化
进行页面的初始化,对页面的初始化布局不是直接写各种的html标签,而是通过 Game 进行的初始化并追加节点上树。具体过程如下
//JS文件内容
function Game(){
// alert('欢迎进入贪吃蛇游戏')
//设置行数和列数
this.row=30
this.cal=30
//初始化
this.Init()
}
//1.在对象的原型上进行初始化方法的书写
Game.prototype.Init=function(){
//1.1 创建一个 table 标签元素 表单
this.dom=document.createElement("table")
var tr,td
//1.2 遍历行和列
for(var i=0;i<this.row;i++){
//1.2.1 遍历行,创建节点上树 行
tr=document.createElement("tr")
for(var j=0;j<this.cal;j++){
//1.2.2 遍历列,创建节点上树 单元格 追加到 tr 中,作为 tr 的子元素
td=document.createElement("td")
//1.2.3 追加节点上树 使每一行都有 20 个单元格
tr.appendChild(td)
}
//1.2.4 插入节点tr,其 父节点为 this.dom,即 table 中的一个子元素
this.dom.appendChild(tr)
}
//1.3 表格上树,即插入 table 至 app中
//获取 id 为 app 的元素,并使 table 上树,即 div 中加入一个子元素 table
//此时可以发现 table 有 20 个 tr,即 20行
document.getElementById("app").appendChild(this.dom)
}
//html 文件内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇游戏</title>
<style>
table{
/* border-collapse属性,属性值collapse可以使边框合并,默认值sparate边框分开 */
border-collapse: collapse;
border: 1px solid #aaa;
}
td{
width: 20px;
height: 20px;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- 引入js文件 -->
<script src="1.贪吃蛇.js"></script>
<script>
new Game()
</script>
</body>
</html>
结果显示,表格初始化完成
3. 蛇类的初始化
再创建一个 js 文件,用来进行对 蛇类 的初始化及方法的书写。先初始化身体,在 Game.js 文件中对 蛇类 进行实例化,有利于相关方法的调用。
4.蛇的运动更新与渲染
注意到,每次运动都会改变位置,那么就都得对已经渲染过的单元格进行清屏操作,否则就会影响到更新渲染的问题,所以蛇的渲染的总逻辑为:1.清屏 ;2.更新;3.渲染。
4.1 清屏操作
4.2 蛇的运动更新
蛇的运动其实就是蛇在进行 body 数组元素的 增减操作。比如,如果要向右运动,本质就是 body 数组中,对数组元素进行:尾部删除,头部增加 的操作,所以蛇就会渲染新的状态。其实蛇不是真正地动起来了,而是我们对于单元格的渲染而导致蛇的身体发生了变化,而被认为是运动的结果。
基于以上,我们对运动模块进行补全:
4.3 蛇的渲染
接下来就是蛇的渲染了,渲染逻辑为:Snake 类调用Game类中的 setColor 方法,因为对蛇的渲染实质上就是对 table 的 单元格 的渲染,table 表格是在 Game 类中创建的,所以 渲染颜色的方法 也应该在 Game 中设置:
4.4 蛇的相关方法的调用(清屏--更新--渲染)
在这里,之所以要用一个定时器来获取实例 game 并调用其方法,是因为如果在 Game() 中直接调用,由于 new Snake 也是 Game 的一部分,snake 实例创建的过程还未完成,这时候就直接调用 实例的方法 就会返回一个 undefined 值,而导致获取不到 game 实例。所以这里要用一个定时器来调用其方法,不影响到 Game 的部分,因为定时器是异步调用。
5.键盘监听事件
由于用户在玩游戏时,是通过按下键盘来完成控制蛇的运动方向的操作的,所以需要增加一个键盘监听事件。同时,为了方便操作,我们把上述的定时器事件作为 Game 的原型上的一个方法,且增加一个属性 timer。如下:
5.1 将定时器事件绑定在Game原型上
补充:如果想改变贪吃蛇运动的速度,只要修改定时器的触发时间就可以了,时间越短则运动速度越快,反之。
5.2 键盘监听事件方法的书写
5.3 运动的合理性规划——解决原地调头的问题
保证蛇运动的合理性: 蛇的运动不能直接掉头。如正在向右运动的蛇,如果想要向左转,就必须要先向下或者上运动一段距离,才能实现左转,否则就是不合理的。
所以,在进行按键的时候,都必须得先进行判断。即按下的按键所对应的控制方向不能与当前的运动方向相反。
第一次优化的代码如下:
不足:当我们用较快地速度按下不同的按键时,仍然会出现原地调头的情况。这是因为定时器每隔固定时间就会执行一次定时器任务,如果此时以较快地速度进行按键操作,那么就会在间隔时间内有了触发,但此时由于时间问题,渲染是在我们设置的500ms之后发生的,就会出现还未完成渲染,但是运动却发生了变化的情况。如,蛇的当前运动方向为 向右运动,若此时按下‘S’键,紧接着按下'A'键,由于两次按键速度过快,定时器来不及执行第一次的‘S’键的渲染(即方向改变的过程)就执行了第二次'A’键的渲染,而发生了原地调头的情况。由上,我们还得需要对按键的响应事件再次进行优化。由于是因为在一次渲染之前出现的调头情况,我们可以通过对将要改变的方向作为突破口来寻找解决方案。
解决方案:由于我们一开始是 按键按下之后 就直接修改 键值所对应的方向,这样就会导致每按一次键都会对方向的变化产生影响,那么在较快的时间内按下多个键,就有可能出现原地调头的情况,所以我们要用一个 中介值 来对 方向改变的量进行赋值。如下,是利用一个 willDrection 来传递 将要改变的方向 的值。如,当蛇正在向右运动,此时按下'S'键(向下运动'D'),这时不再直接改变方向,而是先调用 changeDirection(‘D’),并将 向下运动的值 作为参数传入到这个函数中,赋值给 willDirection。然后在蛇的 update 方法中,willDirection 会把自己的 方向改变值 赋值给 当前的方向(this.direction),然后根据当前被赋值过的 direction 的值,进行方向的改变。这样的话,即使在定时器时间(设为500ms)内再按下‘A’键(向左运动'L'),由于 update 是要每 500ms 执行一次的,所以即使现在改变了 willDirection 的值,但是由于还没有到 update 的下一次执行时间, willDirection 也就没有机会将它的值 赋值 给 direction, 这样就不会有任何影响了。
具体优化部分如下:
6.判定游戏中蛇死亡——游戏结束的方式
蛇死亡,即游戏结束的方式有两种:一种是蛇头撞墙,另一种是蛇头撞到自己的身体。在判定游戏结束时,记得一定要删除尾元素,因为当前蛇头所处位置是临界位置,是非法的,所以在此处时是不能进行正常的在头元素处增加1的。所以为了看起来像是没有增加1,就把尾元素删除了。
由于与运动,即更新有关,则将判断游戏结束的方法放在 update 中,每次位置的更新都会判断游戏是否结束。
7.关于蛇的食物的创建
7.1 创建一个 Food类,用来产生食物
7.2 随机产生食物的初始化设置——食物不能出现在蛇的身上
在食物随机产生时,有可能会出现在蛇的身上,即食物的渲染到了蛇的身上,这是不符合游戏规则的,所以在进行随机产生食物的时候要先对产生的食物的位置进行判断,是否是在蛇的身上?如果在则重新产生一个随机食物;如果不在,则随机产生食物成功。
具体代码及解释如下:
8 蛇吃食物 & 蛇运动速度的更新
蛇在游戏过程中,会通过吃食物的方式来增长蛇身的长度,并且在蛇身变长的同时,蛇的运动速度也会加快。由于这两个变化都是在蛇运动过程中产生的,所以我们将这两个模块放在蛇运动的模块中,即 update 中。如下:
8.1 蛇吃食物——蛇身变长
8.2 对 被吃掉的食物 的清除操作
蛇吃到食物就得对当前食物进行 清除 操作,这部分在 clear 部分中进行
8.3 蛇运动速度的加快
8.4 分数的增加
9.完整代码
9.1 Game.js部分
function Game(){
// alert('欢迎进入贪吃蛇游戏')
//设置行数和列数
this.row=20
this.col=20
//初始化节点
this.Init()
//实例化 蛇类 将蛇类的实例化绑定在了 Game 身上
this.snake=new Snake()
//执行定时器任务
this.start()
//键盘的事件监听
this.bindEvent()
//实例化 食物类,创建一个食物实例
//初始化 食物 由于需要对食物进行判定,是否被吃掉了,所以需要传入当前对象 this
//这里的 this 表示 game
this.food=new Food(this)
// 分数
this.score=0
}
//1.在对象的原型上进行 初始化方法 的书写 页面的初始化
Game.prototype.Init=function(){
//1.1 创建一个 table 标签元素 表单
this.dom=document.createElement("table")
var tr,td
//1.2 遍历行和列
for(var i=0;i<this.row;i++){
//1.2.1 遍历行,创建节点上树 行
tr=document.createElement("tr")
for(var j=0;j<this.col;j++){
//1.2.2 遍历列,创建节点上树 单元格 追加到 tr 中,作为 tr 的子元素
td=document.createElement("td")
//1.2.3 追加节点上树 使每一行都有 20 个单元格
tr.appendChild(td)
}
//1.2.4 插入节点tr,其 父节点为 this.dom,即 table 中的一个子元素
this.dom.appendChild(tr)
}
//1.3 表格上树,即插入 table 至 app中
//获取 id 为 app 的元素,并使 table 上树,即 div 中加入一个子元素 table
//此时可以发现 table 有 20 个 tr,即 20行
document.getElementById("app").appendChild(this.dom)
}
//2.清屏操作 每一次更新 渲染之前都应该要进行一次 清屏操作。避免上一次渲染的影响
Game.prototype.clear=function(){
//遍历表格,清除画布
for(var i=0;i<this.row;i++)
{
for(var j=0;j<this.col;j++)
{
//将蛇身设置为与画布一样的颜色,默认为白色
this.dom.getElementsByTagName("tr")[i].getElementsByTagName("td")[j].style.background='lightgreen'
// 对食物的清除操作,即将 传入的值 等于 ' ' 空字符串
this.dom.getElementsByTagName("tr")[i].getElementsByTagName("td")[j].innerHTML=' '
}
}
}
//3.设置颜色的方法 渲染
Game.prototype.setColor=function(row,col,color){
//获取到表格的 某行元素 再获取到 该行的某个单元格 ,即可开始进行颜色设置
//即让表格的 第几行第几列设置成某个颜色
// getElementsByTagName("tr") 通过 标签名 获取元素
this.dom.getElementsByTagName("tr")[row].getElementsByTagName("td")[col].style.background = color
}
//5.渲染食物方法
Game.prototype.setHTML=function(row,col,html){
// 操作元素 超文本 内容 会在 检查工具中的 elements 看到 html的内容发生改变
// 1.获取:元素.innnerHTML
// 2.设置:元素.innerHTML='新内容'
// 这里的 innerHTML 的内容 就是 传入 html的内容
this.dom.getElementsByTagName("tr")[row].getElementsByTagName("td")[col].innerHTML= html
}
//4.设置键盘监听事件
Game.prototype.bindEvent=function(){
//4.1记录 dom 这里的 this 是 dom
var self=this
//4.2 键盘事件
document.onkeydown=function(event){
//获取按下的 按键 的对应的按键号
// console.log(event.keyCode,'event')
// a/A(右键)---65 s/S(下键)---83 d/D(左键)---68 w/W(上键)----87
//注意:在这里的 this 是键盘事件函数 的 this,而不是 dom 了,所以要对 dom 进行获取
switch(event.keyCode)
{
case 65: //按下 左键
//4.2.1 先进行判断,若当前方向是向右运动,则不能按左键. 以下同理
if(self.snake.direction == 'R') return;
self.snake.changeDirection("L");
break;
case 83: //按下 下键
if(self.snake.direction == 'U') return;
self.snake.changeDirection("D");
break;
case 68: //按下 右键
if(self.snake.direction == 'L') return;
self.snake.changeDirection("R");
break;
case 87: //按下 上键
if(self.snake.direction == 'D') return;
self.snake.changeDirection("U");
break;
}
}
}
//4.有关蛇的方法的调用
//之所以要用一个定时器来获取实例 game并调用其方法,是因为如果在 Game() 中直接调用,由于 new Snake 也是 Game 的一部分,
//snake 实例创建还未完成,就直接调用 实例的方法 就会返回一个 undefined 值
//所以这里要用一个定时器来调用其方法,不影响到 Game 的部分,即异步调用
Game.prototype.start=function(){
//4.5 速度的更新——加快
//4.5.1 加入 帧编号,用来描述 蛇的运动速度 初始化为0
this.frame=0
this.timer=setInterval(function(){
// 定时器里面的本质就是 游戏的渲染本质:1.清屏 2.更新 3.渲染
// 4.5.2 使 frame 在定时器中 递增
// 这里表示每 20ms 增加一次 .
game.frame++
document.getElementById("frame").innerHTML="帧编号:"+game.frame;
// 4.6 渲染分数
document.getElementById("score").innerHTML="分数:"+game.score;
//4.1 清屏操作
game.clear()
// 4.5.3 蛇的速度更新,蛇的长度变长时,速度要加快
var during=game.snake.body.length<30 ? 30-game.snake.body.length : 1
//4.2 蛇的运动 即蛇的更新
game.frame%during==0 && game.snake.update()
//4.3 蛇的渲染 每20ms 渲染一次
game.snake.render()
//4.4 食物的渲染
game.food.render()
},20)
}
9.2 Snake.js 部分
function Snake(){
//蛇的初始化身体
this.body=[
//利用表格的 几行几列 来表示蛇的位置 行与列都是从 0 开始
//这里表示蛇的初始位置为 第一行的1,2,3,4列,即蛇的长度为 4 个单元格长度
//以第0项为蛇头,所以将 col 的顺序倒换一下,即蛇头在右边,蛇身在左边
{"row":0,"col":4},
{"row":0,"col":3},
{"row":0,"col":2},
{"row":0,"col":1},
{"row":0,"col":0}
]
//设置一个运动方向的信号量,初始化为 "R"
this.direction="D"
//即将改变的方向,目的就是为了防止蛇的运动方向出现原地调头的情况
//设置初始改变方向为 向右运动
this.willDirection="R"
}
//2.蛇的运动 每一次运动就是更新的效果
Snake.prototype.update=function(){
// 蛇的身体是一个 数组类型,蛇的长度的增减,即数组元素的 增加 与 删除
//这里就要考虑到:由于对于 蛇的渲染 是对单元格的渲染,无论是 蛇的运动,还是蛇长度的增减,
//对于已经 渲染过的且不需要再保持渲染效果的蛇的部位(单元格),就得进行清除操作
//那么总结起来就是得进行 : 清屏,更新,渲染
// 每一次运动就是更新的效果 ,所以 蛇的运动 应该要发生在 蛇的渲染 之前
//2.1 让当前的direction,接收一下willDirection
this.direction=this.willDirection
switch(this.direction)
{
case "R": //向右运动
this.body.unshift({"row":this.body[0].row,"col":this.body[0].col+1})
break;
case "D": //向下运动
this.body.unshift({"row":this.body[0].row+1,"col":this.body[0].col})
break;
case "L": //向左运动
this.body.unshift({"row":this.body[0].row,"col":this.body[0].col-1})
break;
case "U": //向上运动
this.body.unshift({"row":this.body[0].row-1,"col":this.body[0].col})
break;
}
//4.对蛇的死亡判断,游戏结束
//4.1 蛇撞墙死亡,临界判断
//一碰墙就判定 游戏结束 所以 右边和下边的临界值设为 game.col-1 gama.row-1; 左边和上边的临界值为 0
if(this.body[0].col>game.col-1 || this.body[0].row>game.row-1 || this.body[0].row<0 || this.body[0].col<0)
{
alert('游戏结束')
//删除尾元素
this.body.shift()
//clearInterval() 方法可取消由 setInterval() 函数设定的定时执行操作。
// clearInterval() 方法的参数必须是由 setInterval() 返回的 ID 值。
clearInterval(game.timer)
}
//4.2 蛇头撞到自己的身体,判定结束
//蛇头撞到自己身体,则遍历时要从 身体 开始遍历,即 col为1 开始遍历
for(var i=1;i<this.body.length;i++)
{
//蛇头与身体某个部位的位置重合时,即表示蛇头撞到自己了
if(this.body[0].col == this.body[i].col && this.body[0].row == this.body[i].row)
{
alert('游戏结束')
//删除尾元素
this.body.shift()
clearInterval(game.timer)
}
}
//5. 蛇身与蛇速的更新
//5.1 蛇身变长
// 蛇吃到食物,即蛇头位置与食物位置重合
if(this.body[0].row == game.food.row && this.body[0].col == game.food.col)
{
// 吃到食物了
// alert("吃到食物了")
// 增加一个 长度 到 蛇body 的 末尾元素
// 这里如果直接这样写,就会出现 蛇身 闪动的样子。这是因为蛇在运动的时候都会进行 头部加1,尾部减1 操作,就会有闪动的效果。
// 要解决这个问题,要清楚:尾部不删,头部加1,就是代表长度增加。
// 所以要增加一个 尾部删除 操作的允许操作条件,即没有吃食物的才进行 尾部删除,头部加1;吃到食物时,只进行 头部加1
// this.body.push({'row':this.body[lenght].row, 'col':this.body[lengtg].col})
//5.2 吃到食物 只有 switch-case 中的 头部加1,不进行 尾部删除,长度加1
//5.2.1 创建新的食物
game.food=new Food(game)
//5.2.2 防止蛇吃到食物后更新速度时会 蹿一下,时 帧 归零,从头计算。每20ms 渲染一次
game.frame=0
// 5.2.3 分数增加
game.score++;
}
else
{
// 5.3 没有吃到食物 进行正常长度的运动,头部加1 尾部删除
//蛇的不同方向的运动,都要对 尾部元素 进行 删除操作
this.body.pop()
}
// 5.4 蛇吃到食物就得对当前食物进行 清除 操作,这部分在 clear 部分中进行
}
//3.蛇的改变方向,防止的是在一次渲染之前会出现调头的情况
Snake.prototype.changeDirection=function(dir){
this.willDirection=dir
}
//1.render() 渲染模板
Snake.prototype.render=function(){
// console.log(game) 用来测试是否成功 获取相关元素以及是否能成功使用
//蛇的渲染 ---- 在表格上进行渲染
// body 是数组形式,body[n],表示第n个元素
//设置好蛇头颜色,蛇头颜色与蛇身设置成不同的颜色 蛇头设置为粉色
game.setColor(this.body[0].row,this.body[0].col,'pink')
//开始遍历 蛇身,并设置成相同的颜色 黄色
for(var i=1;i<this.body.length;i++)
{
game.setColor(this.body[i].row,this.body[i].col,'yellow')
}
}
9.3 Food.js 部分
//1. 创建食物类
function Food(gameSnake){
//alert('吃东西喽')
//备份 当前 this
var self=this
//1.1 食物的位置 随机
//由于食物可能会随机出现在蛇的身上,即会渲染到蛇的身上。所以在随机产生食物的时候,
//要先判断食物的 row 和 col 是否在蛇的身上
// 这里用 do-while 循环语句来进行判断处理,作用是先创建一个 row 和 col ,然后再进行判断row和col是否在蛇的身上
//while 中写一个判断方法,作为循环条件
do{
this.row=parseInt(Math.random() * gameSnake.row);
this.col=parseInt(Math.random() * gameSnake.col);
} while( (function(){
//遍历蛇的 row 和 col,然后与随机产生食物的 row 和 col 进行比较,判断是否重合
for(var i=0;i>gameSnake.snake.body.length;i++)
{
//注意:在这个方法函数里,this 不再是指 食物类的对象 了,而是 window 了。
// 所以要 引用 食物对象 就得在上面进行 备份
if(gameSnake.snake.body[i].row == self.row && gameSnake.snake.body[i].col == self.col)
{
//如果 食物 在 蛇的身上,那么就返回 true 值,继续循环,产生 新的随机食物
return true;
}
}
// 如果食物不在蛇身上,说明该 随机食物 是合法的,就不用再重新生成一个随机食物了,返回 false 值
return false;
})())
// console.log(this.row,this.col) 测试每次产生食物的位置是否是不一样的
}
//食物渲染
Food.prototype.render=function(){
game.setHTML(this.row,this.col,"❤")
}
9.4 html 部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇游戏</title>
<style>
body{
background-color: aqua;
}
table{
/* border-collapse属性,属性值collapse可以使边框合并,默认值sparate边框分开 */
border-collapse: collapse;
border: 1px solid #333;
background-color: lightgreen;
}
td{
width: 20px;
height: 20px;
border: 1px solid #aaa;
/* 设置文本颜色 食物颜色 */
color:red;
text-align: center;
}
img{
width: 100px;
height:150px;
}
audio{
margin-left: 600px;
background-color: blue;
}
</style>
</head>
<body>
<img src="../图片资源/我的照片.jpg" alt="">
<h2 id="frame">帧编号:0</h2>
<h2 id="score">分数:0</h2>
<div id="app"></div>
<audio src="../音频格式转换器/王心凌 - 爱你.mp3" controls loop></audio>
<!--1. 引入js文件 Game类 -->
<script src="1.Game.js"></script>
<!-- 2. 蛇 Snake类 -->
<script src="1.Snake.js"></script>
<!-- 3. 食物 Food类 -->
<script src="1.Food.js"></script>
<script>
var game=new Game()
</script>
</body>
</html>