回顾
前面的章节中,我们已经完成了下面的工作:
- 准备所需的音频文件
- 设置游戏所需的屏幕和键盘配置
- 创建了游戏主循环并在其中实现按键
Esc
或q
退出游戏的功能 - 游戏退出前还原配置
目前游戏的界面还是一片空白,本章我们开始为游戏在终端上绘制并渲染界面,这真是一个激动人心的工作。
模块化
对于一个真正是实战项目而已,应该没人会把所有的代码都放在main.rs
文件中。 为了使项目的结构, 功能和逻辑更加清楚明了,我们通常会将项目进行适当的模块化。本章中我们将创建几个新文件,用于存放不同功能的代码:
lib.rs
: 在基础篇中提到了,是项目的根库文件,我们将项目中其他的库和一些全局使用的常量放入其中,以便我们可以在本项目的其他文件中可以直接使用;frame.rs
: 我们的游戏界面需要划定一块框架,然后在这个范围内进行绘制,其中的代码实现相关功能;render.rs:
在游戏运行过程中,需要根据实时的信息,对界面进行渲染,其中的代码实现相关的功能;
当前的目录结构如下:
invaders
├── Cargo.lock
├── Cargo.toml
├── explode.wav
├── lose.wav
├── move.wav
├── pew.wav
├── src
│ ├── frame.rs
│ ├── lib.rs
│ ├── main.rs
│ └── render.rs
├── startup.wav
└── win.wav
lib.rs
pub mod frame;
pub mod render;
pub const NUM_ROWS: usize = 20;
pub const NUM_COLS: usize = 40;
在这里面我们定义了两个常量, 分别是行数(20)和列数(40),也就是说我们游戏界面的大小是(20 * 40)。
frame.rs
先理解一下Frame
是什么, Frame就是我们的游戏界面,在终端中,例如我们的游戏界面是20行, 40列的一块范围,那么我们的Frame
就是由20 * 40个像素单元组成,每个单元就是屏幕上显示的一个字符。
// frame.rs
use crate::{NUM_COLS, NUM_ROWS}; // 定义在本项目的lib.rs文件中的pub常量或模块可以用 crate::<名称> 这种格式导入
pub type Frame = Vec<Vec<&'static str>>;
pub fn new_frame() -> Frame {
let mut cols = Vec::with_capacity(NUM_COLS);
for _ in 0..NUM_COLS {
let mut col = Vec::with_capacity(NUM_ROWS);
for _ in 0..NUM_ROWS {
col.push(" ");
}
cols.push(col);
}
cols
}
pub trait Drawable {
fn draw(&self, frame: &mut Frame);
}
在上面的代码中, 有一个新的语法type Frame = Vec<Vec<&'static str>>
, 这相当于是给Vec<Vec<&'static str>>
这个数据结构起了一个别名叫做Frame
, 在别的语言中也比较常见。这说明Frame是一个二维数组,其中的每个元素都是一个字符串切片的引用, 'static
这个标记是表示这个变量的声明周期存在于整个程序运行期间,不会被销毁。
new_frame
这个函数的内容是初始化我们的界面框架, 将界面中每个位置都初始设置为一个
空字符串。
我们的界面在游戏运行中一定是动态变化的,而且可能由多种不同的情况或主体去进行界面的绘制,因此我们定义了一个Trait
叫做Drawable
, 只要实现了它规定的接口,都可以对界面进行绘制, 其中的draw
函数,返回一个Frame
。
render.rs
渲染(render)的主要目的是根据Frame
的数据,对标准输出进行操作,从而使得屏幕显示出来我们想要的界面效果。
use std::io::{Stdout, Write};
use crate:: frame::Frame;
use crossterm::QueueableCommand;
use crossterm::style::{SetBackgroundColor, Color}; use crossterm::terminal::{Clear, ClearType};
use crossterm::cursor::MoveTo;
pub fn render(stdout: &mut Stdout, last_frame: &Frame, curr_frame: &Frame, force: bool) {
if force {
stdout.queue(SetBackgroundColor(Color::Blue)).unwrap();
stdout.queue(Clear(ClearType::All)).unwrap();
stdout.queue(SetBackgroundColor(Color::Black)).unwrap();
}
for (x, col) in curr_frame.iter().enumerate() {
for (y, s) in col.iter().enumerate() {
if *s != last_frame[x][y] || force {
stdout. queue(MoveTo(x as u16, y as u16)).unwrap();
print!("{}", *s);
}
}
}
stdout.flush().unwrap();
}
现在来看一下文件中的reder()
函数, 首先是参数:
stdout
: 是一个标准输出类型,因为我们需要对终端进行操作,因此是一个对终端的可变引用类型;last_frame
: 上次的界面状态;curr_frame
: 当前的界面状态;force
:是否强制进行渲染,例如刚开始游戏时,需要全部重新渲染。
首先看函数开始,判断了是否强制渲染,如果强制渲染的话,会清除屏幕上的所有文字内容并重置屏幕的背景色(染色 -> 清除 -> 再染色是比较通用的清除屏幕的步骤)。
然后会比较当前Frame和上次刷新的Frame的状态,对其中的每个像素单元进行比较,看是否有变化(如果是强制渲染,则不比较, 直接对每个单元都进行操作)。 如果发现有变化,则移动到当前判断的位置(MoveTo
),然后在这里填入当前Frame信息中该位置的值(print!("{}", *s);
)。
注意stdout.queue()
将对标准输出的操作都先放入了缓冲区, 当在最后调用stdout.flush()
时,才会被一起执行,在屏幕上产生效果。
小结
本章完成了界面绘制和渲染相关功能的函数和模块的编写,接下来我们要做的就是把这些单个的功能串联起来,并使用多线程的方式,来使界面正常工作起来。
下面是各个文件的完整代码:
// lib.rs
pub mod frame;
pub mod render;
pub const NUM_ROWS: usize = 20;
pub const NUM_COLS: usize = 40;
// frame.rs
use crate::{NUM_COLS, NUM_ROWS};
pub type Frame = Vec<Vec<&'static str>>;
pub fn new_frame() -> Frame {
let mut cols = Vec::with_capacity(NUM_COLS);
for _ in 0..NUM_COLS {
let mut col = Vec::with_capacity(NUM_ROWS);
for _ in 0..NUM_ROWS {
col.push(" ");
}
cols.push(col);
}
cols
}
pub trait Drawable {
fn draw(&self, frame: &mut Frame);
}
// render.rs
use std::io::{Stdout, Write};
use crate:: frame::Frame;
use crossterm::QueueableCommand;
use crossterm::style::{SetBackgroundColor, Color}; use crossterm::terminal::{Clear, ClearType};
use crossterm::cursor::MoveTo;
pub fn render(stdout: &mut Stdout, last_frame: &Frame, curr_frame: &Frame, force: bool) {
if force {
stdout.queue(SetBackgroundColor(Color::Blue)).unwrap();
stdout.queue(Clear(ClearType::All)).unwrap();
stdout.queue(SetBackgroundColor(Color::Black)).unwrap();
}
for (x, col) in curr_frame.iter().enumerate() {
for (y, s) in col.iter().enumerate() {
if *s != last_frame[x][y] || force {
stdout.queue(MoveTo(x as u16, y as u16)).unwrap();
print!("{}", *s);
}
}
}
stdout.flush().unwrap();
}