石灰岩洞穴


关于本教程

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

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

Hands-On Rust


设计文档 讨论了第一个真正的地下城关卡是一个石灰岩洞穴网络。石灰岩洞穴在现实生活中非常令人惊叹;约克郡的 Gaping Gill 是我小时候最喜欢去的地方之一(您可能在*《巨蟒与圣杯》*中见过它 - Vorpal 兔子从它的入口处出现!)。涓涓细流,经过数个世纪的作用,可以雕刻出 惊人 的洞穴。洞穴主要由浅灰色岩石构成,这些岩石磨损后变得光滑且具有反射性 - 产生令人惊叹的照明效果!

作弊以帮助关卡设计

在设计新关卡时,快速简便地到达那里会很有帮助!因此,我们将引入作弊模式,让您快速导航到地下城以查看您的创作。 这将非常像我们创建的其他 UI 元素(例如库存管理),所以我们需要做的第一件事是打开 main.rs 并添加一个新的 RunState 来显示作弊菜单:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState { AwaitingInput,
    ...
    ShowCheatMenu
}
}

然后,将以下内容添加到您的大型游戏状态 match 语句中:

#![allow(unused)]
fn main() {
RunState::ShowCheatMenu => {
    let result = gui::show_cheat_mode(self, ctx);
    match result {
        gui::CheatMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
        gui::CheatMenuResult::NoResponse => {}
        gui::CheatMenuResult::TeleportToExit => {
            self.goto_level(1);
            self.mapgen_next_state = Some(RunState::PreRun);
            newrunstate = RunState::MapGeneration;
        }
    }
}
}

这会请求 show_cheat_mode 返回一个响应,并使用“下一关”代码(与玩家激活楼梯时相同)在用户选择 Teleport 时前进。我们尚未编写该函数和枚举,因此我们打开 gui.rs 并添加它:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum CheatMenuResult { NoResponse, Cancel, TeleportToExit }

pub fn show_cheat_mode(_gs : &mut State, ctx : &mut Rltk) -> CheatMenuResult {
    let count = 2;
    let y = (25 - (count / 2)) as i32;
    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Cheating!");
    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");

    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('T'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));

    ctx.print(21, y, "Teleport to exit");

    match ctx.key {
        None => CheatMenuResult::NoResponse,
        Some(key) => {
            match key {
                VirtualKeyCode::T => CheatMenuResult::TeleportToExit,
                VirtualKeyCode::Escape => CheatMenuResult::Cancel,
                _ => CheatMenuResult::NoResponse
            }
        }
    }
}
}

这应该看起来很熟悉:它显示一个作弊菜单,并提供字母 T 表示“传送至出口”。

最后,我们需要在 player.rs 中添加一个输入:

#![allow(unused)]
fn main() {
// 保存并退出
VirtualKeyCode::Escape => return RunState::SaveGame,

// 作弊!
VirtualKeyCode::Backslash => return RunState::ShowCheatMenu,
}

就这样! 如果您现在 cargo run,您可以按 \ (反斜杠) 和 T - 并直接传送到下一关。 这将使我们的关卡设计变得容易得多!

Screenshot

雕刻洞穴

我们将对石灰岩洞穴进行另一次自定义设计,因此打开 map_builders/mod.rs 并找到 level_builder(它应该在文件的末尾):

#![allow(unused)]
fn main() {
pub fn level_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    rltk::console::log(format!("Depth: {}", new_depth));
    match new_depth {
        1 => town_builder(new_depth, rng, width, height),
        2 => forest_builder(new_depth, rng, width, height),
        3 => limestone_cavern_builder(new_depth, rng, width, height),
        _ => random_builder(new_depth, rng, width, height)
    }
}
}

还在顶部添加这个:

#![allow(unused)]
fn main() {
mod limestone_cavern;
use limestone_cavern::limestone_cavern_builder;
}

我们添加了 limestone_cavern_builder - 让我们继续创建它!创建一个新文件 map_builders/limestone_cavern.rs 并添加以下内容:

