19、打造深度地下城:游戏开发进阶指南

打造深度地下城:游戏开发进阶指南

1. 移除不必要的回血代码

在开发游戏时,有些代码可能随着游戏功能的完善而变得不再适用。比如在 systems/player_input.rs 文件中,有一段代码用于在玩家未行动时恢复一定的生命值:

if !did_something {
    if let Ok(mut health) = ecs
      .entry_mut(player_entity)
      .unwrap()
      .get_component_mut::<Health>()
    {
        health.current = i32::min(health.max, health.current + 1);
    }
}

这段代码原本是作为玩家等待策略的奖励机制,但现在玩家可以使用药水来恢复生命值,这种跳过回合恢复生命值的机制就显得过于慷慨了,因此需要将其从源文件中删除。

2. 增加游戏关卡的必要性

对于游戏开发者来说,增加游戏的关卡数量是提升游戏规模和吸引力的有效方法。更多的关卡可以随着玩家的进度逐步提高游戏的挑战性,同时也能引入更多的游戏内容。不过,要注意避免添加过多的关卡,以免让玩家觉得某些关卡只是填充内容。

3. 为地图添加楼梯

楼梯是引导玩家深入地下城的常见元素,它占用单个瓷砖,易于渲染且直观易懂。以下是为游戏地图添加楼梯的具体步骤:
- 定义新的地图瓷砖类型 :打开 map.rs 文件,在 TileType 枚举中添加一个新的 Exit 条目,用于表示楼梯。

#[derive(Copy, Clone, PartialEq)]
pub enum TileType {
    Wall,
    Floor,
    Exit
}
  • 更新地图渲染和主题系统 :由于添加了新的瓷砖类型,需要在 map_builder/themes.rs 文件中更新渲染逻辑。
impl MapTheme for DungeonTheme {
    fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
        match tile_type {
            TileType::Floor => to_cp437('.'),
            TileType::Wall => to_cp437('#'),
            TileType::Exit => to_cp437('>'),
        }
    }
}

impl MapTheme for ForestTheme {
    fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
        match tile_type {
            TileType::Floor => to_cp437(';'),
            TileType::Wall => to_cp437('"'),
            TileType::Exit => to_cp437('>'),
        }
    }
}
  • 更新地下城导航系统 :修改 map.rs 文件中的 can_enter_tile 函数,允许玩家和怪物进入楼梯瓷砖。
pub fn can_enter_tile(&self, point: Point) -> bool {
    self.in_bounds(point) && (
        self.tiles[map_idx(point.x, point.y)] == TileType::Floor ||
        self.tiles[map_idx(point.x, point.y)] == TileType::Exit
    )
}
  • 用楼梯替代护身符 :在 main.rs 文件中,将原本生成护身符的代码注释掉,并添加生成楼梯的代码。
let mut 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);
let exit_idx = map_builder.map.point2d_to_index(map_builder.amulet_start);
map_builder.map.tiles[exit_idx] = TileType::Exit;
  • 修复护身符位置代码 :由于现在只有最后一层会生成护身符,需要修改 systems/end_turn.rs 文件中的护身符位置代码,避免游戏崩溃。
let amulet_default = Point::new(-1, -1);
let amulet_pos = amulet
  .iter(ecs)
  .nth(0)
  .unwrap_or(&amulet_default);
4. 跟踪游戏关卡

为了更好地管理游戏进度,需要跟踪玩家所在的关卡。具体操作如下:
- 添加关卡信息到玩家组件 :在 components.rs 文件中,为 Player 组件添加 map_level 字段。

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Player {
    pub map_level: u32
}
  • 更新玩家生成函数 :在 spawner.rs 文件中,更新 spawn_player 函数,让玩家从第 0 层开始游戏。
pub fn spawn_player(ecs: &mut World, pos: Point) {
    ecs.push(
        (Player { map_level: 0 },
        pos,
        Render {
            color: ColorPair::new(WHITE, BLACK),
            glyph: to_cp437('@')
        },
        Health { current: 10, max: 10 },
        FieldOfView::new(8)
        )
    );
}
  • 添加关卡转换状态 :在 turn_state.rs 文件中,为 TurnState 枚举添加 NextLevel 状态。
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TurnState {
    AwaitingInput,
    PlayerTurn,
    MonsterTurn,
    GameOver,
    Victory,
    NextLevel
}
  • 处理关卡转换 :在 main.rs 文件中,为 NextLevel 状态添加处理逻辑,并创建一个 advance_level 函数的占位符。
TurnState::NextLevel => {
    self.advance_level();
}

impl GameState {
    ...
    fn advance_level(&mut self) {}
}
  • 检测关卡转换 :在 systems/end_turn.rs 文件中,添加对玩家是否到达楼梯的检测,并在玩家到达时设置 NextLevel 状态。
pub fn end_turn(
    ecs: &SubWorld,
    #[resource] turn_state: &mut TurnState,
    #[resource] map: &Map
) {
    player_hp.iter(ecs).for_each(|(hp, pos)| {
        if hp.current < 1 {
            new_state = TurnState::GameOver;
        }
        if pos == amulet_pos {
            new_state = TurnState::Victory;
        }
        let idx = map.point2d_to_index(*pos);
        if map.tiles[idx] == TileType::Exit {
            new_state = TurnState::NextLevel;
        }
    });
}
5. 实现关卡转换的详细步骤

advance_level 函数的具体实现需要完成以下五个步骤:
1. 找到玩家实体 :在 main.rs 文件中,获取玩家的 Entity

let player_entity = *<Entity>::query()
  .filter(component::<Player>())
  .iter(&mut self.ecs)
  .nth(0)
  .unwrap();
  1. 标记要保留的实体 :创建一个 HashSet 来存储要保留的实体,包括玩家和玩家携带的物品。
use std::collections::HashSet;
let mut entities_to_keep = HashSet::new();
entities_to_keep.insert(player_entity);

<(Entity, &Carried)>::query()
  .iter(&self.ecs)
  .filter(|(_e, carry)| carry.0 == player_entity)
  .map(|(e, _carry)| *e)
  .for_each(|e| { entities_to_keep.insert(e); });
  1. 移除其他实体 :使用 CommandBuffer 批量移除不需要的实体。
let mut cb = CommandBuffer::new(&mut self.ecs);
for e in Entity::query().iter(&self.ecs) {
    if !entities_to_keep.contains(e) {
        cb.remove(*e);
    }
}
cb.flush(&mut self.ecs);
  1. 设置视野为脏状态 :确保玩家的视野在下一回合正确渲染。
<&mut FieldOfView>::query()
  .iter_mut(&mut self.ecs)
  .for_each(|fov| fov.is_dirty = true);
  1. 创建新地图并放置玩家 :生成新的地图,更新玩家的位置和关卡信息,并根据当前关卡决定生成楼梯还是护身符。
let mut rng = RandomNumberGenerator::new();
let mut map_builder = MapBuilder::new(&mut rng);

let mut map_level = 0;
<(&mut Player, &mut Point)>::query()
  .iter_mut(&mut self.ecs)
  .for_each(|(player, pos)| {
        player.map_level += 1;
        map_level = player.map_level;
        pos.x = map_builder.player_start.x;
        pos.y = map_builder.player_start.y;
    }
);

if map_level == 2 {
    spawn_amulet_of_yala(&mut self.ecs, map_builder.amulet_start);
} else {
    let exit_idx = map_builder.map.point2d_to_index(map_builder.amulet_start);
    map_builder.map.tiles[exit_idx] = TileType::Exit;
}

map_builder.monster_spawns
  .iter()
  .for_each(|pos| spawn_entity(&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);

通过以上步骤,玩家就可以在游戏中顺利地在不同关卡之间进行转换,并且在到达最后一层时赢得游戏胜利。整个开发过程涉及到多个文件的修改和多个功能的实现,需要开发者仔细处理每一个细节。以下是整个关卡转换流程的 mermaid 流程图:

graph TD;
    A[玩家到达楼梯] --> B[设置NextLevel状态];
    B --> C[调用advance_level函数];
    C --> D[找到玩家实体];
    D --> E[标记要保留的实体];
    E --> F[移除其他实体];
    F --> G[设置视野为脏状态];
    G --> H[创建新地图];
    H --> I[放置玩家并更新关卡信息];
    I --> J[根据关卡生成楼梯或护身符];
    J --> K[更新资源];

在这个流程图中,清晰地展示了从玩家到达楼梯到完成关卡转换的整个过程,有助于开发者更好地理解和实现游戏的关卡转换功能。同时,通过对代码的详细分析和步骤的逐步实现,开发者可以更加深入地掌握游戏开发的技巧和方法。

打造深度地下城:游戏开发进阶指南

6. 技术点分析

在整个游戏开发过程中,涉及到了多个重要的技术点,下面对这些技术点进行详细分析:
- Rust 枚举类型的使用 :在游戏开发中, TileType TurnState 枚举类型的使用非常关键。枚举类型可以清晰地定义不同的状态或类型,使得代码更加易读和可维护。例如,在 TileType 枚举中添加 Exit 类型后,需要在相关的匹配语句中进行更新,以确保所有可能的情况都被处理。
- Option 类型的处理 :在处理护身符位置时,使用了 unwrap_or 函数来处理 Option 类型。当 Option None 时, unwrap_or 函数会返回指定的默认值,避免了程序崩溃。这体现了 Rust 语言对安全性的重视,通过这种方式可以有效地避免空指针异常。
- CommandBuffer 的使用 :在移除不必要的实体时,使用了 CommandBuffer 来批量处理实体的移除操作。这种方式不仅提高了效率,还避免了在迭代实体时修改实体数据可能导致的问题。通过将操作批量处理,可以减少对实体数据的频繁修改,提高程序的稳定性。
- 生命周期管理 :Rust 的生命周期管理是其重要的特性之一。在使用 unwrap_or 函数时,需要注意传递的参数必须是有效的引用,避免出现悬空引用的问题。通过合理管理生命周期,可以确保程序的内存安全。

技术点 作用 示例代码
Rust 枚举类型 清晰定义不同状态或类型,提高代码可读性和可维护性 #[derive(Copy, Clone, PartialEq)] pub enum TileType { Wall, Floor, Exit }
Option 类型处理 避免空指针异常,提高程序安全性 let amulet_pos = amulet.iter(ecs).nth(0).unwrap_or(&amulet_default);
CommandBuffer 使用 批量处理实体操作,提高效率和稳定性 let mut cb = CommandBuffer::new(&mut self.ecs); for e in Entity::query().iter(&self.ecs) { if !entities_to_keep.contains(e) { cb.remove(*e); } } cb.flush(&mut self.ecs);
生命周期管理 确保内存安全,避免悬空引用 创建 amulet_default 变量并传递引用给 unwrap_or 函数
7. 总结与展望

通过以上一系列的操作,成功地为游戏添加了关卡转换功能,使得玩家可以深入探索地下城。从移除不必要的回血代码,到添加楼梯、跟踪游戏关卡,再到实现关卡转换的详细步骤,每一个环节都紧密相连,共同构建了一个更加丰富和有挑战性的游戏世界。

在未来的开发中,可以进一步扩展游戏的功能。例如,可以添加更多的关卡,每个关卡设置不同的难度和特色;可以增加更多的道具和技能,让玩家有更多的策略选择;还可以优化游戏的性能,提高游戏的流畅度。同时,也可以考虑添加多人模式,让玩家之间可以进行互动和合作,增加游戏的趣味性和社交性。

总之,游戏开发是一个不断探索和创新的过程,通过不断地学习和实践,可以打造出更加精彩的游戏作品。希望以上的内容对游戏开发者有所帮助,让大家在游戏开发的道路上取得更好的成果。

以下是一个简单的扩展功能规划列表:
1. 增加关卡 :设计更多具有不同难度和特色的关卡,如迷宫关卡、陷阱关卡等。
2. 丰富道具和技能 :添加新的道具和技能,如魔法药水、特殊武器等,让玩家有更多的策略选择。
3. 优化性能 :对游戏的代码进行优化,减少内存占用,提高游戏的运行速度。
4. 添加多人模式 :实现玩家之间的互动和合作,增加游戏的社交性。

通过这些扩展功能的实现,可以进一步提升游戏的品质和吸引力,为玩家带来更加丰富和精彩的游戏体验。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值