游戏开发:提升玩家体验与数据驱动设计
在游戏开发中,提升玩家体验和优化游戏设计是至关重要的。本文将介绍如何在游戏中显示当前地下城等级,以及如何运用数据驱动设计来优化游戏实体的生成。
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)
);
-
代码解释
:
-
第一部分代码定位
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
),
],
)
-
数据文件解释
:
-
每个实体模板有以下属性:
| 属性 | 说明 |
| ---- | ---- |
|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"
-
扩展Spawner模块
:
-
创建一个名为
spawner的新文件夹。 -
将
spawner.rs移动到新目录中。 -
将
spawner.rs重命名为mod.rs。
-
创建一个名为
-
创建并配置
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上显示当前等级提升玩家的成就感,利用数据驱动设计优化实体生成,能够使游戏开发更加高效、灵活,为玩家提供更多的战术选择和变化。
超级会员免费看
2251

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