#![allow(unused)]
fn main() {
use super::{BuilderChain, DrunkardsWalkBuilder, XStart, YStart, AreaStartingPosition,
    CullUnreachable, VoronoiSpawning, MetaMapBuilder, BuilderMap, TileType, DistantExit};
use rltk::RandomNumberGenerator;
use crate::map;

pub fn limestone_cavern_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Limestone Caverns");
    chain.start_with(DrunkardsWalkBuilder::winding_passages());
    chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER));
    chain.with(CullUnreachable::new());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER));
    chain.with(VoronoiSpawning::new());
    chain.with(DistantExit::new());
    chain
}
}

这非常简单:我们使用醉汉漫步算法以“蜿蜒通道”模式构建地图。然后我们将起点设置为中心,并剔除不可达区域。接下来,我们将入口放置在左侧中心,使用 Voronoi 算法生成,并将出口放置在远处。

这为您提供了一个可玩地图!怪物选择不是很好,但它可以工作。 这是我们一直在使用的灵活地图构建系统的一个很好的例子。

洞穴主题化

洞穴布局是一个良好的开端,但它看起来还不像石灰岩洞穴。 打开 map/themes.rs,我们将纠正这一点!我们将首先修改 get_tile_glyph 以了解此关卡:

#![allow(unused)]
fn main() {
pub fn tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) {
let (glyph, mut fg, mut bg) = match map.depth {
    3 => get_limestone_cavern_glyph(idx, map),
    2 => get_forest_glyph(idx, map),
    _ => get_tile_glyph_default(idx, map)
};
}

现在我们需要编写 get_limestone_cavern_glyph。 我们希望它看起来像石灰岩洞穴。 这是我想出的(也许更有艺术天赋的人可以帮忙!):

#![allow(unused)]
fn main() {
fn get_limestone_cavern_glyph(idx:usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) {
    let glyph;
    let fg;
    let bg = RGB::from_f32(0., 0., 0.);

    match map.tiles[idx] {
        TileType::Wall => { glyph = rltk::to_cp437('▒'); fg = RGB::from_f32(0.7, 0.7, 0.7); }
        TileType::Bridge => { glyph = rltk::to_cp437('.'); fg = RGB::named(rltk::CHOCOLATE); }
        TileType::Road => { glyph = rltk::to_cp437('≡'); fg = RGB::named(rltk::YELLOW); }
        TileType::Grass => { glyph = rltk::to_cp437('"'); fg = RGB::named(rltk::GREEN); }
        TileType::ShallowWater => { glyph = rltk::to_cp437('░'); fg = RGB::named(rltk::CYAN); }
        TileType::DeepWater => { glyph = rltk::to_cp437('▓'); fg = RGB::named(rltk::BLUE); }
        TileType::Gravel => { glyph = rltk::to_cp437(';'); fg = RGB::from_f32(0.5, 0.5, 0.5); }
        TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg = RGB::from_f32(0., 1.0, 1.0); }
        TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); }
        _ => { glyph = rltk::to_cp437('░'); fg = RGB::from_f32(0.4, 0.4, 0.4); }
    }

    (glyph, fg, bg)
}
}

开局不错!环境看起来很像洞穴(而不是凿成的石头),颜色是令人愉悦的中性浅灰色,但又不会刺眼。 它使其他实体非常突出:

Screenshot

仅仅添加水和砾石

我们可以通过添加一些水(像这样的洞穴网络没有水是不寻常的),并将一些地板瓷砖变成砾石来进一步改善地图 - 以显示地图上的巨石。我们还可以添加一些钟乳石和石笋(由滴水缓慢沉积钙质经过数个世纪形成的巨大石柱)以增加风味。 因此,我们将首先在构建器中添加一个新层(作为最后一步):

#![allow(unused)]
fn main() {
chain.with(CaveDecorator::new());
}

然后我们需要编写它:

