游戏地图构建与主题设计
在游戏开发中,地图的构建和主题设计是至关重要的环节。本文将详细介绍如何使用 Rust 语言进行游戏地图的构建,包括随机地图生成、预制地图片段的添加,以及地图主题的设计与应用。
1. 地图构建器的改进
首先,我们需要对地图构建器进行改进。打开
map_builder/mod.rs
文件,将
new()
函数替换为以下代码:
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut architect : Box<dyn MapArchitect> = match rng.range(0, 3) {
0 => Box::new(DrunkardsWalkArchitect{}),
1 => Box::new(RoomsArchitect{}),
_ => Box::new(CellularAutomataArchitect{})
};
let mut mb = architect.new(rng);
mb
}
这里引入了两个新的概念:
Box
和
dyn
。当只使用一种类型的地图构建器时,Rust 能明确知道该构建器的类型。但当存储可能实现
MapArchitect
特性的任意类型时,Rust 难以确定其类型和大小。因此,需要将其放入
Box
中。
Box
是一个智能指针,它创建所需的构建器类型并持有其指针。当
Box
被销毁时,会自动删除包含的对象。而
dyn
是 “dynamic dispatch” 的缩写,用于标注类型,Rust 允许将实现指定特性的任何类型放入
Box
中,并像使用普通变量一样调用其特性函数。
2. 预制地图片段
随机生成的地下城很有趣,但有时我们需要特定的地图部分。许多游戏,如《暗黑破坏神》和《地牢爬行石汤》,会在程序生成的内容中穿插预制的地图片段,称为 “vaults”。
2.1 创建预制地图
创建一个新文件
map_builder/prefab.rs
,并定义第一个预制地图片段:
use crate::prelude::*;
const FORTRESS : (&str, i32, i32) = ("
------------
---######---
---#----#---
---#-M--#---
-###----###-
--M------M--
-###----###-
---#----#---
---#----#---
---######---
------------
", 12, 11);
这里的两个变量表示地图片段的大小,
FORTRESS
字符串用字符表示所需的地图,
-
表示开放空间,
#
表示墙壁,
M
表示怪物生成位置。
2.2 放置预制地图
在
map_builder/prefab.rs
中创建一个新函数
apply_prefab
来放置预制地图:
pub fn apply_prefab(mb: &mut MapBuilder, rng: &mut RandomNumberGenerator) {
let mut placement = None;
let dijkstra_map = DijkstraMap::new(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&vec![mb.map.point2d_to_index(mb.player_start)],
&mb.map,
1024.0
);
let mut attempts = 0;
while placement.is_none() && attempts < 10 {
let dimensions = Rect::with_size(
rng.range(0, SCREEN_WIDTH - FORTRESS.1),
rng.range(0, SCREEN_HEIGHT - FORTRESS.2),
FORTRESS.1,
FORTRESS.2
);
let mut can_place = false;
dimensions.for_each(|pt| {
let idx = mb.map.point2d_to_index(pt);
let distance = dijkstra_map.map[idx];
if distance < 2000.0 && distance > 20.0 && mb.amulet_start != pt {
can_place = true;
}
});
if can_place {
placement = Some(Point::new(dimensions.x1, dimensions.y1));
let points = dimensions.point_set();
mb.monster_spawns.retain(|pt| !points.contains(pt) );
}
attempts += 1;
}
if let Some(placement) = placement {
let string_vec : Vec<char> = FORTRESS.0
.chars().filter(|a| *a != '\r' && *a !='\n')
.collect();
let mut i = 0;
for ty in placement.y .. placement.y + FORTRESS.2 {
for tx in placement.x .. placement.x + FORTRESS.1 {
let idx = map_idx(tx, ty);
let c = string_vec[i];
match c {
'M' => {
mb.map.tiles[idx] = TileType::Floor;
mb.monster_spawns.push(Point::new(tx, ty));
}
'-' => mb.map.tiles[idx] = TileType::Floor,
'#' => mb.map.tiles[idx] = TileType::Wall,
_ => println!("No idea what to do with [{}]", c)
}
i += 1;
}
}
}
}
具体步骤如下:
1. 初始化
placement
为
None
,并构建 Dijkstra 地图。
2. 尝试最多 10 次放置预制地图。每次随机选择一个位置创建矩形区域,检查该区域内的每个瓷砖是否满足条件(Dijkstra 距离小于 2000 且大于 20,不包含护身符位置)。
3. 如果可以放置,记录位置并移除该区域内的怪物生成点。
4. 最后,将预制地图添加到游戏地图中。
2.3 更新地图构建器
在
map_builder/mod.rs
的
new()
函数中添加对
apply_prefab
的调用:
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut architect : Box<dyn MapArchitect> = match rng.range(0, 3) {
0 => Box::new(DrunkardsWalkArchitect{}),
1 => Box::new(RoomsArchitect{}),
_ => Box::new(CellularAutomataArchitect{})
};
let mut mb = architect.new(rng);
apply_prefab(&mut mb, rng);
mb
}
3. 地图主题设计
为了让游戏更具多样性和视觉吸引力,我们可以设计不同的地图主题。
3.1 定义地图主题特性
在
map_builder/mod.rs
中添加新的特性定义:
pub trait MapTheme : Sync + Send {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType;
}
Sync
和
Send
是 Rust 用于确保并发安全的特性。如果一个对象实现了
Sync
,可以从多个线程安全地访问它;如果实现了
Send
,可以在多个线程之间安全地共享它。
3.2 实现地下城主题
创建一个新文件
map_builder/themes.rs
,实现地下城主题:
use crate::prelude::*;
pub struct DungeonTheme {}
impl DungeonTheme {
pub fn new() -> Box<dyn MapTheme> {
Box::new(Self{})
}
}
impl MapTheme for DungeonTheme {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
match tile_type {
TileType::Floor => to_cp437('.'),
TileType::Wall => to_cp437('#')
}
}
}
3.3 实现森林主题
在
themes.rs
文件底部添加森林主题的实现:
pub struct ForestTheme {}
impl MapTheme for ForestTheme {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
match tile_type {
TileType::Floor => to_cp437(';'),
TileType::Wall => to_cp437('"')
}
}
}
impl ForestTheme {
pub fn new() -> Box<dyn MapTheme> {
Box::new(Self{})
}
}
3.4 更新地图构建器
在
map_builder/mod.rs
中为
MapBuilder
添加
theme
字段:
const NUM_ROOMS: usize = 20;
pub struct MapBuilder {
pub map : Map,
pub rooms : Vec<Rect>,
pub monster_spawns : Vec<Point>,
pub player_start : Point,
pub amulet_start : Point,
pub theme : Box<dyn MapTheme>
}
并在每个地图构建器的初始化中添加
theme
字段:
theme: super::themes::DungeonTheme::new()
3.5 随机选择主题
修改
map_builder/mod.rs
的
new()
函数,随机选择主题:
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut architect : Box<dyn MapArchitect> = match rng.range(0, 3) {
0 => Box::new(DrunkardsWalkArchitect{}),
1 => Box::new(RoomsArchitect{}),
_ => Box::new(CellularAutomataArchitect{})
};
let mut mb = architect.new(rng);
apply_prefab(&mut mb, rng);
mb.theme = match rng.range(0, 2) {
0 => DungeonTheme::new(),
_ => ForestTheme::new()
};
mb
}
3.6 使用选定的主题
在
main.rs
中添加主题作为 Legion 资源:
impl State {
fn new() -> Self {
let mut ecs = World::default();
let mut resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut ecs, map_builder.player_start);
spawn_amulet_of_yala(&mut ecs, map_builder.amulet_start);
map_builder.monster_spawns
.iter()
.for_each(|pos| spawn_monster(&mut ecs, &mut rng, *pos));
resources.insert(map_builder.map);
resources.insert(Camera::new(map_builder.player_start));
resources.insert(TurnState::AwaitingInput);
resources.insert(map_builder.theme);
// ...
}
fn reset_game_state(&mut self) {
self.ecs = World::default();
self.resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut self.ecs, map_builder.player_start);
spawn_amulet_of_yala(&mut self.ecs, map_builder.amulet_start);
map_builder.monster_spawns
.iter()
.for_each(|pos| spawn_monster(&mut self.ecs, &mut rng, *pos));
self.resources.insert(map_builder.map);
self.resources.insert(Camera::new(map_builder.player_start));
self.resources.insert(TurnState::AwaitingInput);
self.resources.insert(map_builder.theme);
}
}
3.7 渲染地图时使用主题
在
systems/map_render.rs
中添加对主题资源的访问:
#[system]
#[read_component(FieldOfView)]
#[read_component(Player)]
pub fn map_render(
#[resource] map: &Map,
#[resource] camera: &Camera,
#[resource] theme: &Box<dyn MapTheme>,
ecs: &SubWorld
) {
// ...
}
通过以上步骤,我们可以实现多样化的地图构建和主题渲染,为游戏增添更多乐趣和视觉效果。
以下是地图构建和主题应用的流程图:
graph TD;
A[开始] --> B[地图构建器选择]
B --> C{随机选择建筑师}
C -- 0 --> D[DrunkardsWalkArchitect]
C -- 1 --> E[RoomsArchitect]
C -- 其他 --> F[CellularAutomataArchitect]
D --> G[创建地图]
E --> G
F --> G
G --> H[应用预制地图]
H --> I{随机选择主题}
I -- 0 --> J[DungeonTheme]
I -- 其他 --> K[ForestTheme]
J --> L[设置主题]
K --> L
L --> M[渲染地图]
M --> N[结束]
表格总结不同主题下的瓷砖渲染字符:
| 主题 | 地板 | 墙壁 |
| ---- | ---- | ---- |
| 地下城主题 |
.
|
#
|
| 森林主题 |
;
|
"
|
通过这些技术,我们可以创建出更加丰富和多样化的游戏地图,提升游戏的可玩性和视觉体验。
游戏地图构建与主题设计(续)
4. 代码优化与注意事项
在实现上述地图构建和主题设计的过程中,有一些代码优化和注意事项值得我们关注。
4.1 避免使用空格表示地板
在设计预制地图时,虽然使用空格可以让地图字符串看起来更美观,但这会导致难以区分哪些瓷砖是有意为空,哪些是空白字符。这可能会无意中破坏预制地图片段,因此建议使用
-
等字符来表示开放空间。
4.2 处理未识别字符
在
apply_prefab
函数中,当遇到未识别的字符时,代码会打印警告信息。这有助于我们发现地图定义中的拼写错误。在实际开发中,我们可以根据需求进一步扩展处理逻辑,例如抛出错误或忽略该字符。
4.3 资源管理与并发安全
由于使用了
Sync
和
Send
特性,确保了地图主题对象可以在多线程环境中安全地共享和访问。在使用 Legion 资源时,这一点尤为重要,因为资源可能会被多个系统同时访问。同时,我们要注意资源的初始化和释放,避免出现内存泄漏或数据不一致的问题。
5. 扩展与创新
上述的地图构建和主题设计方案为游戏开发提供了一个基础框架,我们可以在此基础上进行扩展和创新。
5.1 添加更多主题
可以根据游戏的风格和需求,添加更多的地图主题,如沙漠主题、雪地主题、废墟主题等。每个主题可以有不同的瓷砖渲染字符和颜色,从而为玩家带来更加丰富的视觉体验。
以下是一个简单的沙漠主题示例:
pub struct DesertTheme {}
impl MapTheme for DesertTheme {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
match tile_type {
TileType::Floor => to_cp437(','),
TileType::Wall => to_cp437('@')
}
}
}
impl DesertTheme {
pub fn new() -> Box<dyn MapTheme> {
Box::new(Self{})
}
}
同时,需要修改
map_builder/mod.rs
中的
new()
函数,增加对新主题的随机选择:
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut architect : Box<dyn MapArchitect> = match rng.range(0, 3) {
0 => Box::new(DrunkardsWalkArchitect{}),
1 => Box::new(RoomsArchitect{}),
_ => Box::new(CellularAutomataArchitect{})
};
let mut mb = architect.new(rng);
apply_prefab(&mut mb, rng);
mb.theme = match rng.range(0, 3) {
0 => DungeonTheme::new(),
1 => ForestTheme::new(),
_ => DesertTheme::new()
};
mb
}
5.2 动态主题切换
除了在游戏开始时随机选择主题,我们还可以实现动态主题切换。例如,当玩家达到一定等级或完成特定任务时,自动切换地图主题,为游戏增加更多的惊喜和变化。
以下是一个简单的动态主题切换示例:
// 在 main.rs 中添加一个函数来处理主题切换
fn switch_theme(resources: &mut Resources, rng: &mut RandomNumberGenerator) {
let new_theme = match rng.range(0, 3) {
0 => DungeonTheme::new(),
1 => ForestTheme::new(),
_ => DesertTheme::new()
};
resources.insert(new_theme);
}
// 在合适的地方调用 switch_theme 函数
// 例如,当玩家达到一定等级时
if player_level >= 10 {
switch_theme(&mut resources, &mut rng);
}
5.3 自定义预制地图生成规则
可以设计更加复杂的预制地图生成规则,例如根据地图的难度、玩家的进度或随机因素来选择不同的预制地图片段。同时,可以实现预制地图的拼接和组合,创建出更大、更复杂的地图。
6. 总结
通过本文介绍的方法,我们可以使用 Rust 语言实现游戏地图的构建、预制地图片段的添加和地图主题的设计。通过随机选择地图建筑师和主题,我们可以创建出多样化的游戏地图,为玩家带来不同的游戏体验。同时,通过合理使用
Box
和
dyn
等 Rust 特性,确保了代码的灵活性和可维护性。
在实际开发中,我们可以根据游戏的需求和特点,进一步扩展和优化这些功能,例如添加更多的主题、实现动态主题切换、自定义预制地图生成规则等。通过不断的创新和改进,我们可以打造出更加丰富多彩、富有挑战性的游戏世界。
以下是扩展功能的流程图:
graph TD;
A[开始] --> B[地图构建与主题选择]
B --> C{是否满足主题切换条件}
C -- 是 --> D[切换主题]
C -- 否 --> E[继续游戏]
D --> E
E --> F{是否需要新的预制地图}
F -- 是 --> G[生成新的预制地图]
F -- 否 --> H[继续游戏]
G --> H
H --> I{游戏是否结束}
I -- 否 --> B
I -- 是 --> J[结束]
表格总结不同扩展功能的实现要点:
| 扩展功能 | 实现要点 |
| ---- | ---- |
| 添加更多主题 | 定义新的主题结构体,实现
MapTheme
特性,修改
new()
函数增加随机选择 |
| 动态主题切换 | 添加主题切换函数,在合适的时机调用该函数 |
| 自定义预制地图生成规则 | 设计复杂的生成规则,实现预制地图的拼接和组合 |
通过这些扩展和创新,我们可以让游戏地图更加丰富多样,提升游戏的可玩性和吸引力。
超级会员免费看
7237

被折叠的 条评论
为什么被折叠?



