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 蛇的处理
想明白一点,蛇的本质是什么?蛇其实就是一些方块组成的集合罢了。那选用什么数据结构来存储呢,常见数据结构有array
、stack
、queue
、set
、map
。容易想到蛇移动一个位置,就是蛇头前面一个方块进入蛇,蛇尾一个方块离开蛇,这样很容易想到一种数据结构: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;
}
自此,我们的贪吃蛇就完成了。