魔法地图


关于本教程

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

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

Hands-On Rust


在 Roguelike 游戏中,一个非常常见的物品是魔法地图卷轴。 你阅读它,地下城就会被揭示出来。 更精美的 Roguelike 游戏为此提供了漂亮的图形效果。 在本章中,我们将从使其工作开始 - 然后使其变得漂亮!

添加魔法地图组件

我们拥有了所需的一切,除了一个指示物品是魔法地图卷轴(或任何其他物品,真的)的指示器。 因此,在 components.rs 中,我们将为其添加一个组件:

#![allow(unused)]
fn main() {
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct MagicMapper {}
}

与往常一样,我们需要在 main.rssaveload_system.rs 中注册它。 我们将前往 spawners.rs 并为其创建一个新函数,并将其添加到战利品表中:

#![allow(unused)]
fn main() {
fn magic_mapping_scroll(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437(')'),
            fg: RGB::named(rltk::CYAN3),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Scroll of Magic Mapping".to_string() })
        .with(Item{})
        .with(MagicMapper{})
        .with(Consumable{})
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}
}

以及战利品表:

#![allow(unused)]
fn main() {
fn room_table(map_depth: i32) -> RandomTable {
    RandomTable::new()
        .add("Goblin", 10)
        .add("Orc", 1 + map_depth)
        .add("Health Potion", 7)
        .add("Fireball Scroll", 2 + map_depth)
        .add("Confusion Scroll", 2 + map_depth)
        .add("Magic Missile Scroll", 4)
        .add("Dagger", 3)
        .add("Shield", 3)
        .add("Longsword", map_depth - 1)
        .add("Tower Shield", map_depth - 1)
        .add("Rations", 10)
        .add("Magic Mapping Scroll", 400)
}
}

请注意,我们赋予了它 400 的权重 - 绝对是荒谬的。 我们稍后会修复它,现在我们真的想生成卷轴以便我们可以测试它! 最后,我们将其添加到实际的生成函数中:

#![allow(unused)]
fn main() {
match spawn.1.as_ref() {
    "Goblin" => goblin(ecs, x, y),
    "Orc" => orc(ecs, x, y),
    "Health Potion" => health_potion(ecs, x, y),
    "Fireball Scroll" => fireball_scroll(ecs, x, y),
    "Confusion Scroll" => confusion_scroll(ecs, x, y),
    "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
    "Dagger" => dagger(ecs, x, y),
    "Shield" => shield(ecs, x, y),
    "Longsword" => longsword(ecs, x, y),
    "Tower Shield" => tower_shield(ecs, x, y),
    "Rations" => rations(ecs, x, y),
    "Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y),
    _ => {}
}
}

如果您现在运行 cargo run,您可能会发现可以拾取的卷轴 - 但它们不会做任何事情。

映射关卡 - 简单版本

我们将修改 inventory_system.rs 以检测您是否刚刚使用了魔法地图卷轴,并显示整个地图:

#![allow(unused)]
fn main() {
// 如果是魔法地图卷轴...
let is_mapper = magic_mapper.get(useitem.item);
match is_mapper {
    None => {}
    Some(_) => {
        used_item = true;
        for r in map.revealed_tiles.iter_mut() {
            *r = true;
        }
        gamelog.entries.push("The map is revealed to you!".to_string());
    }
}
}

还有一些框架更改(请参阅源代码); 我们已经做过很多次了,我认为不需要再次在这里重复。 如果您现在 cargo run 该项目,找到一个卷轴(它们无处不在)并使用它 - 地图会立即显示:

Screenshot

让它更漂亮

虽然那里展示的代码是有效的,但它在视觉上并不吸引人。 在游戏中加入一些花絮,让用户时不时地对 ASCII 终端的美丽感到惊喜是件好事! 我们将再次修改 inventory_system.rs

#![allow(unused)]
fn main() {
// 如果是魔法地图卷轴...
let is_mapper = magic_mapper.get(useitem.item);
match is_mapper {
    None => {}
    Some(_) => {
        used_item = true;
        gamelog.entries.push("The map is revealed to you!".to_string());
        *runstate = RunState::MagicMapReveal{ row : 0};
    }
}
}

请注意,我们没有修改地图,而是将游戏状态更改为映射模式。 我们实际上还不支持这样做,所以让我们进入 main.rs 中的状态映射器并修改 PlayerTurn 以处理它:

#![allow(unused)]
fn main() {
RunState::PlayerTurn => {
    self.systems.dispatch(&self.ecs);
    self.ecs.maintain();
    match *self.ecs.fetch::<RunState>() {
        RunState::MagicMapReveal{ .. } => newrunstate = RunState::MagicMapReveal{ row: 0 },
        _ => newrunstate = RunState::MonsterTurn
    }
}
}

当我们在这里时,让我们将状态添加到 RunState

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState { AwaitingInput,
    PreRun,
    PlayerTurn,
    MonsterTurn,
    ShowInventory,
    ShowDropItem,
    ShowTargeting { range : i32, item : Entity},
    MainMenu { menu_selection : gui::MainMenuSelection },
    SaveGame,
    NextLevel,
    ShowRemoveItem,
    GameOver,
    MagicMapReveal { row : i32 }
}
}

我们还在 tick 循环中为新状态添加了一些逻辑:

#![allow(unused)]
fn main() {
RunState::MagicMapReveal{row} => {
    let mut map = self.ecs.fetch_mut::<Map>();
    for x in 0..MAPWIDTH {
        let idx = map.xy_idx(x as i32,row);
        map.revealed_tiles[idx] = true;
    }
    if row as usize == MAPHEIGHT-1 {
        newrunstate = RunState::MonsterTurn;
    } else {
        newrunstate = RunState::MagicMapReveal{ row: row+1 };
    }
}
}

这非常简单:它显示当前行上的瓦片,然后如果我们没有到达地图底部 - 它会增加行数。 如果我们到达了底部,它会返回到我们之前的位置 - MonsterTurn。 如果您现在 cargo run,找到魔法地图卷轴并使用它,地图会很好地淡入:

Screenshot

记住要降低生成优先级!

spawners.rs 中,我们目前正在到处生成魔法地图卷轴。 这可能不是我们想要的! 编辑生成表以使其具有更低的优先级:

#![allow(unused)]
fn main() {
fn room_table(map_depth: i32) -> RandomTable {
    RandomTable::new()
        .add("Goblin", 10)
        .add("Orc", 1 + map_depth)
        .add("Health Potion", 7)
        .add("Fireball Scroll", 2 + map_depth)
        .add("Confusion Scroll", 2 + map_depth)
        .add("Magic Missile Scroll", 4)
        .add("Dagger", 3)
        .add("Shield", 3)
        .add("Longsword", map_depth - 1)
        .add("Tower Shield", map_depth - 1)
        .add("Rations", 10)
        .add("Magic Mapping Scroll", 2)
}
}

总结

这是一个相对快速的章节,但我们现在拥有了 Roguelike 类型的另一个主要元素:魔法地图。

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

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


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