如果想做個圓形迷宮,方式之一是透過遮罩,例如,透過圓形遮罩結合〈迷宮與遮罩〉的程式,可以做出以下的迷宮:
不過,也許會覺得用這種方式做圓形迷宮有點投機取巧,畢竟,你可能看過這樣的圓形迷宮:
這種迷宮是基於極座標繪製而成,常被稱為 Theta 迷宮,有沒有可能基於〈遞迴回溯迷宮〉實作 Theta 迷宮呢?畢竟,〈蜂巢狀迷宮〉就是基於遞迴回溯迷宮,只是繪圖上做些變化。
基於方形迷宮變化
基本上是可以,出發點是可以將底下的圖形:
繞為底下的圓:
若要配合遞迴回溯迷宮的細胞索引方式,就像是將矩形迷宮轉 90 度後,將每一列剪下來,變成環狀,細胞索引 y 就是 y 環索引,索引 x 就是每一環細胞的逆時針 x 索引:
如果只是將方拉成圓,你也可以不用如上拉成圓,只不過這樣拉圓,配合原本方形迷宮的索引方式會比較方便罷了,顯然地,若要用這樣的方式建立 Theta 迷宮,Maze
的實作完全不用更改,純粹就是繪圖的方式罷了。
因為將方形迷宮逆時針轉 90 度了,方形迷宮中每個細胞的上與右牆,會變成:
將 TWO_PI
除以行(column) 數,就可以求得 Θ
,方形迷宮的 x
索引,就是對應至的 Θ * x
與 Θ * (x + 1)
間的弧,圓形細胞的大小會以上圖中的「細胞寬度」決定,圖中定義了「內半徑向量」與「外半徑向量」,是因為 p5.js 中提供了 p5.Vector
,透過這兩個向量的操作,可以很簡單的取得繪製上牆、右牆必要的頂點。
繪製 Theta 迷宮
從上頭的描述可以知道,〈遞迴回溯迷宮〉的 Maze
實作不用任何修改,需要的就只是繪製方形改為繪製圓形,一個簡化繪製程式實作的方式是透過 p5.Vector
,這樣就不用直接使用三角函式了:
function drawMaze(maze, cellWidth) {
const thetaStep = TWO_PI / maze.columns;
maze.cells.forEach(cell => {
const innerR = (cell.y) * cellWidth;
const outerR = (cell.y + 1) * cellWidth;
const theta1 = -thetaStep * cell.x;
const theta2 = -thetaStep * (cell.x + 1);
const innerVt1 = p5.Vector.fromAngle(theta1, innerR);
const innerVt2 = p5.Vector.fromAngle(theta2, innerR);
const outerVt2 = p5.Vector.fromAngle(theta2, outerR);
if(cell.wallType === Maze.TOP_WALL || cell.wallType === Maze.TOP_RIGHT_WALL) {
line(innerVt1.x, innerVt1.y, innerVt2.x, innerVt2.y);
}
if(cell.wallType === Maze.RIGHT_WALL || cell.wallType === Maze.TOP_RIGHT_WALL) {
line(innerVt2.x, innerVt2.y, outerVt2.x, outerVt2.y);
}
});
const r = cellWidth * (maze.rows);
for(let theta = 0; theta < TWO_PI; theta = theta + thetaStep) {
const vt1 = p5.Vector.fromAngle(theta, r);
const vt2 = p5.Vector.fromAngle(theta + thetaStep, r);
line(vt1.x, vt1.y, vt2.x, vt2.y);
}
}
然而,若直接使用這個 drawMaze
來繪製 rows
、columns
都是 8 的迷宮,會長這樣:
怎麼越外圍開口越大呢?而且,為什麼我是使用 line
來繪製啊?使用 line
來繪製,就是為了突顯越外圍開口越大這件事,實際上,就算你試圖使用 arc
來繪製,試著讓開口變小,也不能改變這迷宮在繪製時,越外環細胞越大的事實,畢竟,這只是純粹將方形拉成圓形罷了。
單純只是想讓這迷宮好看一些,不要內外環開口差距太明顯的話,可以讓最內環的半徑大一些,然後減少 rows
、增加 columns
:
function drawMaze(maze, cellWidth) {
const thetaStep = TWO_PI / maze.columns;
maze.cells.forEach(cell => {
// 增加最內環的半徑
const innerR = (cell.y + 4) * cellWidth;
const outerR = (cell.y + 5) * cellWidth;
...
});
// 最外的牆也要做調整
const r = cellWidth * (maze.rows + 4);
for(let theta = 0; theta < TWO_PI; theta = theta + thetaStep) {
const vt1 = p5.Vector.fromAngle(theta, r);
const vt2 = p5.Vector.fromAngle(theta + thetaStep, r);
line(vt1.x, vt1.y, vt2.x, vt2.y);
}
}
這樣畫出來就會好一些:
只不過這比較算是迷宮環了,或許在中間開個口會比較好,當寶物室?
function drawMaze(maze, cellWidth) {
const thetaStep = TWO_PI / maze.columns;
maze.cells.forEach(cell => {
// (0, 0) 不畫牆
if(cell.x === 0 && cell.y === 0) {
return;
}
...
});
...
}
另一個問題是,角度為 0 時的牆始終是封閉的,也就是以上的繪製方法,右方總是會有一條實線?當然,因為是將方形拉成圓,記得嗎?方形迷宮時,最右邊的牆是不會打通的,因為是邊界嘛!
如果你讓最右邊的牆可以打通,這意謂著穿越回到最左邊的細胞,反之亦然,這樣拉成圓時,表示可以順、逆時針無阻礙地打牆,這只要在 Maze
的 nextX
小修改一下:
function nextX(maze, x, dir) {
let nx = x + [1, 0, -1, 0][dir];
return nx >= 0 ? (nx % maze.columns) : (nx + maze.columns);
}
當然,呼叫 nextX
的地方,也要記得改一下傳入 Maze
實例,而 visitRight
、visitTop
、visitLeft
、visitBottom
,不能只是對 currentCell.x
加減 1,改呼叫 nextX
才行:
function visitRight(maze, currentCell) {
if(currentCell.wallType === Maze.TOP_RIGHT_WALL) {
currentCell.wallType = Maze.TOP_WALL;
}
else {
currentCell.wallType = Maze.NO_WALL;
}
maze.cells.push(cell(nextX(maze, currentCell.x, R), nextY(currentCell.y, R), Maze.TOP_RIGHT_WALL));
}
function visitTop(maze, currentCell) {
if(currentCell.wallType === Maze.TOP_RIGHT_WALL) {
currentCell.wallType = Maze.RIGHT_WALL;
}
else {
currentCell.wallType = Maze.NO_WALL;
}
maze.cells.push(cell(nextX(maze, currentCell.x, T), nextY(currentCell.y, T), Maze.TOP_RIGHT_WALL));
}
function visitLeft(maze, currentCell) {
maze.cells.push(cell(nextX(maze, currentCell.x, L), nextY(currentCell.y, L), Maze.TOP_WALL));
}
function visitBottom(maze, currentCell) {
maze.cells.push(cell(nextX(maze, currentCell.x, B), nextY(currentCell.y, B), Maze.RIGHT_WALL));
}
這樣的話就可以繪製出以下的迷宮:
要說這是 Theta 迷宮,也算是啦!只不過不是這篇文件一開頭看到的那個 Theta 迷宮,那個 Theta 迷宮的製作方式,就留待下一篇來探討了。
底下列出至今的成果: