空间地图
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
您可能已经注意到本章的文件名是 "57A"。在第 57 章的 AI 更改之后,空间索引系统出现了一些问题。与其在一个已经过长的章节中加入一个本身就很不错的主题,我决定最好插入一个章节。在本章中,我们将修改 map_indexing_system
和相关数据。我们有几个目标:
- 实体存储的位置和 "blocked" 系统应该易于在回合中更新。
- 我们希望消除实体共享空间的情况。
- 我们希望修复在实体被击杀后无法进入瓦片的问题。
- 我们希望保持良好的性能。
这是一个相当高的标准!
构建空间索引 API
与其分散地图的 tile_content
、blocked
列表、定期更新的系统以及对这些数据结构的调用,不如将其移动到一个统一的 API 之后,这样会 干净 得多。然后我们可以访问 API,功能更改会自动随着改进而被引入。这样,我们只需要记住调用 API - 而不是记住它是如何工作的。
我们将从创建一个模块开始。创建一个 src\spatial
目录,并在其中放入一个空的 mod.rs
文件。然后我们将 "桩出" 我们的空间后端,添加一些内容:
#![allow(unused)] fn main() { use std::sync::Mutex; use specs::prelude::*; struct SpatialMap { blocked : Vec<bool>, tile_content : Vec<Vec<Entity>> } impl SpatialMap { fn new() -> Self { Self { blocked: Vec::new(), tile_content: Vec::new() } } } lazy_static! { static ref SPATIAL_MAP : Mutex<SpatialMap> = Mutex::new(SpatialMap::new()); } }
SpatialMap
结构体包含我们存储在 Map
中的空间信息。它刻意地不是 public 的:我们希望停止直接共享数据,而是使用 API。然后我们创建一个 lazy_static
:一个受互斥锁保护的全局变量,并使用它来存储空间信息。以这种方式存储它允许我们访问它,而不会给 Specs 的资源系统带来负担 - 并且更容易从系统内部和外部提供访问。由于我们正在使用互斥锁保护空间地图,我们还可以从线程安全中受益;这会将资源从 Specs 的线程计划中移除。这使得程序作为一个整体更容易使用线程调度器。
地图 API 替换
当地图更改时,我们需要一种方法来调整空间地图的大小。在 spatial/mod.rs
中:
#![allow(unused)] fn main() { pub fn set_size(map_tile_count: usize) { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.blocked = vec![false; map_tile_count]; lock.tile_content = vec![Vec::new(); map_tile_count]; } }
这有点低效,因为它会重新分配 - 但我们不经常这样做,所以应该没问题。我们还需要一种清除空间内容的方法:
#![allow(unused)] fn main() { pub fn clear() { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.blocked.clear(); for content in lock.tile_content.iter_mut() { content.clear(); } } }
我们需要一个类似于地图当前 populate_blocked
的函数(它构建一个 被地形 阻挡的瓦片列表):
#![allow(unused)] fn main() { pub fn populate_blocked_from_map(map: &Map) { let mut lock = SPATIAL_MAP.lock().unwrap(); for (i,tile) in map.tiles.iter().enumerate() { lock.blocked[i] = !tile_walkable(*tile); } } }
更新地图
更新处理空间映射的两个地图函数以使用新的 API。在 map/mod.rs
中:
#![allow(unused)] fn main() { pub fn populate_blocked(&mut self) { crate::spatial::populate_blocked_from_map(self); } pub fn clear_content_index(&mut self) { crate::spatial::clear(); } }
填充空间索引
我们已经有了 map_indexing_system.rs
,它处理空间地图的初始(每帧,所以它不会太不同步)填充。由于我们正在更改存储数据的方式,我们也需要更改系统。索引系统对地图的空间数据执行两个功能:它将瓦片设置为 blocked,并添加索引实体。我们已经创建了它需要的 clear
和 populate_blocked_from_map
函数。将 MapIndexingSystem
的 run
函数的主体替换为:
#![allow(unused)] fn main() { use super::{Map, Position, BlocksTile, spatial}; ... fn run(&mut self, data : Self::SystemData) { let (mut map, position, blockers, entities) = data; spatial::clear(); spatial::populate_blocked_from_map(&*map); for (entity, position) in (&entities, &position).join() { let idx = map.xy_idx(position.x, position.y); // 如果它们阻挡,更新阻挡列表 let _p : Option<&BlocksTile> = blockers.get(entity); if let Some(_p) = _p { spatial::set_blocked(idx); } // 将实体推送到适当的索引槽。它是一个 Copy // 类型,所以我们不需要克隆它(我们想要避免将其移出 ECS!) spatial::index_entity(entity, idx); } } }
在 spatial/mod.rs
中,添加 index_entity
函数:
#![allow(unused)] fn main() { pub fn index_entity(entity: Entity, idx: usize) { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.tile_content[idx].push(entity); } }
地图的构造函数还需要告诉空间系统调整自身大小。将以下内容添加到构造函数:
#![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; crate::spatial::set_size(map_tile_count); ... }
从地图中移除旧的空间数据
是时候破坏一些东西了!这将导致整个源代码库出现问题。从地图中移除 blocked
和 tile_content
。新的 Map
定义如下:
#![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 depth : i32, pub bloodstains : HashSet<usize>, pub view_blocked : HashSet<usize>, pub name : String, pub outdoors : bool, pub light : Vec<rltk::RGB>, } }
您还需要从构造函数中删除这些条目:
#![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; crate::spatial::set_size(map_tile_count); Map{ tiles : vec![TileType::Wall; map_tile_count], width, height, revealed_tiles : vec![false; map_tile_count], visible_tiles : vec![false; 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] } } }
Map
中的 is_exit_valid
函数会崩溃,因为它访问了 blocked
。在 spatial/mod.rs
中,我们将创建一个新函数来提供此功能:
#![allow(unused)] fn main() { pub fn is_blocked(idx: usize) -> bool { SPATIAL_MAP.lock().unwrap().blocked[idx] } }
这允许我们修复地图的 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); !crate::spatial::is_blocked(idx) } }
修复 map/dungeon.rs
map/dungeon.rs
中的 get_map
函数创建了一个新的(未使用的)tile_content
条目。我们不再需要它了,所以我们将删除它。新函数是:
#![allow(unused)] fn main() { pub fn get_map(&self, depth : i32) -> Option<Map> { if self.maps.contains_key(&depth) { let mut result = self.maps[&depth].clone(); Some(result) } else { None } } }
修复 AI
查看 AI 函数,我们经常直接查询 tile_content
。由于我们现在正在尝试使用 API,所以我们不能这样做!最常见的用例是迭代表示瓦片的向量。我们希望避免返回锁,然后确保它被释放所导致的混乱 - 这从 API 中泄漏了太多实现细节。相反,我们将提供一种使用闭包迭代瓦片内容的方法。将以下内容添加到 spatial/mod.rs
:
#![allow(unused)] fn main() { pub fn for_each_tile_content<F>(idx: usize, f: F) where F : Fn(Entity) { let lock = SPATIAL_MAP.lock().unwrap(); for entity in lock.tile_content[idx].iter() { f(*entity); } } }
f
变量是一个泛型参数,使用 where
来指定它必须是一个可变函数,它接受一个 Entity
作为参数。这为我们提供了类似于迭代器上的 for_each
的接口:您可以在瓦片中的每个实体上运行一个函数,依靠闭包捕获来让您在调用它时处理本地状态。
打开 src/ai/adjacent_ai_system.rs
。 evaluate
函数因我们的更改而损坏。使用新的 API,修复它非常简单:
#![allow(unused)] fn main() { fn evaluate(idx : usize, map : &Map, factions : &ReadStorage<Faction>, my_faction : &str, reactions : &mut Vec<(Entity, Reaction)>) { crate::spatial::for_each_tile_content(idx, |other_entity| { if let Some(faction) = factions.get(other_entity) { reactions.push(( other_entity, crate::raws::faction_reaction(my_faction, &faction.name, &crate::raws::RAWS.lock().unwrap()) )); } }); } }
我喜欢这个 API - 它与旧的设置非常相似,但包装得很干净!
Approach API:一些糟糕的代码!
如果您想知道为什么我定义了 API,然后又更改了它:这是为了让您了解香肠是如何制作的。像这样的 API 构建始终是一个迭代过程,看到事物如何演变是件好事。
查看 src/ai/approach_ai_system.rs
。代码非常糟糕:我们在实体移动时手动更改 blocked
。更糟糕的是,我们可能没有做对!它只是取消设置 blocked
;如果由于某种原因瓦片仍然被阻挡,结果将是不正确的。这行不通;我们需要一种 干净 的方法来移动实体,并保留 blocked
状态。
每次移动事物时都为所有内容添加 BlocksTile
检查将会很慢,并且会用更多的引用来污染我们已经很大的 Specs 查找。相反,我们将更改我们存储实体的方式。我们还将更改我们存储 blocked
的方式。在 spatial/mod.rs
中:
#![allow(unused)] fn main() { struct SpatialMap { blocked : Vec<(bool, bool)>, tile_content : Vec<Vec<(Entity, bool)>> } }
blocked
向量现在包含两个 bool 的元组。第一个是 "地图是否阻挡它?",第二个是 "它是否被实体阻挡?"。这要求我们更改一些其他函数。我们还将 删除 set_blocked
函数,并使其从 populate_blocked_from_map
和 index_entity
函数中自动执行。自动是好的:减少了搬起石头砸自己脚的机会!
#![allow(unused)] fn main() { pub fn set_size(map_tile_count: usize) { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.blocked = vec![(false, false); map_tile_count]; lock.tile_content = vec![Vec::new(); map_tile_count]; } pub fn clear() { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.blocked.iter_mut().for_each(|b| { b.0 = false; b.1 = false; }); for content in lock.tile_content.iter_mut() { content.clear(); } } pub fn populate_blocked_from_map(map: &Map) { let mut lock = SPATIAL_MAP.lock().unwrap(); for (i,tile) in map.tiles.iter().enumerate() { lock.blocked[i].0 = !tile_walkable(*tile); } } pub fn index_entity(entity: Entity, idx: usize, blocks_tile: bool) { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.tile_content[idx].push((entity, blocks_tile)); if blocks_tile { lock.blocked[idx].1 = true; } } pub fn is_blocked(idx: usize) -> bool { let lock = SPATIAL_MAP.lock().unwrap(); lock.blocked[idx].0 || lock.blocked[idx].1 } pub fn for_each_tile_content<F>(idx: usize, mut f: F) where F : FnMut(Entity) { let lock = SPATIAL_MAP.lock().unwrap(); for entity in lock.tile_content[idx].iter() { f(entity.0); } } }
这要求我们再次调整 map_indexing_system
。好消息是它变得越来越短:
#![allow(unused)] fn main() { fn run(&mut self, data : Self::SystemData) { let (mut map, position, blockers, entities) = data; spatial::clear(); spatial::populate_blocked_from_map(&*map); for (entity, position) in (&entities, &position).join() { let idx = map.xy_idx(position.x, position.y); spatial::index_entity(entity, idx, blockers.get(entity).is_some()); } } }
完成这些之后,让我们回到 approach_ai_system
。查看代码,我们怀着最好的意图 试图 根据实体的移动来更新 blocked
。我们天真地从源瓦片中清除了 blocked
,并在目标瓦片中设置了它。我们多次使用这种模式,所以让我们创建一个 API 函数(在 spatial/mod.rs
中),它可以真正一致地工作:
#![allow(unused)] fn main() { pub fn move_entity(entity: Entity, moving_from: usize, moving_to: usize) { let mut lock = SPATIAL_MAP.lock().unwrap(); let mut entity_blocks = false; lock.tile_content[moving_from].retain(|(e, blocks) | { if *e == entity { entity_blocks = *blocks; false } else { true } }); lock.tile_content[moving_to].push((entity, entity_blocks)); // 重新计算两个瓦片的 blocks let mut from_blocked = false; let mut to_blocked = false; lock.tile_content[moving_from].iter().for_each(|(_,blocks)| if *blocks { from_blocked = true; } ); lock.tile_content[moving_to].iter().for_each(|(_,blocks)| if *blocks { to_blocked = true; } ); lock.blocked[moving_from].1 = from_blocked; lock.blocked[moving_to].1 = to_blocked; } }
这允许我们用更简洁的代码来修复 ai/approach_ai_system.rs
:
#![allow(unused)] fn main() { if path.success && path.steps.len()>1 { let idx = map.xy_idx(pos.x, pos.y); pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); let new_idx = map.xy_idx(pos.x, pos.y); crate::spatial::move_entity(entity, idx, new_idx); viewshed.dirty = true; } }
文件 ai/chase_ai_system.rs
存在相同的问题。修复方法几乎相同:
#![allow(unused)] fn main() { if path.success && path.steps.len()>1 && path.steps.len()<15 { let idx = map.xy_idx(pos.x, pos.y); pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); let new_idx = map.xy_idx(pos.x, pos.y); viewshed.dirty = true; crate::spatial::move_entity(entity, idx, new_idx); turn_done.push(entity); } else { end_chase.push(entity); } }
修复 ai/default_move_system.rs
这个文件有点复杂。第一个损坏的部分既查询又更新了 blocked 索引。将其更改为:
#![allow(unused)] fn main() { if x > 0 && x < map.width-1 && y > 0 && y < map.height-1 { let dest_idx = map.xy_idx(x, y); if !crate::spatial::is_blocked(dest_idx) { let idx = map.xy_idx(pos.x, pos.y); pos.x = x; pos.y = y; entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); crate::spatial::move_entity(entity, idx, dest_idx); viewshed.dirty = true; } } }
RandomWaypoint
选项的更改非常相似:
#![allow(unused)] fn main() { if path.len()>1 { if !crate::spatial::is_blocked(path[1] as usize) { pos.x = path[1] as i32 % map.width; pos.y = path[1] as i32 / map.width; entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); let new_idx = map.xy_idx(pos.x, pos.y); crate::spatial::move_entity(entity, idx, new_idx); viewshed.dirty = true; path.remove(0); // 移除路径中的第一步 } // 否则我们等待一个回合,看看路径是否畅通 } else { mode.mode = Movement::RandomWaypoint{ path : None }; } }
修复 ai/flee_ai_system.rs
这与默认移动更改非常相似:
#![allow(unused)] fn main() { if let Some(flee_target) = flee_target { if !crate::spatial::is_blocked(flee_target as usize) { crate::spatial::move_entity(entity, my_idx, flee_target); viewshed.dirty = true; pos.x = flee_target as i32 % map.width; pos.y = flee_target as i32 / map.width; entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); } } }
修复 ai/visible_ai_system.rs
AI 的可见性系统使用了一个 evaluate
函数,就像相邻 AI 设置中的那个函数一样。它可以更改为使用闭包:
#![allow(unused)] fn main() { fn evaluate(idx : usize, map : &Map, factions : &ReadStorage<Faction>, my_faction : &str, reactions : &mut Vec<(usize, Reaction, Entity)>) { crate::spatial::for_each_tile_content(idx, |other_entity| { if let Some(faction) = factions.get(other_entity) { reactions.push(( idx, crate::raws::faction_reaction(my_faction, &faction.name, &crate::raws::RAWS.lock().unwrap()), other_entity )); } }); } }
各种 Inventory 系统
在 inventory_system.rs
中,ItemUseSystem
执行空间查找。这是另一个可以用闭包系统替换的:
更改:
#![allow(unused)] fn main() { for mob in map.tile_content[idx].iter() { targets.push(*mob); } }
为:
#![allow(unused)] fn main() { crate::spatial::for_each_tile_content(idx, |mob| targets.push(mob) ); }
再往下,还有另一个。
#![allow(unused)] fn main() { for mob in map.tile_content[idx].iter() { targets.push(*mob); } }
变为:
#![allow(unused)] fn main() { crate::spatial::for_each_tile_content(idx, |mob| targets.push(mob)); }
修复 player.rs
函数 try_move_player
对空间索引系统进行了非常大的查询。它有时也会在计算过程中返回,而我们的 API 目前不支持这一点。我们将在 spatial/mod.rs
文件中添加一个新函数来启用此功能:
#![allow(unused)] fn main() { pub fn for_each_tile_content_with_gamemode<F>(idx: usize, mut f: F) -> RunState where F : FnMut(Entity)->Option<RunState> { let lock = SPATIAL_MAP.lock().unwrap(); for entity in lock.tile_content[idx].iter() { if let Some(rs) = f(entity.0) { return rs; } } RunState::AwaitingInput } }
此函数像另一个函数一样运行,但接受来自闭包的可选游戏模式。如果游戏模式是 Some(x)
,则它返回 x
。如果它在最后没有收到任何模式,则返回 AwaitingInput
。
用新的 API 替换它主要是在于使用新函数,并在闭包内执行索引检查。这是新函数:
#![allow(unused)] fn main() { pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::<Position>(); let players = ecs.read_storage::<Player>(); let mut viewsheds = ecs.write_storage::<Viewshed>(); let entities = ecs.entities(); let combat_stats = ecs.read_storage::<Attributes>(); let map = ecs.fetch::<Map>(); let mut wants_to_melee = ecs.write_storage::<WantsToMelee>(); let mut entity_moved = ecs.write_storage::<EntityMoved>(); let mut doors = ecs.write_storage::<Door>(); let mut blocks_visibility = ecs.write_storage::<BlocksVisibility>(); let mut blocks_movement = ecs.write_storage::<BlocksTile>(); let mut renderables = ecs.write_storage::<Renderable>(); let factions = ecs.read_storage::<Faction>(); let mut result = RunState::AwaitingInput; let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new(); 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 RunState::AwaitingInput; } let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); result = crate::spatial::for_each_tile_content_with_gamemode(destination_idx, |potential_target| { let mut hostile = true; if combat_stats.get(potential_target).is_some() { if let Some(faction) = factions.get(potential_target) { let reaction = crate::raws::faction_reaction( &faction.name, "Player", &crate::raws::RAWS.lock().unwrap() ); if reaction != Reaction::Attack { hostile = false; } } } if !hostile { // 注意,我们想要移动旁观者 swap_entities.push((potential_target, pos.x, pos.y)); // 移动玩家 pos.x = min(map.width-1 , max(0, pos.x + delta_x)); pos.y = min(map.height-1, max(0, pos.y + delta_y)); entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); viewshed.dirty = true; let mut ppos = ecs.write_resource::<Point>(); ppos.x = pos.x; ppos.y = pos.y; return Some(RunState::Ticking); } else { 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 Some(RunState::Ticking); } } let door = doors.get_mut(potential_target); if let Some(door) = door { door.open = true; blocks_visibility.remove(potential_target); blocks_movement.remove(potential_target); let glyph = renderables.get_mut(potential_target).unwrap(); glyph.glyph = rltk::to_cp437('/'); viewshed.dirty = true; return Some(RunState::Ticking); } None }); if !crate::spatial::is_blocked(destination_idx) { let old_idx = map.xy_idx(pos.x, pos.y); pos.x = min(map.width-1 , max(0, pos.x + delta_x)); pos.y = min(map.height-1, max(0, pos.y + delta_y)); let new_idx = map.xy_idx(pos.x, pos.y); entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); crate::spatial::move_entity(entity, old_idx, new_idx); viewshed.dirty = true; let mut ppos = ecs.write_resource::<Point>(); ppos.x = pos.x; ppos.y = pos.y; result = RunState::Ticking; match map.tiles[destination_idx] { TileType::DownStairs => result = RunState::NextLevel, TileType::UpStairs => result = RunState::PreviousLevel, _ => {} } } } for m in swap_entities.iter() { let their_pos = positions.get_mut(m.0); if let Some(their_pos) = their_pos { let old_idx = map.xy_idx(their_pos.x, their_pos.y); their_pos.x = m.1; their_pos.y = m.2; let new_idx = map.xy_idx(their_pos.x, their_pos.y); crate::spatial::move_entity(m.0, old_idx, new_idx); result = RunState::Ticking; } } result } }
注意 TODO
:在我们完成之前,我们将需要查看它。我们正在移动实体 - 而不是更新空间地图。
skip_turn
也需要用新的基于闭包的设置替换 tile_content
的直接迭代:
#![allow(unused)] fn main() { crate::spatial::for_each_tile_content(idx, |entity_id| { let faction = factions.get(entity_id); match faction { None => {} Some(faction) => { let reaction = crate::raws::faction_reaction( &faction.name, "Player", &crate::raws::RAWS.lock().unwrap() ); if reaction == Reaction::Attack { can_heal = false; } } } }); }
修复 Trigger 系统
trigger_system.rs
也需要一些改进。这只是另一个直接的 for
循环替换为新的闭包:
#![allow(unused)] fn main() { crate::spatial::for_each_tile_content(idx, |entity_id| { if entity != entity_id { // 不要费心检查自己是否是陷阱! let maybe_trigger = entry_trigger.get(entity_id); match maybe_trigger { None => {}, Some(_trigger) => { // 我们触发了它 let name = names.get(entity_id); if let Some(name) = name { log.entries.push(format!("{} 触发了!", &name.name)); } hidden.remove(entity_id); // 陷阱不再隐藏 // 如果陷阱是造成伤害的,那就造成伤害 let damage = inflicts_damage.get(entity_id); if let Some(damage) = damage { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); SufferDamage::new_damage(&mut inflict_damage, entity, damage.damage, false); } // 如果它是单次激活,则需要移除它 let sa = single_activation.get(entity_id); if let Some(_sa) = sa { remove_entities.push(entity_id); } } } } }); }
Visibility 系统中更多相同的内容
visibility_system.rs
需要非常相似的修复。 for e in map.tile_content[idx].iter() {
和相关的 body 变为:
#![allow(unused)] fn main() { crate::spatial::for_each_tile_content(idx, |e| { let maybe_hidden = hidden.get(e); if let Some(_maybe_hidden) = maybe_hidden { if rng.roll_dice(1,24)==1 { let name = names.get(e); if let Some(name) = name { log.entries.push(format!("你发现了一个 {}。", &name.name)); } hidden.remove(e); } } }); }
保存和加载
saveload_system.rs
文件也需要一些调整。替换:
#![allow(unused)] fn main() { worldmap.tile_content = vec![Vec::new(); (worldmap.height * worldmap.width) as usize]; }
为:
#![allow(unused)] fn main() { crate::spatial::set_size((worldmap.height * worldmap.width) as usize); }
如果您 cargo build
,它现在可以编译了!这是一个进步。现在 cargo run
运行项目,看看效果如何。游戏以不错的速度运行,并且可以玩。仍然有一些问题 - 我们将依次解决这些问题。
清理死者
我们将从 "死者仍然阻挡瓦片" 的问题开始。出现此问题的原因是实体在调用 delete_the_dead
之前不会消失,并且整个地图会重新索引。这可能不会及时发生,无法帮助移动到目标瓦片。在我们的空间 API 中添加一个新函数(在 spatial/mod.rs
中):
#![allow(unused)] fn main() { pub fn remove_entity(entity: Entity, idx: usize) { let mut lock = SPATIAL_MAP.lock().unwrap(); lock.tile_content[idx].retain(|(e, _)| *e != entity ); let mut from_blocked = false; lock.tile_content[idx].iter().for_each(|(_,blocks)| if *blocks { from_blocked = true; } ); lock.blocked[idx].1 = from_blocked; } }
然后修改 damage_system
以处理移除死亡时的实体:
#![allow(unused)] fn main() { if stats.hit_points.current < 1 && dmg.1 { xp_gain += stats.level * 100; if let Some(pos) = pos { let idx = map.xy_idx(pos.x, pos.y); crate::spatial::remove_entity(entity, idx); } } }
听起来不错 - 但运行它表明我们 仍然 存在问题。一些大量的调试表明,map_indexing_system
在事件之间运行,并恢复了不正确的数据。我们不希望死者出现在我们的索引地图上,所以我们编辑索引系统以进行检查。修复后的索引系统如下所示:我们添加了对死者的检查。
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Map, Position, BlocksTile, Pools, spatial}; pub struct MapIndexingSystem {} impl<'a> System<'a> for MapIndexingSystem { type SystemData = ( ReadExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>, ReadStorage<'a, Pools>, Entities<'a>,); fn run(&mut self, data : Self::SystemData) { let (map, position, blockers, pools, entities) = data; spatial::clear(); spatial::populate_blocked_from_map(&*map); for (entity, position) in (&entities, &position).join() { let mut alive = true; if let Some(pools) = pools.get(entity) { if pools.hit_points.current < 1 { alive = false; } } if alive { let idx = map.xy_idx(position.x, position.y); spatial::index_entity(entity, idx, blockers.get(entity).is_some()); } } } } }
您现在可以移动到最近去世的人占据的空间。
处理实体交换
还记得我们在玩家处理程序中标记的 TODO
吗?当我们想要交换实体位置时。让我们弄清楚这一点。这是一个更新目的地的版本:
#![allow(unused)] fn main() { for m in swap_entities.iter() { let their_pos = positions.get_mut(m.0); if let Some(their_pos) = their_pos { let old_idx = map.xy_idx(their_pos.x, their_pos.y); their_pos.x = m.1; their_pos.y = m.2; let new_idx = map.xy_idx(their_pos.x, their_pos.y); crate::spatial::move_entity(m.0, old_idx, new_idx); result = RunState::Ticking; } } }
总结
它仍然不是绝对完美,但它 好 得多了。我玩了一段时间,在发布模式下它非常流畅。无法进入瓦片的问题已经消失,命中检测正在工作。同样重要的是,我们清理了一些 hacky 代码。
注意:本章处于 alpha 阶段。我仍在将这些修复应用于后续章节,并在完成后更新此章节。
...
本章的源代码可以在 这里 找到
在您的浏览器中使用 web assembly 运行本章的示例 (需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。