一个贪吃蛇游戏的 rust 实现,使用了 piston_window 和 rand crate。
游戏使用 上下左右 方向键进行操控,使用 R 重置游戏,使用 P 进行暂停/启动。
项目结构
| · |
| ├── Cargo.lock |
| ├── Cargo.toml |
| ├── src/ |
| │ ├── main.rs |
| │ ├──snake_game/ |
| │ │ ├── game.rs |
| │ │ └── mod.rs |
| │ ├──snake_snake/ |
| │ │ ├── snake.rs |
| │ │ └── mod.rs |
| │ └──snake_window/ |
| │ ├──draw.rs |
| │ └── mod.rs |
三个mod.rs 文件
| |
| pub mod game; |
| |
| |
| pub mod snake; |
| |
| |
| pub mod draw; |
| |
main.rs
| use piston_window::types::Color; |
| use piston_window::{clear, Button, PistonWindow, PressEvent, UpdateEvent, WindowSettings}; |
| |
| mod snake_game; |
| mod snake_snake; |
| mod snake_window; |
| |
| use crate::snake_game::game::Game; |
| use snake_window::draw::to_coord_u32; |
| |
| |
| const BACK_COLOR: Color = [0.5, 0.5, 0.5, 1.0]; |
| |
| fn main() { |
| |
| let (width, height) = (30, 30); |
| |
| |
| let mut window: PistonWindow = |
| WindowSettings::new("Snake", [to_coord_u32(width), to_coord_u32(height)]) |
| .exit_on_esc(true) |
| .build() |
| .unwrap(); |
| |
| |
| let mut game = Game::new(width, height); |
| |
| |
| while let Some(event) = window.next() { |
| |
| if let Some(Button::Keyboard(key)) = event.press_args() { |
| game.key_pressed(key); |
| } |
| |
| |
| window.draw_2d(&event, |c, g, _| { |
| clear(BACK_COLOR, g); |
| game.draw(&c, g) |
| }); |
| |
| |
| event.update(|arg| { |
| game.update(arg.dt); |
| }); |
| } |
| } |
game.rs
| use crate::snake_snake::snake::{Direction, Snake}; |
| use crate::snake_window::draw::{draw_block, draw_rectangle}; |
| use piston_window::rectangle::Shape; |
| use piston_window::types::Color; |
| use piston_window::{Context, G2d, Key}; |
| use rand::{thread_rng, Rng}; |
| |
| |
| const FOOD_COLOR: Color = [255.0, 0.0, 255.0, 1.0]; |
| |
| const T_BORDER_COLOR: Color = [0.0000, 0.5, 0.5, 0.6]; |
| |
| const B_BORDER_COLOR: Color = [0.0000, 0.5, 0.5, 0.6]; |
| |
| const L_BORDER_COLOR: Color = [0.0000, 0.5, 0.5, 0.6]; |
| |
| const R_BORDER_COLOR: Color = [0.0000, 0.5, 0.5, 0.6]; |
| |
| |
| const GAMEOVER_COLOR: Color = [0.90, 0.00, 0.00, 0.5]; |
| |
| |
| const MOVING_PERIOD: f64 = 0.3; |
| |
| |
| #[derive(Debug)] |
| pub struct Game { |
| |
| snake: Snake, |
| |
| food_exists: bool, |
| |
| food_x: i32, |
| |
| food_y: i32, |
| |
| width: i32, |
| |
| height: i32, |
| |
| game_over: bool, |
| |
| waiting_time: f64, |
| |
| game_pause: bool, |
| } |
| |
| impl Game { |
| |
| pub fn new(width: i32, height: i32) -> Game { |
| Game { |
| snake: Snake::new(2, 2), |
| food_exists: true, |
| food_x: 6, |
| food_y: 4, |
| width, |
| height, |
| game_over: false, |
| waiting_time: 0.0, |
| game_pause: false, |
| } |
| } |
| |
| |
| pub fn key_pressed(&mut self, key: Key) { |
| |
| if key == Key::R { |
| self.restart() |
| } |
| |
| if self.game_over { |
| return; |
| } |
| |
| let dir = match key { |
| Key::Up => Some(Direction::Up), |
| Key::Down => Some(Direction::Down), |
| Key::Left => Some(Direction::Left), |
| Key::Right => Some(Direction::Right), |
| Key::P => { |
| |
| self.game_pause = !self.game_pause; |
| None |
| } |
| _ => None, |
| }; |
| |
| if let Some(d) = dir { |
| |
| if d == self.snake.head_direction().opposite() { |
| return; |
| } |
| } |
| |
| |
| self.update_snake(dir); |
| } |
| |
| |
| fn check_eating(&mut self) { |
| let (head_x, head_y) = self.snake.head_position(); |
| if self.food_exists && self.food_x == head_x && self.food_y == head_y { |
| self.food_exists = false; |
| self.snake.restore_tail(); |
| } |
| } |
| |
| |
| pub fn draw(&self, con: &Context, g: &mut G2d) { |
| self.snake.draw(con, g); |
| if self.food_exists { |
| draw_block( |
| FOOD_COLOR, |
| Shape::Round(8.0, 16), |
| self.food_x, |
| self.food_y, |
| con, |
| g, |
| ); |
| } |
| |
| |
| draw_rectangle(T_BORDER_COLOR, 0, 0, self.width, 1, con, g); |
| |
| draw_rectangle(B_BORDER_COLOR, 0, self.height - 1, self.width, 1, con, g); |
| |
| draw_rectangle(L_BORDER_COLOR, 0, 1, 1, self.height - 2, con, g); |
| |
| draw_rectangle( |
| R_BORDER_COLOR, |
| self.width - 1, |
| 1, |
| 1, |
| self.height - 2, |
| con, |
| g, |
| ); |
| |
| |
| if self.game_over { |
| draw_rectangle(GAMEOVER_COLOR, 0, 0, self.width, self.height, con, g); |
| } |
| } |
| |
| |
| pub fn update(&mut self, delta_time: f64) { |
| |
| if self.game_pause || self.game_over { |
| return; |
| } |
| |
| |
| self.waiting_time += delta_time; |
| |
| if !self.food_exists { |
| self.add_food() |
| } |
| |
| if self.waiting_time > MOVING_PERIOD { |
| self.update_snake(None) |
| } |
| } |
| |
| |
| fn add_food(&mut self) { |
| let mut rng = thread_rng(); |
| |
| let mut new_x = rng.gen_range(1..self.width - 1); |
| let mut new_y = rng.gen_range(1..self.height - 1); |
| |
| while self.snake.over_tail(new_x, new_y) { |
| new_x = rng.gen_range(1..self.width - 1); |
| new_y = rng.gen_range(1..self.height - 1); |
| } |
| self.food_x = new_x; |
| self.food_y = new_y; |
| self.food_exists = true; |
| } |
| |
| |
| fn check_if_snake_alive(&self, dir: Option<Direction>) -> bool { |
| let (next_x, next_y) = self.snake.next_head(dir); |
| |
| if self.snake.over_tail(next_x, next_y) { |
| return false; |
| } |
| |
| next_x > 0 && next_y > 0 && next_x < self.width - 1 && next_y < self.height - 1 |
| } |
| |
| |
| fn update_snake(&mut self, dir: Option<Direction>) { |
| if self.game_pause { |
| return; |
| } |
| if self.check_if_snake_alive(dir) { |
| self.snake.move_forward(dir); |
| self.check_eating(); |
| } else { |
| self.game_over = true; |
| } |
| self.waiting_time = 0.0; |
| } |
| |
| |
| fn restart(&mut self) { |
| self.snake = Snake::new(2, 2); |
| self.waiting_time = 0.0; |
| self.food_exists = true; |
| self.food_x = 6; |
| self.food_y = 4; |
| self.game_over = false; |
| self.game_pause = false; |
| } |
| } |
snake.rs
| use crate::snake_window::draw::draw_block; |
| use piston_window::rectangle::Shape; |
| use piston_window::types::Color; |
| use piston_window::{Context, G2d}; |
| use std::collections::LinkedList; |
| |
| |
| const SNAKE_BODY_COLOR: Color = [0.5, 0.0, 0.0, 1.0]; |
| |
| const SNAKE_HEAD_COLOR: Color = [1.0, 0.00, 0.00, 1.0]; |
| |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub enum Direction { |
| Up, |
| Down, |
| Left, |
| Right, |
| } |
| |
| impl Direction { |
| |
| pub fn opposite(&self) -> Direction { |
| match *self { |
| Direction::Up => Direction::Down, |
| Direction::Down => Direction::Up, |
| Direction::Left => Direction::Right, |
| Direction::Right => Direction::Left, |
| } |
| } |
| } |
| |
| |
| #[derive(Debug, Clone)] |
| struct Block { |
| x: i32, |
| y: i32, |
| } |
| |
| |
| #[derive(Debug)] |
| pub struct Snake { |
| |
| direction: Direction, |
| |
| body: LinkedList<Block>, |
| |
| tail: Option<Block>, |
| } |
| |
| impl Snake { |
| |
| pub fn new(x: i32, y: i32) -> Snake { |
| let mut body: LinkedList<Block> = LinkedList::new(); |
| body.push_back(Block { x: x + 2, y: y }); |
| body.push_back(Block { x: x + 1, y: y }); |
| body.push_back(Block { x: x, y: y }); |
| Snake { |
| direction: Direction::Right, |
| body, |
| tail: None, |
| } |
| } |
| |
| |
| pub fn draw(&self, con: &Context, g: &mut G2d) { |
| let mut is_head = true; |
| for block in &self.body { |
| if is_head { |
| is_head = false; |
| draw_block( |
| SNAKE_HEAD_COLOR, |
| Shape::Round(10.0, 16), |
| block.x, |
| block.y, |
| con, |
| g, |
| ); |
| } else { |
| draw_block( |
| SNAKE_BODY_COLOR, |
| Shape::Round(12.5, 16), |
| block.x, |
| block.y, |
| con, |
| g, |
| ); |
| } |
| } |
| } |
| |
| |
| pub fn head_position(&self) -> (i32, i32) { |
| let head = self.body.front().unwrap(); |
| (head.x, head.y) |
| } |
| |
| |
| pub fn head_direction(&self) -> Direction { |
| self.direction |
| } |
| |
| |
| pub fn next_head(&self, dir: Option<Direction>) -> (i32, i32) { |
| let (head_x, head_y): (i32, i32) = self.head_position(); |
| |
| let mut moving_dir = self.direction; |
| match dir { |
| Some(d) => moving_dir = d, |
| None => {} |
| } |
| |
| match moving_dir { |
| Direction::Up => (head_x, head_y - 1), |
| Direction::Down => (head_x, head_y + 1), |
| Direction::Left => (head_x - 1, head_y), |
| Direction::Right => (head_x + 1, head_y), |
| } |
| } |
| |
| |
| pub fn move_forward(&mut self, dir: Option<Direction>) { |
| match dir { |
| Some(d) => self.direction = d, |
| None => (), |
| } |
| |
| let (x, y) = self.next_head(dir); |
| self.body.push_front(Block { x, y }); |
| let remove_block = self.body.pop_back().unwrap(); |
| self.tail = Some(remove_block); |
| } |
| |
| |
| pub fn restore_tail(&mut self) { |
| let blk = self.tail.clone().unwrap(); |
| self.body.push_back(blk); |
| } |
| |
| |
| pub fn over_tail(&self, x: i32, y: i32) -> bool { |
| let mut ch = 0; |
| for block in &self.body { |
| if x == block.x && y == block.y { |
| return true; |
| } |
| ch += 1; |
| if ch == self.body.len() - 1 { |
| break; |
| } |
| } |
| false |
| } |
| } |
draw.rs
| use piston_window::rectangle::Shape; |
| use piston_window::types::Color; |
| use piston_window::{rectangle, Context, DrawState, G2d, Rectangle}; |
| |
| |
| const BLOCK_SIZE: f64 = 20.0; |
| |
| |
| pub fn to_coord(game_coord: i32) -> f64 { |
| (game_coord as f64) * BLOCK_SIZE |
| } |
| |
| |
| pub fn to_coord_u32(game_coord: i32) -> u32 { |
| to_coord(game_coord) as u32 |
| } |
| |
| |
| |
| pub fn draw_block(color: Color, shape: Shape, x: i32, y: i32, con: &Context, g: &mut G2d) { |
| let rec = Rectangle::new(color).color(color).shape(shape); |
| let gui_x = to_coord(x); |
| let gui_y = to_coord(y); |
| let rectangle = [gui_x, gui_y, BLOCK_SIZE, BLOCK_SIZE]; |
| rec.draw(rectangle, &DrawState::default(), con.transform, g) |
| } |
| |
| |
| pub fn draw_rectangle( |
| color: Color, |
| x: i32, |
| y: i32, |
| width: i32, |
| height: i32, |
| con: &Context, |
| g: &mut G2d, |
| ) { |
| let gui_x = to_coord(x); |
| let gui_y = to_coord(y); |
| let width = to_coord(width); |
| let height = to_coord(height); |
| rectangle(color, [gui_x, gui_y, width, height], con.transform, g); |
| } |
Cargo.toml
| name = "snake" |
| version = "0.1.0" |
| edition = "2021" |
| |
| |
| |
| [dependencies] |
| rand = "0.8.5" |
| piston_window = "0.127.0" |
Rust官网
Rust 中文社区
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!