图层的乐趣


关于本教程

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

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

Hands-On Rust


既然我们有了一个漂亮、干净的图层系统,我们将借此机会稍微玩一下它。 本章收集了一些你可以用图层做的有趣的事情,并将介绍一些新的图层类型。 它的目的是激发你编写更多代码的兴趣:天空才是真正的极限!

将现有算法作为元构建器

让我们首先调整一些现有的算法,使其可以用作过滤器。

应用细胞自动机作为元构建器

当我们编写细胞自动机系统时,我们的目标是构建一个通用的洞穴生成器。 该算法的功能远不止于此——每次迭代基本上都是在前一次迭代上运行的“元构建器”。 一个简单的调整使其也能够成为一个只运行一次迭代的元构建器。

我们将首先将单次迭代的代码移动到它自己的函数中:

#![allow(unused)]
fn main() {
fn apply_iteration(&mut self, build_data : &mut BuilderMap) {
    let mut newtiles = build_data.map.tiles.clone();

    for y in 1..build_data.map.height-1 {
        for x in 1..build_data.map.width-1 {
            let idx = build_data.map.xy_idx(x, y);
            let mut neighbors = 0;
            if build_data.map.tiles[idx - 1] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx + 1] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx - (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx - (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx + (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; }
            if build_data.map.tiles[idx + (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; }

            if neighbors > 4 || neighbors == 0 {
                newtiles[idx] = TileType::Wall;
            }
            else {
                newtiles[idx] = TileType::Floor;
            }
        }
    }

    build_data.map.tiles = newtiles.clone();
    build_data.take_snapshot();
}
}

build 函数可以很容易地修改为在每次迭代时调用它:

#![allow(unused)]
fn main() {
// 现在我们迭代地应用细胞自动机规则
for _i in 0..15 {
    self.apply_iteration(build_data);
}
}

最后,我们将为组合添加 MetaMapBuilder 的实现:

#![allow(unused)]
fn main() {
impl MetaMapBuilder for CellularAutomataBuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, _rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) {
        self.apply_iteration(build_data);
    }
}
}

看看我们是如何调用单个迭代,而不是替换整个地图? 这展示了我们如何将细胞自动机规则应用于地图 - 并相当大地改变结果特征。

现在让我们修改 map_builders/mod.rsrandom_builder,强制它使用这个作为例子:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain {
    let mut builder = BuilderChain::new(new_depth);
    builder.start_with(VoronoiCellBuilder::pythagoras());
    builder.with(CellularAutomataBuilder::new());
    builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    builder.with(CullUnreachable::new());
    builder.with(VoronoiSpawning::new());
    builder.with(DistantExit::new());
    builder
}
}

如果你现在 cargo run 运行项目,你将看到类似这样的内容:

Screenshot

用醉酒的矮人侵蚀方正的地图

醉汉走路算法也可以产生很好的后处理效果,只需进行非常小的修改。 在 drunkard.rs 中,只需添加以下内容:

#![allow(unused)]
fn main() {
impl MetaMapBuilder for DrunkardsWalkBuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) {
        self.build(rng, build_data);
    }
}
}

你可以再次修改 random_builder 来测试它:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(SimpleMapBuilder::new());
builder.with(DrunkardsWalkBuilder::winding_passages());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder
}

如果你 cargo run 运行项目,你将看到类似这样的内容:

Screenshot

请注意,最初的方正设计现在看起来更自然一些,因为醉酒的矮人已经开凿出地图的各个部分!

使用扩散限制聚集攻击你的方正地图

DLA 也可以被修改为侵蚀现有的方正地图。 只需将 MetaBuilder 特征添加到 dla.rs

#![allow(unused)]
fn main() {
impl MetaMapBuilder for DLABuilder {
    #[allow(dead_code)]
    fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) {
        self.build(rng, build_data);
    }
}
}

我们还将添加一个新的模式 heavy_erosion ——它与“向内行走”相同,但需要更大的地板空间百分比:

#![allow(unused)]
fn main() {
#[allow(dead_code)]
pub fn heavy_erosion() -> Box<DLABuilder> {
    Box::new(DLABuilder{
        algorithm: DLAAlgorithm::WalkInwards,
        brush_size: 2,
        symmetry: Symmetry::None,
        floor_percent: 0.35,
    })
}
}

并修改你的 random_builder 测试工具:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(SimpleMapBuilder::new());
builder.with(DLABuilder::heavy_erosion());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder
}

如果你 cargo run 运行项目,你将看到类似这样的内容:

Screenshot

一些新的元构建器

编写新的地图过滤器也有很大的空间。 在本节中,我们将探讨一些更有趣的过滤器。 几乎任何你可能在像 Photoshop (或 GIMP!) 这样的程序中用作图像过滤器的东西都可以为此目的进行调整。 给定过滤器的用处仍然是一个开放/有趣的问题!

侵蚀房间

Nethack 风格的方正房间非常适合早期的 D&D 类型游戏,但人们经常说它们在视觉上并没有那么令人愉悦或有趣。 一种保持基本房间风格,但获得更自然的视觉效果的方法是在每个房间内部运行醉汉走路算法。 我喜欢称之为“炸毁房间”——因为它看起来有点像你在每个房间里引爆了炸药。 在 map_builders/ 中,创建一个新文件 room_exploder.rs

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

pub struct RoomExploder {}

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

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

    fn build(&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!("Room Explosions require a builder with room structures");
        }

        for room in rooms.iter() {
            let start = room.center();
            let n_diggers = rng.roll_dice(1, 20)-5;
            if n_diggers > 0 {
                for _i in 0..n_diggers {
                    let mut drunk_x = start.0;
                    let mut drunk_y = start.1;

                    let mut drunk_life = 20;
                    let mut did_something = false;

                    while drunk_life > 0 {
                        let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y);
                        if build_data.map.tiles[drunk_idx] == TileType::Wall {
                            did_something = true;
                        }
                        paint(&mut build_data.map, Symmetry::None, 1, drunk_x, drunk_y);
                        build_data.map.tiles[drunk_idx] = TileType::DownStairs;

                        let stagger_direction = rng.roll_dice(1, 4);
                        match stagger_direction {
                            1 => { if drunk_x > 2 { drunk_x -= 1; } }
                            2 => { if drunk_x < build_data.map.width-2 { drunk_x += 1; } }
                            3 => { if drunk_y > 2 { drunk_y -=1; } }
                            _ => { if drunk_y < build_data.map.height-2 { drunk_y += 1; } }
                        }

                        drunk_life -= 1;
                    }
                    if did_something {
                        build_data.take_snapshot();
                    }

                    for t in build_data.map.tiles.iter_mut() {
                        if *t == TileType::DownStairs {
                            *t = TileType::Floor;
                        }
                    }
                }
            }
        }
    }
}
}

这段代码中没有什么太令人惊讶的地方:它从父构建数据中获取 rooms 列表,然后迭代每个房间。 然后从每个房间的中心运行随机数量(可以为零)的醉汉,寿命很短,雕刻出每个房间的边缘。 你可以使用以下 random_builder 代码进行测试:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(BspDungeonBuilder::new());
builder.with(RoomExploder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder
}

Screenshot

圆角房间角

另一种使方正地图看起来不那么矩形的快速简便方法是将角稍微平滑一下。 将 room_corner_rounding.rs 添加到 map_builders/

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

pub struct RoomCornerRounder {}

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

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

    fn fill_if_corner(&mut self, x: i32, y: i32, build_data : &mut BuilderMap) {
        let w = build_data.map.width;
        let h = build_data.map.height;
        let idx = build_data.map.xy_idx(x, y);
        let mut neighbor_walls = 0;
        if x > 0 && build_data.map.tiles[idx-1] == TileType::Wall { neighbor_walls += 1; }
        if y > 0 && build_data.map.tiles[idx-w as usize] == TileType::Wall { neighbor_walls += 1; }
        if x < w-2 && build_data.map.tiles[idx+1] == TileType::Wall { neighbor_walls += 1; }
        if y < h-2 && build_data.map.tiles[idx+w as usize] == TileType::Wall { neighbor_walls += 1; }

        if neighbor_walls == 2 {
            build_data.map.tiles[idx] = TileType::Wall;
        }
    }

    fn build(&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!("Room Rounding require a builder with room structures");
        }

        for room in rooms.iter() {
            self.fill_if_corner(room.x1+1, room.y1+1, build_data);
            self.fill_if_corner(room.x2, room.y1+1, build_data);
            self.fill_if_corner(room.x1+1, room.y2, build_data);
            self.fill_if_corner(room.x2, room.y2, build_data);

            build_data.take_snapshot();
        }
    }
}
}

样板代码(重复代码)现在应该看起来很熟悉了,所以我们将专注于 build 中的算法:

  1. 我们获取房间列表,如果没有房间,则 panic!
  2. 对于房间的 4 个角中的每一个,我们调用一个新函数 fill_if_corner
  3. fill_if_corner 计算每个相邻的瓦片,以查看它是否是墙壁。 如果正好有 2 面墙,那么这个瓦片就有资格成为角——所以我们填充一面墙。

你可以使用以下 random_builder 代码尝试一下:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(BspDungeonBuilder::new());
builder.with(RoomCornerRounder::new());
builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
builder.with(CullUnreachable::new());
builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new());
builder
}

结果(如果你 cargo run 运行)应该类似于这样:

Screenshot

解耦房间和走廊

在 BSP 房间放置和“简单地图”房间放置之间存在相当多的共享代码 - 但走廊决策制定方式不同。 如果我们解耦各个阶段会怎样——让房间算法决定房间的去向,另一个算法绘制它们(可能会改变它们的绘制方式),第三个算法放置走廊? 我们改进的框架只需稍微调整一下算法即可支持这一点。

这是删除了走廊代码的 simple_map.rs

#![allow(unused)]
fn main() {
use super::{InitialMapBuilder, BuilderMap, Rect, apply_room_to_map,
    apply_horizontal_tunnel, apply_vertical_tunnel };
use rltk::RandomNumberGenerator;

pub struct SimpleMapBuilder {}

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

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

    fn build_rooms(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        const MAX_ROOMS : i32 = 30;
        const MIN_SIZE : i32 = 6;
        const MAX_SIZE : i32 = 10;
        let mut rooms : Vec<Rect> = Vec::new();

        for i in 0..MAX_ROOMS {
            let w = rng.range(MIN_SIZE, MAX_SIZE);
            let h = rng.range(MIN_SIZE, MAX_SIZE);
            let x = rng.roll_dice(1, build_data.map.width - w - 1) - 1;
            let y = rng.roll_dice(1, build_data.map.height - h - 1) - 1;
            let new_room = Rect::new(x, y, w, h);
            let mut ok = true;
            for other_room in rooms.iter() {
                if new_room.intersect(other_room) { ok = false }
            }
            if ok {
                apply_room_to_map(&mut build_data.map, &new_room);
                build_data.take_snapshot();

                rooms.push(new_room);
                build_data.take_snapshot();
            }
        }
        build_data.rooms = Some(rooms);
    }
}
}

除了将 rooms_and_corridors 重命名为 build_rooms 之外,唯一的更改是删除了放置走廊的掷骰子。

让我们创建一个新文件 map_builders/rooms_corridors_dogleg.rs。 这是我们放置走廊的地方。 现在,我们将使用刚刚从 SimpleMapBuilder 中删除的相同算法:

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

pub struct DoglegCorridors {}

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

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

    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!("Dogleg Corridors require a builder with room structures");
        }

        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 {
                    apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y);
                    apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x);
                } else {
                    apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x);
                    apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y);
                }
                build_data.take_snapshot();
            }
        }
    }
}
}

再说一遍——这是我们刚刚删除的代码,但它自己被放置到一个新的构建器中。 所以真的没有什么新鲜的。 我们可以调整 random_builder 来测试这段代码:

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

cargo run 测试它应该会显示房间被构建,然后是走廊:

Screenshot

与 BSP 地牢再次相同

对我们的 BSPDungeonBuilder 执行相同的操作很容易。 在 bsp_dungeon.rs 中,我们也删除了走廊代码。 为了简洁起见,我们只包含 build 函数:

#![allow(unused)]
fn main() {
fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        let mut rooms : Vec<Rect> = Vec::new();
        self.rects.clear();
        self.rects.push( Rect::new(2, 2, build_data.map.width-5, build_data.map.height-5) ); // 从单个地图大小的矩形开始
        let first_room = self.rects[0];
        self.add_subrects(first_room); // 分割第一个房间

        // 最多 240 次,我们得到一个随机矩形并将其分割。 如果有可能在那里挤出一个
        // 房间,我们放置它并将其添加到房间列表中。
        let mut n_rooms = 0;
        while n_rooms < 240 {
            let rect = self.get_random_rect(rng);
            let candidate = self.get_random_sub_rect(rect, rng);

            if self.is_possible(candidate, &build_data.map) {
                apply_room_to_map(&mut build_data.map, &candidate);
                rooms.push(candidate);
                self.add_subrects(rect);
                build_data.take_snapshot();
            }

            n_rooms += 1;
        }

        build_data.rooms = Some(rooms);
    }
}

我们还将把我们的 BSP 走廊代码移动到一个新的构建器中,不带房间排序(我们将在下一个标题中讨论排序!)。 创建新文件 map_builders/rooms_corridors_bsp.rs

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

pub struct BspCorridors {}

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

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

    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!("BSP Corridors require a builder with room structures");
        }

        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);
            draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y);
            build_data.take_snapshot();
        }
    }
}
}

同样,这 来自 BspDungeonBuilder 的走廊代码 - 只是适合其自身的构建器阶段。 你可以通过再次修改 random_builder 来证明它有效:

#![allow(unused)]
fn main() {
let mut builder = BuilderChain::new(new_depth);
builder.start_with(BspDungeonBuilder::new());
builder.with(BspCorridors::new());
builder.with(RoomBasedSpawner::new());
builder.with(RoomBasedStairs::new());
builder.with(RoomBasedStartingPosition::new());
builder
}

如果你 cargo run 运行它,你将看到类似这样的内容:

Screenshot

看起来 像是有效的 - 但如果你仔细注意,你就会明白为什么我们在原始算法中对房间进行排序:房间/走廊之间有很多重叠,并且走廊没有趋向于最短路径。 这是故意的 - 我们需要制作一个 RoomSorter 构建器,为我们提供更多地图构建选项。 让我们创建 map_builders/room_sorter.rs

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

pub struct RoomSorter {}

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

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

    fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) );
    }
}
}

这与我们之前使用的排序 完全 相同,我们可以通过将其插入到我们的构建器序列中来测试它:

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

如果你 cargo run 运行它,你将看到类似这样的内容:

Screenshot

这样更好 - 我们已经恢复了 BSP 地牢构建器的外观和感觉!

更多房间排序选项

只有当我们打算提出一些不同的房间排序方式时,将排序器分解为自己的步骤才真正有用! 我们目前按最左边的条目排序 - 给出一个逐渐向东移动,但到处跳跃的地图。

让我们添加一个 enum 来为我们提供更多排序选项:

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

pub enum RoomSort { LEFTMOST }

pub struct RoomSorter {
    sort_by : RoomSort
}

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

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

    fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        match self.sort_by {
            RoomSort::LEFTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) )
        }
    }
}
}

很简单:我们将希望使用的排序算法存储在结构中,并在需要执行时 match 它。

让我们添加 RIGHTMOST ——它将简单地反转排序:

#![allow(unused)]
fn main() {
pub enum RoomSort { LEFTMOST, RIGHTMOST }
...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
    match self.sort_by {
        RoomSort::LEFTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
        RoomSort::RIGHTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) )
    }
}
}

这太简单了,简直就像作弊! 让我们也添加 TOPMOST 和 BOTTOMMOST,以完成这种类型的排序:

#![allow(unused)]
fn main() {
#[allow(dead_code)]
pub enum RoomSort { LEFTMOST, RIGHTMOST, TOPMOST, BOTTOMMOST }
...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
    match self.sort_by {
        RoomSort::LEFTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
        RoomSort::RIGHTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) ),
        RoomSort::TOPMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.y1.cmp(&b.y1) ),
        RoomSort::BOTTOMMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.y2.cmp(&a.y2) )
    }
}
}

这是 BOTTOMMOST 的实际效果:

Screenshot

看看这如何在不真正改变结构的情况下改变地图的特性? 微小的调整能做到什么真是令人惊叹!

我们将添加另一个排序,CENTRAL。 这次,我们按到地图中心的距离排序:

#![allow(unused)]
fn main() {
#[allow(dead_code)]
pub enum RoomSort { LEFTMOST, RIGHTMOST, TOPMOST, BOTTOMMOST, CENTRAL }
...
fn sorter(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
    match self.sort_by {
        RoomSort::LEFTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ),
        RoomSort::RIGHTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) ),
        RoomSort::TOPMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.y1.cmp(&b.y1) ),
        RoomSort::BOTTOMMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.y2.cmp(&a.y2) ),
        RoomSort::CENTRAL => {
            let map_center = rltk::Point::new( build_data.map.width / 2, build_data.map.height / 2 );
            let center_sort = |a : &Rect, b : &Rect| {
                let a_center = a.center();
                let a_center_pt = rltk::Point::new(a_center.0, a_center.1);
                let b_center = b.center();
                let b_center_pt = rltk::Point::new(b_center.0, b_center.1);
                let distance_a = rltk::DistanceAlg::Pythagoras.distance2d(a_center_pt, map_center);
                let distance_b = rltk::DistanceAlg::Pythagoras.distance2d(b_center_pt, map_center);
                distance_a.partial_cmp(&distance_b).unwrap()
            };

            build_data.rooms.as_mut().unwrap().sort_by(center_sort);
        }
    }
}
}

你可以修改你的 random_builder 函数来使用它:

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

cargo run 将给你类似这样的内容:

Screenshot

请注意,现在所有道路都通向中间 - 形成一个非常连通的地图!

清理我们的随机构建器

既然我们即将结束本节(尚未结束!),让我们花时间真正利用我们到目前为止构建的内容。 我们将完全重组我们选择随机构建模式的方式。

现在,基于房间的生成不像以前那样令人尴尬地可预测了。 因此,让我们创建一个函数来公开我们到目前为止构建的所有房间种类:

#![allow(unused)]
fn main() {
fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder : &mut BuilderChain) {
    let build_roll = rng.roll_dice(1, 3);
    match build_roll {
        1 => builder.start_with(SimpleMapBuilder::new()),
        2 => builder.start_with(BspDungeonBuilder::new()),
        _ => builder.start_with(BspInteriorBuilder::new())
    }

    // BSP Interior 仍然会在墙壁上打洞
    if build_roll != 3 {
        // 按 5 种可用算法之一排序
        let sort_roll = rng.roll_dice(1, 5);
        match sort_roll {
            1 => builder.with(RoomSorter::new(RoomSort::LEFTMOST)),
            2 => builder.with(RoomSorter::new(RoomSort::RIGHTMOST)),
            3 => builder.with(RoomSorter::new(RoomSort::TOPMOST)),
            4 => builder.with(RoomSorter::new(RoomSort::BOTTOMMOST)),
            _ => builder.with(RoomSorter::new(RoomSort::CENTRAL)),
        }

        let corridor_roll = rng.roll_dice(1, 2);
        match corridor_roll {
            1 => builder.with(DoglegCorridors::new()),
            _ => builder.with(BspCorridors::new())
        }

        let modifier_roll = rng.roll_dice(1, 6);
        match modifier_roll {
            1 => builder.with(RoomExploder::new()),
            2 => builder.with(RoomCornerRounder::new()),
            _ => {}
        }
    }

    let start_roll = rng.roll_dice(1, 2);
    match start_roll {
        1 => builder.with(RoomBasedStartingPosition::new()),
        _ => {
            let (start_x, start_y) = random_start_position(rng);
            builder.with(AreaStartingPosition::new(start_x, start_y));
        }
    }

    let exit_roll = rng.roll_dice(1, 2);
    match exit_roll {
        1 => builder.with(RoomBasedStairs::new()),
        _ => builder.with(DistantExit::new())
    }

    let spawn_roll = rng.roll_dice(1, 2);
    match spawn_roll {
        1 => builder.with(RoomBasedSpawner::new()),
        _ => builder.with(VoronoiSpawning::new())
    }
}
}

这是一个很大的函数,所以我们将逐步介绍它。 它非常简单,只是真的散开并且充满了分支:

  1. 我们掷 1d3,并从 BSP Interior、Simple 和 BSP Dungeon 地图构建器中选择。
  2. 如果我们没有选择 BSP Interior(它自己做了很多事情),我们:
    1. 随机选择一个房间排序算法。
    2. 随机选择我们现在拥有的两个走廊算法之一。
    3. 随机选择(或忽略)房间炸毁器或圆角器。
  3. 我们在基于房间的起始位置和基于区域的起始位置之间随机选择。 对于后者,调用 random_start_position 在 3 个 X 轴和 3 个 Y 轴起始位置之间进行选择以支持。
  4. 我们在基于房间的楼梯放置和“离起点最远”的出口之间随机选择。
  5. 我们在 Voronoi 区域生成和基于房间的生成之间随机选择。

所以这个函数完全是关于掷骰子和制作地图! 即使忽略了每个起始构建器可能产生的数千种布局,它也有很多组合。 有:

2 <带选项的起始房间> * 5 <排序 > * 2 <走廊 > * 3 <修改器 > = 60 个基本房间选项。
+1 用于 BSP 内部地牢 = 61 个房间选项。
*2 <起始位置选项 > = 122 个房间选项。
*2 <出口位置 > = 244 个房间选项。
*2 <生成选项 > = 488 个房间选项!

所以这个函数提供了 488 种可能的构建器组合!

现在我们将为非房间生成器创建一个函数:

#![allow(unused)]
fn main() {
fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder : &mut BuilderChain) {
    let builder_roll = rng.roll_dice(1, 16);
    match builder_roll {
        1 => builder.start_with(CellularAutomataBuilder::new()),
        2 => builder.start_with(DrunkardsWalkBuilder::open_area()),
        3 => builder.start_with(DrunkardsWalkBuilder::open_halls()),
        4 => builder.start_with(DrunkardsWalkBuilder::winding_passages()),
        5 => builder.start_with(DrunkardsWalkBuilder::fat_passages()),
        6 => builder.start_with(DrunkardsWalkBuilder::fearful_symmetry()),
        7 => builder.start_with(MazeBuilder::new()),
        8 => builder.start_with(DLABuilder::walk_inwards()),
        9 => builder.start_with(DLABuilder::walk_outwards()),
        10 => builder.start_with(DLABuilder::central_attractor()),
        11 => builder.start_with(DLABuilder::insectoid()),
        12 => builder.start_with(VoronoiCellBuilder::pythagoras()),
        13 => builder.start_with(VoronoiCellBuilder::manhattan()),
        _ => builder.start_with(PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED)),
    }

    // 将起点设置为中心并剔除
    builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    builder.with(CullUnreachable::new());

    // 现在将起点设置为随机起始区域
    let (start_x, start_y) = random_start_position(rng);
    builder.with(AreaStartingPosition::new(start_x, start_y));

    // 设置出口并生成怪物
    builder.with(VoronoiSpawning::new());
    builder.with(DistantExit::new());
}
}

这与我们之前所做的类似,但有一个转折:我们现在将玩家放置在中心,剔除无法到达的区域,然后将玩家放置在随机位置。 生成地图的中间很可能非常连通 - 因此这消除了死角空间,并最大限度地减少了从“孤立”部分开始并将地图剔除到仅剩几个瓦片的可能性。

这也提供了很多组合,但没有那么多。

14 个基本房间选项
*1 生成选项
*1 出口选项
*6 起始选项
= 84 个选项。

所以这个函数提供了 84 种房间构建器组合

最后,我们将所有内容整合到 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
}
}

这相对简单明了。 我们随机选择一个房间或一个形状构建器,如上定义。 有 1/3 的几率我们会然后在它上面运行 Wave Function Collapse,以及 1/20 的几率我们会向它添加一个 sectional。 最后,我们尝试生成我们可能想要使用的任何 vault。

那么我们的总组合爆炸看起来如何? 在这一点上还不错:

488 种可能的房间构建器 +
84 种可能的形状构建器 =
572 种构建器组合。

我们可能会运行 Wave Function Collapse,再给出 2 个选项:
*2 = 1,144

我们可能会添加一个 sectional:
*2 = 2,288

所以我们现在有 2,288 种可能的构建器组合,仅来自最近的几章。 将其与随机种子结合起来,玩家在一次运行中看到完全相同的地图组合的可能性越来越小。

...

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

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

版权 (C) 2019, Herbert Wolverson。