我们要做的是一个像素块画的贪吃蛇小游戏,效果如下图。
image.png

需求设计

我们都玩过贪吃蛇,在屏幕上随机出现一个苹果,我们要操控蛇去吃到苹果,当蛇吃到苹果后蛇的长度会加一。随着蛇的长度的增加,当蛇碰到自己或者碰到墙游戏就算结束。

依赖

rand:我们需要使用rand的随机数包来设置苹果的位置。
piston_window:这个包是用来调用openGL的,通过这个包可以快速的创建游戏的内容。

项目目录

image.png四个文件分别为,snake.rs用来创建我们的蛇,draw.rs用来创建我们的绘画方法,game.rs用来写我们这个游戏的规则,main.rs用来运行游戏。马上开始写。

draw.rs

  1. use piston_window::{rectangle,Context,G2d};
  2. use piston_window::types::Color;
  3. const BLOCK_SIZE:f64 = 25.0;
  4. pub fn to_coord(game_coord:i32) -> f64 {//因为该游戏是由像素块组成的,所以每一个提供的数组都需要乘以像素块的宽高,以定位到具体位置
  5. (game_coord as f64) * BLOCK_SIZE
  6. }
  7. pub fn to_coord_u32(game_coord:i32) -> u32 {
  8. to_coord(game_coord) as u32
  9. }
  10. pub fn draw_block(color:Color,x:i32,y:i32,con:&Context,g: &mut G2d){ // 该方法是画像素块的方法
  11. let gui_x = to_coord(x);
  12. let gui_y = to_coord(y);
  13. rectangle( //rectangle结构体有四个参数 颜色,[x坐标,y坐标,宽度,高度],绘制方式,画布
  14. color,
  15. [gui_x,gui_y,BLOCK_SIZE,BLOCK_SIZE],
  16. con.transform,
  17. g,
  18. );
  19. }
  20. pub fn draw_rectangle(color:Color,x:i32,y:i32,width:i32,height:i32,con:&Context,g:&mut G2d){ //该方法用来画游戏背景
  21. let x = to_coord(x);
  22. let y = to_coord(y);
  23. rectangle(
  24. color,
  25. [
  26. x,
  27. y,
  28. BLOCK_SIZE * (width as f64),
  29. BLOCK_SIZE * (height as f64),
  30. ],
  31. con.transform,
  32. g,
  33. )
  34. }

