I made a small rust snake game in order to teach myself rust. I would like to know what I am doing well and poorly, and how to improve my rust code
Cargo.toml
[package]
name = "snake-rs"
version = "0.1.0"
authors = ["Hurricane996 <[email protected]>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sdl2 = "0.34"
rand = "0.8"
[profile.release]
lto = true
src/main.rs
extern crate sdl2;
extern crate rand;
mod constants;
mod fruit;
mod snake;
mod vector2;
use constants::BOARD_WIDTH;
use constants::BOARD_HEIGHT;
use constants::CELL_SIZE;
use fruit::Fruit;
use snake::Snake;
use vector2::Vector2;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
use std::collections::VecDeque;
use std::time::Duration;
fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem.window("Rust Snake", BOARD_WIDTH*CELL_SIZE, BOARD_HEIGHT*CELL_SIZE)
.position_centered()
.build()
.unwrap();
let mut canvas = window.into_canvas().build().unwrap();
let mut event_pump = sdl_context.event_pump().unwrap();
let mut rng = rand::thread_rng();
let mut snake = Snake::new();
let mut fruit = Fruit::new();
let mut frame_counter = 0;
let mut input_stack = VecDeque::<Vector2>::with_capacity(32);
fruit.mv(&mut rng);
'running: loop {
for event in event_pump.poll_iter() {
match event {
Event::Quit {..} => {
break 'running;
},
Event::KeyDown {keycode,..} => {
match keycode.expect("") {
Keycode::Up | Keycode:: W => {
input_stack.push_front(-Vector2::J);
}
Keycode::Down | Keycode:: S => {
input_stack.push_front(Vector2::J);
}
Keycode::Left | Keycode:: A => {
input_stack.push_front(-Vector2::I);
}
Keycode::Right | Keycode:: D => {
input_stack.push_front(Vector2::I);
}
_ => {}
}
}
_ => {}
}
}
frame_counter+=1;
if frame_counter > Snake::SPEED {
frame_counter = 0;
snake.mv(&mut input_stack, &fruit);
if !snake.safe() {break 'running; }
if snake.is_eating_fruit(&fruit) {
fruit.mv(&mut rng);
}
}
canvas.set_draw_color(Color::BLACK);
canvas.clear();
snake.draw(&mut canvas);
fruit.draw(&mut canvas);
canvas.present();
std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
}
}
src/fruit.rs
use crate::constants::BOARD_WIDTH;
use crate::constants::BOARD_HEIGHT;
use crate::constants::CELL_SIZE;
use crate::vector2::Vector2;
use rand::Rng;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::render::RenderTarget;
pub struct Fruit(pub Vector2);
impl Fruit {
pub fn new() -> Self {
Fruit ( Vector2 {
x: 0,
y: 0
})
}
pub fn mv<R: Rng>(&mut self, rng: &mut R ) {
self.0.x = rng.gen_range(0..BOARD_WIDTH) as i32;
self.0.y = rng.gen_range(0..BOARD_HEIGHT) as i32;
}
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) {
canvas.set_draw_color(Color::RED);
canvas.fill_rect(Rect::new(self.0.x * CELL_SIZE as i32, self.0.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE)).unwrap();
}
}
src/snake.rs
use crate::constants::BOARD_HEIGHT;
use crate::constants::BOARD_WIDTH;
use crate::constants::CELL_SIZE;
use crate::fruit::Fruit;
use crate::vector2::Vector2;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::render::RenderTarget;
use std::collections::VecDeque;
pub struct Snake {
direction: Vector2,
head: Vector2,
body: VecDeque::<Vector2>
}
impl Snake {
pub fn new() -> Self {
let mut s = Snake {
head: Vector2::new((BOARD_WIDTH/2) as i32, (BOARD_HEIGHT/2) as i32),
direction: Vector2::I,
body: VecDeque::<Vector2>::with_capacity((BOARD_WIDTH*BOARD_HEIGHT) as usize),
};
for i in 1..(Self::INITIAL_SIZE) {
let i = i as i32;
s.body.push_back(Vector2::new(s.head.x - i, s.head.y))
}
return s;
}
pub fn mv(&mut self, input_stack: &mut VecDeque::<Vector2>, fruit: &Fruit) {
//this pushes the old head, so the body does not contain the head.
self.body.push_front(self.head);
'process_input: loop {
let maybe_input = input_stack.pop_back();
match maybe_input {
Some(input) => {
if input != self.direction && input != -self.direction {
self.direction = input;
break 'process_input;
}
}
None => { break 'process_input; }
}
}
self.head = self.head + self.direction;
if self.head != fruit.0 {
self.body.pop_back();
}
}
pub fn is_eating_fruit(&self, fruit: &Fruit) -> bool {
self.head == fruit.0 || self.body.iter().any(|&i|i==fruit.0)
}
pub fn safe(&self) -> bool {
!(
self.body.iter().any(|&i|i==self.head) ||
self.head.x < 0||
self.head.y < 0 ||
self.head.x >= BOARD_WIDTH as i32 ||
self.head.y >= BOARD_HEIGHT as i32
)
}
pub fn draw<T: RenderTarget>(&self, canvas: &mut Canvas<T>) {
canvas.set_draw_color(Color::GREEN);
canvas.fill_rect(Rect::new(self.head.x * CELL_SIZE as i32, self.head.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE)).unwrap();
for segment in &self.body {
canvas.fill_rect(Rect::new(segment.x * CELL_SIZE as i32, segment.y * CELL_SIZE as i32, CELL_SIZE, CELL_SIZE )).unwrap();
}
}
pub const SPEED: u32 = 15;
const INITIAL_SIZE: u32 = 3;
}
src/Vector2.rs
use std::ops;
#[derive(Copy,Clone,PartialEq)]
pub struct Vector2 {
pub x: i32,
pub y: i32
}
impl Vector2 {
pub fn new(x: i32, y: i32) -> Vector2 {
Vector2 {
x: x,
y: y
}
}
pub const I : Vector2 = Vector2 {x: 1,y: 0};
pub const J : Vector2 = Vector2 {x: 0,y: 1};
}
impl ops::Add<Vector2> for Vector2 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Vector2 {
x: self.x + rhs.x,
y: self.y + rhs.y
}
}
}
impl ops::Neg for Vector2 {
type Output = Self;
fn neg(self) -> Self::Output {
Vector2 {
x: -self.x,
y: -self.y
}
}
}
src/constants.rs
pub const BOARD_HEIGHT: u32 = 16;
pub const BOARD_WIDTH: u32 = 16;
pub const CELL_SIZE: u32 = 32;
1 Answer 1
First of all, there are some quick things I'd change. extern crate [crate name];
is not needed in the 2018 edition of rust, and as such can be removed. Running clippy
also shows some easy changes - firstly, in the Vector2::new
function, you can change Vector2 { x: x, y: y }
to Vector2 { x, y }
. You can also replace the return s;
with just s
in Snake::new
, as if a semicolon is omitted on the last line of a block, the value is returned.
There are also some style decisions I would personally change. Firstly, running cargo fmt
can clean up a lot of minor issues (such as no spaces between items in the derive macros). Another is your using statements - you can replace
use constants::BOARD_HEIGHT;
use constants::BOARD_WIDTH;
use constants::CELL_SIZE;
with
use constants::{BOARD_HEIGHT, BOARD_WIDTH, CELL_SIZE};
which personally I prefer, but it is up to you. Similar things can be done with the other blocks, such as
use std::collections::VecDeque;
use std::time::Duration;
to
use std::{collections::VecDeque, time::Duration};
Now for code changes:
- The
'running
label inmain
and the'process_input
inSnake::mv
should be removed as there are no nested loops, so a normalbreak
works fine. In general, you only want to use labels for situations like wanting to break out of an outer loop while in an inner loop. - The
.unwrap()
calls can be replaced with.expect([error message])
calls if you still want to panic on failures, but using expect will give more information. This will make it easier to spot what is actually going wrong. - Functions and structs should have doc comments explaining what they're doing. For example,
Vector2::I
could be commented as follows to better explain it. This is not strictly necessary, but most editors will show the text when function calls/constants/structs are hovered over, so it can help a lot with understanding what programs are doing.
/// The unit vector representing (1, 0).
pub const I: Vector2 = Vector2 { x: 1, y: 0 };
- Type annotations on
input_stack
inmain
andbody
inSnake::new
forVecDeque::<Vector2>
can be removed - so
let mut input_stack = VecDeque::<Vector2>::with_capacity(32);
would become
let mut input_stack = VecDeque::with_capacity(32);
And for what you've done well:
- Using traits for operations such as
Add
is a very good habit. - Good use of iterators in functions like
Snake::safe
. Iterators are almost always more idiomatic than alternatives, so it is a good idea to get comfortable to using them (as well as methods likemap
andfold
). - Initializing vectors with a given capacity is very handy for reducing the number of re-allocations, and this is a perfect use case for them - since you know the upper bound of how many items will ever be in the vector.
- Using constants throughout your code (such as
Snake::SPEED
), compared to the alternative of littering magic numbers throughout your code.