#![allow(unused)]
fn main() {
pub struct CaveDecorator {}

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

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

    fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) {
        let old_map = build_data.map.clone();
        for (idx,tt) in build_data.map.tiles.iter_mut().enumerate() {
            // 砾石生成
            if *tt == TileType::Floor && rng.roll_dice(1, 6)==1 {
                *tt = TileType::Gravel;
            } else if *tt == TileType::Floor && rng.roll_dice(1, 10)==1 {
                // 生成可通行的水池
                *tt = TileType::ShallowWater;
            } else if *tt == TileType::Wall {
                // 生成深水池和钟乳石
                let mut neighbors = 0;
                let x = idx as i32 % old_map.width;
                let y = idx as i32 / old_map.width;
                if x > 0 && old_map.tiles[idx-1] == TileType::Wall { neighbors += 1; }
                if x < old_map.width - 2 && old_map.tiles[idx+1] == TileType::Wall { neighbors += 1; }
                if y > 0 && old_map.tiles[idx-old_map.width as usize] == TileType::Wall { neighbors += 1; }
                if y < old_map.height - 2 && old_map.tiles[idx+old_map.width as usize] == TileType::Wall { neighbors += 1; }
                if neighbors == 2 {
                    *tt = TileType::DeepWater;
                } else if neighbors == 1 {
                    let roll = rng.roll_dice(1, 4);
                    match roll {
                        1 => *tt = TileType::Stalactite,
                        2 => *tt = TileType::Stalagmite,
                        _ => {}
                    }
                }
            }
        }
        build_data.take_snapshot();
    }
}
}

其工作原理如下:

  1. 我们遍历地图的所有瓷砖类型和地图索引。 这是一个可变迭代器 - 我们希望能够更改瓷砖。
  2. 如果瓷砖是 Floor,我们有 1/6 的几率将其变成砾石。
  3. 如果我们没有这样做,我们有 1/10 的几率将其变成浅水池(仍然可以通行)。
  4. 如果它是墙壁,我们会计算有多少其他墙壁环绕着它。
  5. 如果有 2 个邻居,我们将瓷砖替换为 DeepWater - 漂亮的深色水,玩家无法通行。
  6. 如果有 1 个邻居,我们掷一个 4 面骰子。 如果掷出 1,我们将其变成钟乳石; 如果掷出 2,我们将其变成石笋。 否则,我们什么都不做。

这确实需要我们打开 map/tiletype.rs 并引入新的瓷砖类型:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
pub enum TileType {
    Wall,
    Stalactite,
    Stalagmite,
    Floor,
    DownStairs,
    Road,
    Grass,
    ShallowWater,
    DeepWater,
    WoodFloor,
    Bridge,
    Gravel,
    UpStairs
}
}

我们使新的瓷砖类型阻挡视野:

#![allow(unused)]
fn main() {
pub fn tile_opaque(tt : TileType) -> bool {
    match tt {
        TileType::Wall | TileType::Stalactite | TileType::Stalagmite => true,
        _ => false
    }
}
}

我们还将它们添加到 map/themes.rs 中的新石灰岩主题和默认主题中:

#![allow(unused)]
fn main() {
TileType::Stalactite => { glyph = rltk::to_cp437('╨'); fg = RGB::from_f32(0.5, 0.5, 0.5); }
TileType::Stalagmite => { glyph = rltk::to_cp437('╥'); fg = RGB::from_f32(0.5, 0.5, 0.5); }
}

这给出了一个非常令人愉悦的,相当自然(和潮湿)的洞穴:

Screenshot

填充洞穴

洞穴本身就非常可玩,但它们与我们描述的 NPC 在类型方面不太匹配。洞穴里有一些森林怪物和鹿,这不太合理! 让我们首先打开 spawns.rs 并更改一些生物出现的深度以避免这种情况:

