Rust 实现贪吃蛇小游戏源码分享
- 一、效果展示
- 二、源码分享
- 1、main.rs
- 2、Cargo.toml
- 三、rand 包详解
- 1、 添加依赖
- 2、 核心概念
- 2.1、 随机数生成器 (RNG)
- 2.2、 分布 (Distributions)
- 3、 基本用法
- 3.1、 生成随机数
- 3.2 、随机布尔值与选择
- 4、在贪吃蛇游戏中的应用
- 5、 高级特性与性能
- 5.1 、种子与可重现性
- 5.2 、性能优化
- 6、常见问题与陷阱
- 7、总结
一、效果展示
二、源码分享
1、main.rs
usecrossterm::{cursor::{Hide,MoveTo,Show},event::{poll,read,Event,KeyCode,KeyEvent,KeyModifiers},execute,style::{Color,Print,ResetColor,SetBackgroundColor},terminal::{disable_raw_mode,enable_raw_mode,size,Clear,ClearType,EnterAlternateScreen,LeaveAlternateScreen,},};userand::Rng;usestd::{collections::VecDeque,io::{stdout,Result,Write},time::Duration,};/// 蛇的移动方向#[derive(Clone, Copy, PartialEq)]enumDirection{Up,Down,Left,Right,}/// 游戏状态structGame{snake:VecDeque<(usize,usize)>,direction:Direction,next_direction:Direction,food:(usize,usize),score:u32,width:usize,height:usize,game_over:bool,}implGame{fnnew(width:usize,height:usize)->Self{// 蛇从中间开始,初始长度 3letmid_x=width/2;letmid_y=height/2;letmutsnake=VecDeque::new();snake.push_back((mid_x,mid_y));snake.push_back((mid_x-1,mid_y));snake.push_back((mid_x-2,mid_y));letmutgame=Game{snake,direction:Direction::Right,next_direction:Direction::Right,food:(0,0),score:0,width,height,game_over:false,};game.spawn_food();game}/// 在空白位置随机生成食物fnspawn_food(&mutself){letmutrng=rand::thread_rng();loop{letx=rng.gen_range(0..self.width);lety=rng.gen_range(0..self.height);if!self.snake.contains(&(x,y)){self.food=(x,y);break;}}}/// 处理输入方向,禁止 180° 掉头fnset_direction(&mutself,dir:Direction){match(self.direction,dir){(Direction::Up,Direction::Down)|(Direction::Down,Direction::Up)|(Direction::Left,Direction::Right)|(Direction::Right,Direction::Left)=>{}// 忽略反向_=>self.next_direction=dir,}}/// 更新蛇的位置fnupdate(&mutself){ifself.game_over{return;}self.direction=self.next_direction;let(head_x,head_y)=self.snake.front().unwrap();let(nx,ny)=matchself.direction{Direction::Up=>(*head_x,head_y.wrapping_sub(1)),Direction::Down=>(*head_x,head_y+1),Direction::Left=>(head_x.wrapping_sub(1),*head_y),Direction::Right=>(*head_x+1,*head_y),};// 撞墙检测ifnx>=self.width||ny>=self.height{self.game_over=true;return;}// 撞自己检测ifself.snake.contains(&(nx,ny)){self.game_over=true;return;}// 移动蛇:头部插入新位置self.snake.push_front((nx,ny));// 吃到食物加分并生成新食物,否则去掉尾部if(nx,ny)==self.food{self.score+=10;self.spawn_food();}else{self.snake.pop_back();}}/// 渲染游戏画面fnrender(&self)->Result<()>{letmutstdout=stdout();execute!(stdout,Clear(ClearType::All))?;// 绘制上边框forxin0..self.width{execute!(stdout,MoveTo(xasu16,0),Print("─"))?;}// 绘制下边框forxin0..self.width{execute!(stdout,MoveTo(xasu16,(self.height+1)asu16),Print("─"))?;}// 绘制左右边框foryin0..=self.height{execute!(stdout,MoveTo(0,yasu16),Print("│"))?;execute!(stdout,MoveTo((self.width+1)asu16,yasu16),Print("│"))?;}// 绘制食物execute!(stdout,SetBackgroundColor(Color::Red),MoveTo(self.food.0asu16+1,self.food.1asu16+1),Print("●"),ResetColor)?;// 绘制蛇身for(i,(x,y))inself.snake.iter().enumerate(){execute!(stdout,MoveTo(*xasu16+1,*yasu16+1),SetBackgroundColor(ifi==0{Color::DarkGreen}else{Color::Green}),Print("■"),ResetColor)?;}// 显示分数和游戏结束提示letscore_y=(self.height+3)asu16;execute!(stdout,MoveTo(0,score_y),Print(format!("Score: {}",self.score)))?;ifself.game_over{execute!(stdout,MoveTo(0,score_y+1),Print("Game Over! Press 'q' to quit or 'r' to restart"))?;}execute!(stdout,MoveTo(0,score_y+2))?;stdout.flush()}}/// 检查是否退出fnshould_quit(key:KeyCode)->bool{key==KeyCode::Char('q')||key==KeyCode::Esc}fnmain()->Result<()>{let(term_w,term_h)=size()?;letwidth=(term_w.saturating_sub(4)asusize).min(50);letheight=(term_h.saturating_sub(8)asusize).min(25);// 初始化终端enable_raw_mode()?;execute!(stdout(),EnterAlternateScreen,Hide)?;letmutgame=Game::new(width,height);// 游戏主循环:120ms 一帧loop{game.render()?;ifpoll(Duration::from_millis(120))?{ifletEvent::Key(KeyEvent{code,modifiers,..})=read()?{ifshould_quit(code){break;}ifgame.game_over&&code==KeyCode::Char('r'){game=Game::new(width,height);continue;}ifmodifiers==KeyModifiers::NONE{matchcode{KeyCode::Up|KeyCode::Char('w')=>game.set_direction(Direction::Up),KeyCode::Down|KeyCode::Char('s')=>game.set_direction(Direction::Down),KeyCode::Left|KeyCode::Char('a')=>game.set_direction(Direction::Left),KeyCode::Right|KeyCode::Char('d')=>game.set_direction(Direction::Right),_=>{}}}}}game.update();}// 恢复终端execute!(stdout(),Show,LeaveAlternateScreen)?;disable_raw_mode()?;println!("Final score: {}",game.score);Ok(())}2、Cargo.toml
[package]name="ttt"version="0.1.0"edition="2024"[dependencies]crossterm="0.28"rand="0.8"三、rand 包详解
rand是 Rust 生态中用于生成随机数的核心库。它提供了高质量的随机数生成器(RNG)、多种分布采样方法以及方便的实用工具。在本贪吃蛇游戏中,我们使用rand来在游戏区域内随机生成食物位置。
1、 添加依赖
在Cargo.toml中添加rand依赖:
[dependencies] rand = "0.8"版本0.8是当前广泛使用的稳定版本,提供了丰富的 API 和良好的性能。
2、 核心概念
2.1、 随机数生成器 (RNG)
rand库的核心是随机数生成器(RNG),它负责产生随机比特序列。rand提供了多种 RNG 实现:
ThreadRng:线程局部的、密码学安全的 RNG,通过rand::thread_rng()获取。这是最常用的 RNG,性能良好且安全。StdRng:基于 ChaCha 算法的密码学安全 RNG,适合需要可重现随机序列的场景。SmallRng:非密码学安全的、高性能的 RNG,适合模拟和游戏等对速度要求高的场景。
2.2、 分布 (Distributions)
分布定义了如何将 RNG 产生的原始随机比特映射到特定范围的数值或类型。rand提供了多种分布:
- 均匀分布:
Uniform,用于生成指定范围内的整数或浮点数。 - 正态分布:
Normal,生成符合正态(高斯)分布的随机数。 - 伯努利分布:
Bernoulli,以给定概率生成true或false。 - 加权选择:
WeightedIndex,根据权重从列表中随机选择元素。
3、 基本用法
3.1、 生成随机数
userand::Rng;fnmain(){letmutrng=rand::thread_rng();// 生成一个随机整数(i32 类型)letn1:i32=rng.gen();println!("Random i32: {}",n1);// 生成一个 [0, 1) 之间的随机浮点数(f64 类型)letn2:f64=rng.gen();println!("Random f64 in [0,1): {}",n2);// 生成指定范围的随机整数letn3=rng.gen_range(0..10);// 包含 0,不包含 10println!("Random integer in [0, 10): {}",n3);// 生成指定范围的随机浮点数letn4=rng.gen_range(0.0..1.0);println!("Random float in [0.0, 1.0): {}",n4);}3.2 、随机布尔值与选择
userand::Rng;fnmain(){letmutrng=rand::thread_rng();// 以 50% 的概率生成 trueletb:bool=rng.gen_bool(0.5);println!("Random bool: {}",b);// 从数组中随机选择一个元素letitems=["apple","banana","cherry"];letchoice=rng.choose(&items).unwrap();println!("Random choice: {}",choice);// 打乱数组(原地)letmutnums=vec![1,2,3,4,5];rng.shuffle(&mutnums);println!("Shuffled: {:?}",nums);}4、在贪吃蛇游戏中的应用
在我们的贪吃蛇游戏中,rand用于在空白位置生成食物。相关代码位于Game::spawn_food方法中:
/// 在空白位置随机生成食物fnspawn_food(&mutself){letmutrng=rand::thread_rng();loop{letx=rng.gen_range(0..self.width);lety=rng.gen_range(0..self.height);if!self.snake.contains(&(x,y)){self.food=(x,y);break;}}}代码解析:
- 获取 RNG:
let mut rng = rand::thread_rng();获取当前线程的随机数生成器。 - 生成随机坐标:
rng.gen_range(0..self.width)生成一个在[0, self.width)范围内的随机整数作为 x 坐标。同理生成 y 坐标。 - 避免重复:使用
loop循环确保生成的位置不在蛇身上(!self.snake.contains(&(x, y)))。如果冲突,则继续生成新的随机位置,直到找到空白位置为止。 - 设置食物:将找到的空白位置赋值给
self.food。
这种“生成-检查”循环是游戏开发中常见的随机位置生成模式,确保游戏逻辑的正确性。
5、 高级特性与性能
5.1 、种子与可重现性
如果需要可重现的随机序列(例如用于测试或回放),可以使用种子初始化 RNG:
userand::{Rng,SeedableRng};userand_chacha::ChaCha8Rng;// 需要额外依赖 rand_chachafnmain(){letseed=[0;32];// 32 字节的种子letmutrng=ChaCha8Rng::from_seed(seed);// 每次运行都会产生相同的随机数序列println!("{}",rng.gen::<u32>());}5.2 、性能优化
- 重用 RNG:避免在循环中反复创建
thread_rng(),应在循环外创建并传递。 - 选择合适的分布:对于简单的范围随机,
gen_range已足够高效。对于需要大量采样的情况,考虑预先生成随机数数组。 - 使用
SmallRng:如果不需要密码学安全,可以使用SmallRng获得更好的性能。
6、常见问题与陷阱
- 范围包含性:
gen_range(a..b)包含a但不包含b;gen_range(a..=b)包含两端。 - 线程安全:
thread_rng()返回的是线程局部的 RNG,不同线程会得到不同的随机序列。 - 性能瓶颈:在紧密循环中频繁调用
gen_range可能成为性能瓶颈,可以考虑批量生成。 - 均匀性:
gen_range保证均匀分布,但某些自定义分布可能需要手动实现。
7、总结
rand库是 Rust 中处理随机性的标准工具,提供了从简单随机数生成到复杂分布采样的完整功能。在贪吃蛇游戏中,我们使用rand::thread_rng()和gen_range方法实现了食物的随机生成,这是游戏随机性的核心来源。掌握rand的基本用法后,你可以轻松将其应用于游戏开发、模拟、测试数据生成等多种场景。