第六章 - 怪物
关于本教程
本教程是免费且开源的,所有代码使用 MIT 许可证——因此您可以随意使用它。我希望您会喜欢这个教程,并制作出很棒的游戏!
如果你喜欢这个并且希望我继续写作,请考虑支持我的 Patreon。
一个没有怪物的类 Rogue 游戏非常不寻常,所以我们来添加一些吧!好消息是我们已经为此做了一些工作:我们可以渲染它们,并且可以计算它们能看到什么。我们将基于上一章的源代码,并引入一些无害的怪物。
在每个房间的中心渲染一个怪物
我们可以简单地为每个怪物添加一个Renderable
组件(我们还会添加一个Viewshed
,因为我们稍后会用到它)。在我们的main
函数(在main.rs
中),添加以下内容:
#![allow(unused)] fn main() { for room in map.rooms.iter().skip(1) { let (x,y) = room.center(); gs.ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('g'), fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), }) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .build(); } gs.ecs.insert(map); }
注意使用 skip(1)
忽略第一个房间 - 我们不希望玩家一开始就被怪物压在上面!运行这个(使用 cargo run
)会产生类似这样的结果:
这是一个很好的开始!然而,即使我们看不到怪物,我们也在渲染它们。我们可能只想渲染那些我们能看到的。我们可以通过修改我们的渲染循环来实现这一点:
#![allow(unused)] fn main() { let positions = self.ecs.read_storage::<Position>(); let renderables = self.ecs.read_storage::<Renderable>(); let map = self.ecs.fetch::<Map>(); for (pos, render) in (&positions, &renderables).join() { let idx = map.xy_idx(pos.x, pos.y); if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) } } }
我们从 ECS 获取地图,并使用它来获取索引 - 并检查图块是否可见。如果是 - 我们渲染可渲染对象。不需要为玩家设置特殊情况 - 因为他们通常可以看到自己!结果非常好:
增加一些怪物种类
只有一种怪物类型相当无聊,所以我们会对怪物生成器进行修改,使其能够生成哥
布林和兽
人。
这是生成器代码:
#![allow(unused)] fn main() { let mut rng = rltk::RandomNumberGenerator::new(); for room in map.rooms.iter().skip(1) { let (x,y) = room.center(); let glyph : rltk::FontCharType; let roll = rng.roll_dice(1, 2); match roll { 1 => { glyph = rltk::to_cp437('g') } _ => { glyph = rltk::to_cp437('o') } } gs.ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: glyph, fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), }) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .build(); } }
显然,当我们开始加入战斗时,我们会希望有更多的多样性——但这是一个好的开始。运行程序(cargo run
),你会看到大约 50/50 的兽人和哥布林的分布。
让怪物思考
现在开始让怪物思考!目前,除了思考它们孤独的存在之外,它们实际上不会做太多事情。我们应该首先添加一个标签组件,以表明一个实体是怪物。在 components.rs
中,我们添加一个简单的结构体:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct Monster {} }
当然,我们需要在 main.rs
中注册它:gs.ecs.register::<Monster>();
。我们还应该修改我们的生成代码以将其应用于怪物:
#![allow(unused)] fn main() { gs.ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: glyph, fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), }) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Monster{}) .build(); }
现在我们为一个怪物思维系统创建一个文件。我们将创建一个新文件,monster_ai_system.rs
。我们将给它一些基本不存在的智能:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Position, Map, Monster}; use rltk::{field_of_view, Point, console}; pub struct MonsterAI {} impl<'a> System<'a> for MonsterAI { type SystemData = ( ReadStorage<'a, Viewshed>, ReadStorage<'a, Position>, ReadStorage<'a, Monster>); fn run(&mut self, data : Self::SystemData) { let (viewshed, pos, monster) = data; for (viewshed,pos,_monster) in (&viewshed, &pos, &monster).join() { console::log("Monster considers their own existence"); } } } }
注意,我们从 rltk
导入 console
- 并使用 console::log
打印。这是 RLTK 提供的一个辅助工具,可以检测你是否在编译为常规程序或 Web Assembly;如果你使用的是常规程序,它会调用 println!
并输出到控制台。如果你在 WASM
中,它会输出到 浏览器 控制台。
我们还将在main.rs
中扩展系统运行器以调用它:
#![allow(unused)] fn main() { impl State { fn run_systems(&mut self) { let mut vis = VisibilitySystem{}; vis.run_now(&self.ecs); let mut mob = MonsterAI{}; mob.run_now(&self.ecs); self.ecs.maintain(); } } }
如果你现在 cargo run
你的项目,它会非常慢——你的控制台会充满“怪物思考它们的存在”。AI 正在运行——但它每刻都在运行!
回合制游戏,在基于时间的世界中
为了防止这种情况——并制作一个回合制游戏——我们向游戏状态引入了一个新概念。游戏要么是“运行中”,要么是“等待输入”——所以我们创建一个enum
来处理这种情况:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { Paused, Running } }
注意derive
宏!派生是一种让 Rust(和 crates)为你的结构添加代码的方式,以减少样板代码的输入。在这种情况下,enum
需要一些额外的功能。PartialEq
允许你比较RunState
与其他RunState
变量,以确定它们是否相同(或不同)。Copy
将其标记为“复制”类型——它可以安全地在内存中复制(这意味着它没有会在此过程中混乱的指针)。Clone
悄悄地为其添加了一个.clone()
函数,允许你通过这种方式进行内存复制。
接下来,我们需要将其添加到State
结构中:
#![allow(unused)] fn main() { pub struct State { pub ecs: World, pub runstate : RunState } }
反过来,我们需要修改我们的状态创建器以包含 runstate: RunState::Running
:
#![allow(unused)] fn main() { let mut gs = State { ecs: World::new(), runstate : RunState::Running }; }
现在,我们将 tick
函数改为仅在游戏未暂停时运行模拟,否则请求用户输入:
#![allow(unused)] fn main() { if self.runstate == RunState::Running { self.run_systems(); self.runstate = RunState::Paused; } else { self.runstate = player_input(self, ctx); } }
如你所见,player_input
现在返回一个状态。这是它的新代码:
#![allow(unused)] fn main() { pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { // Player movement match ctx.key { None => { return RunState::Paused } // Nothing happened Some(key) => match key { VirtualKeyCode::Left | VirtualKeyCode::Numpad4 | VirtualKeyCode::H => try_move_player(-1, 0, &mut gs.ecs), VirtualKeyCode::Right | VirtualKeyCode::Numpad6 | VirtualKeyCode::L => try_move_player(1, 0, &mut gs.ecs), VirtualKeyCode::Up | VirtualKeyCode::Numpad8 | VirtualKeyCode::K => try_move_player(0, -1, &mut gs.ecs), VirtualKeyCode::Down | VirtualKeyCode::Numpad2 | VirtualKeyCode::J => try_move_player(0, 1, &mut gs.ecs), _ => { return RunState::Paused } }, } RunState::Running } }
如果你现在运行 cargo run
,游戏速度会恢复 - 而怪物只会在你移动时思考下一步该做什么。这是一个基本的回合制 tick 循环!
安静的怪物,直到它们看到你
你可以让怪物在任何东西移动时都思考(当你进入更深层次的模拟时,你可能会这样做),但现在让我们让它们安静一点——如果它们能看到玩家,就让它们做出反应。
系统很可能会经常想知道玩家的位置——所以让我们将其作为一个资源添加。在 main.rs
中,一行代码将其添加进去(我不建议对非玩家实体这样做;资源是有限的——但玩家是我们反复使用的):
#![allow(unused)] fn main() { gs.ecs.insert(Point::new(玩家_x, 玩家_y)); }
在 player.rs
中,try_move_player()
,更新玩家移动时的资源:
#![allow(unused)] fn main() { let mut ppos = ecs.write_resource::<Point>(); ppos.x = pos.x; ppos.y = pos.y; }
我们可以在我们的 monster_ai_system
中使用它。以下是一个工作版本:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Monster}; use rltk::{Point, console}; pub struct MonsterAI {} impl<'a> System<'a> for MonsterAI { type SystemData = ( ReadExpect<'a, Point>, ReadStorage<'a, Viewshed>, ReadStorage<'a, Monster>); fn run(&mut self, data : Self::SystemData) { let (player_pos, viewshed, monster) = data; for (viewshed,_monster) in (&viewshed, &monster).join() { if viewshed.visible_tiles.contains(&*player_pos) { console::log(format!("Monster shouts insults")); } } } } }
如果你 cargo run
这个,你将能够四处移动——当怪物能看到你时,你的控制台会不时显示“怪物大喊侮辱”。
区分我们的怪物
怪物应该有名字,这样我们就知道是谁在对我们大喊大叫!所以我们创建一个新的组件,Name
。在 components.rs
中,我们添加:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct Name { pub name : String } }
我们也会在 main.rs
中注册它,你现在应该已经很熟悉了!我们还会添加一些命令来给我们的怪物和玩家添加名字。所以我们的怪物生成器看起来像这样:
#![allow(unused)] fn main() { for (i,room) in map.rooms.iter().skip(1).enumerate() { let (x,y) = room.center(); let glyph : rltk::FontCharType; let name : String; let roll = rng.roll_dice(1, 2); match roll { 1 => { glyph = rltk::to_cp437('g'); name = "Goblin".to_string(); } _ => { glyph = rltk::to_cp437('o'); name = "Orc".to_string(); } } gs.ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: glyph, fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), }) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Monster{}) .with(Name{ name: format!("{} #{}", &name, i) }) .build(); } }
现在我们调整 monster_ai_system
以包含怪物的名字。新的 AI 如下所示:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Monster, Name}; use rltk::{Point}; pub struct MonsterAI {} impl<'a> System<'a> for MonsterAI { type SystemData = ( ReadExpect<'a, Point>, ReadStorage<'a, Viewshed>, ReadStorage<'a, Monster>, ReadStorage<'a, Name>); fn run(&mut self, data : Self::SystemData) { let (player_pos, viewshed, monster, name) = data; for (viewshed,_monster,name) in (&viewshed, &monster, &name).join() { if viewshed.visible_tiles.contains(&*player_pos) { console::log(&format!("{} shouts insults", name.name)); } } } } }
我们也需要给玩家一个名字;我们在 AI 的加入中明确包含了名字,所以我们最好确保玩家有一个名字!否则,AI 将完全忽略玩家。在main.rs
中,我们将在Player
创建时包含一个名字:
#![allow(unused)] fn main() { gs.ecs .create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { glyph: rltk::to_cp437('@'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), }) .with(Player{}) .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) .with(Name{name: "Player".to_string() }) .build(); }
如果你运行项目,你现在会看到类似 哥布林 #9 大喊侮辱 的内容 - 这样你就能知道谁在喊叫。
第六章就这样结束了;我们添加了各种满口脏话的怪物来侮辱你脆弱的自尊!在本章中,我们开始看到了使用实体组件系统的一些好处:添加新渲染的怪物,带有一些变化,并开始为事物存储名称,都非常容易。我们之前编写的视野代码只需稍作修改即可让怪物可见——而我们新的怪物 AI 能够利用我们已经构建的内容,相当高效地对玩家说坏话。
本章的源代码可以在此处找到这里
在浏览器中使用 WebAssembly 运行本章示例(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.
版权 (C) 2024, myedgetech.com.