改进的走廊


关于本教程

本教程是免费和开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!

如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon

Hands-On Rust


到目前为止,我们的走廊生成方式相当原始,存在重叠现象 - 除非您使用 Voronoi 生成,否则走廊里什么都没有。本章将尝试提供更多生成策略(反过来提供更多地图多样性),并允许走廊包含实体。

新的走廊策略:最近邻

使地图感觉更自然的一种方法是在近邻之间构建走廊。这减少(但不能消除)重叠,并且看起来更像是某人可能实际 建造 的东西。我们将创建一个新文件 map_builders/rooms_corridors_nearest.rs

#![allow(unused)]
fn main() {
use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor };
use rltk::RandomNumberGenerator;
use std::collections::HashSet;

pub struct NearestCorridors {}

impl MetaMapBuilder for NearestCorridors {
    #[allow(dead_code)]
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) {
        self.corridors(rng, build_data);
    }
}

impl NearestCorridors {
    #[allow(dead_code)]
    pub fn new() -> Box<NearestCorridors> {
        Box::new(NearestCorridors{})
    }

    fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        let rooms : Vec<Rect>;
        if let Some(rooms_builder) = &build_data.rooms {
            rooms = rooms_builder.clone();
        } else {
            panic!("Nearest Corridors require a builder with room structures");
        }

        let mut connected : HashSet<usize> = HashSet::new();
        for (i,room) in rooms.iter().enumerate() {
            let mut room_distance : Vec<(usize, f32)> = Vec::new();
            let room_center = room.center();
            let room_center_pt = rltk::Point::new(room_center.0, room_center.1);
            for (j,other_room) in rooms.iter().enumerate() {
                if i != j && !connected.contains(&j) {
                    let other_center = other_room.center();
                    let other_center_pt = rltk::Point::new(other_center.0, other_center.1);
                    let distance = rltk::DistanceAlg::Pythagoras.distance2d(
                        room_center_pt,
                        other_center_pt
                    );
                    room_distance.push((j, distance));
                }
            }

            if !room_distance.is_empty() {
                room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
                let dest_center = rooms[room_distance[0].0].center();
                draw_corridor(
                    &mut build_data.map,
                    room_center.0, room_center.1,
                    dest_center.0, dest_center.1
                );
                connected.insert(i);
                build_data.take_snapshot();
            }
        }
    }
}
}

这里有一些您现在应该熟悉的样板代码,所以让我们过一遍 corridors 函数:

  1. 我们首先获取 rooms 列表,如果没有则 panic!
  2. 我们创建一个名为 connected 的新的 HashSet。当房间获得出口时,我们会将房间添加到其中,以避免重复连接到同一个房间。
  3. 对于每个房间,我们检索一个名为 i 的“枚举”(向量中的索引号)和 room
    1. 我们创建一个名为 room_distance 的新向量。它存储包含正在考虑的房间的索引和浮点数的元组,该浮点数将存储其到当前房间的距离。
    2. 我们计算房间的中心,并将其存储在 RLTK 的 Point 中(为了与距离算法兼容)。
    3. 对于每个房间,我们检索一个名为 j 的枚举(习惯上对计数器使用 ij,大概可以追溯到变量名较长很昂贵的日子!),以及 other_room
      1. 如果 ij 相等,我们正在查看通往/来自同一房间的走廊。我们不想这样做,所以我们跳过它!
      2. 同样,如果 other_room 的索引 (j) 在我们的 connected 集合中,那么我们也不想评估它 - 所以我们跳过它。
      3. 我们计算从外部房间 (room/i) 到我们正在评估的房间 (other_room/j) 的距离。
      4. 我们将距离和 j 索引推送到 room_distance 中。
    4. 如果 room_distance 的列表为空,我们向前跳过。否则:
    5. 我们使用 sort_byroom_distance 向量进行排序,最短距离的排在最前面。
    6. 然后我们使用 draw_corridor 函数从当前 room 的中心绘制到最近的房间(room_distance 中索引为 0 的房间)的走廊。

最后,我们将修改 map_builders/mod.rs 中的 random_builder 以使用此算法:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain {
    /*
    let mut builder = BuilderChain::new(new_depth);
    let type_roll = rng.roll_dice(1, 2);
    match type_roll {
        1 => random_room_builder(rng, &mut builder),
        _ => random_shape_builder(rng, &mut builder)
    }

    if rng.roll_dice(1, 3)==1 {
        builder.with(WaveformCollapseBuilder::new());
    }

    if rng.roll_dice(1, 20)==1 {
        builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT));
    }

    builder.with(PrefabBuilder::vaults());

    builder*/

    let mut builder = BuilderChain::new(new_depth);
    builder.start_with(SimpleMapBuilder::new());
    builder.with(RoomDrawer::new());
    builder.with(RoomSorter::new(RoomSort::LEFTMOST));
    builder.with(NearestCorridors::new());
    builder.with(RoomBasedSpawner::new());
    builder.with(RoomBasedStairs::new());
    builder.with(RoomBasedStartingPosition::new());
    builder
}
}

这提供了连接良好的地图,走廊距离合理地短。如果您 cargo run 该项目,您应该看到类似这样的内容:

Screenshot.

走廊重叠 仍然 可能发生,但现在已经不太可能了。

带有 Bresenham 线段的走廊

我们可以将走廊绘制为直线,而不是绕过角落呈折线形。这对玩家来说导航起来有点麻烦(需要导航更多角落),但可以产生令人愉悦的效果。我们将创建一个新文件 map_builders/rooms_corridors_lines.rs

#![allow(unused)]
fn main() {
use super::{MetaMapBuilder, BuilderMap, Rect, TileType };
use rltk::RandomNumberGenerator;
use std::collections::HashSet;

pub struct StraightLineCorridors {}

impl MetaMapBuilder for StraightLineCorridors {
    #[allow(dead_code)]
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) {
        self.corridors(rng, build_data);
    }
}

impl StraightLineCorridors {
    #[allow(dead_code)]
    pub fn new() -> Box<StraightLineCorridors> {
        Box::new(StraightLineCorridors{})
    }

    fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        let rooms : Vec<Rect>;
        if let Some(rooms_builder) = &build_data.rooms {
            rooms = rooms_builder.clone();
        } else {
            panic!("Straight Line Corridors require a builder with room structures");
        }

        let mut connected : HashSet<usize> = HashSet::new();
        for (i,room) in rooms.iter().enumerate() {
            let mut room_distance : Vec<(usize, f32)> = Vec::new();
            let room_center = room.center();
            let room_center_pt = rltk::Point::new(room_center.0, room_center.1);
            for (j,other_room) in rooms.iter().enumerate() {
                if i != j && !connected.contains(&j) {
                    let other_center = other_room.center();
                    let other_center_pt = rltk::Point::new(other_center.0, other_center.1);
                    let distance = rltk::DistanceAlg::Pythagoras.distance2d(
                        room_center_pt,
                        other_center_pt
                    );
                    room_distance.push((j, distance));
                }
            }

            if !room_distance.is_empty() {
                room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
                let dest_center = rooms[room_distance[0].0].center();
                let line = rltk::line2d(
                    rltk::LineAlg::Bresenham,
                    room_center_pt,
                    rltk::Point::new(dest_center.0, dest_center.1)
                );
                for cell in line.iter() {
                    let idx = build_data.map.xy_idx(cell.x, cell.y);
                    build_data.map.tiles[idx] = TileType::Floor;
                }
                connected.insert(i);
                build_data.take_snapshot();
            }
        }
    }
}
}

这与前一个几乎相同,但是我们没有调用 draw_corridor,而是使用 RLTK 的线条函数来绘制从源房间和目标房间中心点的直线。然后,我们将沿着该线段的每个瓦片标记为地板。如果您修改您的 random_builder 以使用此方法:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(StraightLineCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder
}

然后 cargo run 您的项目,您将看到类似这样的内容:

Screenshot.

存储走廊位置

我们将来可能想对我们的走廊位置做些什么,所以让我们存储它们。在 map_builders/mod.rs 中,让我们添加一个容器来存储我们的走廊位置。我们将使其成为一个 Option,以便保持与不使用此概念的地图类型的兼容性:

#![allow(unused)]
fn main() {
pub struct BuilderMap {
    pub spawn_list : Vec<(usize, String)>,
    pub map : Map,
    pub starting_position : Option<Position>,
    pub rooms: Option<Vec<Rect>>,
    pub corridors: Option<Vec<Vec<usize>>>,
    pub history : Vec<Map>
}
}

我们还需要调整构造函数以确保不会忘记 corridors

#![allow(unused)]
fn main() {
impl BuilderChain {
    pub fn new(new_depth : i32) -> BuilderChain {
        BuilderChain{
            starter: None,
            builders: Vec::new(),
            build_data : BuilderMap {
                spawn_list: Vec::new(),
                map: Map::new(new_depth),
                starting_position: None,
                rooms: None,
                corridors: None,
                history : Vec::new()
            }
        }
    }
    ...
}

现在在 common.rs 中,让我们修改我们的走廊函数以返回走廊放置信息:

#![allow(unused)]
fn main() {
pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) -> Vec<usize> {
    let mut corridor = Vec::new();
    for x in min(x1,x2) ..= max(x1,x2) {
        let idx = map.xy_idx(x, y);
        if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor {
            map.tiles[idx as usize] = TileType::Floor;
            corridor.push(idx as usize);
        }
    }
    corridor
}

pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) -> Vec<usize> {
    let mut corridor = Vec::new();
    for y in min(y1,y2) ..= max(y1,y2) {
        let idx = map.xy_idx(x, y);
        if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor {
            corridor.push(idx);
            map.tiles[idx as usize] = TileType::Floor;
        }
    }
    corridor
}

pub fn draw_corridor(map: &mut Map, x1:i32, y1:i32, x2:i32, y2:i32) -> Vec<usize> {
    let mut corridor = Vec::new();
    let mut x = x1;
    let mut y = y1;

    while x != x2 || y != y2 {
        if x < x2 {
            x += 1;
        } else if x > x2 {
            x -= 1;
        } else if y < y2 {
            y += 1;
        } else if y > y2 {
            y -= 1;
        }

        let idx = map.xy_idx(x, y);
        if map.tiles[idx] != TileType::Floor {
            corridor.push(idx);
            map.tiles[idx] = TileType::Floor;
        }
    }

    corridor
}
}

请注意,它们基本上没有改变,但现在返回一个瓦片索引向量 - 并且仅在被修改的瓦片是地板时才添加到其中? 这将为我们提供走廊每一段的定义。现在我们需要修改走廊绘制算法以存储此信息。在 rooms_corridors_bsp.rs 中,修改 corridors 函数以执行此操作:

#![allow(unused)]
fn main() {
...
let mut corridors : Vec<Vec<usize>> = Vec::new();
for i in 0..rooms.len()-1 {
    let room = rooms[i];
    let next_room = rooms[i+1];
    let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2))-1);
    let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2))-1);
    let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1);
    let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1);
    let corridor = draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y);
    corridors.push(corridor);
    build_data.take_snapshot();
}
build_data.corridors = Some(corridors);
...
}

我们在 rooms_corridors_dogleg.rs 中再次执行相同的操作:

#![allow(unused)]
fn main() {
...
let mut corridors : Vec<Vec<usize>> = Vec::new();
for (i,room) in rooms.iter().enumerate() {
    if i > 0 {
        let (new_x, new_y) = room.center();
        let (prev_x, prev_y) = rooms[i as usize -1].center();
        if rng.range(0,2) == 1 {
            let mut c1 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y);
            let mut c2 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x);
            c1.append(&mut c2);
            corridors.push(c1);
        } else {
            let mut c1 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x);
            let mut c2 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y);
            c1.append(&mut c2);
            corridors.push(c1);
        }
        build_data.take_snapshot();
    }
}
build_data.corridors = Some(corridors);
...
}

您会注意到我们将走廊的第二段附加到第一段,因此我们将其视为一个长走廊,而不是两条走廊。我们需要对我们新创建的 rooms_corridors_lines.rs 应用相同的更改:

#![allow(unused)]
fn main() {
fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
    let rooms : Vec<Rect>;
    if let Some(rooms_builder) = &build_data.rooms {
        rooms = rooms_builder.clone();
    } else {
        panic!("Straight Line Corridors require a builder with room structures");
    }

    let mut connected : HashSet<usize> = HashSet::new();
    let mut corridors : Vec<Vec<usize>> = Vec::new();
    for (i,room) in rooms.iter().enumerate() {
        let mut room_distance : Vec<(usize, f32)> = Vec::new();
        let room_center = room.center();
        let room_center_pt = rltk::Point::new(room_center.0, room_center.1);
        for (j,other_room) in rooms.iter().enumerate() {
            if i != j && !connected.contains(&j) {
                let other_center = other_room.center();
                let other_center_pt = rltk::Point::new(other_center.0, other_center.1);
                let distance = rltk::DistanceAlg::Pythagoras.distance2d(
                    room_center_pt,
                    other_center_pt
                );
                room_distance.push((j, distance));
            }
        }

        if !room_distance.is_empty() {
            room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
            let dest_center = rooms[room_distance[0].0].center();
            let line = rltk::line2d(
                rltk::LineAlg::Bresenham,
                room_center_pt,
                rltk::Point::new(dest_center.0, dest_center.1)
            );
            let mut corridor = Vec::new();
            for cell in line.iter() {
                let idx = build_data.map.xy_idx(cell.x, cell.y);
                if build_data.map.tiles[idx] != TileType::Floor {
                    build_data.map.tiles[idx] = TileType::Floor;
                    corridor.push(idx);
                }
            }
            corridors.push(corridor);
            connected.insert(i);
            build_data.take_snapshot();
        }
    }
    build_data.corridors = Some(corridors);
}
}

我们也会在 rooms_corridors_nearest.rs 中做同样的事情:

#![allow(unused)]
fn main() {
fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
    let rooms : Vec<Rect>;
    if let Some(rooms_builder) = &build_data.rooms {
        rooms = rooms_builder.clone();
    } else {
        panic!("Nearest Corridors require a builder with room structures");
    }

    let mut connected : HashSet<usize> = HashSet::new();
    let mut corridors : Vec<Vec<usize>> = Vec::new();
    for (i,room) in rooms.iter().enumerate() {
        let mut room_distance : Vec<(usize, f32)> = Vec::new();
        let room_center = room.center();
        let room_center_pt = rltk::Point::new(room_center.0, room_center.1);
        for (j,other_room) in rooms.iter().enumerate() {
            if i != j && !connected.contains(&j) {
                let other_center = other_room.center();
                let other_center_pt = rltk::Point::new(other_center.0, other_center.1);
                let distance = rltk::DistanceAlg::Pythagoras.distance2d(
                    room_center_pt,
                    other_center_pt
                );
                room_distance.push((j, distance));
            }
        }

        if !room_distance.is_empty() {
            room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() );
            let dest_center = rooms[room_distance[0].0].center();
            let corridor = draw_corridor(
                &mut build_data.map,
                room_center.0, room_center.1,
                dest_center.0, dest_center.1
            );
            connected.insert(i);
            build_data.take_snapshot();
            corridors.push(corridor);
        }
    }
    build_data.corridors = Some(corridors);
}
}

好的,我们有了走廊数据 - 然后呢?

一个明显的用途是在走廊内生成实体。我们将创建新的 room_corridor_spawner.rs 来做到这一点:

#![allow(unused)]
fn main() {
use super::{MetaMapBuilder, BuilderMap, spawner};
use rltk::RandomNumberGenerator;

pub struct CorridorSpawner {}

impl MetaMapBuilder for CorridorSpawner {
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap)  {
        self.build(rng, build_data);
    }
}

impl CorridorSpawner {
    #[allow(dead_code)]
    pub fn new() -> Box<CorridorSpawner> {
        Box::new(CorridorSpawner{})
    }

    fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        if let Some(corridors) = &build_data.corridors {
            for c in corridors.iter() {
                let depth = build_data.map.depth;
                spawner::spawn_region(&build_data.map,
                    rng,
                    &c,
                    depth,
                    &mut build_data.spawn_list);
            }
        } else {
            panic!("Corridor Based Spawning only works after corridors have been created");
        }
    }
}
}

这是基于 room_based_spawner.rs 的 - 复制/粘贴并更改了名称!然后,roomsif let 被替换为 corridors,并且不是每个房间都生成 - 我们将走廊传递给 spawn_region。实体现在在走廊中生成。

您可以通过将生成器添加到您的 random_builder 来测试这一点:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(SimpleMapBuilder::new());
builder.with(RoomDrawer::new());
builder.with(RoomSorter::new(RoomSort::LEFTMOST));
builder.with(StraightLineCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(CorridorSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder
}

一旦您开始玩游戏,您现在可以在走廊内找到实体:

Screenshot.

恢复随机性

再一次,这是一个小节的结尾 - 所以我们将再次使 random_builder 随机化,但利用我们的新内容!

首先取消注释 random_builder 中的代码,并删除测试工具:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain {
    let mut builder = BuilderChain::new(new_depth);
    let type_roll = rng.roll_dice(1, 2);
    match type_roll {
        1 => random_room_builder(rng, &mut builder),
        _ => random_shape_builder(rng, &mut builder)
    }

    if rng.roll_dice(1, 3)==1 {
        builder.with(WaveformCollapseBuilder::new());
    }

    if rng.roll_dice(1, 20)==1 {
        builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT));
    }

    builder.with(PrefabBuilder::vaults());

    builder
}
}

由于我们在这里所做的一切都是基于 房间 的,我们还将修改 random_room_builder 以包含它。我们将扩展与走廊相关的部分:

#![allow(unused)]
fn main() {
let corridor_roll = rng.roll_dice(1, 4);
match corridor_roll {
    1 => builder.with(DoglegCorridors::new()),
    2 => builder.with(NearestCorridors::new()),
    3 => builder.with(StraightLineCorridors::new()),
    _ => builder.with(BspCorridors::new())
}

let cspawn_roll = rng.roll_dice(1, 2);
if cspawn_roll == 1 {
    builder.with(CorridorSpawner::new());
}
}

所以我们添加了直线走廊和最近邻走廊的相等机会,并且 50% 的时间它将在走廊中生成实体。

...

本章的源代码可以在这里找到

使用 WebAssembly 在您的浏览器中运行本章的示例(需要 WebGL2)

版权 (C) 2019, Herbert Wolverson.