17、游戏地图构建与主题设计

游戏地图构建与主题设计

在游戏开发中,地图的构建和主题设计是至关重要的环节。本文将详细介绍如何使用 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() 函数增加随机选择 |
| 动态主题切换 | 添加主题切换函数,在合适的时机调用该函数 |
| 自定义预制地图生成规则 | 设计复杂的生成规则,实现预制地图的拼接和组合 |

通过这些扩展和创新,我们可以让游戏地图更加丰富多样,提升游戏的可玩性和吸引力。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值