這邊的蜂巢狀迷宮,是指每個細胞的外觀都是正六角形,如蜂巢般排列,例如:
有些人第一眼的想法可能是,因為每個細胞的外觀是正六角形,可以對先前〈遞迴回溯迷宮〉的 Maze
做些修改,讓行進路徑有六個方向,對吧?
其實不用,仔細觀察一下這個迷宮中每個細胞的排列方式,還是基於行列,將每一列以不同顏色表示,很容易就能看出了:
細胞牆面
既然是基於行列,就表示〈遞迴回溯迷宮〉的 Maze
完全不用修改,我們只要改變細胞繪製的方式就可以了,在六角形結構下,往上或往下走沒有問題,就是打通六角形的上或下邊,那麼往左或往右呢?
往左或往右時,可以看到依行的不同,打通的牆會不同,以往右為例,若行索引以 0 開始,那麼偶數索引必須打通右下牆,奇數索引必須打通右上牆。
Maze
有 Maze.TOP_RIGHT_WALL
、Maze.TOP_WALL
、Maze.RIGHT_WALL
、Maze.NO_WALL
,對應的牆面有哪些呢?
同樣地,不必六個邊都畫上,Maze.TOP_RIGHT_WALL
對應的牆面是:
將一組細胞排列,就會形成以下的結構:
最後只要補足上、左、右的牆就可以了:
Maze.TOP_WALL
時沒有問題,只要畫出上牆,Maze.RIGHT_WALL
呢?方才談到,往左或往右時,可以看到依行的不同,打通的牆會不同,若行索引以 0 開始,那麼偶數索引必須打通右下牆,這表示偶數索引必須繪製右上牆,奇數索引必須繪製右下牆。
最後,Maze.NO_WALL
並不是就完全不畫牆,上牆是不用畫沒錯,然而 Maze.NO_WALL
表示在方形細胞中右邊沒有牆面,而在這邊的六角形細胞中,表示偶數索引右下沒有牆面,也就是必須繪製右上牆,奇數索引是右上沒有牆面,也就是必須繪製右下牆。
繪製蜂巢狀迷宮
依照方才的說明,先來繪製單個細胞,一開始先不要管哪邊有牆,先畫出這個:
這是六角形的一部份,不過別急著拿出三角函式,使用向量會更方便一些,底下的 cellWidth
是指六角形最左至最右的寬度,除以 2 就是六角形中心至頂點的長度:
const r = cellWidth / 2;
const vertices = [
p5.Vector.fromAngle(radians(60), r),
p5.Vector.fromAngle(radians(0), r),
p5.Vector.fromAngle(radians(-60), r),
p5.Vector.fromAngle(radians(-120), r)
];
function drawCell(cell, r) {
for(let i = 0; i < 3; i++) {
line(vertices[i].x, vertices[i].y, vertices[i + 1].x, vertices[i + 1].y)
}
}
然後來判斷哪些牆要畫:
function drawCell(cell, r) {
const isXOdd = isOdd(cell.x);
// 往上無法通行
if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.TOP_WALL) {
line(vertices[3].x, vertices[3].y, vertices[2].x, vertices[2].y);
}
// 往右無法通行
if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.RIGHT_WALL) {
line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
}
else {
// 往右可以通行,根據列索引決定要畫右上還是右下
if(isXOdd) {
line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
}
else {
line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
}
}
}
接著要畫出整個迷宮:
function drawMaze(maze, cellWidth) {
const r = cellWidth / 2;
const vertices = [
p5.Vector.fromAngle(radians(60), r),
p5.Vector.fromAngle(radians(0), r),
p5.Vector.fromAngle(radians(-60), r),
p5.Vector.fromAngle(radians(-120), r)
];
function drawCell(cell, r) {
...同前
}
// 每個細胞的基本 x, y 位移
const xStep = cellWidth - (vertices[1].x - vertices[2].x);
const yStep = vertices[0].y - vertices[2].y;
maze.cells.forEach(cell => {
const isXOdd = isOdd(cell.x);
const isXEven = !isXOdd;
const px = r + xStep * cell.x;
const py = r + yStep * cell.y + (isXOdd ? vertices[0].y : 0);
push();
translate(px, py);
drawCell(cell, r);
pop();
});
}
因為每個細胞最多只畫三面牆,上、左、下邊界有缺牆,為此必須補上邊界:
function drawMaze(maze, cellWidth) {
const r = cellWidth / 2;
const vertices = [
p5.Vector.fromAngle(radians(60), r),
p5.Vector.fromAngle(radians(0), r),
p5.Vector.fromAngle(radians(-60), r),
p5.Vector.fromAngle(radians(-120), r),
// 畫邊界需要的頂點
p5.Vector.fromAngle(radians(-180), r),
p5.Vector.fromAngle(radians(-240), r)
];
function drawCell(cell, r) {
const isXOdd = isOdd(cell.x);
if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.TOP_WALL) {
line(vertices[3].x, vertices[3].y, vertices[2].x, vertices[2].y);
}
if(cell.wallType === Maze.TOP_RIGHT_WALL || cell.wallType === Maze.RIGHT_WALL) {
line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
}
else {
if(isXOdd) {
line(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y);
}
else {
line(vertices[1].x, vertices[1].y, vertices[2].x, vertices[2].y);
}
}
}
const xStep = cellWidth - (vertices[1].x - vertices[2].x);
const yStep = vertices[0].y - vertices[2].y;
maze.cells.forEach(cell => {
const isXOdd = isOdd(cell.x);
const isXEven = !isXOdd;
const px = r + xStep * cell.x;
const py = r + yStep * cell.y + (isXOdd ? vertices[0].y : 0);
push();
translate(px, py);
drawCell(cell, r);
// 補上邊界
if(cell.x === 0) {
line(vertices[3].x, vertices[3].y, vertices[4].x, vertices[4].y);
line(vertices[4].x, vertices[4].y, vertices[5].x, vertices[5].y);
}
// 補左邊界
if(cell.y === 0 && isXEven) {
line(vertices[3].x, vertices[3].y, vertices[4].x, vertices[4].y);
}
// 補下邊界
if(cell.y === maze.rows - 1) {
line(vertices[5].x, vertices[5].y, vertices[0].x, vertices[0].y);
if(isXOdd) {
line(vertices[4].x, vertices[4].y, vertices[5].x, vertices[5].y);
}
}
pop();
});
}
有了這個 drawDraw
,就可以結合 Maze
來繪製蜂巢狀迷宮了: