添加对称和笔刷大小作为库函数


关于本教程

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

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

Hands-On Rust


在上一章关于扩散限制聚集(Diffusion-Limited Aggregation)的内容中,我们为地图构建引入了两个新概念:对称性(symmetry)笔刷大小(brush size)。这些概念很容易应用于其他算法,因此我们将花一些时间将它们移入库函数(在 map_builders/common.rs 中),使它们更通用,并演示它们如何改变醉汉步法(Drunkard's Walk)。

构建库版本

我们将从将 DLASymmetry 枚举从 dla.rs 中移到 common.rs 中开始。我们还将更改其名称,因为它不再绑定到特定的算法:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum Symmetry { None, Horizontal, Vertical, Both }
}

common.rs 的末尾,我们可以添加以下内容:

#![allow(unused)]
fn main() {
pub fn paint(map: &mut Map, mode: Symmetry, brush_size: i32, x: i32, y:i32) {
    match mode {
        Symmetry::None => apply_paint(map, brush_size, x, y),
        Symmetry::Horizontal => {
            let center_x = map.width / 2;
            if x == center_x {
                apply_paint(map, brush_size, x, y);
            } else {
                let dist_x = i32::abs(center_x - x);
                apply_paint(map, brush_size, center_x + dist_x, y);
                apply_paint(map, brush_size, center_x - dist_x, y);
            }
        }
        Symmetry::Vertical => {
            let center_y = map.height / 2;
            if y == center_y {
                apply_paint(map, brush_size, x, y);
            } else {
                let dist_y = i32::abs(center_y - y);
                apply_paint(map, brush_size, x, center_y + dist_y);
                apply_paint(map, brush_size, x, center_y - dist_y);
            }
        }
        Symmetry::Both => {
            let center_x = map.width / 2;
            let center_y = map.height / 2;
            if x == center_x && y == center_y {
                apply_paint(map, brush_size, x, y);
            } else {
                let dist_x = i32::abs(center_x - x);
                apply_paint(map, brush_size, center_x + dist_x, y);
                apply_paint(map, brush_size, center_x - dist_x, y);
                let dist_y = i32::abs(center_y - y);
                apply_paint(map, brush_size, x, center_y + dist_y);
                apply_paint(map, brush_size, x, center_y - dist_y);
            }
        }
    }
}

fn apply_paint(map: &mut Map, brush_size: i32, x: i32, y: i32) {
    match brush_size {
        1 => {
            let digger_idx = map.xy_idx(x, y);
            map.tiles[digger_idx] = TileType::Floor;
        }

        _ => {
            let half_brush_size = brush_size / 2;
            for brush_y in y-half_brush_size .. y+half_brush_size {
                for brush_x in x-half_brush_size .. x+half_brush_size {
                    if brush_x > 1 && brush_x < map.width-1 && brush_y > 1 && brush_y < map.height-1 {
                        let idx = map.xy_idx(brush_x, brush_y);
                        map.tiles[idx] = TileType::Floor;
                    }
                }
            }
        }
    }
}
}

这应该不会让人感到惊讶:这与我们在 dla.rs 中拥有的代码完全相同 - 只是移除了 &mut self,而是接受参数。

修改 dla.rs 以使用它

修改 dla.rs 以使用它相对简单。将所有 DLASymmetry 引用替换为 Symmetry。将所有对 self.paint(x, y) 的调用替换为 paint(&mut self.map, self.symmetry, self.brush_size, x, y);。您可以查看源代码以查看更改 - 无需在此处重复所有更改。 确保也在顶部的包含函数列表中包含 paintSymmetry

像许多重构一样,结果好坏的检验标准是,如果您 cargo run 您的代码 - 没有任何变化!我们不会再费心用截图来展示它和上次一样!

修改醉汉步法(Drunkard's Walk)以使用它

我们将首先修改 DrunkardSettings 结构体以接受这两个新特性:

#![allow(unused)]
fn main() {
pub struct DrunkardSettings {
    pub spawn_mode : DrunkSpawnMode,
    pub drunken_lifetime : i32,
    pub floor_percent: f32,
    pub brush_size: i32,
    pub symmetry: Symmetry
}
}

编译器会抱怨我们没有在构造函数中设置这些值,因此我们将添加一些默认值:

#![allow(unused)]
fn main() {
pub fn open_area(new_depth : i32) -> DrunkardsWalkBuilder {
    DrunkardsWalkBuilder{
        map : Map::new(new_depth),
        starting_position : Position{ x: 0, y : 0 },
        depth : new_depth,
        history: Vec::new(),
        noise_areas : HashMap::new(),
        settings : DrunkardSettings{
            spawn_mode: DrunkSpawnMode::StartingPoint,
            drunken_lifetime: 400,
            floor_percent: 0.5,
            brush_size: 1,
            symmetry: Symmetry::None
        }
    }
}
}

我们需要对其他构造函数进行类似的更改 - 只是将 brush_sizesymmetry 添加到每个 DrunkardSettings 构建器中。

我们还需要替换以下行:

#![allow(unused)]
fn main() {
self.map.tiles[drunk_idx] = TileType::DownStairs;
}

替换为:

#![allow(unused)]
fn main() {
paint(&mut self.map, self.settings.symmetry, self.settings.brush_size, drunk_x, drunk_y);
self.map.tiles[drunk_idx] = TileType::DownStairs;
}

双重绘制保留了添加 > 符号以显示步行者路径的功能,同时保留了绘制函数的过度绘制。

制作更宽阔的通道醉汉步法(drunk)

为了测试这一点,我们将在 drunkard.rs 中添加一个新的构造函数:

#![allow(unused)]
fn main() {
pub fn fat_passages(new_depth : i32) -> DrunkardsWalkBuilder {
    DrunkardsWalkBuilder{
        map : Map::new(new_depth),
        starting_position : Position{ x: 0, y : 0 },
        depth : new_depth,
        history: Vec::new(),
        noise_areas : HashMap::new(),
        settings : DrunkardSettings{
            spawn_mode: DrunkSpawnMode::Random,
            drunken_lifetime: 100,
            floor_percent: 0.4,
            brush_size: 2,
            symmetry: Symmetry::None
        }
    }
}
}

我们还将快速修改 map_builders/mod.rs 中的 random_builder 以展示这个:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
    /*let mut rng = rltk::RandomNumberGenerator::new();
    let builder = rng.roll_dice(1, 12);
    match builder {
        1 => Box::new(BspDungeonBuilder::new(new_depth)),
        2 => Box::new(BspInteriorBuilder::new(new_depth)),
        3 => Box::new(CellularAutomataBuilder::new(new_depth)),
        4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
        5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
        6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
        7 => Box::new(MazeBuilder::new(new_depth)),
        8 => Box::new(DLABuilder::walk_inwards(new_depth)),
        9 => Box::new(DLABuilder::walk_outwards(new_depth)),
        10 => Box::new(DLABuilder::central_attractor(new_depth)),
        11 => Box::new(DLABuilder::insectoid(new_depth)),
        _ => Box::new(SimpleMapBuilder::new(new_depth))
    }*/
    Box::new(DrunkardsWalkBuilder::fat_passages(new_depth))
}
}

这显示了地图生成中的即时变化:

Screenshot

请注意,“更胖”的挖掘区域如何提供更开放的大厅。它的运行时间也缩短了一半,因为我们更快地耗尽了所需的地面数量。

添加对称性(Symmetry)

与 DLA 类似,对称的醉汉步法(drunkards)可以制作出看起来有趣的地图。我们将再添加一个构造函数:

#![allow(unused)]
fn main() {
pub fn fearful_symmetry(new_depth : i32) -> DrunkardsWalkBuilder {
    DrunkardsWalkBuilder{
        map : Map::new(new_depth),
        starting_position : Position{ x: 0, y : 0 },
        depth : new_depth,
        history: Vec::new(),
        noise_areas : HashMap::new(),
        settings : DrunkardSettings{
            spawn_mode: DrunkSpawnMode::Random,
            drunken_lifetime: 100,
            floor_percent: 0.4,
            brush_size: 1,
            symmetry: Symmetry::Both
        }
    }
}
}

我们还修改了 random_builder 函数以使用它:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
    /*let mut rng = rltk::RandomNumberGenerator::new();
    let builder = rng.roll_dice(1, 12);
    match builder {
        1 => Box::new(BspDungeonBuilder::new(new_depth)),
        2 => Box::new(BspInteriorBuilder::new(new_depth)),
        3 => Box::new(CellularAutomataBuilder::new(new_depth)),
        4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
        5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
        6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
        7 => Box::new(MazeBuilder::new(new_depth)),
        8 => Box::new(DLABuilder::walk_inwards(new_depth)),
        9 => Box::new(DLABuilder::walk_outwards(new_depth)),
        10 => Box::new(DLABuilder::central_attractor(new_depth)),
        11 => Box::new(DLABuilder::insectoid(new_depth)),
        _ => Box::new(SimpleMapBuilder::new(new_depth))
    }*/
    Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth))
}
}

cargo run 将渲染出类似这样的结果:

Screenshot

请注意对称性(symmetry)是如何应用的(非常快 - 我们现在正在快速生成地面瓷砖!) - 然后剔除无法到达的区域,去除了地图的一部分。这是一个非常不错的地图!

再次恢复随机性

再一次,我们将新的算法添加到 map_builders/mod.rs 中的 random_builder 函数中:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
    let mut rng = rltk::RandomNumberGenerator::new();
    let builder = rng.roll_dice(1, 14);
    match builder {
        1 => Box::new(BspDungeonBuilder::new(new_depth)),
        2 => Box::new(BspInteriorBuilder::new(new_depth)),
        3 => Box::new(CellularAutomataBuilder::new(new_depth)),
        4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)),
        5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)),
        6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)),
        7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)),
        8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)),
        9 => Box::new(MazeBuilder::new(new_depth)),
        10 => Box::new(DLABuilder::walk_inwards(new_depth)),
        11 => Box::new(DLABuilder::walk_outwards(new_depth)),
        12 => Box::new(DLABuilder::central_attractor(new_depth)),
        13 => Box::new(DLABuilder::insectoid(new_depth)),
        _ => Box::new(SimpleMapBuilder::new(new_depth))
    }
}
}

现在我们已经有 14 种算法了!我们拥有越来越多样化的游戏!

总结

本章演示了游戏程序员的一个非常有用的工具:找到一个方便的算法,使其通用化,并在代码的其他部分中使用它。 准确地猜测您预先需要什么是很罕见的(并且对于“您不需要它”有很多话要说 - 在您确实需要它们时才实现功能),因此能够快速重构我们的代码以进行重用是我们武器库中的宝贵武器。

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

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

版权 (C) 2019, Herbert Wolverson.