iT邦幫忙

2025 iThome 鐵人賽

DAY 6
1

在上一篇實作了攻擊系統後,目前勇者已經可以做出揮劍的動作,但是一個空蕩蕩的格子地圖實在談不上什麼冒險體驗,感覺還少了什麼東西。

沒錯!少了場景!尤其是我們預計要做的是 Rogue-lite 遊戲,最少也要有一個類似地牢的房間,並且可以隨機產生出不同的房間。

想像一下,如果 Rogue-lite 遊戲都只有單一的地圖,那還會有人想一玩再玩嗎?

產生隨機地圖正是 Rogue-lite 遊戲的靈魂,它決定了每次遊戲體驗的獨特性。沒有隨機性,玩家很快就會記住所有的陷阱位置、敵人配置,遊戲就失去了探索的樂趣。Rogue-lite 遊戲的特性就是,每個房間都是未知的驚喜——也許是放寶物,也許是打王,又或許是隱藏的關卡。

所以本篇我們要做的的是一個完整的產生房間系統,包含:基本的矩形房間、複雜的 L 型房間、T 型房間,以及最具挑戰性的十字型房間。每種房間都有自己的通道和門系統,為後續的敵人、道具配置奠定基礎。

最重要的是,每次啟動遊戲都會看到不同的房間格局,剛好是 Rogue-lite 遊戲的魅力所在。

如何設計房間

一個房間需要有一個門和牆壁,這是最基本的組成。雖然是可以使用一開始製作藍色方塊的方式來建立,但那種方式有點不太好看。

既然我們在之前的篇章有下載整包的素材,就要善用所有能用的資源。剛好素材中已經有了牆壁以及門,所以就把這些圖片整理好,並且放到 /assets 中。

就像如果用牆壁組成一個房間的雛形大概會是這樣:

牆壁的圖片排列示意圖

重新命名這些圖片很困難,而且要想著應該要怎麼排列才是比較符合我們想要的樣子。

總之,現在的圖片素材大概分成這些:

有角色、武器,還有組成房間的牆壁、門和地板

然後在專案中新增一個 /resources 的 mod 來專門管理房間圖片的處理,這樣模組化的管理對之後比較好擴充跟整理。

有了這些東西就可以自由排列出我們想要的房間,核心設計理念是「基礎矩形 + 組合邏輯」。每個複雜房間都由多個 RoomRect 結構組成,這些不是 ECS 元件,而是單純的資料結構,用來描述房間的幾何形狀。

真正的 ECS 元件是 RoomTile,代表每一個實際的牆壁,以及 CompoundRoom,負責控制如何組合整個房間。

想了三種實作方案:第一種是簡單的矩形填充,像《寶可夢》那樣的規則房間,感覺變化有限;第二種是 BSP(Binary Space Partitioning)分割演算法,適合《暗黑破壞神》那樣的大型地牢,但對我們的 MVP 來說過於複雜;第三種是組合式,透過多個基礎矩形拼接成 L 型、T 型、十字型房間。

最後選擇第三種方案,主要是在複雜度和靈活性之間取得了完美平衡。而且這種設計很容易擴充,未來想加入更奇怪的房間形狀時,只要增加新的矩形組合邏輯就好。

基本的矩形

連接系統使用「覆蓋式走廊」的設計:先替每個矩形產生完整的結構(包含所有牆壁),然後在重疊區域放置更高 Z 層級的地板瓷磚來「覆蓋」牆壁,這樣就有自然的通道。這種方法避免掉複雜的牆壁移除邏輯,同時可以保證房間的結構完整性,不然有時候房間的組成會變的奇怪。

門系統是另一個一個問題:門應該放在哪裡?

我的策略是動態尋找最下方的矩形(y座標最小),確保門總是出現在視覺上合理的位置,而不是隨意出現在房間內部。

十字型
L型

程式碼實作

再一次的體會到 Bevy ECS 的好用,尤其是如何將複雜的遊戲邏輯拆分成清晰的元件和系統,讓我解析整個實作過程。

隨機房間類型選擇系統

產生房間的核心從 spawn_room 系統開始:

pub fn spawn_room(mut commands: Commands, asset_server: Res<AssetServer>) {
    let mut rng = rand::thread_rng();
    let room_type_choice = rng.gen_range(0..4);
    
    match room_type_choice {
        0 => {
            // 基本矩形房間 (30% 機率)
            let room_width = rng.gen_range(8..15);
            let room_height = rng.gen_range(6..10);
            let room_x = -(room_width as i32) / 2;
            let room_y = -(room_height as i32) / 2;
            generate_room_tiles(&mut commands, &asset_server, room_width, room_height, room_x, room_y, true);
        },
        1 => {
            // L 形房間 (25% 機率)
            let compound_room = generate_l_shape_room(&mut rng);
            spawn_compound_room(&mut commands, &asset_server, compound_room);
        },
        // ... T 形和十字形房間
    }
}

這裡的設計在於置中策略:所有房間都以世界座標 (0,0) 為視覺中心產生,確保玩家總是在房間的合理位置開始遊戲。而隨機尺寸的設定讓每個矩形房間都有不同的大小感受。

複合房間的元件設計

我最喜歡的是複合房間的設計,它完美展現了組合優於繼承的理念:

#[derive(Component, Debug)]
pub struct CompoundRoom {
    pub rectangles: Vec<RoomRect>,
    pub room_type: CompoundRoomType,
}

fn generate_l_shape_room(rng: &mut impl Rng) -> CompoundRoom {
    let main_width = rng.gen_range(6..10);
    let main_height = rng.gen_range(8..12);
    let extension_width = rng.gen_range(5..9);
    let extension_height = rng.gen_range(4..7).min(main_height - 1);
    
    // 主房間(垂直部分)
    let main_rect = RoomRect {
        x: -(main_width as i32) / 2,
        y: -(main_height as i32) / 2,
        width: main_width,
        height: main_height,
    };
    
    // 擴展房間(水平部分)- 與主房間重疊 1 格確保連通
    let extension_rect = RoomRect {
        x: main_rect.x + (main_width as i32) - 1, // 關鍵:重疊 1 格
        y: main_rect.y,
        width: extension_width + 1,
        height: extension_height,
    };
    
    CompoundRoom {
        rectangles: vec![main_rect, extension_rect],
        room_type: CompoundRoomType::LShape,
    }
}

重疊設計是這裡的關鍵技術點:extension_rect.x = main_rect.x + main_width - 1 讓兩個矩形重疊一格,為後續的走廊連接提供了基礎。

瓷磚生成的精細化處理

generate_room_tiles 函式是整個系統最複雜的部分,它處理了房間的層次結構:

fn generate_room_tiles(/*...*/) {
    let total_height = height + 1; // 增加一行給南牆外側
    
    for y in 0..total_height {
        for x in 0..width {
            let (tile_type, texture_handle) = if y == total_height - 1 {
                // 北牆(上方,面向玩家)
                if x == 0 {
                    (RoomTileType::WallNInnerCornerW, room_assets.wall_n_inner_corner_w.clone())
                } else if x == width - 1 {
                    (RoomTileType::WallNInnerCornerE, room_assets.wall_n_inner_corner_e.clone())
                } else {
                    (RoomTileType::WallNInnerMid, room_assets.wall_n_inner_mid.clone())
                }
            } else if y == 0 {
                // 南牆外側 - 門的生成邏輯
                let door_x = width / 2;
                if x == door_x && should_generate_door {
                    (RoomTileType::DoorClosed, room_assets.door_closed.clone())
                } else {
                    (RoomTileType::WallSOuterMid, room_assets.wall_s_outer_mid.clone())
                }
            } // ... 其他牆壁和地板邏輯
        }
    }
}

這個三層結構(南牆外側、南牆內側、內部空間、北牆)創造了立體的房間感受,比簡單的邊框式房間更有深度。

覆蓋式走廊連接系統

最巧妙的設計是走廊連接系統,它採用「覆蓋」而非「移除」的策略:

fn create_l_shape_corridor(commands: &mut Commands, room_assets: &RoomAssets, tile_size: f32, rectangles: &[RoomRect]) {
    let main_rect = &rectangles[0];
    let ext_rect = &rectangles[1];
    
    // 在重疊區域建立走廊
    let corridor_x = main_rect.x + main_rect.width as i32 - 1;
    let corridor_y = ext_rect.y + 1;
    
    commands.spawn((
        Sprite::from_image(room_assets.floor_indoor.clone()),
        Transform::from_translation(Vec3::new(
            corridor_x as f32 * tile_size,
            corridor_y as f32 * tile_size,
            Z_LAYER_GRID + 0.2, // 關鍵:更高的 Z 層級
        )).with_scale(Vec3::splat(PLAYER_SCALE)),
        RoomTile { tile_type: RoomTileType::Floor },
    ));
}

Z_LAYER_GRID + 0.2 是這裡的精髓,地板瓷磚的 Z 值比牆壁稍高,自然地「覆蓋」了牆壁,創造出通道效果。這種方法避免了複雜的牆壁實體移除邏輯。

現在每次執行 cargo run 都能看到完全不同的房間類型。

門系統的設定算是取得快速開發跟達到要求的平衡。無論房間形狀多複雜,門總是出現在視覺上最合理的位置——最下方的矩形區域的南牆中央,主要是門的圖片限制,而且也不太想在這個部分投入太多時間調整。

所有房間瓷磚使用 3 倍縮放(PLAYER_SCALE = 3.0),為了跟我們的角色保持完美的比例協調。

實際操作

小結

實作隨機產生房間系統的過程,讓我對 「簡單性與複雜性的平衡」 有了更深的體會。

最一開始我想做一個完美的地牢產生系統,參考了很多遊戲的資料,像是《暗黑破壞神》,但很快就意識到這對 MVP 來說太過複雜==

總之這個架構最棒的地方是可擴展性。目前的 CompoundRoom 設計可以輕鬆加入新的房間類型,不過這部分之後再調整了。而且現在每個 RoomTile 都有獨立的元件,未來可能可以輕鬆加入陷阱等機制。

今天的程式碼分享在 repo


上一篇
拿起勇者之劍攻擊
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
阿鵝
iT邦研究生 5 級 ‧ 2025-09-21 01:08:31

/images/emoticon/emoticon34.gif
分享個製作隨機地圖的方法 https://youtu.be/hzu_Nv6deP4

Bucky iT邦新手 3 級 ‧ 2025-09-21 03:29:19 檢舉

太專業了👍

我要留言

立即登入留言