Snake.rs

  1. use std::collections::LinkedList; //我们使用链表的数据结构来存储我们的蛇
  2. use piston_window::{Context,G2d};
  3. use piston_window::types::Color;
  4. use crate::draw::draw_block;
  5. const SNAKE_COLOR: Color = [0.00,0.80,0.00,1.0]; //定义蛇的颜色
  6. pub struct Snake {
  7. direction:Direction, //我们需要蛇在移动的时候的方向
  8. body:LinkedList<Block>,//用LinkedList来存储蛇的身体,其实就是一个x和y的坐标,我们用Block表示
  9. tail:Option<Block>,//储存蛇的尾巴,因为吃到苹果后蛇的身体从尾部增加一个所以要记住尾部。因为目前的移动方式是加头去尾的方式开始移动之后就会有值存在这里
  10. }
  11. #[derive(Debug,Clone)]
  12. struct Block {
  13. x:i32,
  14. y:i32,
  15. }
  16. #[derive(Copy,Clone,PartialEq)]
  17. pub enum Direction { //我们用枚举来写出四个方向
  18. Up,
  19. Down,
  20. Left,
  21. Right,
  22. }
  23. impl Direction {
  24. pub fn opposite(&self) -> Direction { //给方向设置一个方法,即当我们按的方向与当前蛇运行的方向相反时。我们不做任何操作
  25. match *self {
  26. Direction::Up => Direction::Down,
  27. Direction::Down => Direction::Up,
  28. Direction::Left => Direction::Right,
  29. Direction::Right => Direction::Left,
  30. }
  31. }
  32. }
  33. impl Snake { //给蛇结构体定义方法
  34. pub fn new(x:i32,y:i32)->Snake { //初始化 蛇的长度是三
  35. let mut body:LinkedList<Block> = LinkedList::new();
  36. body.push_back(Block{ //push_back 向List后方添加元素 push_fornt 向List前方添加元素
  37. x:x+2,
  38. y
  39. });
  40. body.push_back(Block{
  41. x:x+1,
  42. y
  43. });
  44. body.push_back(Block{
  45. x:x,
  46. y
  47. });
  48. Snake{
  49. direction:Direction::Right,
  50. body,
  51. tail:None,
  52. }
  53. }
  54. pub fn draw(&self,con:&Context,g:&mut G2d){ //遍历List 把蛇的每一部分画出来
  55. for block in &self.body {
  56. draw_block(SNAKE_COLOR, block.x, block.y, con, g)
  57. }
  58. }
  59. pub fn head_position(&self) -> (i32,i32) {
  60. let head_block = self.body.front().unwrap(); //取List的头节点 放回一个Result 所以要用upwrap()
  61. (head_block.x,head_block.y)
  62. }
  63. pub fn move_forward(&mut self,dir:Option<Direction>) { //蛇的移动
  64. match dir { //判断是由调整了方向
  65. Some(d) => self.direction = d,
  66. None=>(),
  67. }
  68. //移动的策略是向链表的头节点新增一个节点,在尾节点处去除一个节点
  69. let (head_x,head_y):(i32,i32) = self.head_position();
  70. //根据移动方向调整新加节点的位置
  71. let new_block = match self.direction {
  72. Direction::Up => Block{
  73. x:head_x,
  74. y:head_y-1,
  75. },
  76. Direction::Down => Block{
  77. x:head_x,
  78. y:head_y+1,
  79. },
  80. Direction::Left => Block{
  81. x:head_x-1,
  82. y:head_y,
  83. },
  84. Direction::Right=> Block{
  85. x:head_x+1,
  86. y:head_y,
  87. },
  88. };
  89. //把新节点添加到链表中
  90. self.body.push_front(new_block);
  91. //把尾节点吐出来
  92. let removed_block = self.body.pop_back().unwrap();
  93. self.tail = Some(removed_block);
  94. }
  95. pub fn head_direction(&self)->Direction {
  96. self.direction
  97. }
  98. //判断蛇头的下一个位置
  99. pub fn next_head(&self,dir:Option<Direction>) -> (i32,i32) {
  100. let (head_x,head_y):(i32,i32) = self.head_position();
  101. let mut moving_dir = self.direction;
  102. match dir {
  103. Some(d) => moving_dir =d,
  104. None => {}
  105. };
  106. match moving_dir {
  107. Direction::Up => (head_x,head_y-1),
  108. Direction::Down => (head_x,head_y+1),
  109. Direction::Left => (head_x-1,head_y),
  110. Direction::Right => (head_x+1,head_y),
  111. }
  112. }
  113. pub fn restore_tail(&mut self){ // 当吃到苹果的时候,把因为移动而去掉的尾巴加回来
  114. let blk = self.tail.clone().unwrap();
  115. self.body.push_back(blk);
  116. }
  117. //判断下一个位置的蛇头是不是会碰到蛇身体
  118. pub fn overlap_tail(&self,x:i32,y:i32) -> bool {
  119. let mut ch = 0;
  120. for block in &self.body {
  121. if x == block.x && y == block.y {
  122. return true
  123. }
  124. ch += 1;
  125. if ch == self.body.len() -1 {
  126. break;
  127. }
  128. }
  129. return false;
  130. }
  131. }

