20、游戏开发:提升玩家体验与数据驱动设计

游戏开发:提升玩家体验与数据驱动设计

在游戏开发中,提升玩家体验和优化游戏设计是至关重要的。本文将介绍如何在游戏中显示当前地下城等级,以及如何运用数据驱动设计来优化游戏实体的生成。

1. 在HUD上显示当前等级

当前的抬头显示(HUD)仅显示玩家的生命值和当前库存,未显示玩家的当前等级。将当前等级添加到HUD中,能让玩家在看到等级数字提升时获得成就感。

以下是具体操作步骤:
1. 定位代码位置 hud_system 负责抬头显示的渲染。打开 systems/hud.rs 文件,在开始显示玩家库存之前添加以下代码:

DeeperDungeons/more_levels/src/systems/hud.rs
let (player, map_level) = <(Entity, &Player)>::query()
   .iter(ecs)
   .find_map(|(entity, player)| Some((*entity, player.map_level)))
   .unwrap();
draw_batch.print_color_right(
    Point::new(SCREEN_WIDTH*2, 1),
    format!("Dungeon Level: {}", map_level+1),
    ColorPair::new(YELLOW, BLACK)
);
  1. 代码解释
    • 第一部分代码定位 Player 组件和实体。
    • print_color_right() 函数用于右对齐文本输出,所有文本将显示在指定坐标的左侧。
    • format!() 宏方便地将当前等级包含在显示字符串中,注意这里给 map_level 加了 1 ,因为Rust程序员习惯从0开始计数,而大多数人更喜欢从1开始。

这段代码获取 Player 组件并读取其中存储的当前地下城等级,然后将该等级显示在玩家的HUD上。运行游戏,就能在屏幕上看到等级进度指示器。

2. 数据驱动设计概述

为游戏中的每个物品、怪物或其他实体编写生成函数会很耗时,而且等待游戏重新编译来测试新想法也不有趣。数据驱动设计可以解决这个问题。

数据驱动设计是指尽可能从数据文件中加载数据,然后使用这些数据来填充游戏实体。这种设计方式对于设计团队和开发团队协作很有用,因为团队成员无需学习Rust就能更改游戏。

3. 设计数据驱动的地下城

游戏中的实体有很多共同点,如名称、起始位置、渲染信息和游戏统计数据。为了避免为每个实体使用自定义生成函数,我们将创建一个通用的生成函数,从游戏的数据文件中读取数据并添加组件。

以下是具体步骤:
1. 创建数据文件 :使用RON(Rusty Object Notation)文件来存储游戏定义。创建一个名为 resources/template.ron 的新文件,并粘贴以下游戏定义:

Templates(
    entities : [
        Template(
            entity_type: Item,
            name : "Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
            provides: Some([ ("Healing", 6) ]),
            frequency: 2
        ),
        Template(
            entity_type: Item,
            name : "Dungeon Map", glyph : '{', levels : [ 0, 1, 2 ],
            provides: Some([ ("MagicMap", 0) ]),
            frequency: 1
        ),
        Template(
            entity_type: Enemy,
            name : "Goblin", glyph : 'g', levels : [ 0, 1, 2 ],
            hp : Some(1),
            frequency: 3
        ),
        Template(
            entity_type: Enemy,
            name : "Orc", glyph : 'o', levels : [ 0, 1, 2 ],
            hp : Some(2),
            frequency: 2
        ),
    ],
)
  1. 数据文件解释
    • 每个实体模板有以下属性:
      | 属性 | 说明 |
      | ---- | ---- |
      | entity_type | 可以是 Item Enemy |
      | name | 实体的显示名称 |
      | glyph | 用于渲染实体的字符 |
      | provides | 物品提供的效果列表,如 Healing MagicMap ,可选 |
      | hp | 生命值,可选 |
      | levels | 实体可以生成的等级列表,从0开始 |
      | frequency | 物品生成的频率,数字越高,生成越频繁 |

这个 template.ron 文件描述了游戏中除玩家和 Yala护符 之外的所有怪物和物品。

以下是创建数据驱动地下城的流程图:

graph TD;
    A[创建template.ron文件] --> B[定义实体模板];
    B --> C[添加属性信息];
    C --> D[完成数据文件];
4. 读取地下城数据

为了读取RON文件,需要使用 Serde ron 这两个crate。

具体操作步骤如下:
1. 添加依赖 :打开项目的 Cargo.toml 文件,添加以下依赖:

[dependencies]
bracket-lib = "~0.8.1"
legion = "=0.3.1"
serde = { version = "=1.0.115" }
ron = "=0.6.1"
  1. 扩展Spawner模块
    • 创建一个名为 spawner 的新文件夹。
    • spawner.rs 移动到新目录中。
    • spawner.rs 重命名为 mod.rs
  2. 创建并配置 template.rs 文件 :在新的 spawner 文件夹中创建一个名为 template.rs 的文件,并在 spawner/mod.rs 文件顶部添加 mod template; 。在 template.rs 文件中添加以下代码:
Loot/loot_tables/src/spawner/template.rs
use crate::prelude::*;
use serde::Deserialize;
use ron::de::from_reader;
use std::fs::File;
use std::collections::HashSet;
use legion::systems::CommandBuffer;

#[derive(Clone, Deserialize, Debug)]
pub struct Template {
    pub entity_type : EntityType,
    pub levels : HashSet<usize>,
    pub frequency : i32,
    pub name : String,
    pub glyph : char,
    pub provides : Option<Vec<(String, i32)>>,
    pub hp : Option<i32>
}
#[derive(Clone, Deserialize, Debug, PartialEq)]
pub enum EntityType {
    Enemy, Item
}
#[derive(Clone, Deserialize, Debug)]
pub struct Templates {
    pub entities : Vec<Template>,
}

impl Templates {
    pub fn load() -> Self {
        let file = File::open("resources/template.ron")
           .expect("Failed opening file");
        from_reader(file).expect("Unable to load templates")
    }
}

上述代码定义了与 template.ron 文件格式匹配的结构体,并实现了一个 load 方法来从磁盘加载模板文件。

通过以上步骤,你可以在游戏中显示当前地下城等级,并运用数据驱动设计优化游戏实体的生成,提升游戏的可维护性和扩展性。后续将继续介绍如何使用这些数据来生成实体。

游戏开发:提升玩家体验与数据驱动设计

5. 数据驱动的实体生成

数据驱动的实体生成是指读取游戏定义数据,并输出游戏内对象的过程。下面我们将创建一个新的函数来实现这一功能。

首先,定义函数签名:

impl Template {
    pub fn spawn_entities(
        &self,
        ecs: &mut World,
        rng: &mut RandomNumberGenerator,
        level: usize,
        spawn_points: &[Point]
    ) {

该函数的参数与 mod.rs 中现有的生成函数密切相关,包括对ECS世界和随机数生成器的引用,当前游戏等级,以及可生成实体的位置列表。

接下来,构建当前等级的生成表:

Loot/loot_tables/src/spawner/template.rs
let mut available_entities = Vec::new();
self.entities
   .iter()
   .filter(|e| e.levels.contains(&level))
   .for_each(|t| {
        for _ in 0 .. t.frequency {
            available_entities.push(t);
        }
    }
);

此步骤的操作流程如下:
1. 创建一个可变向量 available_entities ,用于存储当前等级可生成的实体列表。
2. 遍历加载的实体模板列表。
3. 使用 filter 函数筛选出 levels 列表包含当前等级的实体。
4. 根据实体的 frequency 值,将每个可用实体多次添加到 available_entities 列表中。

available_entities 列表包含对 entities 集合成员的引用,避免了数据的重复存储,节省了内存。每个可能的实体在列表中出现的次数与其频率成正比,方便后续随机选择。

以下是实体筛选和添加到可用列表的流程图:

graph TD;
    A[遍历实体模板] --> B[筛选符合等级的实体];
    B --> C[根据频率添加到可用列表];

然后,决定实体的生成位置:

Loot/loot_tables/src/spawner/template.rs
let mut commands = CommandBuffer::new(ecs);
spawn_points.iter().for_each(|pt| {
    if let Some(entity) = rng.random_slice_entry(&available_entities) {
        self.spawn_entity(pt, entity, &mut commands);
    }
});
commands.flush(ecs);

操作步骤如下:
1. 创建一个 CommandBuffer ,用于存储所有的生成命令,提高效率并避免借用检查器问题。
2. 遍历传入的生成点列表。
3. 使用 random_slice_entry 函数随机选择一个实体。
4. 调用 spawn_entity 函数生成实体。
5. 刷新 CommandBuffer 以应用所有命令。

接着,定义 spawn_entity 函数:

impl Template {
    fn spawn_entity(
        &self,
        pt: &Point,
        template: &Template,
        commands: &mut legion::systems::CommandBuffer
    ) {
        let entity = commands.push((
            pt.clone(),
            Render{
                color: ColorPair::new(WHITE, BLACK),
                glyph: to_cp437(template.glyph)
            },
            Name(template.name.clone())
        ));

        match template.entity_type {
            EntityType::Item => commands.add_component(entity, Item{}),
            EntityType::Enemy => {
                commands.add_component(entity, Enemy{});
                commands.add_component(entity, FieldOfView::new(6));
                commands.add_component(entity, ChasingPlayer{});
                commands.add_component(entity, Health{
                    current: template.hp.unwrap(),
                    max: template.hp.unwrap()
                });
            }
        }

        if let Some(effects) = &template.provides {
            effects.iter().for_each(|(provides, n)| {
                match provides.as_str() {
                    "Healing" => commands.add_component(entity, ProvidesHealing{ amount: *n}),
                    "MagicMap" => commands.add_component(entity, ProvidesDungeonMap{}),
                    _ => {
                        println!("Warning: we don't know how to provide {}"
                           , provides);
                    }
                }
            });
        }
    }
}

该函数的操作步骤如下:
1. 使用 commands.push 函数创建一个新实体,包含位置、渲染和名称组件。
2. 根据 entity_type 决定添加额外的组件:
- 如果是 Item ,添加 Item 标签。
- 如果是 Enemy ,添加 Enemy FieldOfView ChasingPlayer Health 组件。
3. 处理 provides 字段,根据效果列表添加相应的组件。

6. 清理旧代码与提供接口

由于新的通用生成函数替代了 spawner/mod.rs 中的许多功能,需要删除以下不再需要的函数:
- spawn_entity
- spawn_monster
- goblin
- orc
- spawn_healing_potion
- spawn_magic_mapper

然后,提供一个接口供主函数调用生成代码。在 spawner/mod.rs 中添加以下代码:

Loot/loot_tables/src/spawner/mod.rs
pub fn spawn_level(
    ecs: &mut World,
    rng: &mut RandomNumberGenerator,
    level: usize,
    spawn_points: &[Point]
) {
    let template = Templates::load();
    template.spawn_entities(ecs, rng, level, spawn_points);
}

main.rs 中进行相应的修改:
- 在 new() 函数中,将旧的生成代码替换为:

spawn_level(
    &mut self.ecs,
    &mut rng,
    0,
    &map_builder.monster_spawns
);
  • reset_game_level() 中进行相同的替换。
  • advance_level() 中,需要包含当前地图等级:
spawn_level(&mut self.ecs, &mut rng, map_level as usize,
    &map_builder.monster_spawns);

通过以上步骤,我们完成了从数据文件读取游戏定义、生成实体以及清理旧代码的过程,实现了数据驱动的游戏实体生成,提高了游戏的可维护性和扩展性,为玩家带来更丰富的游戏体验。

综上所述,在游戏开发中,通过在HUD上显示当前等级提升玩家的成就感,利用数据驱动设计优化实体生成,能够使游戏开发更加高效、灵活,为玩家提供更多的战术选择和变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值