30分钟用tui-rs构建会动的贪吃蛇:从0到1实现终端游戏
你还在为终端应用单调乏味而烦恼吗?想不想用Rust快速开发一个交互性强的终端游戏?本文将带你从零开始,使用tui-rs库构建经典贪吃蛇游戏,掌握终端UI渲染、用户输入处理和碰撞检测核心技术,最终成品可直接作为命令行应用运行。
读完本文你将学会:
- 配置tui-rs开发环境并理解项目结构
- 使用Canvas组件绘制游戏场景和蛇身
- 实现键盘控制和游戏循环逻辑
- 添加碰撞检测和得分系统
- 打包发布Rust终端应用
开发环境准备
tui-rs是一个用Rust编写的终端UI库,允许开发者构建丰富的终端用户界面和仪表盘。项目基于即时渲染和中间缓冲区原理,支持crossterm和termion两种后端,提供多种现成组件如Block、Chart、List等。
首先通过GitCode克隆项目仓库:
git clone https://gitcode.com/gh_mirrors/tu/tui-rs
cd tui-rs
项目核心文件结构:
- 官方文档:README.md
- 示例代码:examples/
- 核心源码:src/
- 项目配置:Cargo.toml
创建新的游戏示例文件:
cp examples/user_input.rs examples/snake_game.rs
在Cargo.toml中添加必要依赖:
[dependencies]
tui = { path = ".", features = ["crossterm"] }
crossterm = "0.25"
rand = "0.8"
游戏核心架构设计
贪吃蛇游戏需要以下核心模块,我们将基于tui-rs的事件循环和渲染系统构建:
定义游戏状态结构体
打开examples/snake_game.rs,首先定义游戏所需的数据结构:
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Canvas},
Frame, Terminal,
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{enable_raw_mode, disable_raw_mode},
};
use rand::Rng;
use std::time::{Duration, Instant};
// 游戏状态
struct GameState {
snake: Vec<(i32, i32)>, // 蛇身坐标集合
direction: (i32, i32), // 当前移动方向
next_direction: (i32, i32), // 下次移动方向
food: (i32, i32), // 食物位置
score: u32, // 当前得分
game_over: bool, // 游戏结束标志
width: i32, // 游戏区域宽度
height: i32, // 游戏区域高度
last_move_time: Instant, // 上次移动时间
move_interval: Duration, // 移动间隔
}
初始化游戏状态
实现GameState的默认初始化方法,设置初始蛇位置、食物位置和游戏参数:
impl Default for GameState {
fn default() -> Self {
let width = 40;
let height = 20;
// 初始蛇身:位于屏幕中间的3个方块
let snake = vec![
(width / 2, height / 2),
(width / 2 - 1, height / 2),
(width / 2 - 2, height / 2),
];
GameState {
snake,
direction: (1, 0), // 初始向右移动
next_direction: (1, 0),
food: Self::generate_food(width, height, &snake),
score: 0,
game_over: false,
width,
height,
last_move_time: Instant::now(),
move_interval: Duration::from_millis(200), // 初始速度
}
}
}
impl GameState {
// 随机生成食物位置,确保不在蛇身上
fn generate_food(width: i32, height: i32, snake: &[(i32, i32)]) -> (i32, i32) {
let mut rng = rand::thread_rng();
loop {
let x = rng.gen_range(1..width-1);
let y = rng.gen_range(1..height-1);
if !snake.contains(&(x, y)) {
return (x, y);
}
}
}
}
游戏逻辑实现
处理用户输入
基于examples/user_input.rs中的事件处理逻辑,实现键盘控制方向功能:
fn handle_input(game: &mut GameState) -> bool {
if event::poll(Duration::from_millis(50)).unwrap() {
if let Event::Key(key) = event::read().unwrap() {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return false, // 退出游戏
KeyCode::Up => {
if game.direction.1 == 0 { // 防止直接反向移动
game.next_direction = (0, -1);
}
}
KeyCode::Down => {
if game.direction.1 == 0 {
game.next_direction = (0, 1);
}
}
KeyCode::Left => {
if game.direction.0 == 0 {
game.next_direction = (-1, 0);
}
}
KeyCode::Right => {
if game.direction.0 == 0 {
game.next_direction = (1, 0);
}
}
KeyCode::Char('r') if game.game_over => {
*game = GameState::default(); // 重新开始游戏
}
_ => {}
}
}
}
}
true
}
实现游戏主循环
游戏循环负责状态更新、碰撞检测和画面渲染,是整个应用的核心:
fn run_game<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut game = GameState::default();
loop {
// 处理用户输入
if !handle_input(&mut game) {
break;
}
// 游戏逻辑更新
if !game.game_over && game.last_move_time.elapsed() >= game.move_interval {
game.update();
game.last_move_time = Instant::now();
}
// 渲染画面
terminal.draw(|f| ui(f, &game))?;
}
Ok(())
}
碰撞检测实现
碰撞检测是贪吃蛇游戏的关键部分,需要检测以下三种碰撞情况:
- 蛇头碰到墙壁
- 蛇头碰到自己的身体
- 蛇头碰到食物(加分并增长)
impl GameState {
fn update(&mut self) {
self.direction = self.next_direction;
let (head_x, head_y) = self.snake[0];
let (dx, dy) = self.direction;
let new_head = (head_x + dx, head_y + dy);
// 墙壁碰撞检测
if new_head.0 <= 0 || new_head.0 >= self.width-1 ||
new_head.1 <= 0 || new_head.1 >= self.height-1 {
self.game_over = true;
return;
}
// 自碰撞检测
if self.snake.contains(&new_head) {
self.game_over = true;
return;
}
// 将新头部添加到蛇身
self.snake.insert(0, new_head);
// 食物碰撞检测
if new_head == self.food {
self.score += 10;
// 每吃5个食物加快速度
if self.score % 50 == 0 && self.move_interval > Duration::from_millis(100) {
self.move_interval -= Duration::from_millis(10);
}
self.food = Self::generate_food(self.width, self.height, &self.snake);
} else {
// 如果没吃到食物,移除尾部
self.snake.pop();
}
}
}
游戏画面渲染
使用tui-rs的Canvas组件绘制游戏场景,包括边框、蛇身和食物:
fn ui<B: Backend>(f: &mut Frame<B>, game: &GameState) {
// 创建布局
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(1), // 得分显示
Constraint::Min(20), // 游戏区域
Constraint::Length(1), // 操作说明
]
.as_ref(),
)
.split(f.size());
// 绘制得分
let score_text = vec![Spans::from(Span::styled(
format!("Score: {}", game.score),
Style::default().add_modifier(Modifier::BOLD),
))];
let score_widget = Paragraph::new(score_text)
.style(Style::default().fg(Color::Green))
.block(Block::default().borders(Borders::ALL));
f.render_widget(score_widget, chunks[0]);
// 绘制游戏区域
let canvas = Canvas::default()
.block(Block::default().title("Snake Game").borders(Borders::ALL))
.x_bounds([0.0, game.width as f64])
.y_bounds([0.0, game.height as f64])
.paint(|ctx| {
// 绘制围墙
ctx.draw(&Rectangle {
x1: 0.0,
y1: 0.0,
x2: game.width as f64,
y2: game.height as f64,
color: Color::White,
});
// 绘制蛇身
for &(x, y) in &self.snake {
ctx.draw(&Rectangle {
x1: x as f64,
y1: y as f64,
x2: (x + 1) as f64,
y2: (y + 1) as f64,
color: if (x, y) == self.snake[0] {
Color::Red // 蛇头红色
} else {
Color::Green // 蛇身绿色
},
});
}
// 绘制食物
let (fx, fy) = self.food;
ctx.draw(&Rectangle {
x1: fx as f64,
y1: fy as f64,
x2: (fx + 1) as f64,
y2: (fy + 1) as f64,
color: Color::Yellow,
});
// 游戏结束文字
if self.game_over {
ctx.print(
game.width as f64 / 2.0 - 5.0,
game.height as f64 / 2.0,
Style::default().fg(Color::Red),
"GAME OVER",
);
ctx.print(
game.width as f64 / 2.0 - 10.0,
game.height as f64 / 2.0 + 1.0,
Style::default().fg(Color::White),
"Press R to restart",
);
}
});
f.render_widget(canvas, chunks[1]);
// 绘制操作说明
let help_text = vec![Spans::from(vec![
Span::raw("方向键控制 | "),
Span::styled("Q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("退出 | "),
Span::styled("R", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("重新开始"),
])];
let help_widget = Paragraph::new(help_text)
.block(Block::default().borders(Borders::ALL));
f.render_widget(help_widget, chunks[2]);
}
运行与打包游戏
完成以上代码后,添加main函数并运行游戏:
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 终端配置
enable_raw_mode()?;
let mut stdout = std::io::stdout();
crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// 运行游戏
let result = run_game(&mut terminal);
// 恢复终端设置
disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
crossterm::terminal::LeaveAlternateScreen
)?;
terminal.show_cursor()?;
result?;
Ok(())
}
编译并运行游戏:
cargo run --example snake_game
游戏操作说明:
- 方向键:控制蛇移动方向
- Q键:退出游戏
- R键:游戏结束后重新开始
tui-rs提供了丰富的终端UI组件,本游戏使用了Canvas组件绘制游戏元素。更多组件示例可参考:
总结与扩展
本文使用tui-rs库实现了一个功能完整的贪吃蛇游戏,包含以下核心技术点:
- 使用Canvas组件进行图形绘制
- 实现游戏循环和状态管理
- 处理键盘输入和事件响应
- 实现三种碰撞检测逻辑
- 根据得分动态调整游戏难度
你可以通过以下方式扩展游戏功能:
- 添加音效(使用rodio库)
- 实现关卡系统和不同地图
- 添加最高分排行榜(使用本地文件存储)
- 支持游戏暂停功能
- 实现不同难度级别选择
tui-rs不仅可用于游戏开发,还广泛应用于系统监控、终端工具和开发调试工具等领域。项目中提供了多种实用示例,如:
- 系统监控仪表盘:examples/demo/
- 表格组件应用:examples/table.rs
- 自定义组件开发:examples/custom_widget.rs
通过本文的学习,你已经掌握了tui-rs的核心用法,能够开发交互丰富的终端应用。查看CONTRIBUTING.md了解如何为项目贡献代码,或CHANGELOG.md了解版本更新历史。
希望这篇教程能帮助你入门终端UI开发,如有任何问题,欢迎在项目仓库提交issue或参与讨论。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



