In this article, we’ll finish the snake.rs file, and also continue with the rest of the files (main.rs, draw.rs, game.rs).
欢迎来到本教程的第二部分,在本文中,我们将完成 snake.rs 文件,并继续处理其余文件(main.rs、draw.rs、game.rs)。
查看第一部分:
Rust教程:贪吃蛇游戏(第 1/2 部分)-CSDN博客
snake.rs
As a reminder from the [1st part], we had finished working with the functions draw, head_position and move_forward in the snake.rs file.
作为[第 1 部分] 的提醒,我们已经完成了 Snake.rs 文件中的函数 draw 、 head_position 和 move_forward 的使用。
Functions: head_direction, next_head, restore_tail and overlap_tail
函数: head_direction 、 next_head 、 restore_tail 和 overlap_tail
Time to create a new function that will allow us to take in our snake or a reference to our snake and then get a direction.
是时候创建一个新函数了,它允许我们接收蛇或对蛇的引用,然后获得方向。
pub fn head_direction(&self) -> Direction {self.direction
} Alright, so we want another method, I’m going to call it next_head. This will take in a reference to &self and an Option<Direction>, and then it will output a tuple of i32. So we'll say let (head_x, head_y): (i32, i 32) and then we'll get the head_position using our head_position method.
好吧,我们想要另一种方法,我将其命名为 next_head 。这将接受对 &self 和 Option<Direction> 的引用,然后输出 i32 的元组。因此,我们会说 let (head_x, head_y): (i32, i 32) ,然后我们将使用 head_position 方法获取 head_position 。
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),}} We’ll get the snake direction with the mutable moving direction let mut moving_dir = self.direction; and then we're going to match on the direction that we're passing into the method.
我们将通过可变的移动方向 let mut moving_dir = self.direction; 获得蛇的方向,然后我们将在传递给方法的方向上 match 。
Then we’re going to match again on this new moving_dir, this will help with accuracy.
然后我们将在这个新的 moving_dir 上再次 match ,这将有助于提高准确性。
Finally, we have two more methods we want to create. Create another public function called restore_tail. It will take in a reference to our mutable Snake. We'll also create a block which will be based on our tail. Then we're going to push_back our cloned tail into the back of our body.
最后,我们还有两个要创建的方法。创建另一个名为 restore_tail 的公共函数。它将引用我们可变的 Snake。我们还将创建一个基于我们的尾巴的块。然后我们将 push_back 我们克隆的尾巴放入我们的身体后部。
Basically, as you know the tail doesn’t get rendered unless we eat an apple. So if we eat an apple this method will be run and the tail will be pushed into our linked list body. This is how our snake is growing in size.
基本上,如你所知,除非我们吃苹果,否则尾巴不会被渲染。因此,如果我们吃一个苹果,该方法将运行,并且尾部将被推入我们的链表主体中。我们的蛇就是这样长大的。
pub fn restore_tail(&mut self) {let blk = self.tail.clone().unwrap();self.body.push_back(blk);} Last but not least, we have our last method for this file. Let’s call this method overlap_tail. It will take in our Snake an x and a y, then we will pass back a boolean.Let's also create a mutable value and set it to equal to zero. We'll iterate through our snake body and we'll check to see if x equals block.x and if y equals block.x. So in other words:
最后但并非最不重要的一点是,我们有这个文件的最后一个方法。我们将此方法称为 overlap_tail 。它将接受我们的 Snake 一个 x 和一个 y ,然后我们将传回一个 boolean 。我们还创建一个可变值并将其设置为等于零。我们将迭代我们的蛇体,并检查 x 是否等于 block.x 以及 y 是否等于 block.x 。换句话说:
- If our snake is overlapping with any other part of its actual body then we’ll
return true.
如果我们的蛇与其实际身体的任何其他部分重叠,那么我们将return true。 - Otherwise, we’re going to increment
ch.
否则,我们将增加ch。
Then we’re going to check if ch equals == self.body.len() - 1, what we're doing with this part of our method is checking to see if our snake is actually overpassing the tail. If the tail and the head overlap in the same block there is actually a moment where the head will be in that block and so will the tail and we don't want this to cause a failure state so we break.
然后我们将检查 ch 是否等于 == self.body.len() - 1 ,我们在这部分方法中所做的就是检查我们的蛇是否实际上越过了尾巴。如果尾部和头部在同一个块中重叠,实际上有一段时间头部将在该块中,尾部也会在该块中,我们不希望这导致失败状态,所以我们 break 。
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;} That’s it for our snake file! Woohoo! Take a moment to reflect on the code we wrote so far, cause quite honestly we have a few more functions to write in the other files! 😊
这就是我们的蛇文件!呜呼!花点时间反思一下我们到目前为止编写的代码,因为老实说我们还有一些函数要在其他文件中编写! 😊
game.rs
Let’s go to the game.rs file. Same as with our other files we want to come into our main file and type mod game; to link it up with our project.
让我们转到 game.rs 文件。与我们的其他文件一样,我们希望进入 main 文件并输入 mod game; 将其与我们的项目链接起来。
Then, back on the game.rs files, we want to import all of the piston_window (that's why we'll use the asterisk).
然后,回到 game.rs 文件,我们要导入所有 piston_window (这就是我们使用星号的原因)。
We also want the random library and we want to get out thread_rng as it allows us to create a thread local random number generator (this way we're using our operating system to create a random number). We're also bringing in the Rng .
我们还需要 random 库,并且希望使用 thread_rng ,因为它允许我们创建一个线程本地随机数生成器(这样我们就可以使用我们的操作系统来创建一个随机数)。我们还引入了 Rng 。
use piston_window::types::Color;
use piston_window::*; use rand::{thread_rng, Rng}; Then we also want to bring in our Snake direction and then the Snake itself.
然后我们还想引入蛇的方向,然后引入蛇本身。
use crate::snake::{Direction, Snake};
And we also want to bring in our Draw block and our Draw rectangle functions.
我们还想引入 Draw 块和 Draw 矩形函数。
use crate::draw::{draw_block, draw_rectangle};
We want to create 3 constants:
我们想要创建 3 个常量:
FOOD_COLOR: This will be red, so 0.8 and it will have an opacity of 1.FOOD_COLOR:这将是红色,因此为 0.8,并且不透明度为 1。BORDER_COLOR: This will be completely black.BORDER_COLOR:这将是全黑的。GAMEOVER_COLOR: This will be 0.9 so it will be red again, but it will have an opacity of 0.5.GAMEOVER_COLOR:这将是 0.9,因此它将再次变为红色,但其不透明度为 0.5。
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.5]; Then we also want to create 2 other constants.
然后我们还想创建另外 2 个常量。
MOVING_PERIOD: This is essentially the frames per second that our snake will move at.MOVING_PERIOD:这本质上是我们的蛇移动的每秒帧数。RESTART_TIME: The restart time is 1 second. When we hit a failure state with our snake this will pause the game for one second before resetting it. If you find this to be too fast you can fiddle around with it.RESTART_TIME:重启时间为1秒。当我们的蛇遇到失败状态时,这将使游戏暂停一秒钟,然后再重置。如果你发现这太快了,你可以摆弄它。
const MOVING_PERIOD: f64 = 0.1;
const RESTART_TIME: f64 = 1.0; Alright, now we’re going to create a new struct called Game. This will have a snake in it but also the food which will be a boolean. If food_exists on the board then we don't need to spawn more. We'll have the food_x and food_y coordinates, and then we'll have the width and the height of the actual game board. Finally, we'll have the game state ( game_over) as a boolean and the waiting_time which is the restart time up.
好吧,现在我们要创建一个名为 Game 的新 struct 。里面会有一条蛇,还有 boolean 的食物。如果 food_exists 在棋盘上,那么我们不需要生成更多。我们将拥有 food_x 和 food_y 坐标,然后我们将拥有实际游戏板的 width 和 height 坐标。最后,我们将游戏状态 ( game_over ) 作为 boolean 和 waiting_time (重启时间到了)。
pub struct Game {snake: Snake,food_exists: bool,food_x: i32,food_y: i32,width: i32,height: i32,game_over: bool,waiting_time: f64,
} Implementation block Game
实现块 Game
We want to make an implementation block for our game so we can create some methods. We’re going to create a new method so that we can instantiate a new game. This will take in the width and the height of the actual game board itself and then we'll output a Game which will then run the Snake::new(2,2) function (2,2 is 2 units out and 2 units down). Then our waiting_time will be 0 so the snake will automatically start moving. food_exists will be true so the food will spawn and it will spawn at this food_x and food_y. Then we have our width and height, these are the size of the board and then our game_over will be false. When the game is running this will be false and then once we hit a wall or we hit ourselves it will turn to true.
我们想为我们的游戏制作一个实现块,以便我们可以创建一些方法。我们将创建一个 new 方法,以便我们可以实例化一个新游戏。这将接收实际游戏板本身的 width 和 height ,然后我们将输出 Game ,然后运行 Snake::new(2,2) 将是 0 所以蛇会自动开始移动。 food_exists 将是 true ,因此食物将生成,并将在此 food_x 和 food_y 处生成。然后我们有 width 和 height ,它们是板的大小,然后我们的 game_over 将是 false 。当游戏运行时,这将是 false ,然后一旦我们撞到墙壁或撞到自己,它就会变成 true 。
impl Game { pub fn new(width: i32, height: i32) -> Game {Game {snake: Snake::new(2, 2),waiting_time: 0.0,food_exists: true,food_x: 6,food_y: 4,width,height,game_over: false,}} Now we want to create another method called key_pressed, this will allow us to figure out whether or not the user has pressed the key and then react accordingly. So key_pressed takes in a mutable game self and then it takes in a key type. If game_over then we want to just quit but if it's not then we want to match on key and:
现在我们要创建另一个名为 key_pressed 的方法,这将使我们能够确定用户是否按下了 key ,然后做出相应的反应。因此 key_pressed 接受可变游戏 self ,然后接受 key 类型。如果 game_over 那么我们只想退出,但如果不是那么我们想要 match 在 key 上并且:
- If
Key::Up => Some(Direction::Up)then we're going to go up.
如果Key::Up => Some(Direction::Up)那么我们就会上升。 - If
Key::Down => Some(Direction::Down)then we're going to go down.
如果Key::Down => Some(Direction::Down)那么我们就会下降。 - Etc…
Then we’re going to check dir, if dir == self.snake.head_direction().opposite() then we're going to quit out of this function. So for example, if the snake is moving up and we try to hit down then nothing will happen.
然后我们将检查 dir ,如果 dir == self.snake.head_direction().opposite() 那么我们将退出这个函数。举例来说,如果蛇向上移动,而我们尝试向下击打,那么什么也不会发生。
pub fn key_pressed(&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),_ => Some(self.snake.head_direction()),};if let Some(dir) = dir {if dir == self.snake.head_direction().opposite() {return;}}self.update_snake(dir);} Alright, as you can see above, in the last line, I have the self.update_snake(dir);, but we haven't written it yet. We'll do that pretty soon... Keep reading and coding with me.
好吧,正如你在上面看到的,在最后一行,我有 self.update_snake(dir); ,但我们还没有写它。我们很快就会做到这一点...继续和我一起阅读和编码。
Let’s create a public draw function. It will take in a reference to our game board, the context and our graphics buffer. First, we're going to call self.snake.draw and what this will do is to iterate through our linked list and then draw_block based on those linked lists. Then we're going to check and see if food_exists. If this comes back as true then we're going to draw_block with the FOOD_COLOR , self.food.x and self.food.y.
让我们创建一个公共 draw 函数。它将引用我们的游戏板、上下文和图形缓冲区。首先,我们将调用 self.snake.draw ,这将迭代我们的链接列表,然后基于这些链接列表迭代 draw_block 。然后我们将检查是否 food_exists 。如果返回为 true 那么我们将使用 FOOD_COLOR 、 self.food.x 和 self.food.y 来 draw_block 。
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_rectangle(BORDER_COLOR, 0, 0, self.width, 1, con, g);draw_rectangle(BORDER_COLOR, 0, self.height - 1, self.width, 1, con, g);draw_rectangle(BORDER_COLOR, 0, 0, 1, self.height, con, g);draw_rectangle(BORDER_COLOR, self.width - 1, 0, 1, self.height, con, g);if self.game_over {draw_rectangle(GAMEOVER_COLOR, 0, 0, self.width, self.height, con, g);}} Then we’re going to draw the borders and finally, we will run another check: if self.game_over then we want to draw the entire screen.
然后我们将绘制边框,最后,我们将运行另一项检查: if self.game_over 然后我们要绘制整个屏幕。
All right, now we’re going to make an update function. We'll pass our game state as a mutable and then a time ( delta_time: f64). Then we're going to iterate our waiting_time and if the game is over and if self.waiting_time > RESTART_TIME then restart the game. We'll use this function restart , we haven't written it yet, but keep it up and you'll write it soon with me! Otherwise, we're just going to return .
好吧,现在我们要创建一个 update 函数。我们将把游戏状态作为可变参数传递,然后传递时间 ( delta_time: f64 )。然后我们将迭代 waiting_time ,如果游戏结束,如果 self.waiting_time > RESTART_TIME 则重新启动游戏。我们将使用这个函数 restart ,我们还没有写它,但是继续坚持,你很快就会和我一起写它!否则,我们只会转到 return 。
If the food does not exist then we’re going to call the add_food method (we'll write it soon). Then we're going to update the snake ( update_snake~ see the function below).
如果食物不存在,那么我们将调用 add_food 方法(我们很快就会写)。然后我们将更新蛇( update_snake ~ 请参阅下面的函数)。
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);}} Now let’s check and see if the snake has eaten. We have a new function check_eating which takes the mutable game state. We're going to find the head_x and head_y of the head using our head_position method. Then we're going to check if the food_exists and if self.food_x == head_x && self.food_y == head_y. If the head overlaps with our food then we're going to say that food doesn't exist anymore (false) and call our restore_tail function. In other words, our snake is going to grow one block!
现在让我们检查一下蛇是否吃过东西。我们有一个新函数 check_eating ,它采用可变的游戏状态。我们将使用 head_position 方法找到头部的 head_x 和 head_y 。然后我们将检查是否 food_exists 和 self.food_x == head_x && self.food_y == head_y 。如果头部与我们的食物重叠,那么我们会说食物不再存在( false )并调用我们的 restore_tail 函数。换句话说,我们的蛇会长一格!
fn check_eating(&mut self) {let (head_x, head_y): (i32, i32) = 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();}} Now we want to check if the snake is alive! We have a new function check_if_snake_alive and we pass in our reference to self and then an Option of Direction , we're also going to pass back a boolean. We're going to check if the snake head overlaps with the tail self.snake.overlap_tail(next_x, next_y) , in this case, we'll return false. If we go out of bounds of the window then the game will end and it will restart after a second.
现在我们要检查蛇是否还活着!我们有一个新函数 check_if_snake_alive ,我们传入对 self 的引用,然后传入 Direction 的 Option ,我们也将传回 boolean 。我们将检查蛇头是否与尾部重叠 self.snake.overlap_tail(next_x, next_y) ,在本例中,我们将 return false 。如果我们超出了窗口范围,那么游戏将结束并在一秒钟后重新开始。
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} Now let’s actually add the food! The add_food is the method that we were calling in the update function. It takes a mutable game state and then we create an rng element and call our thread_rng . We'll check if the snake is overlapping with the tail (we don't want the snake to overall with the apple), and then we'll set the food_x and food_y and also the food_exists to true.
现在让我们实际添加食物! add_food 是我们在 update 函数中调用的方法。它需要一个可变的游戏状态,然后我们创建一个 rng 元素并调用 thread_rng 。我们将检查蛇是否与尾巴重叠(我们不希望蛇与苹果整体重叠),然后我们将设置 food_x 和 food_y 和还有 food_exists 到 true 。
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;} Perfect, we’re getting closer! We just need a few more functions.
完美,我们越来越近了!我们只需要更多的功能。
Let’s create the update_snake function which was mentioned above, in the update and key_pressed functions. We pass in our reference to self and then an Option of Direction. We'll check if the snake is alive, and if it is then we'll move_forward and check for eating, if it's not the game_over becomes true and we set the waiting_time to 0.0.
让我们在 update 和 key_pressed 函数中创建上面提到的 update_snake 函数。我们传入对 self 的引用,然后传入 Direction. 的 Option 我们将检查蛇是否还活着,如果是的话我们将< b6> 并检查是否吃东西,如果不是,则 game_over 变为 true ,我们将 waiting_time 设置为 0.0 。
fn update_snake(&mut self, dir: Option<Direction>) {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;} Let’s also write the restart method that we saw in the restart function. We pass in our reference to self and then we create a new Snake game, and set all the other parameters as well (like wating_time, food_exists, etc). This is very similar to the new function. The reason we don't call it it's because we don't want to render a new window every time the game resets!
我们还编写在 restart 函数中看到的 restart 方法。我们传入对 self 的引用,然后创建一个新的贪吃蛇游戏,并设置所有其他参数(如 wating_time 、 food_exists 等) 。这与 new 函数非常相似。我们不这么称呼它的原因是因为我们不想每次游戏重置时都渲染一个新窗口!
main.rs
Alright! Time to move on to main.rs.
好吧!是时候转到 main.rs 了。
Make sure you have imported the piston_window and the crates game and draw. We also want a CONST for BACK_COLOR (the color looks like gray):
确保您已导入 piston_window 以及包 game 和 draw 。我们还想要 BACK_COLOR 的 CONST (颜色看起来像灰色):
use piston_window::*;
use piston_window::types::Color; use crate::game::Game;
use crate::draw::to_coord_u32; const BACK_COLOR: Color = [0.5, 0.5, 0.5, 1.0]; Note the to_coord_u32 function. This is very similar to to_coord from draw.rs except here we don't want to return an f64 but a u32.
请注意 to_coord_u32 函数。这与 draw.rs 中的 to_coord 非常相似,只不过这里我们不想返回 f64 而是 u32 。
In the fn main() we'll get the width and the height and set it to (20, 20) (you can obviously set it to whatever you prefer), then we're going to create a mutable window which will be a PistonWindow and we'll create: a Snake game, a game window ([to_coord_u32(width), to_coord_u32(height)] ), we want to build the actual window and finally we have the unwrap to deal with any errors.
在 fn main() 中,我们将获取 width 和 height 并将其设置为 (20, 20) (显然,您可以将其设置为您想要的任何值)更喜欢),然后我们将创建一个可变窗口,它将是 PistonWindow 我们将创建:一个贪吃蛇游戏,一个游戏窗口( [to_coord_u32(width), to_coord_u32(height)] ),我们想要 build 实际窗口,最后我们有 unwrap 来处理任何错误。
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();...
} Then we’ll create a new Game with width and height. If the player presses a button, we're going to call the press_args and then pass a key in key_pressed , otherwise, we're going to draw_2d and pass in the event, clear the window and then draw the game.
然后我们将使用 width 和 height 创建一个新游戏。如果玩家按下按钮,我们将调用 press_args ,然后在 key_pressed 中传递 key ,否则,我们将 draw_2d 并传入事件, clear 窗口,然后 draw 游戏。
Lastly, we’re going to update the game with arg.dt.
最后,我们将使用 arg.dt update 进行游戏。
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);});} That’s it, our game is finished! 👏👏
就这样,我们的游戏就完成了! 👏👏
Run the Game 运行游戏
You can run in your terminal cargo check to check if there are any errors and then cargo run to play the game! Enjoy and congrats on building it.
您可以在终端中运行 cargo check 检查是否有错误,然后 cargo run 开始玩游戏!享受并祝贺构建它。
Thank you for staying with me in this long, 2-parts, tutorial.
感谢您和我一起阅读这个由两部分组成的长教程。
第一部分:
Rust教程:贪吃蛇游戏(第 1/2 部分)-CSDN博客
Find the code here. 在这里找到代码。
EleftheriaBatsou/snake-game-rust (github.com)