Snake game features in Rust
Table of contents
Feature 1: Wall-through travel -
Initially, if the snake touched the walls around the game screen it would die and the game would reset.
I removed some constraints to enable the snake to travel through the wall and come out the opposite side first.
So, I updated the move_forward()
function in the implementation of the snake trait.
For the Up Direction:
When the snake is moving up, it decreases its
y
coordinate (last_y - 1
).To handle wrapping, it adds
height
to ensure the value doesn't become negative, and then it takes a moduloheight
to wrap around if the newy
coordinate goes above the top boundary.This effectively moves the snake to the bottom of the screen when it tries to go beyond the top boundary.
pub fn move_forward(&mut self, dir: Option<Direction>, width: i32, height: i32) {
// rest of the code........
// changed code =====>
let new_block= match self.direction {
Direction::Up => Block { x: last_x, y: (last_y - 1 + height) % height },
Direction::Down => Block { x: last_x, y: (last_y + 1) % height },
Direction::Left => Block { x: (last_x - 1 + width) % width, y: last_y },
Direction::Right => Block { x: (last_x + 1) % width, y: last_y },
};
// changed code =====>
// rest of the code
}
Similarly, for other directions, the same logic is applied to accommodate the snake to travel through walls.
Feature 2: In-game Obstacles-
For this, I chose to draw cactuses to match the Desert aesthetic of the game. I already had a draw_rectangle()
function to create the borders with
// Dark green color of cactuses
const OBSTACLE_COLOR: Color = [0.0, 0.3, 0.1, 1.0];
pub struct Obstacle { // co-ordinates
x: i32,
y: i32,
}
pub struct Game {
snake: Snake,
obstacles: Vec<Obstacle>, // a vector of Obstacle co-ordinates
// rest of the properties of game...
}
Now I have included the add_obstacles()
function to the Game struct's implementation block, which takes a mutable self-reference from the Game. This function :
Clears previous obstacles
Loops for 4 obstacles
Takes random x and y coordinates between the window space
If the coordinates overlap with the Snake or any food then take another coordinates.
Now push the coordinates into the obstacles vector.
This function is used while creating any new game in the make_new()
function.
impl Game {
pub fn make_new(width: i32, height: i32) -> Game {
let mut game = Game {snake: Snake::make_new(2, 2), obstacles: Vec::new(), waiting_time: 0.0, food_exists: true,
food_x: 10, food_y: 12, width, height, game_over: false,
};
game.add_obstacles(); // obstacles added
game
}
// ===========>
// rest of the functions.......
// ===========>
pub fn add_obstacles(&mut self) {
self.obstacles.clear(); // clearing any previous obstacles
let mut rng = thread_rng();
for _ in 0..4 { // 4 obstacles
// choose random coordinates within the window
let mut x = rng.gen_range(2..self.width - 2);
let mut y = rng.gen_range(2..self.height - 2);
// checking if the obstacle overlaps with any of the food items or the snake itself
while self.snake.overlap_tail(x, y) || self.check_overlap_with_food(x, y) {
// choosing random coordinates again until it doesn't match with food or snake
x = rng.gen_range(2..self.width - 2);
y = rng.gen_range(2..self.height - 2);
}
self.obstacles.push(Obstacle { x, y });
}
}
}
So, I also add the overlap_tail()
function in the Snake struct implementation block which takes the snake's self-reference and the obstacles' coordinates as parameters.
impl Snake {
// ===========>
// rest of the functions.......
// ===========>
// function to match the coordinate of obstacle with the snake's
pub fn overlap_tail(&self, x: i32,y: i32)-> bool {
for block in &self.body {
if x== block.x && y == block.y{
return true;
}
}
return false;
}
}
and the check_overlap_with_food()
function in the implementation block of the Game struct which takes a self-reference of the Game and the coordinates of the obstacles as parameters.
impl Game {
// ===========>
// rest of the functions.......
// ===========>
// function to match the coordinate of obstacle with the food's
fn check_overlap_with_food(&self, x: i32, y: i32) -> bool {
// creating a vector with food item's coordinates
let food_positions = vec![
(self.food_x, self.food_y),
(self.food_x - 1, self.food_y - 1),
(self.food_x + 1, self.food_y + 1)
];
// creating a vector with obstacle's coordinates
let obstacle_positions = vec![
(x, y),
(x, y + 1), (x, y - 1),
(x, y + 2), (x, y - 2),
(x - 1, y - 1), (x + 1, y + 1)
];
// matching them
for (ox, oy) in obstacle_positions {
if food_positions.contains(&(ox, oy)) {
return true;
}
}
false // default return
}
}
In the draw()
function of the game struct implementation a loop is run through the obstacles vector to draw the cactuses.
impl Game {
// ===========>
// rest of the functions.......
// ===========>
pub fn draw(&self, con: &Context, g: &mut G2d) {
// rest of the code
for obs in &self.obstacles {
draw_rectangle(OBSTACLE_COLOR, obs.x, obs.y-2, 1, 5, con, g);
draw_rotated_rectangle(OBSTACLE_COLOR, obs.x - 1, obs.y - 1, 2.0, 0.4, true, con, g);
draw_rotated_rectangle(OBSTACLE_COLOR, obs.x + 1, obs.y + 1, 2.0, 0.4, false, con, g,);
}
// rest of the code
}
}
Here I have used the draw-rectangle()
function ( previously defined) to create the body of the cactus.
And I have created a new function draw_rotated_rectangle()
that is similar to the draw-rectangle()
function but it creates slanted/rotated rectangles perfect for the arms of the cactuses. Here's the function for reference:
use piston_window::{rectangle, Context, G2d, Transformed};
use piston_window::types::Color;
// ===========>
// rest of the code.......
// ===========>
pub fn draw_rotated_rectangle( color: Color, x: i32, y: i32, width: f32,
height: f32, clockwise: bool, con: &Context, g: &mut G2d, // parameters
) {
let gui_x = to_coord(x);
let gui_y = to_coord(y);
let rotation_angle = if clockwise {
std::f64::consts::FRAC_PI_4 // 45 degrees clockwise
} else {
-std::f64::consts::FRAC_PI_4 // 45 degrees counterclockwise
};
// using the rectangle method from piston window crate
rectangle(
color,
[
gui_x,
gui_y,
BLOCK_SIZE * (width as f64),
BLOCK_SIZE * (height as f64),
],
con.transform
.trans(gui_x, gui_y)
.rot_rad(rotation_angle)
.trans(-gui_x, -gui_y),
g,
);
}
And that's how the cute cactuses are ready. So make sure you avoid them while navigating the snake :)
Follow me for more Rust and other tech content!!
Subscribe to my newsletter
Read articles from Shanit Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by