十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊
量身定制 + 運營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
這篇文章主要介紹了如何使用Canvas寫一個貪吃蛇游戲,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
成都創(chuàng)新互聯(lián)公司,為您提供網(wǎng)站建設(shè)公司、成都網(wǎng)站制作、網(wǎng)站營銷推廣、網(wǎng)站開發(fā)設(shè)計,對服務(wù)鑿毛機(jī)等多個行業(yè)擁有豐富的網(wǎng)站建設(shè)及推廣經(jīng)驗。成都創(chuàng)新互聯(lián)公司網(wǎng)站建設(shè)公司成立于2013年,提供專業(yè)網(wǎng)站制作報價服務(wù),我們深知市場的競爭激烈,認(rèn)真對待每位客戶,為客戶提供賞心悅目的作品。 與客戶共同發(fā)展進(jìn)步,是我們永遠(yuǎn)的責(zé)任!
之前在慕課網(wǎng)看了幾集Canvas的視頻,一直想著寫點東西練練手。感覺貪吃蛇算是比較簡單的了,當(dāng)年大學(xué)的時候還寫過C語言字符版的,沒想到還是遇到了很多問題。
最終效果如下(圖太大的話 時間太長 錄制gif的軟件有時限…)

首先定義游戲區(qū)域。貪吃蛇的屏幕上只有蛇身和蘋果兩種元素,而這兩個都可以用正方形格子構(gòu)成。正方形之間添加縫隙。為什么要添加縫隙?你可以想象當(dāng)你成功填滿所有格子的時候,如果沒有縫隙,就是一個實心的大正方形……你根本不知道蛇身什么樣。
畫了一個圖。
格子是左上角的坐標(biāo)是(0, 0),向右是橫坐標(biāo)增加,向下是縱坐標(biāo)增加。這個方向和Canvas相同。
每次畫一個格子的時候,要從左上角開始,我們直知道Canvas的左上角坐標(biāo)是(0, 0),假設(shè)格子的邊長是 GRID_WIDTH 縫隙的寬度是 GAP_WIDTH ,可以得到第(i, j)個格子的左上角坐標(biāo) (i*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH, j*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH) 。
假設(shè)現(xiàn)在蛇身是由三個藍(lán)色的格子組成的,我們不能只繪制三個格子,兩個紫色的空隙也一定要繪制,否則,還是之前說的,你根本不知道蛇身什么樣。如下圖,不畫縫隙雖然也能玩,但是體驗肯定不一樣。
繪制相鄰格子之間間隙
不繪制間隙
現(xiàn)在我們可以嘗試著畫一條蛇了。蛇身其實就是一個格子的集合,每個格子用包含兩個位置信息的數(shù)組表示,整條蛇可以用二維數(shù)組表示。
blog_snack
我初始化了一條蛇,看起來是符合預(yù)期的。

接下來要做的是讓蛇動起來。蛇動起來這事很簡單,蛇向著當(dāng)前運動的方向前進(jìn)一格,刪掉蛇尾,也就是最后一個格子就可以了。之前說的二維數(shù)組表示一條蛇, 現(xiàn)在規(guī)定其中snack[0]表示蛇尾,snack[snack.length-1]表示蛇頭。 動畫就簡單的用setInterval實現(xiàn)了。
const GRID_WIDTH = 10; // 格子的邊長
const GAP_WIDTH = 2; // 空隙的邊長
const ROW = 10; // 一共有多少行格子&每行有多少個格子
const COLOR = '#fff'; // 蛇的顏色
const BG_COLOR = '#000';// 背景顏色
const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定義蛇前進(jìn)的方向
const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每個方向前進(jìn)時格子坐標(biāo)的變化
let canvas = document.getElementById('canvas');
canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
let ctx = canvas.getContext('2d');
let snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一條?
let dir = RIGHT; // 初始化一個方向
drawSnack(ctx, snack, COLOR);
let timer = setInterval(() => {
// 每隔一段時間就刷新一次
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個格子前進(jìn)位置
let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
snack.push(newGrid); // 新格子加入蛇身的數(shù)組中
ctx.fillStyle = COLOR;
ctx.fillRect(...getGridULCoordinate(newGrid), GRID_WIDTH, GRID_WIDTH); // 畫新格子
ctx.fillRect(...getBetweenTwoGridGap(head, newGrid)); // 新蛇頭和舊蛇頭之間的縫隙
ctx.fillStyle = BG_COLOR;
let delGrid = snack.shift(); // 刪除蛇尾-最后一個元素
ctx.fillRect(...getGridULCoordinate(delGrid), GRID_WIDTH, GRID_WIDTH); // 擦除刪除元素
ctx.fillRect(...getBetweenTwoGridGap(delGrid, snack[0])); // 擦除刪除元素和當(dāng)前最后一個元素之間的縫隙
}, 1000);
..... // 和之前相同現(xiàn)在蛇已經(jīng)可以動起來了。

