一个简单的rust项目贪吃蛇
一个贪吃蛇游戏的 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);
}