面向對象+原型實現貪喫蛇
前面的那篇貪喫蛇(https://blog.csdn.net/F_Felix/article/details/89676816)儘管也是用到了面向對象的,但是還有一些可以優化的地方,比如遊戲本身同樣是個對象,比如使用 JavaScript 中的原型
同樣,我們先來分析一下這個貪喫蛇
思路分析
1、總共有幾個對象? 這裏有4個對象,分別是Snake、Food、Game、map,但是爲了簡單點就把map對象給省略瞭如果需要可以直接用之前那篇的
2、各個對象分別有哪些屬性及方法呢?
- Food對象:寬高、顏色、位置屬性以及渲染方法(width、height、bgColor、x、y 和render)
- Snake對象:寬高、方向、頭顏色、身體顏色、身體屬性以及渲染和移動方法(width、height、direction、headColor、bodyColor、body、render、move)
- Game對象:地圖、蛇、食物屬性和初始化、初始化遊戲、開始遊戲、暫停遊戲、重置遊戲、結束遊戲、操作蛇對象(map、snake、food、init、start、pause、control、reset)
3、怎麼在之前的基礎上進行優化?
- 把方法定義在原型上,屬性定義在構造函數中
- 使用沙箱把代碼包裹起來,避免全局污染
(解釋:https://blog.csdn.net/F_Felix/article/details/90111118)
然後就不多解釋了,代碼中有註釋描述,直接上代碼
頁面代碼
<!-- 樣式部分 -->
<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,初始速度200</p>
<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>
Food 對象
// 優化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)
Snake 對象
;(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)
Game 對象
// 優化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、什麼叫節流閥?
解:用白話來說就是,多個人使用同一個東西,需要等別人用完這個東西之後你纔可以再使用,這就是節流閥的思想。