30分钟用tui-rs构建会动的贪吃蛇:从0到1实现终端游戏

30分钟用tui-rs构建会动的贪吃蛇:从0到1实现终端游戏

【免费下载链接】tui-rs Build terminal user interfaces and dashboards using Rust 【免费下载链接】tui-rs 项目地址: https://gitcode.com/gh_mirrors/tu/tui-rs

你还在为终端应用单调乏味而烦恼吗?想不想用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

项目核心文件结构:

创建新的游戏示例文件:

cp examples/user_input.rs examples/snake_game.rs

在Cargo.toml中添加必要依赖:

[dependencies]
tui = { path = ".", features = ["crossterm"] }
crossterm = "0.25"
rand = "0.8"

游戏核心架构设计

贪吃蛇游戏需要以下核心模块,我们将基于tui-rs的事件循环和渲染系统构建:

mermaid

定义游戏状态结构体

打开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(())
}

碰撞检测实现

碰撞检测是贪吃蛇游戏的关键部分,需要检测以下三种碰撞情况:

  1. 蛇头碰到墙壁
  2. 蛇头碰到自己的身体
  3. 蛇头碰到食物(加分并增长)
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组件进行图形绘制
  • 实现游戏循环和状态管理
  • 处理键盘输入和事件响应
  • 实现三种碰撞检测逻辑
  • 根据得分动态调整游戏难度

你可以通过以下方式扩展游戏功能:

  1. 添加音效(使用rodio库)
  2. 实现关卡系统和不同地图
  3. 添加最高分排行榜(使用本地文件存储)
  4. 支持游戏暂停功能
  5. 实现不同难度级别选择

tui-rs不仅可用于游戏开发,还广泛应用于系统监控、终端工具和开发调试工具等领域。项目中提供了多种实用示例,如:

通过本文的学习,你已经掌握了tui-rs的核心用法,能够开发交互丰富的终端应用。查看CONTRIBUTING.md了解如何为项目贡献代码,或CHANGELOG.md了解版本更新历史。

希望这篇教程能帮助你入门终端UI开发,如有任何问题,欢迎在项目仓库提交issue或参与讨论。

【免费下载链接】tui-rs Build terminal user interfaces and dashboards using Rust 【免费下载链接】tui-rs 项目地址: https://gitcode.com/gh_mirrors/tu/tui-rs

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值