《游戏开发:从数据驱动到成品发布》
数据驱动的游戏设计
运行游戏时,你会看到熟悉的怪物和物品,而且这次生成代码不会有冗余,你已经拥有了一个数据驱动的地下城。你还能直接从数据文件添加新怪物,并更改它们出现的关卡,无需编程。
例如,打开
resources/template.ron
文件,添加一种新的治疗药剂:
Template(
entity_type: Item,
name : "Weak Healing Potion", glyph : '!', levels : [ 0, 1, 2 ],
provides: Some([ ("Healing", 2) ]),
frequency: 2
)
运行游戏,你会发现一些药剂变成了 “弱治疗药剂”,这就是数据驱动设计的强大之处,能轻松对游戏进行修改。
扩展战斗系统
当前的战斗系统很简单,一个实体攻击另一个实体时总是造成 1 点伤害。随着冒险者在地下城中深入探索,这种战斗系统没有提供足够的难度曲线。
为了解决这个问题,我们要让不同实体造成不同程度的伤害。具体来说,要调整游戏,使玩家找到更好的武器时能造成更多伤害,一些怪物凭借更大的爪子对玩家造成更多伤害。
武器和爪子的伤害
-
打开
spawner/template.rs,在Template结构体中添加base_damage字段:
#[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>,
pub base_damage : Option<i32>
}
-
打开
resources/template.ron,添加怪物的伤害统计信息,并调整怪物的关卡分布,让更危险的怪物在游戏后期出现:
Template(
entity_type: Enemy,
name : "Goblin", glyph : 'g', levels : [ 0 ],
hp : Some(1),
frequency: 3,
base_damage: Some(1)
),
Template(
entity_type: Enemy,
name : "Orc", glyph : 'o', levels : [ 0, 1, 2 ],
hp : Some(2),
frequency: 2,
base_damage: Some(1)
),
Template(
entity_type: Enemy,
name : "Ogre", glyph : 'O', levels : [ 1, 2 ],
hp : Some(5),
frequency: 1,
base_damage: Some(2)
),
Template(
entity_type: Enemy,
name : "Ettin", glyph : 'E', levels : [ 2 ],
hp : Some(10),
frequency: 1,
base_damage: Some(3)
)
这样就实现了难度曲线,随着冒险者探索地下城关卡,怪物会逐渐变得更危险:
| 关卡 | 怪物(基础伤害) |
| ---- | ---- |
| 0 | 哥布林(1)、兽人(2) |
| 1 | 兽人(2)、食人魔(2) |
| 2 | 兽人(2)、食人魔(2)、双头怪(3) |
-
在实体列表中添加武器。武器是能造成伤害的物品,添加时包含新的
base_damage字段:
Template(
entity_type: Item,
name : "Rusty Sword", glyph: '/', levels: [ 0, 1, 2 ],
frequency: 1,
base_damage: Some(1)
)
伤害组件
-
打开
components.rs,添加新的组件类型:
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Damage(pub i32);
-
为了识别物品是武器,在
components.rs文件中添加另一个组件:
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Weapon;
-
让玩家能造成伤害。打开
spawner/mod.rs,在spawn_player函数创建玩家时添加Damage组件:
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),
Damage(1)
)
);
}
-
扩展
spawn_entity函数以处理额外的伤害信息。打开spawner/template.rs,找到spawn_entity函数,在效果生成代码后插入以下代码:
if let Some(damage) = &template.base_damage {
commands.add_component(entity, Damage(*damage));
if template.entity_type == EntityType::Item {
commands.add_component(entity, Weapon{});
}
}
这个函数会判断是否有伤害值,若有则添加到组件中,再检查实体是否为物品,若是则添加
Weapon
组件。
造成伤害
打开
systems/combat.rs
,战斗系统需要访问
Damage
和
Carried
组件:
#[system]
#[read_component(WantsToAttack)]
#[read_component(Player)]
#[write_component(Health)]
#[read_component(Damage)]
#[read_component(Carried)]
由于计算攻击者的伤害输出需要其数据,所以要扩展受害者列表计算器,使其也返回攻击者的
Entity
:
let victims : Vec<(Entity, Entity, Entity)> = attackers
.iter(ecs)
.map(|(entity, attack)| (*entity, attack.attacker, attack.victim) )
.collect();
victims.iter().for_each(|(message, attacker, victim)| {
之前是将受害者的生命值减 1,现在要检查攻击者并计算其造成的伤害:
let base_damage = if let Ok(v) = ecs.entry_ref(*attacker) {
if let Ok(dmg) = v.get_component::<Damage>() {
dmg.0
} else {
0
}
} else {
0
};
let weapon_damage : i32 = <(&Carried, &Damage)>::query().iter(ecs)
.filter(|(carried, _)| carried.0 == *attacker)
.map(|(_, dmg)| dmg.0)
.sum();
let final_damage = base_damage + weapon_damage;
if let Ok(mut health) = ecs
.entry_mut(*victim)
.unwrap()
.get_component_mut::<Health>()
{
health.current -= final_damage;
运行游戏,在地下城中找到一把生锈的剑,捡起它,你造成的伤害将从 1 点变为 2 点。同时,怪物现在对玩家造成不同的伤害,使与地下城居民的战斗更有吸引力。
解决玩家武器携带问题
当冒险者捡起一把生锈的剑时会使用它,捡起第二把时也会使用。但捡起第三把剑时就不合理了。
为了让玩家一次只装备一把武器,打开
systems/player_input.rs
,添加
#[read_component(Weapon)]
到系统的组件规范中,并调整物品收集代码:
let mut items = <(Entity, &Item, &Point)>::query();
items.iter(ecs)
.filter(|(_entity, _item, &item_pos)| item_pos == player_pos)
.for_each(|(entity, _item, _item_pos)| {
commands.remove_component::<Point>(*entity);
commands.add_component(*entity, Carried(player));
if let Ok(e) = ecs.entry_ref(*entity) {
if e.get_component::<Weapon>().is_ok() {
<(Entity, &Carried, &Weapon)>::query()
.iter(ecs)
.filter(|(_, c, _)| c.0 == player)
.for_each(|(e, c, w)| {
commands.remove(*e);
})
}
}
});
这个代码会检查选中的物品是否为武器,若是则删除玩家已持有的其他武器。
添加更多武器
现在游戏支持多种武器类型,需要添加一些图形在地图上表示它们。打开
resources/template.ron
,添加更多类型的剑:
Template(
entity_type: Item,
name : "Rusty Sword", glyph: 's', levels: [ 0, 1, 2 ],
frequency: 1,
base_damage: Some(1)
),
Template(
entity_type: Item,
name : "Shiny Sword", glyph: 'S', levels: [ 0, 1, 2 ],
frequency: 1,
base_damage: Some(2)
),
Template(
entity_type: Item,
name : "Huge Sword", glyph: '/', levels: [ 1, 2 ],
frequency: 1,
base_damage: Some(3)
)
游戏现在有三种剑,其中一种只在最后两个关卡出现。运行游戏,你会在地图上找到各种武器。
游戏打包与发布
在之前的开发中,我们是在调试模式下编译和运行游戏。调试模式会给可执行文件添加额外信息,方便调试,但会禁用一些能让游戏运行更快的编译器优化。由于玩家不需要调试游戏,我们可以在发布版构建中利用这些优化。
启用发布模式和链接时优化
-
可以使用
cargo run --release命令在发布模式下运行游戏。运行后你会发现游戏更快、响应更灵敏。不过要测试确保游戏仍能正常运行,因为优化有时可能产生意外结果。 -
还可以使用链接时优化(LTO)进一步优化游戏。正常编译只优化每个模块的内容,而 LTO 会分析整个程序,包括所有依赖项。要启用 LTO,打开
Cargo.toml,在末尾添加以下部分:
[profile.release]
lto = "thin"
启用 LTO 后,每次在发布模式下编译或运行游戏时,LTO 都会优化完整的二进制文件。虽然编译和链接速度会变慢,但性能提升有时很显著。再次使用
cargo run --release
运行游戏,会看到进一步的性能提升,但不如启用发布模式时明显。
游戏分发
-
先运行
cargo clean命令,清除所有当前编译的代码。因为 Rust 支持增量编译,有时会在可执行文件中添加内容而不是完全重新构建。 -
项目清理后,输入
cargo build --release以发布模式编译整个游戏。 -
在计算机其他位置创建一个新目录来存储打包的游戏。需要从项目目录复制一些文件和目录到新目录:
-
游戏可执行文件,位于
target/release目录,名称与Cargo.toml中的项目名称相同。如果在dungeoncrawl项目中工作,在类 UNIX 系统上查找dungeoncrawl,在 Windows 上查找dungeoncrawl.exe。 -
将
resources文件夹完整复制到新目录。
-
游戏可执行文件,位于
Rust 会将所有依赖项静态链接到可执行文件中,所以不需要任何编译后的依赖项,也不需要额外的文件或运行时。最终目录结构如下:
resources/
dungeoncrawl (或 dungeoncrawl.exe)
dungeonfont.png
MyDungeonCrawler
terminal8x8.png
template.ron
从新目录运行游戏,一切应正常运行。若有问题,需验证是否复制了所有必要的文件和文件夹。最后,压缩游戏目录并与朋友分享压缩文件,也可以在
Itch
等网站上分享游戏。
个性化地下城爬行游戏
作为游戏开发者,调整游戏并使其个性化很重要。你可以尝试以下项目想法:
-
添加或修改怪物和物品
:直接在数据文件中添加新的怪物和物品,或者修改现有怪物和物品的属性。
-
改变游戏主题
:使用不同的图块集彻底改变游戏的感觉。
-
添加计分机制
:设计一套计分规则,根据玩家的行为和成就给予相应分数。
-
转为实时游戏
:如果不喜欢回合制游戏,可以对大部分系统进行微调,并添加类似
Flappy Dragon
的计时代码,使其成为实时游戏。
-
添加动画效果
:回顾
Flappy Dragon
的额外内容,尝试为游戏添加动画。
-
使用更高级的引擎
:研究
Amethyst Engine
和
Bevy
等基于 ECS 的引擎,为更高级的游戏开发提供框架。
最重要的是,思考你想开发的游戏,遵循设计建议,从简单开始,逐步构建梦想中的游戏。
额外内容与总结
开发者还计划发布一些额外内容,包括 Web Assembly(WASM),可将游戏发布到网页上在浏览器中玩;粒子效果,为游戏添加更多交互式图形;游戏保存功能,学习如何序列化 ECS 以便轻松保存和恢复游戏。
恭喜你,你已经完成了从学习 Rust 的 “Hello, World” 到开发
Flappy Dragon
和地下城爬行游戏的旅程。现在你有能力创建有趣的游戏,并与世界分享你的创意。希望这不是你游戏开发或 Rust 学习之旅的终点。
《游戏开发:从数据驱动到成品发布》
游戏设计文档辅助开发
在游戏开发过程中,一份好的游戏设计文档能起到很大的帮助。它可以让开发者聚焦核心游戏理念,避免在周边功能上花费过多时间,还能克服写作障碍,助力完成游戏。
记录创意灵感
在玩游戏和创作的过程中,灵感可能随时涌现,比如在洗澡、工作或者睡觉的时候。所以要准备一个记事本(实体或电子的都可以),随时记录灵感。记录时不需要太详细,只要能让自己想起当时的想法就行。例如 “Pong,但有激光” 这样的记录就很容易让人记住创意。
可以使用各种笔记系统,像 EMACS Org Mode、Evernote、Google Keep、Microsoft OneNote 等,甚至用一本纸质笔记本和一支笔也没问题。并且最好按照游戏创意来整理笔记,先写一个包含基本游戏创意的顶层笔记,再在下面记录相关的具体想法。
其他额外资源
除了上述的游戏开发内容,还提供了一些额外的资源,帮助开发者更好地进行游戏开发。
ASCII/Codepage 437 图表
提供了 ASCII/Codepage 437 图表,这个图表有助于开发者在进行基于终端的渲染原型设计时使用。它可以帮助开发者更方便地选择合适的字符来呈现游戏元素,提升游戏在终端界面的视觉效果。
Rust 语法速查表
还有一份 Rust 语法速查表,它总结了很多 Rust 语言的语法。对于开发者来说,这是一个很好的参考工具,在开发过程中遇到语法问题时,可以快速查阅,提高开发效率。
总结与展望
通过一系列的学习和实践,你已经从最初学习 Rust 的 “Hello, World” 开始,逐步掌握了开发
Flappy Dragon
和地下城爬行游戏的技能。现在的你能够创建出具有 procedurally generated maps(程序生成地图)、健康系统、近战战斗、物品、战利品和数据驱动设计等功能的游戏。
在这个过程中,你学会了使用数据文件来定义游戏的实体和组件,通过数据驱动的方式可以更轻松地对游戏进行修改和扩展。同时,你也对战斗系统进行了扩展,让游戏的战斗更具挑战性和趣味性。还学会了如何将游戏打包并发布,以及如何让游戏更符合自己的个性化需求。
未来,开发者计划发布的 Web Assembly(WASM)、粒子效果和游戏保存功能等额外内容,将进一步丰富游戏的功能和玩法。希望你能继续在游戏开发和 Rust 学习的道路上前行,不断发挥自己的创意,创造出更多有趣的游戏。
以下是一个简单的 mermaid 流程图,展示游戏开发的大致流程:
graph LR
A[学习 Rust 基础] --> B[开发简单游戏(如 Flappy Dragon)]
B --> C[开发地下城爬行游戏]
C --> D[数据驱动设计与战斗系统扩展]
D --> E[游戏打包与发布]
E --> F[个性化游戏开发]
F --> G[利用额外资源持续优化]
总之,游戏开发是一个充满挑战和乐趣的过程,现在你已经具备了一定的能力和知识,大胆地去尝试和创新吧!
超级会员免费看
3913

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



