用户界面
关于本教程
本教程是免费且开源的,所有代码使用 MIT 许可证——因此您可以随意使用它。我希望您会喜欢这个教程,并制作出很棒的游戏!
如果你喜欢这个并且希望我继续写作,请考虑支持我的 Patreon。
在本章中,我们将为游戏添加用户界面。
缩小地图
我们将从 map.rs
开始,并添加一些常量:MAPWIDTH
、MAPHEIGHT
和 MAPCOUNT
:
#![allow(unused)] fn main() { const MAPWIDTH: usize = 80; const MAPHEIGHT: usize = 50; const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; }
然后我们将逐一更改对 80*50 的引用为MAPCOUNT
,并对地图大小的引用使用常量。完成后并运行时,我们将MAPHEIGHT
更改为 43 - 以便在屏幕底部留出用户界面面板的空间。
一些最小的图形用户界面元素
我们将创建一个新文件 gui.rs
来存放我们的代码。我们将从一个非常简单的开始:
#![allow(unused)] fn main() { use rltk::{ RGB, Rltk, Console }; use specs::prelude::*; pub fn draw_ui(ecs: &World, ctx : &mut Rltk) { ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); } }
我们在main.rs
顶部的导入块中添加一个mod gui
,并在tick
的末尾调用它:
#![allow(unused)] fn main() { gui::draw_ui(&self.ecs, ctx); }
如果我们现在运行 cargo run
,我们会看到地图已经缩小了——并且我们有一个白色框代替了面板。
添加生命条
这会帮助玩家了解他们还剩下多少生命值。幸运的是,RLTK 提供了方便的辅助工具。我们需要从 ECS 中获取玩家的生命值并进行渲染。这很简单,你现在应该已经很熟悉了。代码如下:
#![allow(unused)] fn main() { use rltk::{ RGB, Rltk, Console }; use specs::prelude::*; use super::{CombatStats, Player}; pub fn draw_ui(ecs: &World, ctx : &mut Rltk) { ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); let combat_stats = ecs.read_storage::<CombatStats>(); let players = ecs.read_storage::<Player>(); for (_player, stats) in (&players, &combat_stats).join() { let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp); ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health); ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); } } }
添加消息日志
游戏日志作为资源是有意义的:任何想要告诉你一些信息的系统都可以使用它,而且几乎没有限制什么可能想要告诉你一些信息。我们将从建模日志本身开始。创建一个新文件,gamelog.rs
。我们将从非常简单的方式开始:
#![allow(unused)] fn main() { pub struct GameLog { pub entries : Vec<String> } }
在 main.rs
中,我们添加一行 mod gamelog;
,并将其作为资源插入,使用 gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty Roguelike".to_string()] });
。我们在开始时插入一条日志记录,使用 vec!
宏来构造向量。这样我们就有东西可以显示了——所以我们将在 gui.rs
中开始编写日志显示代码。在我们的 GUI 绘制函数中,我们只需添加:
#![allow(unused)] fn main() { let log = ecs.fetch::<GameLog>(); let mut y = 44; for s in log.entries.iter().rev() { if y < 49 { ctx.print(2, y, s); } y += 1; } }
如果你现在cargo run
这个项目,你会看到类似这样的内容:
记录攻击
在我们的 melee_combat_system
中,我们从 super
导入中添加 gamelog::GameLog
,为日志添加读/写访问器(WriteExpect<'a, GameLog>,
),并扩展解构以包含它:let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
。然后只需将 print!
宏替换为插入游戏日志即可。以下是结果代码:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{CombatStats, WantsToMelee, Name, SufferDamage, gamelog::GameLog}; pub struct MeleeCombatSystem {} impl<'a> System<'a> for MeleeCombatSystem { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, WriteExpect<'a, GameLog>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, ReadStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data; for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { if stats.hp > 0 { let target_stats = combat_stats.get(wants_melee.target).unwrap(); if target_stats.hp > 0 { let target_name = names.get(wants_melee.target).unwrap(); let damage = i32::max(0, stats.power - target_stats.defense); if damage == 0 { log.entries.push(format!("{} is unable to hurt {}", &name.name, &target_name.name)); } else { log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage)); SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); } } } } wants_melee.clear(); } } }
现在如果你运行游戏并玩一会儿(cargo run
,玩法由你决定!),你会在日志中看到战斗消息:
通知死亡
我们可以用delete_the_dead
来通知死亡。以下是完成的代码:
#![allow(unused)] fn main() { pub fn delete_the_dead(ecs : &mut World) { let mut dead : Vec<Entity> = Vec::new(); // Using a scope to make the borrow checker happy { let combat_stats = ecs.read_storage::<CombatStats>(); let players = ecs.read_storage::<Player>(); let names = ecs.read_storage::<Name>(); let entities = ecs.entities(); let mut log = ecs.write_resource::<GameLog>(); for (entity, stats) in (&entities, &combat_stats).join() { if stats.hp < 1 { let player = players.get(entity); match player { None => { let victim_name = names.get(entity); if let Some(victim_name) = victim_name { log.entries.push(format!("{} is dead", &victim_name.name)); } dead.push(entity) } Some(_) => console::log("You are dead") } } } } for victim in dead { ecs.delete_entity(victim).expect("Unable to delete"); } } }
鼠标支持和工具提示
让我们从如何从 RLTK 获取鼠标信息开始。这非常简单;在你的draw_ui
函数的底部添加以下内容:
#![allow(unused)] fn main() { // Draw mouse cursor let mouse_pos = ctx.mouse_pos(); ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::MAGENTA)); }
这将鼠标指向的单元格的背景设置为品红色。如您所见,鼠标信息作为上下文的一部分从 RLTK 到达。
现在我们将介绍一个新功能,draw_tooltips
,并在draw_ui
的末尾调用它。新功能如下所示:
#![allow(unused)] fn main() { fn draw_tooltips(ecs: &World, ctx : &mut Rltk) { let map = ecs.fetch::<Map>(); let names = ecs.read_storage::<Name>(); let positions = ecs.read_storage::<Position>(); let mouse_pos = ctx.mouse_pos(); if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; } let mut tooltip : Vec<String> = Vec::new(); for (name, position) in (&names, &positions).join() { let idx = map.xy_idx(position.x, position.y); if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] { tooltip.push(name.name.to_string()); } } if !tooltip.is_empty() { let mut width :i32 = 0; for s in tooltip.iter() { if width < s.len() as i32 { width = s.len() as i32; } } width += 3; if mouse_pos.0 > 40 { let arrow_pos = Point::new(mouse_pos.0 - 2, mouse_pos.1); let left_x = mouse_pos.0 - width; let mut y = mouse_pos.1; for s in tooltip.iter() { ctx.print_color(left_x, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s); let padding = (width - s.len() as i32)-1; for i in 0..padding { ctx.print_color(arrow_pos.x - i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string()); } y += 1; } ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"->".to_string()); } else { let arrow_pos = Point::new(mouse_pos.0 + 1, mouse_pos.1); let left_x = mouse_pos.0 +3; let mut y = mouse_pos.1; for s in tooltip.iter() { ctx.print_color(left_x + 1, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), s); let padding = (width - s.len() as i32)-1; for i in 0..padding { ctx.print_color(arrow_pos.x + 1 + i, y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &" ".to_string()); } y += 1; } ctx.print_color(arrow_pos.x, arrow_pos.y, RGB::named(rltk::WHITE), RGB::named(rltk::GREY), &"<-".to_string()); } } } }
它首先获取我们需要用于工具提示的组件的读取权限:名称和位置。它还获取地图本身的读取权限。然后我们检查鼠标光标是否实际在地图上,如果不在,则退出——没有必要为永远不会有任何东西的地方绘制工具提示!
余下的部分说“如果我们有任何工具提示,查看鼠标位置”——如果在左边,我们会将工具提示放在右边,否则放在左边。
如果你现在 cargo run
你的项目,它看起来像这样:
可选的后处理,以获得真正的复古感觉
由于我们在讨论外观和感觉,让我们考虑启用一个 RLTK 功能:后处理以产生扫描线和屏幕烧伤,以获得真正的复古感觉。你是否想使用这个完全取决于你!在main.rs
中,初始设置只是将第一个init
命令替换为:
#![allow(unused)] fn main() { use rltk::RltkBuilder; let mut context = RltkBuilder::simple80x50() .with_title("Roguelike Tutorial") .build()?; context.with_post_scanlines(true); }
如果你选择这样做,游戏看起来有点像经典的Qud 的洞穴:
总结
现在我们有了一个图形用户界面,看起来相当不错!
本章的源代码可以在此处找到这里
运行本章的示例与 Web Assembly,在您的浏览器中(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.
版权 (C) 2024, myedgetech.com.