"spawn_table" : [
    { "name" : "Goblin", "weight" : 10, "min_depth" : 3, "max_depth" : 100 },
    { "name" : "Orc", "weight" : 1, "min_depth" : 3, "max_depth" : 100, "add_map_depth_to_weight" : true },
    { "name" : "Health Potion", "weight" : 7, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Fireball Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
    { "name" : "Confusion Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
    { "name" : "Magic Missile Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Dagger", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Shield", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Longsword", "weight" : 1, "min_depth" : 3, "max_depth" : 100 },
    { "name" : "Tower Shield", "weight" : 1, "min_depth" : 3, "max_depth" : 100 },
    { "name" : "Rations", "weight" : 10, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Magic Mapping Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Bear Trap", "weight" : 5, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Battleaxe", "weight" : 1, "min_depth" : 2, "max_depth" : 100 },
    { "name" : "Kobold", "weight" : 15, "min_depth" : 3, "max_depth" : 5 },
    { "name" : "Rat", "weight" : 15, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Mangy Wolf", "weight" : 13, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Deer", "weight" : 14, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Bandit", "weight" : 9, "min_depth" : 2, "max_depth" : 3 }
],

我们将土匪留在了洞穴中,因为他们可能会在那里寻求庇护 - 但不再有狼、鹿或异常大的啮齿动物(反正我们现在可能已经厌倦了它们!)。在洞穴里还会发现什么? d20 系统遭遇表 提出了一些建议:

恐狼,火甲虫,人类骷髅,巨型蜈蚣,蜘蛛群,人类僵尸,扼杀怪,骷髅勇士,地精,食尸鬼,巨型蜘蛛,鸡蛇兽,凝胶状立方怪,锈蚀怪,阴影,幽灵,翼龙,暗潜怪,穴居人,熊地精,瓦格伊,
灰色软泥怪,拟像怪和食人魔(我的天哪)

真是个长长的清单! 考虑到地下城的 这一层,其中一些是有道理的:蜘蛛肯定会喜欢阴暗的地方。“翼龙”基本上是邪恶的蝙蝠,所以我们应该添加蝙蝠。我们有地精和狗头人以及偶尔的兽人。我们已经决定暂时厌倦老鼠了! 我是凝胶状立方怪的忠实粉丝,所以我也很想把它们放进去! 由于难度原因,许多其他怪物最好留到以后的关卡。

所以让我们将它们添加到生成表中:

{ "name" : "Bat", "weight" : 15, "min_depth" : 3, "max_depth" : 3 },
{ "name" : "Large Spider", "weight" : 3, "min_depth" : 3, "max_depth" : 3 },
{ "name" : "Gelatinous Cube", "weight" : 3, "min_depth" : 3, "max_depth" : 3 }

我们将蝙蝠设为非常常见,大型蜘蛛和凝胶状立方怪非常稀有。 让我们继续将它们添加到 spawns.jsonmobs 部分:

{
    "name" : "Bat",
    "renderable": {
        "glyph" : "b",
        "fg" : "#995555",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 6,
    "ai" : "herbivore",
    "attributes" : {
        "Might" : 3,
        "Fitness" : 3
    },
    "skills" : {
        "Melee" : -1,
        "Defense" : -1
    },
    "natural" : {
        "armor_class" : 11,
        "attacks" : [
            { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" }
        ]
    }
},

{
    "name" : "Large Spider",
    "level" : 2,
    "attributes" : {},
    "renderable": {
        "glyph" : "s",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 6,
    "ai" : "carnivore",
    "natural" : {
        "armor_class" : 12,
        "attacks" : [
            { "name" : "bite", "hit_bonus" : 1, "damage" : "1d12" }
        ]
    }
},

{
    "name" : "Gelatinous Cube",
    "level" : 2,
    "attributes" : {},
    "renderable": {
        "glyph" : "▄",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "ai" : "carnivore",
    "natural" : {
        "armor_class" : 12,
        "attacks" : [
            { "name" : "engulf", "hit_bonus" : 0, "damage" : "1d8" }
        ]
    }
}

所以蝙蝠是无害的食草动物,它们大部分时间都在逃离你。 蜘蛛和立方体会猎杀其他人并吃掉它们。 我们还将它们设为 2 级 - 因此它们值得更多经验,并且更难杀死。 玩家很可能已经准备好迎接这个挑战。 所以我们可以 cargo run 并试一试!

Screenshot

还不错! 它可玩,正确的怪物出现,总体而言体验还不错。

光照!

使石灰岩洞穴如此令人惊叹的事情之一是光照; 您可以使用头盔火炬的光线穿过大理石向外窥视,投下阴影并赋予一切怪异的外观。 我们可以轻松地为游戏添加装饰性照明(它可能会在某个时候进入潜行系统!)

让我们首先创建一个新的组件 LightSource。 在 components.rs 中:

#![allow(unused)]
fn main() {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct LightSource {
    pub color : RGB,
    pub range: i32
}
}

与往常一样,在 main.rssaveload_system.rs 中注册您的新组件! 光源定义了两个值:color(光的颜色)和 range - 这将控制其强度/衰减。 我们还需要将光照信息添加到地图中。 在 map/mod.rs 中:

#![allow(unused)]
fn main() {
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct Map {
    pub tiles : Vec<TileType>,
    pub width : i32,
    pub height : i32,
    pub revealed_tiles : Vec<bool>,
    pub visible_tiles : Vec<bool>,
    pub blocked : Vec<bool>,
    pub depth : i32,
    pub bloodstains : HashSet<usize>,
    pub view_blocked : HashSet<usize>,
    pub name : String,
    pub outdoors : bool,
    pub light : Vec<rltk::RGB>,

    #[serde(skip_serializing)]
    #[serde(skip_deserializing)]
    pub tile_content : Vec<Vec<Entity>>
}
}

这里有两个新值:outdoors,表示“有自然光,不要应用照明”,以及 light - 这是一个 RGB 颜色向量,指示每个瓷砖上的光照水平。 您还需要更新 new 构造函数以包含这些内容:

#![allow(unused)]
fn main() {
pub fn new<S : ToString>(new_depth : i32, width: i32, height: i32, name: S) -> Map {
    let map_tile_count = (width*height) as usize;
    Map{
        tiles : vec![TileType::Wall; map_tile_count],
        width,
        height,
        revealed_tiles : vec![false; map_tile_count],
        visible_tiles : vec![false; map_tile_count],
        blocked : vec![false; map_tile_count],
        tile_content : vec![Vec::new(); map_tile_count],
        depth: new_depth,
        bloodstains: HashSet::new(),
        view_blocked : HashSet::new(),
        name : name.to_string(),
        outdoors : true,
        light: vec![rltk::RGB::from_f32(0.0, 0.0, 0.0); map_tile_count]
    }
}
}

请注意,我们将 outdoors 设置为默认模式 - 因此照明不会突然应用于所有地图(可能会搞砸我们已经完成的工作;很难解释为什么你早上醒来天空是黑色的 - 嗯,这可能是另一个游戏的故事钩子!)。 我们还将照明初始化为全黑,每个瓷砖一种颜色。

现在,我们将调整 map/themes.rs 以处理照明。 我们故意不使实体变暗(这样您仍然可以发现它们),只是地图瓷砖:

#![allow(unused)]
fn main() {
pub fn tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) {
    let (glyph, mut fg, mut bg) = match map.depth {
        3 => get_limestone_cavern_glyph(idx, map),
        2 => get_forest_glyph(idx, map),
        _ => get_tile_glyph_default(idx, map)
    };

    if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.); }
    if !map.visible_tiles[idx] {
        fg = fg.to_greyscale();
        bg = RGB::from_f32(0., 0., 0.); // 不显示视野范围外的血迹
    } else if !map.outdoors {
        fg = fg * map.light[idx];
        bg = bg * map.light[idx];
    }

    (glyph, fg, bg)
}
}

这非常简单:如果我们看不到瓷砖,我们仍然会使用灰度。 如果我们可以看到瓷砖,并且 outdoorsfalse - 那么我们将颜色乘以光强度。

接下来,让我们给玩家一个光源。 现在,我们将始终给他/她/它一个略带黄色的火炬。 在 spawner.rs 中,将此添加到为玩家构建的组件列表中:

#![allow(unused)]
fn main() {
.with(LightSource{ color: rltk::RGB::from_f32(1.0, 1.0, 0.5), range: 8 })
}

我们还将更新我们的 map_builders/limestone_caverns.rs 以在洞穴中使用照明。 在自定义构建器的末尾,将 take_snapshot 更改为:

#![allow(unused)]
fn main() {
build_data.take_snapshot();
build_data.map.outdoors = false;
}

最后,我们需要一个 系统 来实际计算照明。 创建一个新文件 lighting_system.rs

#![allow(unused)]
fn main() {
use specs::prelude::*;
use super::{Viewshed, Position, Map, LightSource};
use rltk::RGB;

pub struct LightingSystem {}

impl<'a> System<'a> for LightingSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( WriteExpect<'a, Map>,
                        ReadStorage<'a, Viewshed>,
                        ReadStorage<'a, Position>,
                        ReadStorage<'a, LightSource>);

    fn run(&mut self, data : Self::SystemData) {
        let (mut map, viewshed, positions, lighting) = data;

        if map.outdoors {
            return;
        }

        let black = RGB::from_f32(0.0, 0.0, 0.0);
        for l in map.light.iter_mut() {
            *l = black;
        }

        for (viewshed, pos, light) in (&viewshed, &positions, &lighting).join() {
            let light_point = rltk::Point::new(pos.x, pos.y);
            let range_f = light.range as f32;
            for t in viewshed.visible_tiles.iter() {
                if t.x > 0 && t.x < map.width && t.y > 0 && t.y < map.height {
                    let idx = map.xy_idx(t.x, t.y);
                    let distance = rltk::DistanceAlg::Pythagoras.distance2d(light_point, *t);
                    let intensity = (range_f - distance) / range_f;

                    map.light[idx] = map.light[idx] + (light.color * intensity);
                }
            }
        }
    }
}
}

这是一个非常简单的系统! 如果地图在户外,它只是简单地返回。 否则:

  1. 它将整个地图照明设置为黑暗。
  2. 它迭代所有具有位置、视野和光源的实体。
  3. 对于这些实体中的每一个,它都会迭代所有可见的瓷砖。
  4. 它计算可见瓷砖到光源的距离,并将其反转 - 因此距离光源越远就越暗。 然后将其除以光的范围,以将其缩放到 0..1 范围。
  5. 此照明量被添加到瓷砖的照明中。

最后,我们将系统添加到 main.rsrun_systems 函数中(作为要运行的最后一个系统):

#![allow(unused)]
fn main() {
let mut lighting = lighting_system::LightingSystem{};
lighting.run_now(&self.ecs);
}

如果您现在 cargo run,您将拥有一个功能齐全的照明系统!

Screenshot

剩下的就是让 NPC 也拥有光照。 在 raws/mob_structs.rs 中,添加一个新类:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct MobLight {
    pub range : i32,
    pub color : String
}
}

并将其添加到主要的 mob 结构中:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Mob {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub blocks_tile : bool,
    pub vision_range : i32,
    pub ai : String,
    pub quips : Option<Vec<String>>,
    pub attributes : MobAttributes,
    pub skills : Option<HashMap<String, i32>>,
    pub level : Option<i32>,
    pub hp : Option<i32>,
    pub mana : Option<i32>,
    pub equipped : Option<Vec<String>>,
    pub natural : Option<MobNatural>,
    pub loot_table : Option<String>,
    pub light : Option<MobLight>
}
}

现在我们可以修改 raws/rawmaster.rs 中的 spawn_named_mob 以支持它:

#![allow(unused)]
fn main() {
if let Some(light) = &mob_template.light {
    eb = eb.with(LightSource{ range: light.range, color : rltk::RGB::from_hex(&light.color).expect("Bad color") });
}
}

让我们修改凝胶状立方怪使其发光。 在 spawns.json 中:

{
    "name" : "Gelatinous Cube",
    "level" : 2,
    "attributes" : {},
    "renderable": {
        "glyph" : "▄",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 4,
    "ai" : "carnivore",
    "natural" : {
        "armor_class" : 12,
        "attacks" : [
            { "name" : "engulf", "hit_bonus" : 0, "damage" : "1d8" }
        ]
    },
    "light" : {
        "range" : 4,
        "color" : "#550000"
    }
}

我们还将给土匪一个火炬:

{
    "name" : "Bandit",
    "renderable": {
        "glyph" : "☻",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 6,
    "ai" : "melee",
    "quips" : [ "Stand and deliver!", "Alright, hand it over" ],
    "attributes" : {},
    "equipped" : [ "Dagger", "Shield", "Leather Armor", "Leather Boots" ],
    "light" : {
        "range" : 6,
        "color" : "#FFFF55"
    }
},

现在,当您 cargo run 并在洞穴中漫游时 - 您将看到从这些实体发出的光。 这是一个拿着火炬的土匪:

Screenshot

总结

在本章中,我们添加了一个全新的关卡和主题 - 并点亮了洞穴! 进展还不错。 游戏真的开始整合在一起了。

...

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

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

版权所有 (C) 2019, Herbert Wolverson。