一个简单的rust项目贪吃蛇

发布时间 2023-04-04 13:05:36作者: 贤云曳贺

一个贪吃蛇游戏的 rust 实现,使用了 piston_window 和 rand crate。
游戏使用 上下左右 方向键进行操控,使用 R 重置游戏,使用 P 进行暂停/启动。

项目结构

·
├── Cargo.lock
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├──snake_game/
│   │  ├── game.rs.rs
│   │  └── mod.rs
│   ├──snake_snake/
│   │   ├── snake.rs
│   │   └── mod.rs
│   └──snake_window/
│       ├──draw.rs
│       └── mod.rs

三个mod.rs 文件

// snake_game/mod.rs
pub mod game;

// snake_snake/mod.rs
pub mod snake;

// snake_window/mod.rs
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,
    /// 食物x坐标
    food_x: i32,
    /// 食物y坐标
    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) {
        // 输入 R 快速重新游戏
        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 => {
                // 输入 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;

/// 将 i32 转为 f64
pub fn to_coord(game_coord: i32) -> f64 {
    (game_coord as f64) * BLOCK_SIZE
}

/// 将 i32 转为 u32
pub fn to_coord_u32(game_coord: i32) -> u32 {
    to_coord(game_coord) as u32
}

/// 块图形绘制
/// * shape : piston_window::rectangle::Shape
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);
}

Rust官网
Rust 中文社区