前面的那篇贪吃蛇(https://blog.csdn.net/F_Felix/article/details/89676816)尽管也是用到了面向对象的,但是还有一些可以优化的地方,比如游戏本身同样是个对象,比如使用 JavaScript 中的原型
同样,我们先来分析一下这个贪吃蛇
1、总共有几个对象? 这里有4个对象,分别是Snake、Food、Game、map,但是为了简单点就把map对象给省略了如果需要可以直接用之前那篇的
2、各个对象分别有哪些属性及方法呢?
3、怎么在之前的基础上进行优化?
然后就不多解释了,代码中有注释描述,直接上代码
<style>
* {
margin: 0;
padding: 0;
}
.map {
width: 800px;
height: 500px;
background-color: #000;
margin: 0 auto;
position: relative;
}
p {
text-align: center;
line-height: 30px;
}
.btn {
margin: 0 auto;
text-align: center;
}
style>
<body>
<div class="btn">
<button class="begin">开始游戏button>
<button class="pause">暂停游戏button>
<button class="reset">重启游戏button>
div>
<p>
<strong>游戏说明:strong>Enter--> 开始游戏 Space--> 暂停游戏 Esc--->
重置游戏 ←↑→↓ 控制方向
p>
<p>蛇每吃10个点速度增加,极限速度50,初始速度200p>
<div class="map">div>
<script src="food.js">script>
<script src="snake.js">script>
<script src="game.js">script>
<script>
var map = document.querySelector('.map')
var btn1 = document.querySelector('.begin')
var btn2 = document.querySelector('.pause')
var btn3 = document.querySelector('.reset')
var g = new Game({
map: map
})
g.init()
btn1.onclick = function() {
g.start()
}
btn2.onclick = function() {
g.pause()
}
btn3.onclick = function() {
g.reset()
}
// 解决js中点击了一次按钮,再按回车会触发之前点击按钮的事件
document.onkeydown = function(e) {
switch (e.keyCode) {
case 13:
btn1.focus()
break
case 32:
btn2.focus()
break
case 27:
btn3.focus()
break
}
}
script>
body>
// 优化4---- 把代码放在沙箱里
;(function(window) {
/**
* 食物构造函数
* @param {object} options 对象参数
* 食物属性:宽度、高度、颜色、位置
* 食物方法:渲染
*/
function Food(options) {
options = options || {}
this.width = options.width || 20
this.height = options.height || 20
this.bgColor = options.bgColor || 'cyan'
this.x = 0
this.y = 0
}
Food.prototype.render = function(target) {
// 优化1,每次渲染之前需要把之前的点移除了,主要是用于蛇吃到点后
this.node && target.removeChild(this.node)
// 创建食物圆点,并且把圆点添加到map中去
var div = document.createElement('div')
this.node = div
target.appendChild(div)
div.style.width = this.width + 'px'
div.style.height = this.height + 'px'
div.style.backgroundColor = this.bgColor
div.style.position = 'absolute'
this.x = ((Math.random() * target.offsetWidth) / this.width) | 0
this.y = ((Math.random() * target.offsetHeight) / this.height) | 0
div.style.left = this.x * this.width + 'px'
div.style.top = this.y * this.height + 'px'
div.style.borderRadius = '50%'
}
window.Food = Food
})(window)
;(function(window) {
/**
* 蛇构造函数
* @param {*} options 蛇的一些属性(对象)
* 蛇属性:每一节的宽度 高度,颜色,位置,蛇头的方向
* 蛇方法:渲染、移动
*/
function Snake(options) {
options = options || {}
this.width = options.width || 20
this.height = options.height || 20
this.direction = options.direction || 'right'
this.headColor = options.headColor || 'skyblue'
this.bodyColor = options.bodyColor || 'yellowgreen'
this.body = [{ x: 2, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 0 }]
}
// 把蛇渲染到map中
Snake.prototype.render = function(target) {
// 优化2 渲染蛇之前把之前的蛇移除掉
var spans = document.querySelectorAll('span')
for (var i = 0; i < spans.length; i++) {
target.removeChild(spans[i])
}
for (var i = 0; i < this.body.length; i++) {
var span = document.createElement('span')
target.appendChild(span)
span.style.width = this.width + 'px'
span.style.height = this.height + 'px'
span.style.backgroundColor = i === 0 ? this.headColor : this.bodyColor
span.style.position = 'absolute'
span.style.left = this.body[i].x * this.width + 'px'
span.style.top = this.body[i].y * this.height + 'px'
span.style.borderRadius = '50%'
}
}
// 蛇的移动
Snake.prototype.move = function(target, food) {
/*
移动逻辑:
复制一个蛇头节点,控制这个节点进行移动
如果没有吃到点那么就删除蛇尾的点
如果吃到了点则不删除结尾的点
其实逻辑与让后一个点移动到前一个点的逻辑是一致的
*/
var head = {
x: this.body[0].x,
y: this.body[0].y
}
switch (this.direction) {
case 'right':
head.x++
break
case 'left':
head.x--
break
case 'up':
head.y--
break
case 'down':
head.y++
break
}
this.body.unshift(head)
// 当蛇头的位置和点的位置重叠后,就是吃到了点
if (head.x == food.x && head.y == food.y) {
// 重新渲染点,因此对food渲染进行优化 ---> 优化1
food.render(target)
} else {
this.body.pop()
}
// 优化4 当判断蛇撞边界时,不会继续执行渲染导致蛇头超出边界
if (
head.x < 0 ||
head.y < 0 ||
head.x >= target.offsetWidth / this.width ||
head.y >= target.offsetHeight / this.height
) {
return
}
// 优化2 重新渲染蛇的时候,同样需要把之前的蛇给移除了,然后在渲染
this.render(target)
}
window.Snake = Snake
})(window)
// 优化4 -- 把代码放在沙箱中
;(function(window) {
/**
* 游戏对象
* @param {*} options 游戏对象参数
* 属性包括:地图、蛇、食物、定时器时间
* 方法:初始化游戏、开始游戏、暂停游戏、重置游戏、结束游戏、操作蛇对象(添加键盘事件)
*/
function Game(options) {
options = options || {}
this.map = options.map
this.snake = options.snake || new Snake()
this.food = options.food || new Food()
this.duration = 200 // 初始速度
this.limit = 50 // 速度最小是50ms
this.level = 10 // 每吃到十个点就加速
}
// 优化3 给游戏增加节流阀
var dirFlag = true // 方向节流阀---> 避免因为方向按得过快导致的,可以反向运动
var gameFlag = true // 游戏节流阀 ---> 避免因为多次点击开始游戏导致的定时器失效
var tId // 定时器id
var these // 用于存储在start中调用定时器时使用的this,必须是全局变量,不然在定时器中无法识别
var prevLength = 3 // 初始时蛇的长度为3
// 初始化游戏
Game.prototype.init = function() {
this.snake.render(this.map)
this.food.render(this.map)
// 监听事件可以放这里也可以放在start里
this.control()
}
// 游戏开始
Game.prototype.start = function() {
if (gameFlag) {
gameFlag = false
// 因为这里的this和定时器里的this不是同一个指向,因此先保存一下
// var that = this;
// this.timeId = setInterval(function () {
// // 注意:这里的this和外面的this不是同一个,这个指向的是window对象
// that.snake.move(that.map, that.food);
// // 游戏结束
// that.gameOver();
// // 暴力设置经过多少时间后节流阀重启
// setTimeout(() => {
// dirFlag = true;
// }, 100);
// }, this.duration);
these = this
// tId = setInterval('execute(these)', this.duration)
tId = setInterval(() => {
execute(these)
}, this.duration)
}
}
// 递归调用,让速度越来越快
function execute(these) {
// 注意:这里的this和外面的this不是同一个,这个指向的是window对象
these.snake.move(these.map, these.food)
// 游戏结束
these.gameOver()
clearInterval(tId)
if (
these.snake.body.length - prevLength >= these.level &&
these.duration >= these.limit
) {
prevLength = these.snake.body.length
these.duration -= 10
console.log(these.duration)
}
// var that = tt;
// 如果游戏节流阀重启了,表示前一次游戏结束了,那么就不再开启定时器
if (!gameFlag) {
tId = setInterval(() => {
execute(these)
}, these.duration)
}
// 暴力设置经过多少时间后节流阀重启
setTimeout(() => {
dirFlag = true
}, 100)
}
// 游戏结束
Game.prototype.gameOver = function() {
var head = this.snake.body[0]
// 1、撞到了边界--->死
if (
head.x < 0 ||
head.y < 0 ||
head.x >= this.map.offsetWidth / this.snake.width ||
head.y >= this.map.offsetHeight / this.snake.height
) {
alert('Game Over!')
this.pause()
gameFlag = true
}
// 2、撞到了自己--->死
for (var i = 4; i < this.snake.body.length; i++) {
if (head.x == this.snake.body[i].x && head.y == this.snake.body[i].y) {
alert('Game Over!')
this.pause()
gameFlag = true
}
}
}
// 暂停游戏
Game.prototype.pause = function() {
// clearInterval(this.timeId);
clearInterval(tId)
gameFlag = true
dirFlag = true
}
// 操纵蛇移动以及游戏的开始、暂停、重置
Game.prototype.control = function() {
var that = this
document.addEventListener('keydown', function(e) {
if (dirFlag) {
dirFlag = false
switch (e.keyCode) {
case 37:
if (that.snake.direction != 'right') {
that.snake.direction = 'left'
}
break
case 38:
if (that.snake.direction != 'down') {
that.snake.direction = 'up'
}
break
case 39:
if (that.snake.direction != 'left') {
that.snake.direction = 'right'
}
break
case 40:
if (that.snake.direction != 'up') {
that.snake.direction = 'down'
}
break
case 32:
that.pause()
break
case 13:
that.start()
break
case 27:
that.reset()
break
}
}
})
}
// 重置游戏
Game.prototype.reset = function() {
this.snake.body = [{ x: 2, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 0 }]
this.snake.direction = 'right'
this.init()
// clearInterval(this.timeId);
clearInterval(tId)
gameFlag = true
dirFlag = true
}
window.Game = Game
})(window)
针对游戏中的方法进行简单解释一下
1、为什么需要these = this?(start方法中)
解: 因为在定时器中,this是指向window的,但是我们的this需要指向的是Game对象,因此我们把这个this赋值给了these,这样我们就可以在后面通过使用these相当于使用Game了。(其实这里不用these = this也可以,因为使用了指针函数)
如果没有使用指针函数,如下,还有另一种解决方法通过使用'上下文调用模式'中的bind方法,让定时器里的this指向了Game对象的this
tId = setInterval(function(){
execute(this)
}.bind(this),this.duration)
2、为什么这里要另外调用一个execute方法?
解:为了实现能够在吃掉一定数量的点之后可以让蛇的速度越来越快,但是如果在function中直接修改this.duration是不起作用的,因此为了实现效果,我们就需要通过递归调用的方式,每次先清除之前的定时器,然后在创建新的定时器的方式来实现速度越来越快
3、两个节流阀的作用?
解:大家可以试一下,就是如果把两个节流阀去掉,方向节流阀如果去掉就会因为我们有时候按方向键过快,导致蛇可以直接反向运动然后咬身体over,举例说:一开始蛇往右移动,但是你在非常短的时间里,先按了上(或者下)然后在按了左,这样蛇就会瞬间由朝右运动变为朝左,然后要自己了。至于游戏节流阀,如果没有这个节流阀,就会导致我可以一直按开始按妞,然后因为定时器的存在,会导致蛇越来越快,并且最后蛇停不下来了,因此才会加上这个节流阀。
4、什么叫节流阀?
解:用白话来说就是,多个人使用同一个东西,需要等别人用完这个东西之后你才可以再使用,这就是节流阀的思想。