在 Day 11 中,我們成功建立了「1x1 模組化立體堆疊」系統,並透過動態材質判定,讓每一塊城牆磚石在堆疊時都能維持 1:1 的完美像素長寬比。然而,在昨天的程式碼中,方塊堆疊的高度是直接讀取自 wallHeightMap[row][col]。如果沒有一套精密的建築學演算法來為這張高度映射表賦予數值,地圖上的牆壁就只會是一根根高度固定、排列呆板的積木方柱,完全無法傳達出地下城歷經戰火與歲月侵蝕的「古堡廢墟」質感。
真正的歷史遺跡絕不會整齊劃一。受重力、風化與結構應力的影響,殘存的石牆往往呈現「中心尚存、兩端崩塌」的自然斷裂面,且斷壁周圍的地面必然會散落大量坍塌的碎石。今天我們將深入地圖生成的演算法核心,透過結構化的牆段資料陣列、邊界侵蝕條件判斷、碎石動態噴射以及 L 型轉角延伸,為地城建構出極具說服力的殘垣斷壁!
要生成既有隨機自然感、又符合關卡空間動線的古堡城牆,我們不能使用純粹的亂數在地圖上亂點方塊,否則很容易把道路徹底堵死。我們採用「結構化片段(Segment-based)」的資料設計法,將地城劃分為左右兩側區域,並透過物件陣列來定義每一道殘破城牆的起源與走向。
我們在 src/main.js 中宣告 wallSegments 陣列,定義左側與右側區域的牆體分佈,並建立 lanternPositions 陣列以收集後續需要安裝壁燈的座標點:
// 3) 生成古堡殘破城牆 (具備高低起伏破壞層次與轉角)
const wallSegments = [
// 左側區域城牆遺跡
{ r: 12, c: 8, len: 6, dir: 'H', corner: true },
{ r: 20, c: 6, len: 7, dir: 'H', corner: false },
{ r: 28, c: 10, len: 5, dir: 'V', corner: true },
{ r: 35, c: 7, len: 6, dir: 'H', corner: false },
{ r: 16, c: 15, len: 5, dir: 'V', corner: false },
{ r: 32, c: 16, len: 6, dir: 'H', corner: true },
// 右側區域城牆遺跡
{ r: 11, c: 32, len: 7, dir: 'H', corner: true },
{ r: 22, c: 30, len: 6, dir: 'V', corner: false },
{ r: 18, c: 38, len: 6, dir: 'H', corner: true },
{ r: 30, c: 34, len: 7, dir: 'H', corner: false },
{ r: 36, c: 31, len: 5, dir: 'V', corner: true },
{ r: 26, c: 41, len: 5, dir: 'V', corner: false },
];
const lanternPositions = [];
接下來,我們使用 forEach 遍歷 wallSegments,並在內層以長度索引 k(從 0 遞增到 seg.len - 1)推進座標。 在填充方塊前,我們必須嚴格過濾非法座標:排除靠近地圖外框 2 格邊界內的區域(避免物件溢出地圖),並強制排除中央主路線區域 curC >= 23 && curC <= 26。通過邊界檢驗後,最核心的「高低漸變邏輯」正式登場:
wallSegments.forEach((seg, sIdx) => {
const isH = seg.dir === 'H';
for (let k = 0; k < seg.len; k++) {
const curR = isH ? seg.r : seg.r + k;
const curC = isH ? seg.c + k : seg.c;
if (curR < 2 || curR >= gridRows - 2 || curC < 2 || curC >= gridCols - 2) continue;
// 避開中間主路線
if (curC >= 23 && curC <= 26) continue;
// 計算殘破牆壁的高低漸變 (中間高 3 層,兩端逐漸倒塌成 2 或 1 層)
let height = 3;
if (k === 0 || k === seg.len - 1) {
height = Math.random() < 0.5 ? 1 : 2;
} else if (k === 1 || k === seg.len - 2) {
height = Math.random() < 0.7 ? 2 : 3;
}
mapGrid[curR][curC] = 5;
wallHeightMap[curR][curC] = height;
石牆崩塌時,碎落的石磚不會在空氣中憑空蒸發,它們必然會散落跌落在斷壁四周的地面上。為了實現這個物理細節,我們在確認目前節點處於最外側斷口(k === 0 || k === seg.len - 1)時,加入了 45% 機率的掉落演算法:
// 在牆壁斷裂處隨機掉落散落碎石塊
if ((k === 0 || k === seg.len - 1) && Math.random() < 0.45) {
const offsetR = curR + (Math.random() > 0.5 ? 1 : -1);
const offsetC = curC + (Math.random() > 0.5 ? 1 : -1);
if (offsetR >= 0 && offsetR < gridRows && offsetC >= 0 && offsetC < gridCols) {
if (mapGrid[offsetR][offsetC] === 0 && !(offsetC >= 24 && offsetC <= 25)) {
mapGrid[offsetR][offsetC] = 2;
}
}
}
}
在長度迴圈結束後,我們針對該牆段進行額外的幾何擴充與照明定錨:
// 轉角 L 型延伸
if (seg.corner) {
const cornerR = isH ? seg.r + 1 : seg.r;
const cornerC = isH ? seg.c + seg.len - 1 : seg.c + 1;
if (cornerR < gridRows - 2 && cornerC < gridCols - 2 && !(cornerC >= 23 && cornerC <= 26)) {
mapGrid[cornerR][cornerC] = 5;
wallHeightMap[cornerR][cornerC] = 2;
}
}
// 選定部分主要牆端設立壁燈
if (sIdx % 2 === 0) {
const lampR = isH ? seg.r : seg.r + 2;
const lampC = isH ? seg.c + 2 : seg.c;
lanternPositions.push({ r: lampR, c: lampC });
}
});
【本日結語】
至此,我們徹底告別了隨機雜亂的方塊分佈!透過 wallSegments 的佈局規劃、高低漸變演算法、碎石動態噴射以及 L 型拐角延伸,整座 50x50 的地宮現在擁有了錯落有致的房間結構與充滿歷史殘破感的石牆立面。
明天(Day 13),我們將為這座幽暗的古堡點燃電影級光影——開啟 WebGL 軟陰影映射(PCFSoftShadowMap),調配深藍紫的環境冷底光與斜射月光,並用指數地城迷霧(FogExp2)為場景染上深邃的 HD-2D 神秘氛圍