但這肯定不是我想要的效果——它的移動是一頓一頓的,而我想要順滑的。
現(xiàn)在每一次變化都是直接移動一個格子邊長的距離,保證蛇移動速度不變的情況下,動畫是不可能變得順滑的。所以想要移動變得順滑,一種可行的方法是,移動一個格子的距離的過程分多次繪制。
blog_snack
實話,代碼寫的非常糟糕……我也很無奈……
反正現(xiàn)在蛇可以緩慢順滑的移動了。

接下來要做的是判斷是否觸碰到邊緣或者觸碰到自身導(dǎo)致游戲結(jié)束,以及響應(yīng)鍵盤事件。
這里的改動很簡單。用一個map標(biāo)記每一個格子是否被占。每一個格子(i, j)可以被編號i*row+j。
const GRID_WIDTH = 10; // 格子的邊長
const GAP_WIDTH = 2; // 空隙的邊長
const ROW = 10; // 一共有多少行格子&每行有多少個格子
const COLOR = '#fff'; // 蛇的顏色
const BG_COLOR = '#000';// 背景顏色
const INTERVAL = 300;
const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定義蛇前進(jìn)的方向
const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每個方向前進(jìn)時格子坐標(biāo)的變化
let canvas = document.getElementById('canvas');
canvas.height = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
canvas.width = GRID_WIDTH * ROW + GAP_WIDTH * (ROW + 1);
let ctx = canvas.getContext('2d');
let snack, dir, map, nextDir;
function initialize() {
snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一條?
nextDir = dir = RIGHT; // 初始化一個方向
map = [];
for (let i = 0; i < ROW * ROW; i++) map[i] = 0;
for (let i = 0; i < snack.length; i++) map[ getGridNumber(snack[i]) ] = 1;
window.onkeydown = function(e) {
// e.preventDefault();
if (e.key === 'ArrowUp') nextDir = UP;
if (e.key === 'ArrowDown') nextDir = DOWN;
if (e.key === 'ArrowRight') nextDir = RIGHT;
if (e.key === 'ArrowLeft') nextDir = LEFT;
}
drawSnack(ctx, snack, COLOR);
}
initialize();
let timer = setInterval(() => {
// 每隔一段時間就刷新一次
// 只有轉(zhuǎn)頭方向與當(dāng)前方向垂直的時候 才改變方向
if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個格子前進(jìn)位置
let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
if (!isValidPosition(newGrid)) { // 新位置不合法 游戲結(jié)束
clearInterval(timer);
return;
}
snack.push(newGrid); // 新格子加入蛇身的數(shù)組中
map[getGridNumber(newGrid)] = 1;
gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
let delGrid = snack.shift(); // 刪除蛇尾-最后一個元素
map[getGridNumber(delGrid)] = 0;
gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])),
getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
}, INTERVAL);
function isValidPosition(g) {
if (g[0] >= 0 && g[0] < ROW && g[1] >= 0 && g[1] < ROW && !map[getGridNumber(g)]) return true;
return false;
}
// 獲取一個格子的編號
function getGridNumber(g) {
return g[0] * ROW + g[1];
}
// 給定一個格子的坐標(biāo)和一個格子間隙的矩形(左上角,寬,高) 返回兩個合并的矩形 的左上角、右下角 坐標(biāo)
function getUniteRect(g, rect) {
/// ... 后面代碼不改變 略....這時已經(jīng)可以控制蛇的移動了。

最后一個步驟了,畫蘋果。蘋果的位置應(yīng)該是隨機(jī)的,且不與蛇身重疊,另外蛇吃到蘋果的時候,長度會加一。
blog_snack
我不管 我寫完了 我的代碼最棒了(口區(qū)

如果蛇能自己動就好了。。。我的想法很單純。。。但是想了很久沒結(jié)果的時候,Google一下才發(fā)現(xiàn)這好像涉及到AI了。。。頭疼。。。
最終我選取的方案是:
if 存在蛇頭到蘋果的路徑 and 蛇身長度小于整個地圖的一半 虛擬蛇去嘗試吃蘋果 if 吃完蘋果后能找到蛇頭到蛇尾的路徑 BFS到蛇尾 else if 存在蛇頭到蛇尾的路徑 走蛇頭到蛇尾的最長路徑 else 隨機(jī)一個方向
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“如何使用Canvas寫一個貪吃蛇游戲”這篇文章對大家有幫助,同時也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!