关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon 。
您可能已经注意到本章的文件名是 "57A"。在第 57 章的 AI 更改之后,空间索引系统出现了一些问题。与其在一个已经过长的章节中加入一个本身就很不错的主题,我决定最好插入一个章节。在本章中,我们将修改 map_indexing_system
和相关数据。我们有几个目标:
实体存储的位置和 "blocked" 系统应该易于在回合中更新。
我们希望消除实体共享空间的情况。
我们希望修复在实体被击杀后无法进入瓦片的问题。
我们希望保持良好的性能。
这是一个相当高的标准!
与其分散地图的 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 的线程计划中移除。这使得程序作为一个整体更容易使用线程调度器。
当地图更改时,我们需要一种方法来调整空间地图的大小。在 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);
}
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
中的 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 函数,我们经常直接查询 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 - 它与旧的设置非常相似,但包装得很干净!
如果您想知道为什么我定义了 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));
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);
}
}
这个文件有点复杂。第一个损坏的部分既查询又更新了 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 };
}
}
这与默认移动更改非常相似:
#![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 的可见性系统使用了一个 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_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));
}
函数 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_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_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 阶段。我仍在将这些修复应用于后续章节,并在完成后更新此章节。
...
本章的源代码可以在 这里 找到
版权所有 (C) 2019, Herbert Wolverson。