重构:通用地图接口
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出精彩的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
到目前为止,我们实际上只有一个地图设计。每次都不同(除非您使用重复的随机种子),这是一个很好的开始 - 但程序化生成的世界留下了更多可能性。在接下来的几个章节中,我们将开始构建几种不同的地图类型。
重构构建器 - 定义接口
到目前为止,我们所有的地图生成代码都位于 map.rs
文件中。对于单一风格来说,这很好,但是如果我们想要有很多风格呢?现在是创建合适的构建器系统的绝佳时机!如果您查看 main.rs
中的地图生成代码,我们会发现一个已定义的接口的雏形:
- 我们调用
Map::new_map_rooms_and_corridors
,它构建了一组房间。 - 我们将其传递给
spawner::spawn_room
以填充每个房间。 - 然后我们将玩家放置在第一个房间中。
为了更好地组织我们的代码,我们将创建一个模块。Rust 允许您创建一个目录,其中包含一个名为 mod.rs
的文件 - 该目录现在就是一个模块。模块通过 mod
和 pub mod
暴露,并提供了一种将代码部分放在一起的方法。mod.rs
文件提供了一个接口 - 也就是说,模块提供的内容以及如何与之交互的列表。模块中的其他文件可以做任何他们想做的事情,安全地与代码的其余部分隔离。
因此,我们将在 src
之外创建一个名为 map_builders
的目录。在该目录中,我们将创建一个名为 mod.rs
的空文件。我们正在尝试定义一个接口,所以我们将从一个骨架开始。在 mod.rs
中:
#![allow(unused)] fn main() { use super::Map; trait MapBuilder { fn build(new_depth: i32) -> Map; } }
trait
的使用是新的!trait 就像其他语言中的接口:您是在说任何其他类型都可以实现该 trait,然后可以将其视为该类型的变量。Rust by Example 有一个关于 trait 的精彩章节,The Rust Book 也是如此。我们声明的是,任何东西都可以声明自己是 MapBuilder
- 这包括承诺他们将提供一个 build
函数,该函数接受一个 ECS World
对象,并返回一个地图。
打开 map.rs
,并添加一个新函数 - 恰如其分地命名为 new
:
#![allow(unused)] fn main() { /// 生成一个空地图,完全由实心墙组成 pub fn new(new_depth : i32) -> Map { Map{ tiles : vec![TileType::Wall; MAPCOUNT], rooms : Vec::new(), width : MAPWIDTH as i32, height: MAPHEIGHT as i32, revealed_tiles : vec![false; MAPCOUNT], visible_tiles : vec![false; MAPCOUNT], blocked : vec![false; MAPCOUNT], tile_content : vec![Vec::new(); MAPCOUNT], depth: new_depth, bloodstains: HashSet::new() } } }
我们需要这个用于其他地图生成器,并且 Map
知道如何返回一个新的作为构造函数 - 而不必封装地图布局的所有逻辑,这是有意义的。我们的想法是,任何 Map
基本上都以相同的方式工作,而与我们决定如何填充它无关。
现在我们将创建一个新文件,也在 map_builders
目录中。我们将它命名为 simple_map.rs
- 它将是我们放置现有地图生成系统的地方。我们也会在这里放置一个骨架:
#![allow(unused)] fn main() { use super::MapBuilder; use super::Map; use specs::prelude::*; pub struct SimpleMapBuilder {} impl MapBuilder for SimpleMapBuilder { fn build(new_depth: i32) -> Map { Map::new(new_depth) } } }
这只是返回一个不可用的、实心的地图。我们稍后会充实细节 - 让我们先让接口工作起来。
现在,回到 map_builders/mod.rs
,我们添加一个公共函数。目前,它只是调用 SimpleMapBuilder
中的构建器:
#![allow(unused)] fn main() { pub fn build_random_map(new_depth: i32) -> Map { SimpleMapBuilder::build(new_depth) } }
最后,我们将告诉 main.rs
实际包含该模块:
#![allow(unused)] fn main() { pub mod map_builders; }
好的,做了相当多的工作,但实际上没有做任何事情 - 但我们获得了一个干净的接口,提供地图创建(通过一个函数),并设置了一个 trait 以要求我们的地图构建器以类似的方式工作。这是一个好的开始。
充实简单地图构建器
现在我们开始将功能从 map.rs
移到我们的 SimpleMapBuilder
中。我们将首先向 map_builders
添加另一个文件 - common.rs
。这将保存以前是地图一部分的函数,现在在构建时常用。
该文件看起来像这样:
#![allow(unused)] fn main() { use super::{Map, Rect, TileType}; use std::cmp::{max, min}; pub fn apply_room_to_map(map : &mut Map, room : &Rect) { for y in room.y1 +1 ..= room.y2 { for x in room.x1 + 1 ..= room.x2 { let idx = map.xy_idx(x, y); map.tiles[idx] = TileType::Floor; } } } pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) { for x in min(x1,x2) ..= max(x1,x2) { let idx = map.xy_idx(x, y); if idx > 0 && idx < map.width as usize * map.height as usize { map.tiles[idx as usize] = TileType::Floor; } } } pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) { for y in min(y1,y2) ..= max(y1,y2) { let idx = map.xy_idx(x, y); if idx > 0 && idx < map.width as usize * map.height as usize { map.tiles[idx as usize] = TileType::Floor; } } } }
这些与 map.rs
中的函数完全相同,但 map
作为可变引用传递(因此您正在处理原始地图,而不是新地图),并且所有 self
的痕迹都消失了。这些是自由函数 - 也就是说,它们是从任何地方可用的函数,不与类型绑定。pub fn
表示它们在模块内是公共的 - 除非我们向模块本身添加 pub use
,否则它们不会传递到主程序。这有助于保持代码的组织性。
现在我们有了这些助手函数,我们可以开始移植地图构建器本身了。在 simple_map.rs
中,我们首先充实 build
函数:
#![allow(unused)] fn main() { impl MapBuilder for SimpleMapBuilder { fn build(new_depth: i32) -> Map { let mut map = Map::new(new_depth); SimpleMapBuilder::rooms_and_corridors(&mut map); map } } }
我们正在调用一个新函数 rooms_and_corridors
。让我们构建它:
#![allow(unused)] fn main() { impl SimpleMapBuilder { fn rooms_and_corridors(map : &mut Map) { const MAX_ROOMS : i32 = 30; const MIN_SIZE : i32 = 6; const MAX_SIZE : i32 = 10; let mut rng = RandomNumberGenerator::new(); for i in 0..MAX_ROOMS { let w = rng.range(MIN_SIZE, MAX_SIZE); let h = rng.range(MIN_SIZE, MAX_SIZE); let x = rng.roll_dice(1, map.width - w - 1) - 1; let y = rng.roll_dice(1, map.height - h - 1) - 1; let new_room = Rect::new(x, y, w, h); let mut ok = true; for other_room in map.rooms.iter() { if new_room.intersect(other_room) { ok = false } } if ok { apply_room_to_map(map, &new_room); if !map.rooms.is_empty() { let (new_x, new_y) = new_room.center(); let (prev_x, prev_y) = map.rooms[map.rooms.len()-1].center(); if rng.range(0,2) == 1 { apply_horizontal_tunnel(map, prev_x, new_x, prev_y); apply_vertical_tunnel(map, prev_y, new_y, new_x); } else { apply_vertical_tunnel(map, prev_y, new_y, prev_x); apply_horizontal_tunnel(map, prev_x, new_x, new_y); } } map.rooms.push(new_room); } } let stairs_position = map.rooms[map.rooms.len()-1].center(); let stairs_idx = map.xy_idx(stairs_position.0, stairs_position.1); map.tiles[stairs_idx] = TileType::DownStairs; } } }
您会注意到,这是作为附加到 SimpleMapBuilder
结构的方法构建的。它不是 trait 的一部分,所以我们不能在那里定义它 - 但我们希望将其与其他构建器分开,其他构建器可能有自己的函数。代码本身应该看起来非常熟悉:它与 map.rs
中的生成器相同,但 map
作为一个变量,而不是在函数内部生成。
这只是生成的第一部分,但这是一个好的开始!现在转到 map.rs
,并删除整个 new_map_rooms_and_corridors
函数。还要删除我们在 common.rs
中复制的那些。map.rs
文件现在看起来更简洁了,没有任何对地图构建策略的引用!当然,您的编译器/IDE 可能会告诉您,我们破坏了一堆东西。没关系 - 这是“重构”的正常部分 - 更改代码以使其更易于使用的过程。
main.rs
中有三行代码现在被编译器标记。
- 我们可以将
*worldmap_resource = Map::new_map_rooms_and_corridors(current_depth + 1);
替换为*worldmap_resource = map_builders::build_random_map(current_depth + 1);
。 *worldmap_resource = Map::new_map_rooms_and_corridors(1);
可以变为*worldmap_resource = map_builders::build_random_map(1);
。let map : Map = Map::new_map_rooms_and_corridors(1);
转换为let map : Map = map_builders::build_random_map(1);
。
如果您现在 cargo run
,您会注意到:游戏完全相同!很好:我们已成功地将功能从 Map
重构到 map_builders
中。
放置玩家
如果您查看 main.rs
,几乎每次我们构建地图时 - 我们都会寻找第一个房间,并用它来放置玩家。我们很可能不想在未来的地图中使用相同的策略,因此我们应该指示在构建地图时玩家要去哪里。让我们扩展 map_builders/mod.rs
中的接口,使其也返回一个位置:
#![allow(unused)] fn main() { trait MapBuilder { fn build(new_depth: i32) -> (Map, Position); } pub fn build_random_map(new_depth: i32) -> (Map, Position) { SimpleMapBuilder::build(new_depth) } }
请注意,我们使用元组一次返回两个值。我们之前已经讨论过这些,但这很好地说明了为什么它们很有用!我们现在需要进入 simple_map
以使 build
函数实际返回正确的数据。simple_map.rs
中 build
的定义现在看起来像这样:
#![allow(unused)] fn main() { fn build(new_depth: i32) -> (Map, Position) { let mut map = Map::new(new_depth); let playerpos = SimpleMapBuilder::rooms_and_corridors(&mut map); (map, playerpos) } }
我们将更新 rooms_and_corridors
的签名:
#![allow(unused)] fn main() { fn rooms_and_corridors(map : &mut Map) -> Position { }
我们将在最后一行添加返回房间 0 的中心:
#![allow(unused)] fn main() { let start_pos = map.rooms[0].center(); Position{ x: start_pos.0, y: start_pos.1 } }
这当然破坏了我们在 main.rs
中更新的代码。我们可以快速处理它!第一个错误可以使用以下代码处理:
#![allow(unused)] fn main() { // 构建新地图并放置玩家 let worldmap; let current_depth; let player_start; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); current_depth = worldmap_resource.depth; let (newmap, start) = map_builders::build_random_map(current_depth + 1); *worldmap_resource = newmap; player_start = start; worldmap = worldmap_resource.clone(); } // 生成坏人 for room in worldmap.rooms.iter().skip(1) { spawner::spawn_room(&mut self.ecs, room, current_depth+1); } // 放置玩家并更新资源 let (player_x, player_y) = (player_start.x, player_start.y); }
请注意,我们如何使用解构从构建器中检索地图和起始位置。然后我们将它们放在适当的位置。由于 Rust 中的赋值是移动操作,因此这非常高效 - 并且编译器可以为我们摆脱临时赋值。
我们在第二个错误(大约在第 369 行)再次执行相同的操作。它几乎是完全相同的代码,因此如果您遇到困难,请随时查看本章的源代码。
最后,最后一个错误可以简单地替换为:
#![allow(unused)] fn main() { let (map, player_start) = map_builders::build_random_map(1); let (player_x, player_y) = (player_start.x, player_start.y); }
好的,让我们 cargo run
那个小家伙!如果一切顺利,那么... 没有任何变化。但是,我们取得了重大进展:我们的地图构建策略现在决定了玩家在关卡中的起点,而不是地图本身。
清理房间生成
在某些地图设计中,我们很可能不会有房间的概念,因此我们也希望将生成移动为地图构建器的函数。我们将在 map_builders/mod.rs
中的接口中添加一个通用生成器:
#![allow(unused)] fn main() { trait MapBuilder { fn build(new_depth: i32) -> (Map, Position); fn spawn(map : &Map, ecs : &mut World, new_depth: i32); } }
足够简单:它需要 ECS(因为我们正在添加实体)和地图。我们还将添加一个公共函数 spawn
,以提供一个外部接口来布置怪物:
#![allow(unused)] fn main() { pub fn spawn(map : &mut Map, ecs : &mut World, new_depth: i32) { SimpleMapBuilder::spawn(map, ecs, new_depth); } }
现在我们打开 simple_map.rs
并实际实现 spawn
。幸运的是,这非常简单:
#![allow(unused)] fn main() { fn spawn(map : &mut Map, ecs : &mut World) { for room in map.rooms.iter().skip(1) { spawner::spawn_room(ecs, room, 1); } } }
现在,我们可以进入 main.rs
并找到每次我们循环调用 spawn_room
并将其替换为对 map_builders::spawn
的调用。
再一次,cargo run
应该给您与我们在 22 章中看到的相同的游戏!
维护构建器状态
如果您仔细查看我们目前的内容,会发现一个问题:构建器无法知道第二次调用构建器(生成事物)应该使用什么。这是因为我们的函数是无状态的 - 我们实际上并没有创建一个构建器并给它一种记住任何东西的方法。由于我们希望支持各种各样的构建器,因此我们应该纠正这一点。
这引入了一个新的 Rust 概念:动态分发。The Rust Book 如果您熟悉这个概念,那么其中有一个关于此的很好的章节。如果您以前使用过面向对象编程语言,那么您也会遇到过这种情况。基本思想是您有一个“基本对象”,它指定一个接口 - 并且多个对象实现来自该接口的函数。然后,您可以在运行时(程序运行时,而不是编译时)将任何实现该接口的对象放入由接口键入的变量中 - 并且当您调用接口中的方法时,实现会从实际类型运行。这很好,因为您的底层程序不必了解实际的实现 - 只需了解如何与接口对话。这有助于保持程序的整洁。
动态分发确实会带来成本,这就是实体组件系统(以及一般的 Rust)倾向于不将其用于性能关键代码的原因。实际上有两个成本:
- 由于您事先不知道对象的类型,因此必须通过指针分配它。Rust 通过提供
Box
系统(稍后会详细介绍)使其变得容易,但有一个成本:代码不是简单地跳转到预定义的内存片段(您的 CPU/内存通常可以提前轻松地弄清楚并确保缓存已准备就绪),而是必须跟随指针 - 然后运行在指针末尾找到的内容。这就是为什么一些 C++ 程序员将->
(解引用运算符)称为“缓存未命中运算符”。仅仅通过装箱,您的代码就会稍微变慢。 - 由于多种类型可以实现方法,因此计算机需要知道要运行哪一种。它通过
vtable
来实现这一点 - 也就是说,方法实现的“虚拟表”。因此,每次调用都必须检查表,找出要运行的方法,然后从那里运行。那是另一个缓存未命中,并且 CPU 需要更多时间来弄清楚该做什么。
在这种情况下,我们只是在生成地图 - 并且很少调用构建器。这使得速度减慢是可以接受的,因为它真的很小并且不经常运行。如果可以避免,您将不希望在主循环中执行此操作!
所以 - 实现。我们将首先将我们的 trait 更改为 public,并让方法接受 &mut self
- 这意味着“此方法是 trait 的成员,并且应该在调用时接收对 self
- 附加对象的访问权限”。代码看起来像这样:
#![allow(unused)] fn main() { pub trait MapBuilder { fn build_map(&mut self, new_depth: i32) -> (Map, Position); fn spawn_entities(&mut self, map : &Map, ecs : &mut World, new_depth: i32); } }
请注意,我还花时间使名称更具描述性!现在我们用一个工厂函数替换我们的自由函数调用:它创建一个 MapBuilder
并返回它。在有更多地图实现之前,这个名字有点谎言 - 它声称是随机的,但是当只有一个选择时,不难猜测它会选择哪一个(只需询问苏联选举系统!):
#![allow(unused)] fn main() { pub fn random_builder() -> Box<dyn MapBuilder> { // 请注意,在我们有第二种地图类型之前,这甚至不是稍微随机的 Box::new(SimpleMapBuilder{}) } }
请注意,它不返回 MapBuilder
- 而是返回 Box<dyn MapBuilder>
!这相当复杂(在 Rust 的早期版本中,dyn
是可选的)。Box
是一种包装在指针中的类型,其大小在编译时可能未知。它与 C++ MapBuilder *
相同 - 它指向 MapBuilder
而不是实际是一个。dyn
是一个标志,表示“这应该使用动态分发”;代码在没有它的情况下也能工作(它将被推断出来),但标记您正在这里做一些复杂/昂贵的事情是一种好的做法。
该函数仅返回 Box::new(SimpleMapBuilder{})
。这实际上是两个调用,现在:我们用 Box::new(...)
创建一个 box,并将一个空的 SimpleMapBuilder
放入 box 中。
在 main.rs
中,我们再次必须更改对地图构建器的所有三个调用。我们现在需要使用以下模式:
- 从工厂获取 boxed
MapBuilder
对象。 - 将
build_map
作为方法调用 - 也就是说,附加到对象的函数。 - 也将
spawn_entities
作为方法调用。
来自 goto_next_level
的实现现在读取如下:
#![allow(unused)] fn main() { // 构建新地图并放置玩家 let mut builder = map_builders::random_builder(current_depth + 1); let worldmap; let current_depth; let player_start; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); current_depth = worldmap_resource.depth; let (newmap, start) = builder.build_map(current_depth + 1); *worldmap_resource = newmap; player_start = start; worldmap = worldmap_resource.clone(); } // 生成坏人 builder.spawn_entities(&worldmap, &mut self.ecs, current_depth+1); }
它并没有什么不同,但现在我们保留了构建器对象 - 因此对构建器的后续调用将应用于相同的实现(有时称为“具体对象” - 实际物理存在的对象)。
如果我们添加 5 个以上的地图构建器,main.rs
中的代码将不会在意!我们可以将它们添加到工厂中,程序的其余部分愉快地不知道地图构建器的工作原理。这是一个很好的例子,说明动态分发如何有用:您有一个明确定义的接口,程序的其余部分不需要理解内部工作原理。
向 SimpleMapBuilder 添加构造函数
我们目前正在将 SimpleMapBuilder 作为空对象创建。如果它需要跟踪一些数据怎么办?以防我们需要它,让我们向其添加一个简单的构造函数,并使用它来代替空白对象。在 simple_map.rs
中,修改 struct
实现如下:
#![allow(unused)] fn main() { impl SimpleMapBuilder { pub fn new(new_depth : i32) -> SimpleMapBuilder { SimpleMapBuilder{} } ... }
现在这只是返回一个空对象。在 mod.rs
中,更改 random_map_builder
函数以使用它:
#![allow(unused)] fn main() { pub fn random_builder(new_depth : i32) -> Box<dyn MapBuilder> { // 请注意,在我们有第二种地图类型之前,这甚至不是稍微随机的 Box::new(SimpleMapBuilder::new(new_depth)) } }
这并没有给我们带来任何好处,但更简洁一些 - 当您编写更多地图时,它们可能会在其构造函数中做一些事情!
清理 trait - 简单、明显的步骤和单一返回类型
既然我们已经走了这么远,让我们扩展 trait 以在一个函数中获取玩家的位置,在另一个函数中获取地图,并分别构建/生成。使用小函数往往使代码更易于阅读,这本身就是一个有价值的目标。在 mod.rs
中,我们按如下方式更改接口:
#![allow(unused)] fn main() { pub trait MapBuilder { fn build_map(&mut self); fn spawn_entities(&mut self, ecs : &mut World); fn get_map(&mut self) -> Map; fn get_starting_position(&mut self) -> Position; } }
这里有几件事需要注意:
build_map
不再返回任何内容。我们将其用作构建地图状态的函数。spawn_entities
不再要求 Map 参数。由于所有地图构建器必须实现地图才能有意义,因此我们将假定地图构建器有一个地图。get_map
返回一个地图。同样,我们假设构建器实现保留一个。get_starting_position
也假设构建器会保留一个。
显然,我们的 SimpleMapBuilder
现在需要修改为以这种方式工作。我们将首先修改 struct
以包含所需的变量。这是地图构建器的状态 - 并且由于我们正在进行动态面向对象的代码,因此状态仍然附加到对象。这是来自 simple_map.rs
的代码:
#![allow(unused)] fn main() { pub struct SimpleMapBuilder { map : Map, starting_position : Position, depth: i32 } }
接下来,我们将实现 getter 函数。这些非常简单:它们只是返回结构状态中的变量:
#![allow(unused)] fn main() { impl MapBuilder for SimpleMapBuilder { fn get_map(&self) -> Map { self.map.clone() } fn get_starting_position(&self) -> Position { self.starting_position.clone() } ... }
我们还将更新构造函数以创建状态:
#![allow(unused)] fn main() { pub fn new(new_depth : i32) -> SimpleMapBuilder { SimpleMapBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth } } }
这也简化了 build_map
和 spawn_entities
:
#![allow(unused)] fn main() { fn build_map(&mut self) { SimpleMapBuilder::rooms_and_corridors(); } fn spawn_entities(&mut self, ecs : &mut World) { for room in self.map.rooms.iter().skip(1) { spawner::spawn_room(ecs, room, self.depth); } } }
最后,我们需要修改 rooms_and_corridors
以使用此接口:
#![allow(unused)] fn main() { fn rooms_and_corridors(&mut self) { const MAX_ROOMS : i32 = 30; const MIN_SIZE : i32 = 6; const MAX_SIZE : i32 = 10; let mut rng = RandomNumberGenerator::new(); for i in 0..MAX_ROOMS { let w = rng.range(MIN_SIZE, MAX_SIZE); let h = rng.range(MIN_SIZE, MAX_SIZE); let x = rng.roll_dice(1, self.map.width - w - 1) - 1; let y = rng.roll_dice(1, self.map.height - h - 1) - 1; let new_room = Rect::new(x, y, w, h); let mut ok = true; for other_room in self.map.rooms.iter() { if new_room.intersect(other_room) { ok = false } } if ok { apply_room_to_map(&mut self.map, &new_room); if !self.map.rooms.is_empty() { let (new_x, new_y) = new_room.center(); let (prev_x, prev_y) = self.map.rooms[self.map.rooms.len()-1].center(); if rng.range(0,2) == 1 { apply_horizontal_tunnel(&mut self.map, prev_x, new_x, prev_y); apply_vertical_tunnel(&mut self.map, prev_y, new_y, new_x); } else { apply_vertical_tunnel(&mut self.map, prev_y, new_y, prev_x); apply_horizontal_tunnel(&mut self.map, prev_x, new_x, new_y); } } self.map.rooms.push(new_room); } } let stairs_position = self.map.rooms[self.map.rooms.len()-1].center(); let stairs_idx = self.map.xy_idx(stairs_position.0, stairs_position.1); self.map.tiles[stairs_idx] = TileType::DownStairs; let start_pos = self.map.rooms[0].center(); self.starting_position = Position{ x: start_pos.0, y: start_pos.1 }; } }
这与我们之前的非常相似,但现在使用 self.map
来引用其自己的地图副本,并将玩家位置存储在 self.starting_position
中。
对 main.rs
中新代码的调用再次更改。来自 goto_next_level
的调用现在看起来像这样:
#![allow(unused)] fn main() { let mut builder; let worldmap; let current_depth; let player_start; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); current_depth = worldmap_resource.depth; builder = map_builders::random_builder(current_depth + 1); builder.build_map(); *worldmap_resource = builder.get_map(); player_start = builder.get_starting_position(); worldmap = worldmap_resource.clone(); } // 生成坏人 builder.spawn_entities(&mut self.ecs); }
我们基本上为其他调用重复这些更改(请参阅源代码)。我们现在有一个非常舒适的地图构建器接口:它暴露了足够多的内容使其易于使用,而没有暴露其用于实际构建地图的魔法的细节!
如果您现在 cargo run
该项目:再一次,没有任何可见的变化 - 它仍然像以前一样工作。当您进行重构时,这是一件好事!
那么为什么地图仍然有房间?
房间实际上在游戏中并没有做太多事情:它们是我们构建地图的方式的产物。以后的地图构建器很可能实际上并不关心房间,至少不是在“这是一个矩形,我们称之为房间”的意义上。让我们尝试将这种抽象移出地图,也移出生成器。
第一步,在 map.rs
中,我们完全删除 rooms
结构:
#![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 blocked : Vec<bool>, pub depth : i32, pub bloodstains : HashSet<usize>, #[serde(skip_serializing)] #[serde(skip_deserializing)] pub tile_content : Vec<Vec<Entity>> } }
我们也从 new
函数中删除它。看看您的 IDE,您会注意到您只破坏了 simple_map.rs
中的代码!我们没有在其他任何地方使用 rooms
- 这很明显地表明它们不属于我们在整个主程序中传递的地图中。
我们可以通过将 rooms
放入构建器而不是地图来修复 simple_map
。我们将其放入结构中:
#![allow(unused)] fn main() { pub struct SimpleMapBuilder { map : Map, starting_position : Position, depth: i32, rooms: Vec<Rect> } }
这需要我们修复构造函数:
#![allow(unused)] fn main() { pub fn new(new_depth : i32) -> SimpleMapBuilder { SimpleMapBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, rooms: Vec::new() } } }
生成函数变为:
#![allow(unused)] fn main() { fn spawn_entities(&mut self, ecs : &mut World) { for room in self.rooms.iter().skip(1) { spawner::spawn_room(ecs, room, self.depth); } } }
我们在 rooms_and_corridors
中将每个 map.rooms
实例替换为 self.rooms
:
#![allow(unused)] fn main() { fn rooms_and_corridors(&mut self) { const MAX_ROOMS : i32 = 30; const MIN_SIZE : i32 = 6; const MAX_SIZE : i32 = 10; let mut rng = RandomNumberGenerator::new(); for i in 0..MAX_ROOMS { let w = rng.range(MIN_SIZE, MAX_SIZE); let h = rng.range(MIN_SIZE, MAX_SIZE); let x = rng.roll_dice(1, self.map.width - w - 1) - 1; let y = rng.roll_dice(1, self.map.height - h - 1) - 1; let new_room = Rect::new(x, y, w, h); let mut ok = true; for other_room in self.rooms.iter() { if new_room.intersect(other_room) { ok = false } } if ok { apply_room_to_map(&mut self.map, &new_room); if !self.rooms.is_empty() { let (new_x, new_y) = new_room.center(); let (prev_x, prev_y) = self.rooms[self.rooms.len()-1].center(); if rng.range(0,2) == 1 { apply_horizontal_tunnel(&mut self.map, prev_x, new_x, prev_y); apply_vertical_tunnel(&mut self.map, prev_y, new_y, new_x); } else { apply_vertical_tunnel(&mut self.map, prev_y, new_y, prev_x); apply_horizontal_tunnel(&mut self.map, prev_x, new_x, new_y); } } self.rooms.push(new_room); } } let stairs_position = self.rooms[self.rooms.len()-1].center(); let stairs_idx = self.map.xy_idx(stairs_position.0, stairs_position.1); self.map.tiles[stairs_idx] = TileType::DownStairs; let start_pos = self.rooms[0].center(); self.starting_position = Position{ x: start_pos.0, y: start_pos.1 }; } }
再一次,cargo run
该项目:应该没有任何变化。
总结
这是一个有趣的章节,因为目标是以与之前完全相同的代码结束 - 但地图构建器已清理到其自己的模块中,与代码的其余部分完全隔离。这为我们提供了一个很好的起点,可以开始构建新的地图构建器,而无需更改游戏本身。
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)。没有太多意义,因为重构旨在不更改可见结果!
版权所有 (C) 2019, Herbert Wolverson。