之前在慕課網(wǎng)看了幾集Canvas的視頻,一直想著寫(xiě)點(diǎn)東西練練手。感覺(jué)貪吃蛇算是比較簡(jiǎn)單的了,當(dāng)年大學(xué)的時(shí)候還寫(xiě)過(guò)C語(yǔ)言字符版的,沒(méi)想到還是遇到了很多問(wèn)題。
最終效果如下(圖太大的話(huà) 時(shí)間太長(zhǎng) 錄制gif的軟件有時(shí)限…)
![](/d/20211016/82f99682f91ab3f7e16d5e517cdf15e5.gif)
首先定義游戲區(qū)域。貪吃蛇的屏幕上只有蛇身和蘋(píng)果兩種元素,而這兩個(gè)都可以用正方形格子構(gòu)成。正方形之間添加縫隙。為什么要添加縫隙?你可以想象當(dāng)你成功填滿(mǎn)所有格子的時(shí)候,如果沒(méi)有縫隙,就是一個(gè)實(shí)心的大正方形……你根本不知道蛇身什么樣。
畫(huà)了一個(gè)圖。
格子是左上角的坐標(biāo)是(0, 0),向右是橫坐標(biāo)增加,向下是縱坐標(biāo)增加。這個(gè)方向和Canvas相同。
每次畫(huà)一個(gè)格子的時(shí)候,要從左上角開(kāi)始,我們直知道Canvas的左上角坐標(biāo)是(0, 0),假設(shè)格子的邊長(zhǎng)是 GRID_WIDTH 縫隙的寬度是 GAP_WIDTH ,可以得到第(i, j)個(gè)格子的左上角坐標(biāo) (i*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH, j*(GRID_WIDTH+GAP_WIDTH)+GAP_WIDTH) 。
假設(shè)現(xiàn)在蛇身是由三個(gè)藍(lán)色的格子組成的,我們不能只繪制三個(gè)格子,兩個(gè)紫色的空隙也一定要繪制,否則,還是之前說(shuō)的,你根本不知道蛇身什么樣。如下圖,不畫(huà)縫隙雖然也能玩,但是體驗(yàn)肯定不一樣。
繪制相鄰格子之間間隙
不繪制間隙
現(xiàn)在我們可以嘗試著畫(huà)一條蛇了。蛇身其實(shí)就是一個(gè)格子的集合,每個(gè)格子用包含兩個(gè)位置信息的數(shù)組表示,整條蛇可以用二維數(shù)組表示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>blog_snack</title>
<style>
#canvas {
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const GRID_WIDTH = 10; // 格子的邊長(zhǎng)
const GAP_WIDTH = 2; // 空隙的邊長(zhǎng)
const ROW = 10; // 一共有多少行格子&每行有多少個(gè)格子
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] ]; // 初始化一條🐍
drawSnack(ctx, snack, '#fff');
function drawSnack(ctx, snack, color) {
ctx.fillStyle = color;
for (let i = 0; i < snack.length; i++) {
ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
if (i) {
ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
}
}
}
// 傳入一個(gè)格子 返回左上角坐標(biāo)
function getGridULCoordinate(g) {
return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
}
// 傳入兩個(gè)格子 返回兩個(gè)格子之間的矩形縫隙
// 這里傳入的兩個(gè)格子必須是相鄰的
// 返回一個(gè)數(shù)組 分別是這個(gè)矩形縫隙的 左上角橫坐標(biāo) 左上角縱坐標(biāo) 寬 高
function getBetweenTwoGridGap(g1, g2) {
let width = GRID_WIDTH + GAP_WIDTH;
if (g1[0] === g2[0]) { // 橫坐標(biāo)相同 是縱向相鄰的兩個(gè)格子
let x = g1[0] * width + GAP_WIDTH;
let y = Math.min(g1[1], g2[1]) * width + width;
return [x, y, GRID_WIDTH, GAP_WIDTH];
} else { // 縱坐標(biāo)相同 是橫向相鄰的兩個(gè)格子
let x = Math.min(g1[0], g2[0]) * width + width;
let y = g1[1] * width + GAP_WIDTH;
return [x, y, GAP_WIDTH, GRID_WIDTH];
}
}
</script>
</body>
</html>
我初始化了一條蛇,看起來(lái)是符合預(yù)期的。
![](/d/20211016/5c7dc3e49e9717359459ae602a28ecef.gif)
接下來(lái)要做的是讓蛇動(dòng)起來(lái)。蛇動(dòng)起來(lái)這事很簡(jiǎn)單,蛇向著當(dāng)前運(yùn)動(dòng)的方向前進(jìn)一格,刪掉蛇尾,也就是最后一個(gè)格子就可以了。之前說(shuō)的二維數(shù)組表示一條蛇, 現(xiàn)在規(guī)定其中snack[0]表示蛇尾,snack[snack.length-1]表示蛇頭。 動(dòng)畫(huà)就簡(jiǎn)單的用setInterval實(shí)現(xiàn)了。
const GRID_WIDTH = 10; // 格子的邊長(zhǎng)
const GAP_WIDTH = 2; // 空隙的邊長(zhǎng)
const ROW = 10; // 一共有多少行格子&每行有多少個(gè)格子
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] ]; // 每個(gè)方向前進(jìn)時(shí)格子坐標(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; // 初始化一個(gè)方向
drawSnack(ctx, snack, COLOR);
let timer = setInterval(() => {
// 每隔一段時(shí)間就刷新一次
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個(gè)格子前進(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); // 畫(huà)新格子
ctx.fillRect(...getBetweenTwoGridGap(head, newGrid)); // 新蛇頭和舊蛇頭之間的縫隙
ctx.fillStyle = BG_COLOR;
let delGrid = snack.shift(); // 刪除蛇尾-最后一個(gè)元素
ctx.fillRect(...getGridULCoordinate(delGrid), GRID_WIDTH, GRID_WIDTH); // 擦除刪除元素
ctx.fillRect(...getBetweenTwoGridGap(delGrid, snack[0])); // 擦除刪除元素和當(dāng)前最后一個(gè)元素之間的縫隙
}, 1000);
..... // 和之前相同
現(xiàn)在蛇已經(jīng)可以動(dòng)起來(lái)了。
![](/d/20211016/b209776e8038c7bba84c530c2bb929dd.gif)
但這肯定不是我想要的效果——它的移動(dòng)是一頓一頓的,而我想要順滑的。
現(xiàn)在每一次變化都是直接移動(dòng)一個(gè)格子邊長(zhǎng)的距離,保證蛇移動(dòng)速度不變的情況下,動(dòng)畫(huà)是不可能變得順滑的。所以想要移動(dòng)變得順滑,一種可行的方法是,移動(dòng)一個(gè)格子的距離的過(guò)程分多次繪制。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>blog_snack</title>
<style>
#canvas {
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const GRID_WIDTH = 10; // 格子的邊長(zhǎng)
const GAP_WIDTH = 2; // 空隙的邊長(zhǎng)
const ROW = 10; // 一共有多少行格子&每行有多少個(gè)格子
const COLOR = '#fff'; // 蛇的顏色
const BG_COLOR = '#000';// 背景顏色
const INTERVAL = 1000;
const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定義蛇前進(jìn)的方向
const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每個(gè)方向前進(jìn)時(shí)格子坐標(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; // 初始化一個(gè)方向
drawSnack(ctx, snack, COLOR);
let timer = setInterval(() => {
// 每隔一段時(shí)間就刷新一次
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個(gè)格子前進(jìn)位置
let newGrid = [head[0] + change[0], head[1] + change[1]]; // 新格子的位置
snack.push(newGrid); // 新格子加入蛇身的數(shù)組中
gradientRect(ctx, ...getUniteRect(newGrid, getBetweenTwoGridGap(head, newGrid)), dir, COLOR, INTERVAL);
let delGrid = snack.shift(); // 刪除蛇尾-最后一個(gè)元素
gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])),
getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
}, INTERVAL);
// 給定一個(gè)格子的坐標(biāo)和一個(gè)格子間隙的矩形(左上角,寬,高) 返回兩個(gè)合并的矩形 的左上角、右下角 坐標(biāo)
function getUniteRect(g, rect) {
let p = getGridULCoordinate(g);
if (p[0] === rect[0] && p[1] < rect[1] || // 矩形是在格子正下方
p[1] === rect[1] && p[0] < rect[0]) { // 矩形在格子的正右方
return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
} else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
}
}
// 從格子1 移動(dòng)到格子2 的方向
function getDirection(g1, g2) {
if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
}
// 慢慢的填充一個(gè)矩形 (真的不知道則怎么寫(xiě) 瞎寫(xiě)...動(dòng)畫(huà)的執(zhí)行時(shí)間可能不等于duration 但一定要保證<=duration
// 傳入的是矩形左上角和右下角的坐標(biāo) 以及漸變的方向
function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
let dur = 20;
let times = Math.floor(duration / dur); // 更新次數(shù)
let nowX1 = x1, nowY1 = y1, nowX2 = x2, nowY2 = y2;
let dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0;
if (dir === UP) { dy1 = (y1 - y2) / times; nowY1 = y2; }
if (dir === DOWN) { dy2 = (y2 - y1) / times; nowY2 = y1; }
if (dir === LEFT) { dx1 = (x1 - x2) / times; nowX1 = x2; }
if (dir === RIGHT) { dx2 = (x2 - x1) / times; nowX2 = x1; }
let startTime = Date.now();
let timer = setInterval(() => {
nowX1 += dx1, nowX2 += dx2, nowY1 += dy1, nowY2 += dy2; // 更新
let runTime = Date.now() - startTime;
if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
nowX1 = x1, nowX2 = x2, nowY1 = y1, nowY2 = y2;
clearInterval(timer);
}
ctx.fillStyle = color;
ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
}, dur);
}
// 根據(jù)snack二維數(shù)組畫(huà)一條蛇
function drawSnack(ctx, snack, color) {
ctx.fillStyle = color;
for (let i = 0; i < snack.length; i++) {
ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
if (i) {
ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
}
}
}
// 傳入一個(gè)格子 返回左上角坐標(biāo)
function getGridULCoordinate(g) {
return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
}
// 傳入兩個(gè)格子 返回兩個(gè)格子之間的矩形縫隙
// 這里傳入的兩個(gè)格子必須是相鄰的
// 返回一個(gè)數(shù)組 分別是這個(gè)矩形縫隙的 左上角橫坐標(biāo) 左上角縱坐標(biāo) 寬 高
function getBetweenTwoGridGap(g1, g2) {
let width = GRID_WIDTH + GAP_WIDTH;
if (g1[0] === g2[0]) { // 橫坐標(biāo)相同 是縱向相鄰的兩個(gè)格子
let x = g1[0] * width + GAP_WIDTH;
let y = Math.min(g1[1], g2[1]) * width + width;
return [x, y, GRID_WIDTH, GAP_WIDTH];
} else { // 縱坐標(biāo)相同 是橫向相鄰的兩個(gè)格子
let x = Math.min(g1[0], g2[0]) * width + width;
let y = g1[1] * width + GAP_WIDTH;
return [x, y, GAP_WIDTH, GRID_WIDTH];
}
}
</script>
</body>
</html>
實(shí)話(huà),代碼寫(xiě)的非常糟糕……我也很無(wú)奈……
反正現(xiàn)在蛇可以緩慢順滑的移動(dòng)了。
![](/d/20211016/9ec28c98999e2578c6d93c90f55496ca.gif)
接下來(lái)要做的是判斷是否觸碰到邊緣或者觸碰到自身導(dǎo)致游戲結(jié)束,以及響應(yīng)鍵盤(pán)事件。
這里的改動(dòng)很簡(jiǎn)單。用一個(gè)map標(biāo)記每一個(gè)格子是否被占。每一個(gè)格子(i, j)可以被編號(hào)i*row+j。
const GRID_WIDTH = 10; // 格子的邊長(zhǎng)
const GAP_WIDTH = 2; // 空隙的邊長(zhǎng)
const ROW = 10; // 一共有多少行格子&每行有多少個(gè)格子
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] ]; // 每個(gè)方向前進(jìn)時(shí)格子坐標(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; // 初始化一個(gè)方向
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(() => {
// 每隔一段時(shí)間就刷新一次
// 只有轉(zhuǎn)頭方向與當(dāng)前方向垂直的時(shí)候 才改變方向
if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個(gè)格子前進(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(); // 刪除蛇尾-最后一個(gè)元素
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;
}
// 獲取一個(gè)格子的編號(hào)
function getGridNumber(g) {
return g[0] * ROW + g[1];
}
// 給定一個(gè)格子的坐標(biāo)和一個(gè)格子間隙的矩形(左上角,寬,高) 返回兩個(gè)合并的矩形 的左上角、右下角 坐標(biāo)
function getUniteRect(g, rect) {
/// ... 后面代碼不改變 略....
這時(shí)已經(jīng)可以控制蛇的移動(dòng)了。
![](/d/20211016/9f3ee94184e6384508746770ac1d8e0b.gif)
最后一個(gè)步驟了,畫(huà)蘋(píng)果。蘋(píng)果的位置應(yīng)該是隨機(jī)的,且不與蛇身重疊,另外蛇吃到蘋(píng)果的時(shí)候,長(zhǎng)度會(huì)加一。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>blog_snack</title>
<style>
#canvas {
background-color: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const GRID_WIDTH = 10; // 格子的邊長(zhǎng)
const GAP_WIDTH = 2; // 空隙的邊長(zhǎng)
const ROW = 10; // 一共有多少行格子&每行有多少個(gè)格子
const COLOR = '#fff'; // 蛇的顏色
const BG_COLOR = '#000';// 背景顏色
const FOOD_COLOR = 'red'; // 食物顏色
const INTERVAL = 300;
const UP = 0, LEFT = 1, RIGHT = 2, DOWN = 3; // 定義蛇前進(jìn)的方向
const CHANGE = [ [0, -1], [-1, 0], [1, 0], [0, 1] ]; // 每個(gè)方向前進(jìn)時(shí)格子坐標(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, food;
function initialize() {
snack = [ [2, 3], [2, 4], [2, 5], [3, 5], [4, 5], [4, 4], [5, 4], [5, 5] ]; // 初始化一條🐍
nextDir = dir = RIGHT; // 初始化一個(gè)方向
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);
drawFood();
}
initialize();
let timer = setInterval(() => {
// 每隔一段時(shí)間就刷新一次
// 只有轉(zhuǎn)頭方向與當(dāng)前方向垂直的時(shí)候 才改變方向
if (nextDir !== dir && nextDir + dir !== 3) dir = nextDir;
let head = snack[snack.length - 1]; // 蛇頭
let change = CHANGE[dir]; // 下一個(gè)格子前進(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);
if (newGrid[0] === food[0] && newGrid[1] === food[1]) {
drawFood();
return;
}
let delGrid = snack.shift(); // 刪除蛇尾-最后一個(gè)元素
map[getGridNumber(delGrid)] = 0;
gradientRect(ctx, ...getUniteRect(delGrid, getBetweenTwoGridGap(delGrid, snack[0])),
getDirection(delGrid, snack[0]), BG_COLOR, INTERVAL);
}, INTERVAL);
// 畫(huà)食物
function drawFood() {
food = getFoodPosition();
ctx.fillStyle = FOOD_COLOR;
ctx.fillRect(...getGridULCoordinate(food), GRID_WIDTH, GRID_WIDTH);
}
// 判斷一個(gè)新生成的格子位置是否合法
function isValidPosition(g) {
if (g[0] >= 0 && g[0] < ROW && g[1] >= 0 && g[1] < ROW && !map[getGridNumber(g)]) return true;
return false;
}
// 獲取一個(gè)格子的編號(hào)
function getGridNumber(g) {
return g[0] * ROW + g[1];
}
function getFoodPosition() {
let r = Math.floor(Math.random() * (ROW * ROW - snack.length)); // 隨機(jī)獲取一個(gè)數(shù)字 數(shù)字范圍和剩余的格子數(shù)相同
for (let i = 0; ; i++) { // 只有遇到空位的時(shí)候 計(jì)數(shù)君 r 才減一
if (!map[i] && --r < 0) return [Math.floor(i / ROW), i % ROW];
}
}
// 給定一個(gè)格子的坐標(biāo)和一個(gè)格子間隙的矩形(左上角,寬,高) 返回兩個(gè)合并的矩形 的左上角、右下角 坐標(biāo)
function getUniteRect(g, rect) {
let p = getGridULCoordinate(g);
if (p[0] === rect[0] && p[1] < rect[1] || // 矩形是在格子正下方
p[1] === rect[1] && p[0] < rect[0]) { // 矩形在格子的正右方
return [p[0], p[1], rect[0] + rect[2], rect[1] + rect[3]];
} else if (p[0] === rect[0] && p[1] > rect[1] || // 矩形是在格子正上方
p[1] === rect[1] && p[0] > rect[0]) { // 矩形在格子的正左方
return [rect[0], rect[1], p[0] + GRID_WIDTH, p[1] + GRID_WIDTH];
}
}
// 從格子1 移動(dòng)到格子2 的方向
function getDirection(g1, g2) {
if (g1[0] === g2[0] && g1[1] < g2[1]) return DOWN;
if (g1[0] === g2[0] && g1[1] > g2[1]) return UP;
if (g1[1] === g2[1] && g1[0] < g2[0]) return RIGHT;
if (g1[1] === g2[1] && g1[0] > g2[0]) return LEFT;
}
// 慢慢的填充一個(gè)矩形 (真的不知道則怎么寫(xiě) 瞎寫(xiě)...動(dòng)畫(huà)的執(zhí)行時(shí)間可能不等于duration 但一定要保證<=duration
// 傳入的是矩形左上角和右下角的坐標(biāo) 以及漸變的方向
function gradientRect(ctx, x1, y1, x2, y2, dir, color, duration) {
let dur = 20;
let times = Math.floor(duration / dur); // 更新次數(shù)
let nowX1 = x1, nowY1 = y1, nowX2 = x2, nowY2 = y2;
let dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0;
if (dir === UP) { dy1 = (y1 - y2) / times; nowY1 = y2; }
if (dir === DOWN) { dy2 = (y2 - y1) / times; nowY2 = y1; }
if (dir === LEFT) { dx1 = (x1 - x2) / times; nowX1 = x2; }
if (dir === RIGHT) { dx2 = (x2 - x1) / times; nowX2 = x1; }
let startTime = Date.now();
let timer = setInterval(() => {
nowX1 += dx1, nowX2 += dx2, nowY1 += dy1, nowY2 += dy2; // 更新
let runTime = Date.now() - startTime;
if (nowX1 < x1 || nowX2 > x2 || nowY1 < y1 || nowY2 > y2 || runTime >= duration - dur) {
nowX1 = x1, nowX2 = x2, nowY1 = y1, nowY2 = y2;
clearInterval(timer);
}
ctx.fillStyle = color;
ctx.fillRect(nowX1, nowY1, nowX2 - nowX1, nowY2 - nowY1);
}, dur);
}
// 根據(jù)snack二維數(shù)組畫(huà)一條蛇
function drawSnack(ctx, snack, color) {
ctx.fillStyle = color;
for (let i = 0; i < snack.length; i++) {
ctx.fillRect(...getGridULCoordinate(snack[i]), GRID_WIDTH, GRID_WIDTH);
if (i) {
ctx.fillRect(...getBetweenTwoGridGap(snack[i], snack[i - 1]));
}
}
}
// 傳入一個(gè)格子 返回左上角坐標(biāo)
function getGridULCoordinate(g) {
return [g[0] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH, g[1] * (GRID_WIDTH + GAP_WIDTH) + GAP_WIDTH];
}
// 傳入兩個(gè)格子 返回兩個(gè)格子之間的矩形縫隙
// 這里傳入的兩個(gè)格子必須是相鄰的
// 返回一個(gè)數(shù)組 分別是這個(gè)矩形縫隙的 左上角橫坐標(biāo) 左上角縱坐標(biāo) 寬 高
function getBetweenTwoGridGap(g1, g2) {
let width = GRID_WIDTH + GAP_WIDTH;
if (g1[0] === g2[0]) { // 橫坐標(biāo)相同 是縱向相鄰的兩個(gè)格子
let x = g1[0] * width + GAP_WIDTH;
let y = Math.min(g1[1], g2[1]) * width + width;
return [x, y, GRID_WIDTH, GAP_WIDTH];
} else { // 縱坐標(biāo)相同 是橫向相鄰的兩個(gè)格子
let x = Math.min(g1[0], g2[0]) * width + width;
let y = g1[1] * width + GAP_WIDTH;
return [x, y, GAP_WIDTH, GRID_WIDTH];
}
}
</script>
</body>
</html>
我不管 我寫(xiě)完了 我的代碼最棒了(口區(qū)
![](/d/20211016/c8975b41db2f295cbe59d4d7ad1f03a2.gif)
如果蛇能自己動(dòng)就好了。。。我的想法很單純。。。但是想了很久沒(méi)結(jié)果的時(shí)候,Google一下才發(fā)現(xiàn)這好像涉及到AI了。。。頭疼。。。
最終我選取的方案是:
if 存在蛇頭到蘋(píng)果的路徑 and 蛇身長(zhǎng)度小于整個(gè)地圖的一半
虛擬蛇去嘗試吃蘋(píng)果
if 吃完蘋(píng)果后能找到蛇頭到蛇尾的路徑
BFS到蛇尾
else if 存在蛇頭到蛇尾的路徑
走蛇頭到蛇尾的最長(zhǎng)路徑
else
隨機(jī)一個(gè)方向
我只是想練習(xí)Canvas而已…所以就沒(méi)有好好寫(xiě)。代碼有點(diǎn)長(zhǎng)就不貼了。
(因?yàn)槲业纳吆艽?。。是真的蠢。?!?/p>
完整代碼可見(jiàn)github --> https://github.com/G-lory/front-end-practice/blob/master/canvas/blog_snack.html
這次寫(xiě)完感覺(jué)我的代碼能力實(shí)在是太差了,寫(xiě)了兩遍還是很亂。 以后還是要多練習(xí)。
反正沒(méi)有bug是不可能的,這輩子是不可能的。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。