game.rs

  1. use piston_window::*;
  2. use piston_window::types::Color;
  3. use rand::{thread_rng,Rng};
  4. use crate::snake::{Direction,Snake};
  5. use crate::draw::{draw_block,draw_rectangle};
  6. //设置颜色
  7. const FOOD_COLOR:Color= [0.80,0.00,0.00,1.0];
  8. const BORDER_COLOR:Color= [0.00,0.00,0.00,1.0];
  9. const GAMEOVER_COLOR:Color= [0.90,0.00,0.00,1.0];
  10. //设置速度和重开时间
  11. const MOVING_PERIOD: f64 = 0.1;
  12. const RESTART_TIME:f64 = 1.0;
  13. //Game结构体
  14. pub struct Game {
  15. snake:Snake,
  16. food_exists:bool,
  17. food_x:i32,
  18. food_y:i32,
  19. width:i32,
  20. height:i32,
  21. game_over:bool,
  22. waiting_time:f64,
  23. }
  24. impl Game {
  25. //初始化Game
  26. pub fn new(width:i32,height:i32) -> Game {
  27. Game{
  28. snake:Snake::new(2,2),
  29. waiting_time:0.0,
  30. food_exists:true,
  31. food_x:6,
  32. food_y:4,
  33. width,
  34. height,
  35. game_over:false
  36. }
  37. }
  38. //监听键盘 操作
  39. pub fn key_press(&mut self,key:Key){
  40. if self.game_over{
  41. return
  42. }
  43. let dir = match key {
  44. Key::Up => Some(Direction::Up),
  45. Key::Down => Some(Direction::Down),
  46. Key::Left => Some(Direction::Left),
  47. Key::Right => Some(Direction::Right),
  48. _ => None
  49. };
  50. if dir.unwrap() == self.snake.head_direction().opposite() {
  51. return
  52. }
  53. self.update_snake(dir);
  54. }
  55. //将蛇 苹果 边框 GameOver都画出来
  56. pub fn draw(&self,con:&Context,g:&mut G2d){
  57. self.snake.draw(con, g);
  58. if self.food_exists {
  59. draw_block(FOOD_COLOR, self.food_x, self.food_y, con, g);
  60. }
  61. //四边边框
  62. draw_rectangle(BORDER_COLOR,0,0, self.width, 1, con, g);
  63. draw_rectangle(BORDER_COLOR,0,self.width-1, self.width, 1, con, g);
  64. draw_rectangle(BORDER_COLOR,0,0, 1, self.height, con, g);
  65. draw_rectangle(BORDER_COLOR,self.width-1,0, 1, self.height, con, g);
  66. //GameOver
  67. if self.game_over {
  68. draw_rectangle(GAMEOVER_COLOR, 0, 0, self.width, self.height, con, g)
  69. }
  70. }
  71. //检查是否gameover 吃到苹果 以及生么也不操作时候的如何更新
  72. pub fn update(&mut self,delta_time:f64){
  73. self.waiting_time += delta_time;
  74. if self.game_over {
  75. if self.waiting_time > RESTART_TIME {
  76. self.restart();
  77. }
  78. return
  79. }
  80. if !self.food_exists {
  81. self.add_food();
  82. self.update_snake(None);
  83. }
  84. if self.waiting_time > MOVING_PERIOD{
  85. self.update_snake(None);
  86. }
  87. }
  88. //检查是否吃到苹果了
  89. fn check_eating(&mut self){
  90. let (head_x,head_y):(i32,i32) = self.snake.head_position();
  91. if self.food_exists && self.food_y == head_y && self.food_x == head_x {
  92. self.food_exists = false;
  93. self.snake.restore_tail();
  94. }
  95. }
  96. //检查蛇是否活着
  97. fn check_if_snake_alive(&self,dir:Option<Direction>) -> bool {
  98. let (next_x,next_y) = self.snake.next_head(dir);
  99. if self.snake.overlap_tail(next_x, next_y) {
  100. return false;
  101. }
  102. next_x > 0 && next_y > 0 && next_x < self.width -1 && next_y < self.height -1
  103. }
  104. //添加随机位置的苹果
  105. fn add_food(&mut self) {
  106. let mut rng = thread_rng();
  107. let mut new_x = rng.gen_range(1..=(self.width - 1));
  108. let mut new_y = rng.gen_range(1..=(self.height - 1));
  109. while self.snake.overlap_tail(new_x,new_y) {
  110. new_x = rng.gen_range(1..=(self.width - 1));
  111. new_y = rng.gen_range(1..=(self.height - 1));
  112. }
  113. self.food_x = new_x;
  114. self.food_y = new_y;
  115. self.food_exists = true;
  116. }
  117. //更新蛇的位置 同时检查是否吃到苹果
  118. fn update_snake(&mut self, dir:Option<Direction>) {
  119. if self.check_if_snake_alive(dir) {
  120. self.snake.move_forward(dir);
  121. self.check_eating();
  122. } else {
  123. self.game_over = true;
  124. }
  125. self.waiting_time = 0.0;
  126. }
  127. //重开
  128. fn restart (&mut self){
  129. self.snake = Snake::new(2,2);
  130. self.waiting_time = 0.0;
  131. self.food_exists = true;
  132. self.food_x = 6;
  133. self.food_y = 4;
  134. self.game_over = false;
  135. }
  136. }

main.rs

extern crate rand;
extern crate piston_window;

mod draw;
mod snake;
mod game;

use piston_window::*;
use piston_window::types::Color;
use game::Game;
use draw::to_coord_u32;

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

fn main() {
    let (width,height) = (30,30);
    //  https://docs.rs/piston_window/latest/piston_window/struct.PistonWindow.html 查看PistonWindow结构体的作用
    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);
    //一直监听事件 一直根据arg.dt的时间更新 
    while let Some(event) = window.next() {
        if let Some(Button::Keyboard(key)) = event.press_args() {
            game.key_press(key);
        }
        window.draw_2d(&event, |c,g,_|{
            clear(BACK_COLOR, g);
            game.draw(&c, g);
        });

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