空间地图


关于本教程

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

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

Hands-On Rust


您可能已经注意到本章的文件名是 "57A"。在第 57 章的 AI 更改之后,空间索引系统出现了一些问题。与其在一个已经过长的章节中加入一个本身就很不错的主题,我决定最好插入一个章节。在本章中,我们将修改 map_indexing_system 和相关数据。我们有几个目标:

  • 实体存储的位置和 "blocked" 系统应该易于在回合中更新。
  • 我们希望消除实体共享空间的情况。
  • 我们希望修复在实体被击杀后无法进入瓦片的问题。
  • 我们希望保持良好的性能。

这是一个相当高的标准!

构建空间索引 API

与其分散地图的 tile_contentblocked 列表、定期更新的系统以及对这些数据结构的调用,不如将其移动到一个统一的 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,并添加索引实体。我们已经创建了它需要的 clearpopulate_blocked_from_map 函数。将 MapIndexingSystemrun 函数的主体替换为:

#![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);
    ...
}

从地图中移除旧的空间数据

是时候破坏一些东西了!这将导致整个源代码库出现问题。从地图中移除 blockedtile_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.rsevaluate 函数因我们的更改而损坏。使用新的 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_mapindex_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。