造成伤害(并承受一些!)
关于本教程
本教程是免费且开源的,所有代码使用 MIT 许可证——因此您可以随意使用它。我希望您会喜欢这个教程,并制作出很棒的游戏!
如果你喜欢这个并且希望我继续写作,请考虑支持我的 Patreon。
现在我们有了怪物,我们希望它们不仅仅是站在控制台上对你大喊大叫!本章将使它们追逐你,并引入一些基本的游戏统计数据,让你能够杀出重围。
追逐玩家
我们需要做的第一件事是为我们的Map
类完成实现BaseMap
。特别是,我们需要支持get_available_exits
——这是用于路径查找的。
在我们的Map
实现中,我们需要一个辅助函数:
#![allow(unused)] fn main() { fn is_exit_valid(&self, x:i32, y:i32) -> bool { if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; } let idx = self.xy_idx(x, y); self.tiles[idx as usize] != TileType::Wall } }
这需要一个索引,并计算它是否可以进入。
然后我们使用这个辅助工具来实现特征:
#![allow(unused)] fn main() { fn get_available_exits(&self, idx:usize) -> rltk::SmallVec<[(usize, f32); 10]> { let mut exits = rltk::SmallVec::new(); let x = idx as i32 % self.width; let y = idx as i32 / self.width; let w = self.width as usize; // Cardinal directions if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) }; if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) }; if self.is_exit_valid(x, y-1) { exits.push((idx-w, 1.0)) }; if self.is_exit_valid(x, y+1) { exits.push((idx+w, 1.0)) }; exits } }
提供没有距离启发式的出口会导致一些糟糕的行为(并且在 RLTK 的未来版本中会导致崩溃)。因此,也要为你的地图实现这一点:
#![allow(unused)] fn main() { impl BaseMap for Map { ... fn get_pathing_distance(&self, idx1:usize, idx2:usize) -> f32 { let w = self.width as usize; let p1 = Point::new(idx1 % w, idx1 / w); let p2 = Point::new(idx2 % w, idx2 / w); rltk::DistanceAlg::Pythagoras.distance2d(p1, p2) } }
相当直接:我们评估每个可能的出口,如果可以通行,则将其添加到 exits
向量中。接下来,我们修改 monster_ai_system
中的主循环:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Monster, Name, Map, Position}; use rltk::{Point, console}; pub struct MonsterAI {} impl<'a> System<'a> for MonsterAI { #[allow(clippy::type_complexity)] type SystemData = ( WriteExpect<'a, Map>, ReadExpect<'a, Point>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Monster>, ReadStorage<'a, Name>, WriteStorage<'a, Position>); fn run(&mut self, data : Self::SystemData) { let (mut map, player_pos, mut viewshed, monster, name, mut position) = data; for (mut viewshed,_monster,name,mut pos) in (&mut viewshed, &monster, &name, &mut position).join() { if viewshed.visible_tiles.contains(&*player_pos) { console::log(&format!("{} shouts insults", name.name)); let path = rltk::a_star_search( map.xy_idx(pos.x, pos.y) as i32, map.xy_idx(player_pos.x, player_pos.y) as i32, &mut *map ); if path.success && path.steps.len()>1 { pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; viewshed.dirty = true; } } } } } }
我们更改了一些内容以允许写访问,请求访问地图。我们还添加了一个 #[allow...]
来告诉代码检查工具我们确实打算在一个类型中使用这么多内容!核心是 a_star_search
调用;RLTK 包含一个高性能的 A* 实现,所以我们要求它从怪物的位置到玩家的路径。然后我们检查路径是否成功,并且有超过 2 步(第 0 步总是当前位置)。如果是,那么我们将怪物移动到该点,并将其视野设置为脏状态。
如果你运行项目,怪物现在会追逐玩家——如果失去视线就会停止。我们没有阻止怪物互相站立——或者你——我们也没有让它们做任何其他事情,只是对着你的控制台大喊——但这是一个好的开始。追逐机制并不难实现!
阻止访问
我们不希望怪物互相踩踏,也不希望它们在寻找玩家时陷入交通堵塞;我们宁愿它们愿意尝试包抄玩家!我们将通过跟踪地图上哪些部分被阻挡来辅助这一点。
首先,我们将向我们的 Map
添加另一个布尔向量:
#![allow(unused)] fn main() { #[derive(Default)] pub struct Map { pub tiles : Vec<TileType>, pub rooms : Vec<Rect>, pub width : i32, pub height : i32, pub revealed_tiles : Vec<bool>, pub visible_tiles : Vec<bool>, pub blocked : Vec<bool> } }
我们也会初始化它,就像其他向量一样:
#![allow(unused)] fn main() { let mut map = Map{ tiles : vec![TileType::Wall; 80*50], rooms : Vec::new(), width : 80, height: 50, revealed_tiles : vec![false; 80*50], visible_tiles : vec![false; 80*50], blocked : vec![false; 80*50] }; }
让我们引入一个新功能来填充一个瓦片是否被阻挡。在Map
实现中:
#![allow(unused)] fn main() { pub fn populate_blocked(&mut self) { for (i,tile) in self.tiles.iter_mut().enumerate() { self.blocked[i] = *tile == TileType::Wall; } } }
这个功能非常简单:如果是墙壁,则将瓦片的 blocked
设置为 true,否则设置为 false(当我们添加更多瓦片类型时会扩展它)。在处理 Map
时,让我们调整 is_exit_valid
以使用这些数据:
#![allow(unused)] fn main() { fn is_exit_valid(&self, x:i32, y:i32) -> bool { if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; } let idx = self.xy_idx(x, y); !self.blocked[idx] } }
这很简单:它检查 x
和 y
是否在地图范围内,如果出口在地图外则返回 false
(这种 边界检查 是值得做的,它可以防止你的程序因为试图读取有效内存区域外的内容而崩溃)。然后它检查瓦片数组中指定坐标的 索引,并返回 blocked
的 反值(!
在大多数语言中与 not
相同 - 所以可以读作“在 idx
处未被阻挡”)。
现在我们将创建一个新的组件,BlocksTile
。你现在应该知道怎么做;在 Components.rs
中:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct BlocksTile {} }
然后在 main.rs
中注册它:gs.ecs.register::<BlocksTile>();
我们应该将 BlocksTile
应用于 NPC - 因此我们的 NPC 创建代码变为:
#![allow(unused)] fn main() { gs.ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ 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) }) .with(BlocksTile{}) .build(); }
最后,我们需要填充阻止列表。我们可能会在以后扩展这个系统,所以我们选择一个很好的通用名称 map_indexing_system.rs
:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Map, Position, BlocksTile}; pub struct MapIndexingSystem {} impl<'a> System<'a> for MapIndexingSystem { type SystemData = ( WriteExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>); fn run(&mut self, data : Self::SystemData) { let (mut map, position, blockers) = data; map.populate_blocked(); for (position, _blocks) in (&position, &blockers).join() { let idx = map.xy_idx(position.x, position.y); map.blocked[idx] = true; } } } }
这告诉地图从地形设置阻挡,然后遍历所有带有 BlocksTile
组件的实体,并将它们应用到阻挡列表中。我们需要在 main.rs
中使用 run_systems
注册它。
#![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); let mut mapindex = MapIndexingSystem{}; mapindex.run_now(&self.ecs); self.ecs.maintain(); } } }
如果你现在运行 cargo run
,怪物不再重叠在一起——但它们确实会出现在玩家上方。我们应该修复这个问题。我们可以让怪物只在靠近玩家时才大叫。在 monster_ai_system.rs
中,在可见性测试上方添加以下内容:
#![allow(unused)] fn main() { let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); if distance < 1.5 { // Attack goes here console::log(&format!("{} shouts insults", name.name)); return; } }
最后,我们希望阻止玩家走过怪物。在 player.rs
中,我们将查找墙壁的 if
语句替换为:
#![allow(unused)] fn main() { if !map.blocked[destination_idx] { }
由于我们已经将墙壁加入到阻止列表中,这应该暂时解决了这个问题。cargo run
显示怪物现在会阻挡玩家。它们阻挡得完美无缺——所以一个想要挡在你面前的怪物是一个无法通过的障碍!
允许对角移动
能够绕过怪物会很不错——而对角线移动是 Roguelike 游戏的主要特点。所以让我们继续支持它。在 map.rs
的 get_available_exits
函数中,我们添加它们:
#![allow(unused)] fn main() { fn get_available_exits(&self, idx:usize) -> rltk::SmallVec<[(usize, f32); 10]> { let mut exits = rltk::SmallVec::new(); let x = idx as i32 % self.width; let y = idx as i32 / self.width; let w = self.width as usize; // Cardinal directions if self.is_exit_valid(x-1, y) { exits.push((idx-1, 1.0)) }; if self.is_exit_valid(x+1, y) { exits.push((idx+1, 1.0)) }; if self.is_exit_valid(x, y-1) { exits.push((idx-w, 1.0)) }; if self.is_exit_valid(x, y+1) { exits.push((idx+w, 1.0)) }; // Diagonals if self.is_exit_valid(x-1, y-1) { exits.push(((idx-w)-1, 1.45)); } if self.is_exit_valid(x+1, y-1) { exits.push(((idx-w)+1, 1.45)); } if self.is_exit_valid(x-1, y+1) { exits.push(((idx+w)-1, 1.45)); } if self.is_exit_valid(x+1, y+1) { exits.push(((idx+w)+1, 1.45)); } exits } }
我们还修改了 player.rs
输入代码:
#![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), // Diagonals VirtualKeyCode::Numpad9 | VirtualKeyCode::Y => try_move_player(1, -1, &mut gs.ecs), VirtualKeyCode::Numpad7 | VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs), VirtualKeyCode::Numpad3 | VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs), VirtualKeyCode::Numpad1 | VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs), _ => { return RunState::Paused } }, } RunState::Running } }
你现在可以斜向躲避怪物 - 它们也可以斜向移动/攻击。
给怪物和玩家一些战斗属性
你可能已经猜到了,添加属性到实体的方法是通过另一个组件!在 components.rs
中,我们添加 CombatStats
。以下是一个简单的定义:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct CombatStats { pub max_hp : i32, pub hp : i32, pub defense : i32, pub power : i32 } }
像往常一样,别忘了在 main.rs
中注册它!
我们将给Player
30 点生命值,2 点防御和 5 点力量:
#![allow(unused)] fn main() { .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 }) }
同样,我们会给怪物一组较弱的属性(我们稍后再考虑怪物的差异化):
#![allow(unused)] fn main() { .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 }) }
索引什么在哪里
在地图上旅行时——无论是作为玩家还是怪物——了解一个格子里的内容非常方便。你可以将其与可见性系统结合,以便对可见的事物做出明智的选择,你可以用它来查看是否试图进入敌人的空间(并攻击他们),等等。一种方法是迭代Position
组件并查看我们是否击中了任何东西;对于少量实体来说,这已经足够快了。我们将采取不同的方法,并让map_indexing_system
帮助我们。我们将首先在地图中添加一个字段:
#![allow(unused)] fn main() { #[derive(Default)] pub struct Map { pub tiles : Vec<TileType>, pub rooms : Vec<Rect>, pub width : i32, pub height : i32, pub revealed_tiles : Vec<bool>, pub visible_tiles : Vec<bool>, pub blocked : Vec<bool>, pub tile_content : Vec<Vec<Entity>> } }
并且我们将为新地图代码添加一个基本初始化器:
#![allow(unused)] fn main() { tile_content : vec![Vec::new(); 80*50] }
当我们在 map
中时,我们还需要一个函数:
#![allow(unused)] fn main() { pub fn clear_content_index(&mut self) { for content in self.tile_content.iter_mut() { content.clear(); } } }
这也很简单:它迭代(访问)tile_content
列表中的每个向量,可变地(iter_mut
获取一个可变迭代器)。然后它告诉每个向量清除自身——移除所有内容(它实际上并不保证会释放内存;向量可以保留空的部分以备更多数据。这实际上是一件好事,因为获取新内存是程序可以做的最慢的事情之一——所以它有助于保持运行速度)。
然后我们将升级索引系统,按图块索引所有实体:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Map, Position, BlocksTile}; pub struct MapIndexingSystem {} impl<'a> System<'a> for MapIndexingSystem { type SystemData = ( WriteExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>, Entities<'a>,); fn run(&mut self, data : Self::SystemData) { let (mut map, position, blockers, entities) = data; map.populate_blocked(); map.clear_content_index(); for (entity, position) in (&entities, &position).join() { let idx = map.xy_idx(position.x, position.y); // If they block, update the blocking list let _p : Option<&BlocksTile> = blockers.get(entity); if let Some(_p) = _p { map.blocked[idx] = true; } // Push the entity to the appropriate index slot. It's a Copy // type, so we don't need to clone it (we want to avoid moving it out of the ECS!) map.tile_content[idx].push(entity); } } } }
让玩家进行攻击
大多数 Roguelike 游戏角色花费大量时间攻击事物,所以让我们实现这一点!碰撞攻击(走进目标)是实现这一点的典型方式。我们希望扩展player.rs
中的try_move_player
,以检查我们试图进入的瓷砖是否包含目标。
我们将为CombatStats
添加一个读取器到数据存储列表中,并快速插入一个敌人检测器:
#![allow(unused)] fn main() { let combat_stats = ecs.read_storage::<CombatStats>(); let map = ecs.fetch::<Map>(); for (_player, pos, viewshed) in (&mut players, &mut positions, &mut viewsheds).join() { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); for potential_target in map.tile_content[destination_idx].iter() { let target = combat_stats.get(*potential_target); match target { None => {} Some(t) => { // Attack it console::log(&format!("From Hell's Heart, I stab thee!")); return; // So we don't move after attacking } } } }
如果你 cargo run
这个,你会看到你可以走到一个生物旁边并试图移动到它上面。从地狱的心脏,我刺向你! 出现在控制台上。所以检测是有效的,攻击也在正确的位置。
玩家攻击和杀死事物
我们将以 ECS 的方式进行,所以有一些样板代码。在components.rs
中,我们添加一个表示攻击意图的组件:
#![allow(unused)] fn main() { #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct WantsToMelee { pub target : Entity } }
我们也希望跟踪传入的伤害。在一个回合中,你可能会从多个来源受到伤害,而 Specs 非常不喜欢你在实体上尝试使用多个相同类型的组件。这里有两种可能的方法:将伤害本身作为一个实体(并跟踪受害者),或者将伤害作为一个向量。后者似乎更容易实现;因此我们将创建一个SufferDamage
组件来跟踪伤害,并附加/实现一个方法以便于使用:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct SufferDamage { pub amount : Vec<i32> } impl SufferDamage { pub fn new_damage(store: &mut WriteStorage<SufferDamage>, victim: Entity, amount: i32) { if let Some(suffering) = store.get_mut(victim) { suffering.amount.push(amount); } else { let dmg = SufferDamage { amount : vec![amount] }; store.insert(victim, dmg).expect("Unable to insert damage"); } } } }
(别忘了在main.rs
中注册它们!)。我们修改玩家的移动命令以创建一个表示攻击意图的组件(将wants_to_melee
附加到攻击者上):
#![allow(unused)] fn main() { let entities = ecs.entities(); let mut wants_to_melee = ecs.write_storage::<WantsToMelee>(); for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() { if pos.x + delta_x < 1 || pos.x + delta_x > map.width-1 || pos.y + delta_y < 1 || pos.y + delta_y > map.height-1 { return; } let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); for potential_target in map.tile_content[destination_idx].iter() { let target = combat_stats.get(*potential_target); if let Some(_target) = target { wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed"); return; } } } }
我们需要一个近战战斗系统
来处理近战。这使用了我们创建的新伤害
系统,以确保在一个回合中可以应用多个伤害来源:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{CombatStats, WantsToMelee, Name, SufferDamage}; pub struct MeleeCombatSystem {} impl<'a> System<'a> for MeleeCombatSystem { type SystemData = ( Entities<'a>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, ReadStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut wants_melee, names, combat_stats, mut inflict_damage) = data; for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { if stats.hp > 0 { let target_stats = combat_stats.get(wants_melee.target).unwrap(); if target_stats.hp > 0 { let target_name = names.get(wants_melee.target).unwrap(); let damage = i32::max(0, stats.power - target_stats.defense); if damage == 0 { console::log(&format!("{} is unable to hurt {}", &name.name, &target_name.name)); } else { console::log(&format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage)); SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); } } } } wants_melee.clear(); } } }
我们需要一个damage_system
来应用伤害(我们将其分开,因为伤害可能来自多种来源!)。我们使用迭代器来求和伤害,确保所有伤害都被应用:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{CombatStats, SufferDamage}; pub struct DamageSystem {} impl<'a> System<'a> for DamageSystem { type SystemData = ( WriteStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage> ); fn run(&mut self, data : Self::SystemData) { let (mut stats, mut damage) = data; for (mut stats, damage) in (&mut stats, &damage).join() { stats.hp -= damage.amount.iter().sum::<i32>(); } damage.clear(); } } }
我们还将添加一个方法来清理死亡的实体:
#![allow(unused)] fn main() { pub fn delete_the_dead(ecs : &mut World) { let mut dead : Vec<Entity> = Vec::new(); // Using a scope to make the borrow checker happy { let combat_stats = ecs.read_storage::<CombatStats>(); let entities = ecs.entities(); for (entity, stats) in (&entities, &combat_stats).join() { if stats.hp < 1 { dead.push(entity); } } } for victim in dead { ecs.delete_entity(victim).expect("Unable to delete"); } } }
这是在我们运行系统后从 tick
命令调用的:damage_system::delete_the_dead(&mut self.ecs);
。
如果你现在 cargo run
,你可以在地图上四处跑动并击打物体——它们死后会消失!
让怪物击中你
由于我们已经编写了处理攻击和伤害的系统,因此使用相同的代码来处理怪物相对容易——只需添加一个WantsToMelee
组件,它们就可以攻击/杀死玩家。
我们将首先将玩家实体变成一个游戏资源,这样它可以很容易地被引用。像玩家的位置一样,这是我们可能需要在各处使用的东西——而且由于实体 ID 是稳定的,我们可以依赖它的存在。在main.rs
中,我们将玩家的create_entity
改为返回实体对象:
#![allow(unused)] fn main() { let player_entity = 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() }) .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 }) .build(); }
然后我们将其插入到世界中:
#![allow(unused)] fn main() { gs.ecs.insert(player_entity); }
现在我们修改 monster_ai_system
。这里有一些清理工作,并且“投掷侮辱”代码被完全替换为一个单一组件插入:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Monster, Map, Position, WantsToMelee, RunState}; use rltk::{Point}; pub struct MonsterAI {} impl<'a> System<'a> for MonsterAI { #[allow(clippy::type_complexity)] type SystemData = ( WriteExpect<'a, Map>, ReadExpect<'a, Point>, ReadExpect<'a, Entity>, ReadExpect<'a, RunState>, Entities<'a>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Monster>, WriteStorage<'a, Position>, WriteStorage<'a, WantsToMelee>); fn run(&mut self, data : Self::SystemData) { let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = data; for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() { let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); if distance < 1.5 { wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack"); } else if viewshed.visible_tiles.contains(&*player_pos) { // Path to the player let path = rltk::a_star_search( map.xy_idx(pos.x, pos.y), map.xy_idx(player_pos.x, player_pos.y), &mut *map ); if path.success && path.steps.len()>1 { let mut idx = map.xy_idx(pos.x, pos.y); map.blocked[idx] = false; pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; idx = map.xy_idx(pos.x, pos.y); map.blocked[idx] = true; viewshed.dirty = true; } } } } } }
如果你现在运行 cargo run
,你可以杀死怪物——它们也可以攻击你。如果一个怪物杀死了你——游戏会崩溃!它会崩溃,因为 delete_the_dead
删除了玩家。这显然不是我们想要的结果。以下是一个不会崩溃的 delete_the_dead
版本:
#![allow(unused)] fn main() { pub fn delete_the_dead(ecs : &mut World) { let mut dead : Vec<Entity> = Vec::new(); // Using a scope to make the borrow checker happy { let combat_stats = ecs.read_storage::<CombatStats>(); let players = ecs.read_storage::<Player>(); let entities = ecs.entities(); for (entity, stats) in (&entities, &combat_stats).join() { if stats.hp < 1 { let player = players.get(entity); match player { None => dead.push(entity), Some(_) => console::log("You are dead") } } } } for victim in dead { ecs.delete_entity(victim).expect("Unable to delete"); } } }
我们将在后面的章节中讨论结束游戏的问题。
扩展转向系统
如果你仔细观察,你会发现即使敌人已经受到致命伤害,他们仍然可以反击。虽然这与某些莎士比亚戏剧相符(他们真的应该发表演讲),但这不是 rogue 类游戏鼓励的战术玩法。问题在于我们的游戏状态只有“运行”和“暂停”——甚至在玩家行动时我们也没有运行系统。此外,系统不知道我们处于哪个阶段——所以它们无法考虑到这一点。
让我们用更能描述每个阶段的名称替换 RunState
:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn } }
如果你在使用带有 RLS 的 Visual Studio Code,你的项目一半可能会变成红色。没关系,我们会一步一步地重构。我们将从主GameState
中完全移除RunState
:
#![allow(unused)] fn main() { pub struct State { pub ecs: World } }
这使得更多的红色出现!我们这样做,是因为我们要将 RunState
变成一个资源。所以在 main.rs
中我们插入其他资源的地方,我们添加:
#![allow(unused)] fn main() { gs.ecs.insert(RunState::PreRun); }
现在开始重构 Tick
。我们的新 tick
函数如下:
#![allow(unused)] fn main() { fn tick(&mut self, ctx : &mut Rltk) { ctx.cls(); let mut newrunstate; { let runstate = self.ecs.fetch::<RunState>(); newrunstate = *runstate; } match newrunstate { RunState::PreRun => { self.run_systems(); newrunstate = RunState::AwaitingInput; } RunState::AwaitingInput => { newrunstate = player_input(self, ctx); } RunState::PlayerTurn => { self.run_systems(); newrunstate = RunState::MonsterTurn; } RunState::MonsterTurn => { self.run_systems(); newrunstate = RunState::AwaitingInput; } } { let mut runwriter = self.ecs.write_resource::<RunState>(); *runwriter = newrunstate; } damage_system::delete_the_dead(&mut self.ecs); draw_map(&self.ecs, ctx); 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) } } } } }
注意我们现在有一个状态机在进行,有一个“预运行”阶段来启动游戏!这非常清晰,而且很明显发生了什么。为了使借用检查器满意,我们使用了一些作用域魔法:如果你在一个作用域内声明和使用一个变量,它在作用域退出时会被丢弃(你也可以手动丢弃事物,但我认为这样看起来更清晰)。
在 player.rs
中,我们只需将所有 Paused
替换为 AwaitingInput
,并将 Running
替换为 PlayerTurn
。
最后,我们修改 monster_ai_system
仅在状态为 MonsterTurn
时运行(代码片段):
#![allow(unused)] fn main() { impl<'a> System<'a> for MonsterAI { #[allow(clippy::type_complexity)] type SystemData = ( WriteExpect<'a, Map>, ReadExpect<'a, Point>, ReadExpect<'a, Entity>, ReadExpect<'a, RunState>, Entities<'a>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Monster>, WriteStorage<'a, Position>, WriteStorage<'a, WantsToMelee>); fn run(&mut self, data : Self::SystemData) { let (mut map, player_pos, player_entity, runstate, entities, mut viewshed, monster, mut position, mut wants_to_melee) = data; if *runstate != RunState::MonsterTurn { return; } }
不要忘记确保所有系统现在都在 run_systems
(在 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); let mut mapindex = MapIndexingSystem{}; mapindex.run_now(&self.ecs); let mut melee = MeleeCombatSystem{}; melee.run_now(&self.ecs); let mut damage = DamageSystem{}; damage.run_now(&self.ecs); self.ecs.maintain(); } } }
如果你 cargo run
这个项目,它现在会按预期运行:玩家移动,他/她杀死的生物在还击之前就会死亡。
总结
那是相当精彩的一章!我们加入了位置索引、伤害和击杀功能。好消息是这是最难的部分;你现在有了一个简单的地牢闯关游戏!虽然不是特别有趣,而且你肯定会死(因为没有治疗)——但基本功能已经齐备。
本章的源代码可以在此处找到这里
在浏览器中使用 WebAssembly 运行本章示例(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.
版权 (C) 2024, myedgetech.com.