Rust小白自练手项目——贪吃蛇

序:rust小白通过写博客来记录自己的学习过程,降低遗忘,也强迫自己养成写博客的习惯

1. 贪吃蛇游戏长什么样子

图片来源于网络
图片来源于网络

可以看出,基本的贪吃蛇游戏有几个部分,1. 背板;2. 蛇;3. 食物。知道了这些我们就可以进行贪吃蛇的设计了。

2. 贪吃蛇游戏的设计

2.1 方块

显然可以看出,无论是食物、蛇还是背景板,其基本构成都是方块。食物和背景板是一个方块,蛇是很多个方块。因此,先写一个方块struct

const BLOCK_SIZE: f64 = 25.0;

#[derive(Debug, Clone)]
pub struct Block {
    pub x: i32,
    pub y: i32,
}

x、y是方块位置坐标,方块大小就使用一个常量BLOCK_SIZE了,毕竟蛇身体每一节和食物的方块的一样大。当然,我们需要把方块位置映射具体一点:

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, x: i32, y: i32, con: &Context, g: &mut G2d) {
    let gui_x = to_coord(x);
    let gui_y = to_coord(y);

    rectangle(
        color,
        [gui_x, gui_y, BLOCK_SIZE, BLOCK_SIZE],
        con.transform,
        g,
    );
}
/// 绘制一个矩形
pub fn draw_rangtangle(
    color: Color,
    x: i32,
    y: i32,
    width: i32,
    height: i32,
    con: &Context,
    g: &mut G2d,
) {
    let x = to_coord(x);
    let y = to_coord(y);

    rectangle(
        color,
        [
            x,
            y,
            BLOCK_SIZE * (width as f64),
            BLOCK_SIZE * (height as f64),
        ],
        con.transform,
        g,
    )
}

好了,我们已经完成方块的绘制了,很多事件就显然了,比如说食物就是随机在面板的一个位置绘制一个方块,这个问题暂且不表。下面来看看难一点的snake需要怎么处理。

2.2 蛇的处理

想明白一点,蛇的本质是什么?蛇其实就是一些方块组成的集合罢了。那选用什么数据结构来存储呢,常见数据结构有arraystackqueuesetmap。容易想到蛇移动一个位置,就是蛇头前面一个方块进入蛇,蛇尾一个方块离开蛇,这样很容易想到一种数据结构:queue。因此可以描述队列的结构都行,我们使用LinkList描述蛇的身体,当然也可以使用VecDeque

pub struct Snake {
    direction: Direction,
    body: LinkedList<Block>,
    tail: Option<Block>,
}

这里面还包含着蛇的一个重要特性:移动方向。这是一个枚举,显然只有上、下、左、右咯,

#[derive(Clone, Copy, PartialEq)]
pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

我们知道,如果蛇头向上,那它下一步可以向左、右、上移动,但是不可以向反方向移动,我们在Direction上实现一个关联函数表示其反方向,

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,
        }
    }
}

接下来,我们来实现蛇的一些逻辑,

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 });
    body.push_back(Block { x: x + 1, y });
    body.push_back(Block { x, y });

    Snake {
        direction: Direction::Right,
        body,
        tail: None,
    }
}

这条蛇身长3个单位,横着占连续3个方块,而且是向右移动的。

然后我们需要一个方法,将这条蛇绘制出来,显然就是遍历LinkList,然后调用绘制Block的方法了,没啥难度。

pub fn draw(&self, con: &Context, g: &mut G2d) {
    for block in &self.body {
        draw_block(SNAKE_COLOR, block.x, block.y, con, g)
    }
}

给一个蛇头位置的函数,蛇下一时刻所在的位置需要当前蛇头位置和运行方向确定,先给出来,

pub fn head_position(&self) -> (i32, i32) {
    let head_block = self.body.front().unwrap();
    (head_block.x, head_block.y)
}

蛇下一时刻位置,不多说,代码清晰易懂。首先拿到蛇头位置(x, y),向上:(x, y-1),向下:(x, y+1),向左:(x-1, y),向右:(x+1, y)

pub fn move_foward(&mut self, dir: Option<Direction>) {
    match dir {
        Some(d) => self.direction = d,
        None => (),
    };
    let (last_x, last_y): (i32, i32) = self.head_position();
    let new_block = match self.direction {
        Direction::Up => Block {
            x: last_x,
            y: last_y - 1,
        },
        Direction::Down => Block {
            x: last_x,
            y: last_y + 1,
        },
        Direction::Left => Block {
            x: last_x - 1,
            y: last_y,
        },
        Direction::Right => Block {
            x: last_x + 1,
            y: last_y,
        },
    };
    self.body.push_front(new_block);
    let removed_block = self.body.pop_back().unwrap();
    self.tail = Some(removed_block);
}

然后需要将下一个时刻所在方块入队,尾巴出队。而且我们的蛇不会一直存活,当蛇撞到墙或者身体时,游戏就结束。我们使用蛇头的下一个位置来描述,这块不清楚就先搁置。

