本文首發于我的博客
在csdn上看到一位大神用20行代碼就寫出了一個貪吃蛇的小游戲,鏈接請點這里,感覺被驚艷到了,就試著讀了一下這段代碼,閱讀過程中不斷為作者寫法的巧妙而叫絕,其中我發現自己對運算符優先級和一些js的技巧不是很清楚,所以看完之后決定把思路分享出來,和大家一起學習。可以在這里預覽效果,可能需要翻墻,你也可以在我的github上直接查看源碼。
我對代碼稍稍做了些修改,并添加了一些注釋,方便理解。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>貪吃蛇重構</title>
<style>
body {
display: flex;
height: 100vh;
margin: 0;
padding: 0;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<canvas id="can" width="400" height="400" style="background-color: black">對不起,您的瀏覽器不支持canvas</canvas>
<script>
var snake = [41, 40], //snake隊列表示蛇身,初始節點存在但不顯示
direction = 1, //1表示向右,-1表示向左,20表示向下,-20表示向上
food = 43, //食物的位置
n, //與下次移動的位置有關
box = document.getElementById('can').getContext('2d');
//從0到399表示box里[0~19]*[0~19]的所有節點,每20px一個節點
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 *20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
//用color填充一個矩形,以前兩個參數為x,y坐標,后兩個參數為寬和高。
}
document.onkeydown = function(evt) {
//當鍵盤上下左右鍵摁下的時候改變direction
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
!function() {
snake.unshift(n = snake[0] + direction);
//此時的n為下次蛇頭出現的位置,n進入隊列
if(snake.indexOf(n, 1) > 0 || n < 0 || n > 399 || direction == 1 && n % 20 == 0 || direction == -1 && n % 20 == 19) {
//if語句判斷貪吃蛇是否撞到自己或者墻壁,碰到時返回,結束程序
return alert("GAME OVER!");
}
draw(n, "lime"); //畫出蛇頭下次出現的位置
if(n == food) { //如果吃到食物時,產生一個蛇身以外的隨機的點,不會去掉蛇尾
while (snake.indexOf(food = ~~(Math.random() * 400)) >= 0);
draw(food, "yellow");
} else { //沒有吃到食物時正常移動,蛇尾出隊列
draw(snake.pop(),"black");
}
setTimeout(arguments.callee, 150);
//每隔0.15秒執行函數一次,可以調節蛇的速度
}();
</script>
</body>
</html>
首先,我們要知道做一個貪吃蛇最主要的是什么,是做出蛇活動的場所和如何使蛇動起來。
??我們先看蛇活動的場所:
<!-- html -->
<canvas id="can" width="400" height="400" style="background-color: black">
對不起,您的瀏覽器不支持canvas
</canvas>
<!-- js -->
box = document.getElementById('can').getContext('2d');
這是一個400px*400px
的canvas
,思路是以20px*20px
為一個方格,組成20
行20
列的方陣,總共400
格,然后綠色填充的格子表示蛇身,用黃色表示食物。這400
個格子和數字0~399
一一對應,對應的方式就是以20
作為基數,n / 20
再取整表示第幾行,n % 20
表示第幾列。行數和列數都用0~19
表示。
??蛇用一個一維數組表示,每個值都是這400
個數中的一個,用var snake = [41, 40];
初始化這條蛇,索引0
為蛇頭。food
表示食物的位置,direction
表示蛇頭下一次運動的轉向。蛇的運動就用添加和刪除數組元素來實現,每次執行繪制蛇頭,去掉蛇尾,循環執行使蛇運動。
??下邊從函數運行的起始處(39
行)開始看:
!function() {}();
什么鬼?這其實是立即執行函數IIFE
的另一種寫法。關于IIFE
,這篇文章講的挺不錯的。繼續往下看,給蛇頭添加一個節點n
,其值為當前蛇頭的值加direction
的值,如此一來就能理解為什么要用20
表示向下,-20
表示向上了。再下一行是一個if
語句,其中值得提醒的是&&
的優先級高于||
,這個語句就是判斷即將出現的蛇頭是不是屬于蛇身,或者跑到box外邊去了。如果沒有死亡,就把這個蛇頭繪制出來,下邊就看看繪制的代碼:
function draw(seat, color) {
box.fillStyle = color;
box.fillRect(seat % 20 *20 + 1, ~~(seat / 20) * 20 + 1, 18, 18);
}
填充時填充18*18
的像素,留1px
邊框。.fillRect()
中第一個參數就是要繪制的矩形的x
坐標seat % 20 *20 + 1
,即先得到所要繪制的矩形塊在方陣中的位置:第~~(seat / 20)
行,第seat % 20
列,再* 20 + 1
具體到像素點。可能這個~~
有點難理解,我感覺在這里的用處應該和Math.floor()
差不多,可以用來截除小數。這兩者具體還有一些區別,可以去看《你不知道的js(中卷)》62頁。
??回到47
行,又是一個判斷語句,判斷下次蛇頭出現的位置是不是和當前的食物的位置相同,如果相同,生成下一個食物,食物的位置為一個隨機數,但是要判斷這個點不是出現在當前的蛇身上,繪制食物。如果沒有吃到食物,即蛇在正常運動時,每向前一次,將蛇尾彈出,并利用其返回值將這個點重新繪制為黑色。
??最后的setTimeout
,循環執行當前函數,設置執行周期來調蛇的移動速度。
??到了這里,我們發現這條蛇已經可以動了,加上鍵盤的操作就完成了:
document.onkeydown = function(evt) {
direction = snake[1] - snake[0] == (n = [-1, -20, 1, 20][(evt || event).keyCode - 37] || direction) ? direction : n;
};
將這個函數綁定到鍵盤事件上,evt || event
用法的原因這里有詳細的解釋,是為了兼容ie
。
??三目運算符?
前邊的判斷語句又可分為兩部分:
-
snake[1] - snake[0]
的值應該就是-direction
,按理說此處寫成-direction
應該和原來是一個效果,那為什么沒有這么做呢,因為如果這樣寫,玩家可能在一個函數周期中多次改變direction
的值,最后使得direction
和當前真正的運動方向不一致,導致游戲崩潰。 - 在
==
后邊,[-1, -20, 1, 20][(evt || event).keyCode - 37]
中前邊的[]
是一個數組,后邊的[]
是取索引,左上右下四個鍵的keyCode
分別為37, 38, 39, 40
,計算后的索引為0, 1, 2, 3
,使方向鍵與direction
的取值對應起來。這里的巧妙之處在于如果按下的按鍵不是方向鍵,在數組中將得不到對應的值,返回undefine
。此時,由于之后的||
運算符,n
會取到direction
原來的值。
再用三目運算符來判斷,如果按鍵方向不是反方向,就更新direction
的值。
以上就是本篇的全部內容了,雖然都是一些基礎的東西,但是感覺還是挺好玩的。要是哪里理解的不對還希望指證出來,共同進步。如果你覺得有所收獲記得在github上點個star哦。