回顾
在前面的章节中,我们大致已经完成了如下的工作:
- 为游戏添加了音频文件
- 为游戏准备了备用屏幕及设置
- 为游戏准备了键盘的即时捕获输入的设置
- 在退出游戏前恢复上述的设置
众所周知,游戏在不手动退出的情况下应该一直运行下去,因此所有的游戏机会都应该有一个主循环来保证游戏不会自动退出。
创建主循环
那么游戏主循环应该从哪里开始呢?当然是在我们准备好所有的配置之后了。 因此,我们将在// Terminal
和// Cleanup
两部分之间加入我们的游戏主循环:
fn main() -> Result<(), Box<dyn Error>>{
snip[..]
// Terminal
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
stdout.execute(EnterAlternateScreen)?;
stdout.execute(Hide)?;
// Game Loop
'gameloop: loop {
}
// Cleanup
audio.wait(); // 等待播放结束
stdout.execute(Show)?;
stdout.execute(LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
Ok(())
}
在前面的章节中,我们提到了可以为循环打标签,上面的代码中,我们给主循环打了一个名为'gameloop
的标签, 以便我们可以在游戏进行的任意位置跳出主循环,以结束游戏。
监听事件
在游戏中,我们主要是通过键盘的操作来控制玩家的行为,因此第一步我们需要定期的监控键盘事件,根据键盘事件来决定我们的行为:
snip[..]
// Game Loop
'gameloop: loop {
// Input
while event::poll(Duration::default())? { // 监听所有事件
if let Event::Key(key_event) = event::read()? { // 检测到事件属于一个键盘事件
match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => { // 检测到`Esc`或`q`键被按下
audio.play("lose"); // 播放游戏失败的音乐
break 'gameloop; // 退出游戏主循环
}
_ => {} // 其他键被按下不做操作
}
}
}
}
snip[..]
上面的代码中, 用到了event::poll
, 这是一个事件监听器,他会定期的监听事件,间隔事件在参数中指定, 这个例子中使用Duration::default()
作为参数,表示使用默认的间隔时间。
由于我们只监听键盘事件, 因此使用了if let
这种模式匹配语法,当监听到键盘事件时,再用match
去匹配判断具体是什么键盘事件。
这个例子中, 第一条匹配规则是Esc
或q
键被按下,则播放游戏失败的音乐,并退出游戏主循环。注意_ => {}
这一行是不可或缺的,因为在前面的基础篇中提到了, match后面的分支必须是穷举的,如果不想一个一个列出来,就要用_
代表一个默认分支。
测试退出功能
上面的代码实际上是实现了一个退出游戏的功能,即当游戏开始后, 如果玩家按下了Esc
或q
键,则会播放失败音乐并退出,现在让我们来测试一下, 先运行cargo run
, 会看到代码的编译构建消息:
紧接着程序开始运行,这时会进入备用屏幕,我们会看到一块空白的终端屏幕:
这时, 程序会一直监听系统重发生的事件, 这时我们按下任意键, 例如k
, 这时主循环内监听到了键盘事件并对其进行匹配, 但是这个键并不是Esc
或者q
, 因此会进入match
语句的默认分支,也就是不进行任何操作, 所以我们在界面上也看不到任何的反应。
此时我们再尝试按下q
键, 会听到游戏播放失败的音乐“You lose”, 然后退出空白界面,回到正常的命令行终端中,如下:
小结
本章在前面的基础上加入了游戏主循环,并且实现了按指定键退出游戏的功能,到此为止,我们的游戏界面还是一片空白,没有任何的显示,下一章中,我们开始进行界面的绘制与渲染,并且将对我们的代码结构进行优化,使得代码结构更加的模块化。目前,我们的main.rs
文件的完整内容如下:
// main.rs
use std::{error::Error, io, time::Duration};
use crossterm::{terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, cursor::{Hide, Show}, event::{KeyCode, self, Event}};
use rusty_audio::Audio;
fn main() -> Result<(), Box<dyn Error>>{
let mut audio = Audio::new();
audio.add("explode", "./explode.wav");
audio.add("lose", "./lose.wav");
audio.add("move", "./move.wav");
audio.add("pew", "./pew.wav");
audio.add("startup", "./startup.wav");
audio.add("win", "./win.wav");
audio.play("startup");
// Terminal
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
stdout.execute(EnterAlternateScreen)?;
stdout.execute(Hide)?;
// Game Loop
'gameloop: loop {
// Input
while event::poll(Duration::default())? {
if let Event::Key(key_event) = event::read()? {
match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => {
break 'gameloop;
}
_ => {}
}
}
}
}
// Cleanup
audio.wait(); // 等待播放结束
stdout.execute(Show)?;
stdout.execute(LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
Ok(())
}