pub fn head_direction(&self) -> Direction {
    self.direction
}

pub fn next_head(&self, dir: Option<Direction>) -> (i32, i32) {
    let (head_x, head_y) = 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),
    }
}

当然,我们的蛇尾巴也不总是像上述移动一样被pop出队,当蛇吃一个食物时就不必被pop出队。因此,我们有这样一个方法,

pub fn restore_tail(&mut self) {
    let blk = self.tail.clone().unwrap();
    self.body.push_back(blk);
}

那么如上所说,怎么判断蛇是不是撞到自己呢,显然,蛇头当前位置和身体重叠呗,怎么办?遍历呗,给这样一个函数,

pub fn overlap_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;
        }
    }
    return false;
}

好了,自此就是蛇的全部功能了,实现起来也不是很难。接下来,我们就要组织游戏了。

3. 游戏

首先定义几个颜色,分别是背景板颜色,食物颜色和游戏结束的颜色

// [红, 绿, 蓝, 透明度]
const FOOD_COLOR: Color = [0.80, 0.00, 0.00, 1.0];
const BORDER_COLOR: Color = [0.00, 0.00, 0.00, 1.0];
const GAMEOVER_COLOR: Color = [0.90, 0.00, 0.00, 0.50];

显然,游戏中有几个要素,要有一个背景板、一条蛇、一个食物,除此之外呢?

pub struct Game {
    snake: Snake,

    food_exists: bool,
    food_x: i32,
    food_y: i32,

    width: i32,
    height: i32,

    game_over: bool,
    waiting_time: f64,
}

来为这个游戏实现些功能吧,

impl Game {

}

3.1 要一个创建游戏的实例的关联函数

pub fn new(width: i32, height: i32) -> Self {
    Game {
        snake: Snake::new(2, 2),
        waiting_time: 0.0,
        food_exists: true,
        food_x: 6,
        food_y: 4,
        width,
        height,
        game_over: false,
    }
}

3.2 实现控制蛇的移动方向

通过按压键盘的上下左右键,实现控制蛇的移动方向,

pub fn key_press(&mut self, key: Key) {
    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),
        _ => None,
    };

    if dir.unwrap() == self.snake.head_direction().opposite() {
        return;
    }
    self.update_snake(dir);
}

如果按键方向和蛇移动方向相反,那么什么也不会发生,否则就更新蛇。

fn update_snake(&mut self, dir: Option<Direction>) {
    if self.check_if_snake_alive(dir) {
        self.snake.move_foward(dir);
        self.check_eating();
    } else {
        self.game_over = true;
    }
    self.waiting_time = 0.0;
}

首先判断蛇是否还活着,不然就直接结束游戏了。或者,让其移动一格,然后判断有没有食物可以吃。

fn check_if_snake_alive(&self, dir: Option<Direction>) -> bool {
    let (next_x, next_y) = self.snake.next_head(dir);

    if self.snake.overlap_tail(next_x, next_y) {
        return false;
    }

    next_x > 0 && next_y > 0 && next_x < self.width - 1 && next_y < self.height - 1
}

判断蛇是否存活的逻辑不再赘述。check_eating的逻辑也是显而易见的,

pub 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, self.food_x, self.food_y, con, g);
    }
    draw_rangtangle(BORDER_COLOR, 0, 0, self.width, 1, con, g);
    draw_rangtangle(BORDER_COLOR, 0, self.height - 1, self.width, 1, con, g);
    draw_rangtangle(BORDER_COLOR, 0, 0, 1, self.height, con, g);
    draw_rangtangle(BORDER_COLOR, self.width - 1, 0, 1, self.height, con, g);

    if self.game_over {
        draw_rangtangle(GAMEOVER_COLOR, 0, 0, self.width, self.height, con, g)
    }
}

这里面包括:绘制蛇,绘制食物,绘制墙体四周,以及如果游戏结束,绘制游戏结束。

4. 启动游戏

const BACK_COLOR: Color = [0.5, 0.5, 0.5, 1.0];

fn main() {
    let (width, height) = (20, 20);    // 20个方块的正方形
    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_press(key);
        }
        // 绘制并更新game
        window.draw_2d(&event, |c, g| {
            clear(BACK_COLOR, g);
            game.draw(&c, g);
        });

        event.update(|args| {
            game.update(args.dt);
        });
    }
}

剩下的几个Game中的函数,

// /Game.rs
const MOVING_PERIOD: f64 = 0.5;
const RESTART_TIME: f64 = 1.0;

pub fn update(&mut self, delta_time: f64) {
    self.waiting_time += delta_time;

    if self.game_over {
        if self.waiting_time > RESTART_TIME {
            self.restart();
        }
        return;
    }
    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.overlap_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 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;
}

自此,我们的贪吃蛇就完成了。

  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值