Learning Rust by Coding a Game of Snake

snake.rustbridge.com

In this tutorial you will write a very simple game engine for a snake game, while learning the basics of Rust. You can also use and adapt the game engine to write any other old school pixely 2D game.

This tutorial is still in beta. If you find typos or error or have ideas for improvement, please file an issue or a pull request at Rusty-Snake-Book

Installation

Standard Installation of the Rust Compiler and Tools

Linux and MacOS Users

For these platforms, it is recommended to use the Official Rust Installation Process via rustup.rs. When asked, the default settings (including the stable compiler) are suitable for this course.

This will process will install a number of tools required for this course, including rustc, cargo, and rustup.

Windows Users

Windows Users may need to install additional tools. Please refer to the following guide for installation instructions for Windows.

Simple DirectMedia Layer

Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D. It is written in C, and we will only use Rust bindings later on, you need to install the C library separately.

MacOS

brew install sdl2

Ubuntu

sudo apt-get install libsdl2-dev

Windows

Follow the instructions here, following the steps "For Rustup users".

Other Operating Systems

Download and install the development library according to your system.

Cargo and Dependencies

To initialize a new project, run cargo new:

$ cargo new rusty_snake

Cargo generates a new folder called rusty_snake. cd into the directory to see what cargo generated.

Open the file cargo.toml in the editor of your choice. This is called a manifest, and it contains all of the metadata that Cargo needs to compile your project.

It should look like this:

cargo.toml


#![allow(unused_variables)]
fn main() {
[package]
name = "rusty-snake"
version = "0.1.0"
authors = ["Your Name"]
edition = "2018"

See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

}

To use SDL2 in rust, we need to add the sdl2 crate as a dependency.

Add the following line in the [dependencies] section: sdl2 = "0.30.0"

open src/main.rs

Substitute the content of the file with the following code:

// Dependencies go here




// this function initializes the canvas
fn init(width: u32, height: u32) -> (Canvas<Window>, EventPump) {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window = video_subsystem
        .window("Game", width + 1, height + 1)
        .position_centered()
        .build()
        .unwrap();

    let mut canvas = window.into_canvas().present_vsync().build().unwrap();

    canvas.set_draw_color(Color::RGB(0, 0, 0));
    canvas.clear();
    canvas.present();

    let event_pump = sdl_context.event_pump().unwrap();
    (canvas, event_pump)
}

// this is main
fn main() {
    println!("Hello, world!");
}

Right in the beginning of the file, in the section // Dependencies go here, add the following lines:


#![allow(unused_variables)]
fn main() {
use sdl2::video::Window;
use sdl2::pixels::Color;
use sdl2::render::Canvas;
use sdl2::EventPump;
}

In your terminal, go into the folder /rusty-snake, and run the command cargo run. What do you see? To get rid of this warning, we need to call fn init in main().

Initializing the Canvas

Our game needs a canvas on which all the games' graphics will be rendered. Let's initialize it.

Functions and variables

Let's take a closer look at fn init.

main.rs


#![allow(unused_variables)]

fn main() {
// this function initializes the canvas
fn init(width: u32, height: u32) -> (Canvas<Window>, EventPump) {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window = video_subsystem
        .window("Rusty Snake", width as u32 + 1, height as u32 + 1)
        .position_centered()
        .build()
        .unwrap();

    let mut canvas = window.into_canvas().present_vsync().build().unwrap();

    canvas.set_draw_color(Color::RGB(0, 0, 0));
    canvas.clear();
    canvas.present();

    let event_pump = sdl_context.event_pump().unwrap();
    (canvas, event_pump)
}

}

fn declares the function, init is its name. The function takes two parameters, x and y. Rust is a type safe language, so the types of the parameters need to be explicitly indicated. In this example, the type of both parameters is u32, a 32bit unsigned integer. The return types also need to be specifically named. fn init returns two values in a bracket, separated by a comma. The types of these values are defined in the sdl2-crate. We'll ignore the body of the function for now.

  1. In main(), delete the println! statement.

  2. Declare the variables canvas_width and canvas_height, each with the value 720_u32. _u32 makes this number explicitly an unsigned 32bit integer.

  3. Call the function by adding the following line:

    
    #![allow(unused_variables)]
    fn main() {
    let (canvas, events) = init(canvas_width, canvas_height);
    }
    
  4. In the terminal enter cargo run. What happens?

The Game Loop

Loops

When running the program, we shouldn't see anything. If you don't see any error messages, the program compiles. We have initialized the canvas, but in order for the program to run continuously, we need a game loop (aka event loop).

💡 Unlike a command-line program, which performs a single pre-specified task and once done terminates, a program with a GUI (such as a game) is commonly expected to stay open until the user decides to close the program (e.g. by clicking on "Quit" in the main menu). For this to work one commonly makes use of an infinite loop (here: loop { … }), within which on each iteration one asks an event source for any events that have occurred since the last iteration and passes them to the program for handling. Over and over again.

  1. After the last line inside fn main() add a

    
    #![allow(unused_variables)]
    fn main() {
    loop {
        
    }
    }
    
  2. Inside this loop, iterate over event in events.poll_iter() with a for loop.

  3. Run the program. In your group, read and discuss the output on the screen. What information does it provide?

Debugging

Iterators

One of Rusts features are the error messages that actually help. From the error message we learn, that the type of events is called EventPump, and where in sdl2 it is defined. In order to be able to iterate over a type, it has to implement the Iterator trait, and apparently, EventPump does not. To fix this, we consult the documentation. Besides looking it up on the internet, using the command cargo doc --open is an elegant way of getting the documentation for all the crates in your project locally and open in a browser window.

In the sdl2 crate documentation, go to EventPump. Look at the implementation section for this type. Can you find the Iterator trait? On the same page, look for a function, that returns an Iterator. On the page of this return type, check, if the Iterator trait is implemented.

  1. Call the function on events.
  2. To make things easier for later, we will give this loop the name 'game.

It should look like this:


#![allow(unused_variables)]
fn main() {
'game: loop {
    for event in events.poll_iter() {
        // handle user input here
    }
}
}
  1. Run the program. From the output on your screen, do you know what to do?

Mutability

In Rust, variables are immutable by default. You have to make them mutable by declaring them as such with let mut instead of just let. Iterators need to be mutable, because in each step, an item from the iterator is consumed, to keep track of where it currently is in the iteration sequence. So the internal state of the iterator is changed, which is only be possible, if the variable is declared as mutable.

  1. Run the program. Close the window.

Handling User Input

enums and match

Yeah, it's kind of hard to close the window. This is because we have not told our program how to deal with events, so it continues to run and we have to force it to quit. That's uncomfortable, let's change that.

Go to the sdl2 documentation. Go to the event module, find the Event enum. An enum in Rust is a type that represents data that is one of several possible variants. Each variant in the enum can optionally have data associated with it. Look at the possible event variants in the declaration of Event. We want to end the program by pushing esc. What variant are we looking for?

In our game loop, we iterate over events. Depending on what kind of event is happening, we want the program to react in different ways. Pushing space means something different than pushing esc. We will match the event to the variants of the enum Event. match is a way to control the flow of the program when there are several possible options, similar to if else statements.

  1. To be able to use the enum and work with keyboard input, we need to add the following dependencies:

#![allow(unused_variables)]
fn main() {
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
}
  1. Enter the following lines into the for loop. match compares the value of the event-variable to the variants of the enum Event. If the value of the event-variable is a pressed esc-key, the 'game-loop breaks. If the value is something else, the loop continues. The last part of a match always needs to be _ =>, the case that covers all cases that are not explicitly defined.

#![allow(unused_variables)]
fn main() {
match event {
    Event::KeyDown {
        keycode: Some(Keycode::Escape),
        ..
    } => break 'game,
    _ => continue 'game,
}
}
  1. Run the program, try if the user input you implemented works!
  2. To be able to close the window with a mouse click, change the first line of the match to include Event::Quit { .. }. The 'game -loop breaks now if either the esc-key is pressed, or the quit-button is clicked with the cursor.

#![allow(unused_variables)]
fn main() {
match event {
    Event::Quit {
        ..
    } | Event::KeyDown {
        keycode: Some(Keycode::Escape),
        ..
    } => break 'game,
    _ => continue 'game,
}
}

Multithreading

Rust is famous for its safe and easy way to write multithreaded programs. We don't need this program to be multithreaded yet, but it comes with a handy sleep mode, that we need.

For the following code examples, we need to bring the thread and time modules in scope at the top of our file:


#![allow(unused_variables)]
fn main() {
use std::thread;
use std::time;
}

To spawn a thread, add these lines before 'game:


#![allow(unused_variables)]
fn main() {
thread::spawn(move || {
    // some work here
});
}

Inside, at the end of 'game, add this line:


#![allow(unused_variables)]
fn main() {
thread::sleep(time::Duration::from_millis(800));
}

'game will now sleep 800 milliseconds, before starting over.

Frames.

Each animation step is displayed in a new frame. We will now draw a rectangle of the size of the canvas, with a different random color.

sdl offers methods to draw basic shapes, like rectangles, this needs to be imported. It also offers methods that convert our u8 values into RGB colors, that can be displayed.


#![allow(unused_variables)]
fn main() {
use sdl2::rect::Rect;
use rand;

}

rand is another library we are using. To activate it, add it to your Cargo.toml as a dependency. It should now look like this:

[dependencies]
sdl2 = "0.30.0"
rand = "0.7"

Add the following function to your program.


#![allow(unused_variables)]

fn main() {
fn display_rectangle (
    renderer: &mut Canvas<Window>,
    canvas_width: &u32,
    canvas_height: &u32,

) {
    let red: u8 = rand::random();
    let green: u8 = rand::random();
    let blue: u8 = rand::random();

    renderer.clear();

    let drawing_color = Color::RGB(red, green, blue);
    renderer.set_draw_color(drawing_color);

    let square_definition = Rect::new(0, 0, *canvas_width, *canvas_height);
    renderer.fill_rect(square_definition);

    renderer.present();
}

}

The function takes the canvas, as well as the canvas width and height as arguments. It does not return a value. In the body of the function, the variables red, green and blue are assigned random u8 numbers. The clear() method is called on the renderer, this clears the canvas. When, like in our case, the entire canvas is drawn over, this does not really matter, but it's a good habit to think of this. Then, the drawing color is defined. We use a function that is provided by sdl2. The function takes in three u8 values and returns a color. The method set_draw_color() is called on the renderer, with drawing_color as argument.

We create a new rectangle with its minimum and maximum x and y values as arguments and bind it to the variable square_definition. This variable is then passed to the method fill_rect(). Our square is rendered and put into the back buffer.

The last line updates the screen with all the rendering done since the last update.

Do you notice anything peculiar about some of the type declarations in this function?

Try calling this function for every 'game loop iteration in your main program.

Run the program.

Error Handling with the Result Type

Check the warnings in the terminal output. What does it say about the Result Type in fn display_rectangle()?

  1. Check the sdl2 documentation for the return value of the fill_rect() method. Research in the std documentation how to use this value.

  2. Change the code.

Find the solution on the next page.

Solution for Using the ResultType


#![allow(unused_variables)]
fn main() {
let square = renderer.fill_rect(square_definition);
match square {
    Ok(()) => {}
    Err(error) => println!("{}", error),
}
}

What happened so far?

This setup is the base for any other program with graphic output.

So far you have learned:

  • how to install Rust and initialize a new Rust project with cargo.
  • what a cargo manifest is, and how you can add dependencies.
  • how to declare mutable and immutable variables and how to write functions.
  • how to use two different types of loops and iterate over something.
  • You got to know Rust's awesome error messages.
  • You know what an enum is and can use match for control flow.
  • You can look things up in the documentation.
  • You made your first experience in handling errors and took the first step to write a multithreaded program.

Awesome!!!!

Ownership and Borrowing

We're finally getting to something that is very exclusively Rusty: Ownership, referencing and borrowing. Ownership guarantees the memory safety of Rust. Each value has a variable, that is called its owner. There can only be one owner at a time. When the value goes out of scope, the value will be dropped.

  1. Uncomment one println! statement at a time, to find out, where s is in scope und where it is not. Run the code in this window.
  2. Why is s not in scope, after it moved into the function?
fn main() {
    let s = String::from("hello");  
    //println!("{}", s);

    takes_ownership(s);

    //println!("{}", s);

}
//println!("{}", s);

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

So yeah, if you don't want to hand over the ownership of your value to the function, the function can borrow it. A borrowed value is indicated by the & operator. This is also called referencing a value.

fn main() {
    let s = String::from("hello");  

    borrows_value(&s);

    println!("{}", s);

}

fn borrows_value(some_string: &String) {
    println!("{}", some_string);
}
  1. What happens, if the borrowed value is changed?
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

References are immutable by default. To make the reference mutable, the variable has to be declared mutable, and &s changes to &mut s, same goes for &String. Try it! But there are two restrictions:

  • There can only be one mutable reference to a value!
  • A value can not be borrowed mutable and immutable!

Take another look at fn display_frame. Where are borrowed values?


#![allow(unused_variables)]

fn main() {
fn display_frame (
    renderer: &mut Canvas<Window>,
    canvas_width: &i32,
    canvas_height: &i32,

) {
    let red: u8 = rand::random();
    let green: u8 = rand::random();
    let blue: u8 = rand::random();

    renderer.clear();

    let drawing_color = Color::RGB(red, green, blue);
    renderer.set_draw_color(drawing_color);

    let square_definition = Rect::new(0, 0, *canvas_width as u32, *canvas_height as u32);
    let square = renderer.fill_rect(square_definition);
    match square {
        Ok(()) => {}
        Err(error) => println!("{}", error),
    }
    renderer.present();
}
}

Dereferencing

Looking at the function from our game, we can now explain almost all the peculiar signs. One is missing... try running your program without the *s.


#![allow(unused_variables)]
fn main() {
fn display_rectangle (
    renderer: &mut Canvas<Window>,
    canvas_width: &u32,
    canvas_height: &u32,

) {
    let red: u8 = rand::random();
    let green: u8 = rand::random();
    let blue: u8 = rand::random();

    renderer.clear();

    let drawing_color = Color::RGB(red, green, blue);
    renderer.set_draw_color(drawing_color);

    // let square_definition = Rect::new(0, 0, *canvas_width, *canvas_height);
    let square_definition = Rect::new(0, 0, canvas_width, canvas_height);
    let square = renderer.fill_rect(square_definition);
    match square {
        Ok(()) => {}
        Err(error) => println!("{}", error),

    renderer.present();
}
}

A reference is a type of pointer. You can imagine it as an arrow to a value stored somewhere else.


#![allow(unused_variables)]
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, y);
}

Execute this code.

x and y don't have the same value. x is an integer, and y is &x, an arrow that points to that integer behind x. In order to get to the value &x is pointing to, y needs to be dereferenced: *y. Change the code above and execute!

Modularization

Creating Modules

As this project will be bigger than just a couple of lines of code, things will be more organized if we split it up into modules. To do this, make a new folder inside the src folder, and call it lib. Inside lib, create mod.rs and types.rs.

.
├── rusty snake
│    ├── src
│    │    ├── lib
│    │    │    ├── mod.rs
│    │    │    └── types.rs
│    │    │
│    │    └──  main.rs
│    │
│    ├── Cargo.lock
│    └── Cargo.toml
└── ...

Move all functions you wrote to mod.rs. In order for this to work, all dependencies that these functions need, also need to be declared in mod.rs. If they are not used in main.rs, you can delete them there.

Dependencies

Dependencies for the moved functions have to be moved to mod.rs too.

Making Functions and structs Public

In order for a function to be accessible in main.rs, it needs to be marked with pub.

Accessing the Modules in main.rs

Before fn main() add the following line:


#![allow(unused_variables)]
fn main() {
pub mod lib;
}

Functions now have to be called in the namespace of the module:


#![allow(unused_variables)]
fn main() {
let (canvas, mut events) = lib::init(canvas_width, canvas_height);
}

The Grid

Now that we have set up the very basic framework for graphic output, we want to add some actual game elements.

In the very basic version, the snake will be a square block, moving around in a grid. The food for the snake is a static square block of a different color. As the snake moves and eats the food by moving over it, the snake gets longer by one block. If the snake moves over its own parts, the game is over and restarts. If the snake moves out of the canvas, it reenters it on the other side.

Structs and Vectors

Structs are a datatype, that can contain several fields, the fields do not have to be of the same datatype. Using structs, we will define our own data types for the grid and the cells. Both struct definition go into types.rs

  1. Let's define the Grid-struct. It has one field named grid. The datatypes are vectors of Cells inside a vector. The items in the inner vector are rows, the items in the outer vector are columns.

    
    #![allow(unused_variables)]
    fn main() {
    pub struct Grid {
        pub grid: Vec<Vec<Cell>>,
    }
    }
    
  2. Write your own struct for the Cell type. This datatype is used to define the color of a cell, so it needs fields for each RGB-value. The data type of the RGB-values is an unsigned 8-bit integer: u8.

  3. Add the following lines to mod.rs to be able to use the types there.

    
    #![allow(unused_variables)]
    fn main() {
    pub mod types;
    
    use types::{Grid, Cell};
    }
    

    Next, we need to initialize the grid.

  4. Add this function to mod.rs

    
    #![allow(unused_variables)]
    fn main() {
    pub fn grid_init(nx_cells: u32, ny_cells: u32) -> Grid {
        let mut grid_vector = Vec::new();
    
        for row in 0..ny_cells {
            grid_vector.push(Vec::new());
            for _column in 0..nx_cells {
                grid_vector[row as usize].push(Cell {
                    red: 35_u8,
                    green: 15_u8,
                    blue: 13_u8,
                });
            }
        }
        let grid = Grid { grid: grid_vector };
    
        grid
    }
    }
    
  5. Discuss in your group, what this function does, line by line.

  6. Add the following line in fn main() before you spawn the thread.

    
    #![allow(unused_variables)]
    fn main() {
    let mut grid = lib::grid_init(columns, rows);
    }
    

Displaying the grid in Frames

For each frame, the grid is drawn by displaying each single cell as a rectangle.

In fn main(), right after we defined the variables for rows and columns, define a variable for cell_width and calculate its value.

fn display_cell converts row column values of a single Cell into x and y pixels and draws a rectangle in the specified color at the specified coordinate.


#![allow(unused_variables)]
fn main() {
pub fn display_cell(
    renderer: &mut Canvas<Window>,
    row: u32,
    col: u32,
    grid_data: &Grid,
    cell_width: &u32,
) {
    let cell_height = cell_width;

    let grid = &grid_data.grid;

    let x = cell_width * col;
    let y = cell_width * row;

    // For now, we want random colors, to see what happens.
    // let cell_color = &grid[row as usize][col as usize];
    // let drawing_color = Color::RGB(cell_color.red, cell_color.green, cell_color.blue);

    let red: u8 = rand::random();
    let green: u8 = rand::random();
    let blue: u8 = rand::random();

    let drawing_color = Color::RGB(red, green, blue);

    renderer.set_draw_color(drawing_color);
    let square = renderer.fill_rect(Rect::new(x, y, *cell_width, *cell_height));
    match square {
        Ok(()) => {}
        Err(error) => println!("{}", error),
    }
}
}

fn display_frame displays the whole grid by repeatedly calling fn display_cell on every cell in the grid.


#![allow(unused_variables)]
fn main() {
pub fn display_frame(
    renderer: &mut Canvas<Window>,
    grid: &Grid,
    nx_cells: &u32,
    ny_cells: &u32,
    cell_width: &u32,
) {


    renderer.set_draw_color(Color::RGB(0, 0, 0));
    renderer.clear();

    for row in 0..*ny_cells {
        for column in 0..*nx_cells {
            display_cell(renderer, row, column, &grid, &cell_width)
        }
    }
    renderer.present();
}
}

Add both of these functions to your program. Discuss how they compare to fn display_rectangle. Then delete fn display_rectangle, as it is not needed anymore.

Substitute the line that calls fn display_rectangle with the following line:


#![allow(unused_variables)]
fn main() {
lib::display_frame(&mut canvas, &grid, &columns, &rows, &cell_width);
}

Run the program! Enjoy!

Adding the snake

Well, maybe not a snake yet, but a square that moves according to your wishes! In this section, you can try to apply what you have learned so far. If you don't know how to continue, you can either ask for help, or look at one way of solving the problems on the next page.

  1. In the lib folder, make a new file called snake.rs.

  2. Add the following line to the top of snake.rs

    
    #![allow(unused_variables)]
    fn main() {
    use crate::lib::types::{Cell, SnakeHead, Grid};
    }
    
  3. Add pub mod snake; right below the existing pub mod types;.

  4. Write a function that initializes the snake as a SnakeHead. SnakeHead is a struct that contains fields for a row, a column and a Cell value. The row and column values need to be i32 instead of u32. Why?

Defining a tuple

In fn main(), after the grid is initialized, we define a tuple for direction. direction.0 is the row value, direction.1 is the column value.


#![allow(unused_variables)]
fn main() {
let mut direction = (1, 0);
}

Movement

  1. In snake.rs, write a function that takes a mutable reference of the SnakeHead. It calculates a new position with direction values and the coordinates from SnakeHead and then returns a new SnakeHead with an updated position.

  2. Write a function that takes ownership of the grid and changes the color of the square, where the current SnakeHead is located. The grid is then the return value.

Adding User Input

  1. Add Events for up, down, left and right key.

  2. How does the direction.0 and the direction.1 value change, when each of these buttons is pushed? Implement it!

Changing the Game Loop

  1. Add this line to the top of main.rs:

    
    #![allow(unused_variables)]
    fn main() {
    use crate::lib::snake;
    }
    
  2. Call the functions in the following order:

  • update position of snake
  • update grid with position of snake
  • display frame

Run the game! Play the game!

Solution for the Snake

snake.rs


#![allow(unused_variables)]
fn main() {
use crate::lib::types::{Cell, SnakeHead, Grid};

pub fn snake_init() -> SnakeHead {
    let snake = SnakeHead {
        row: 6,
        column: 6,
        color: Cell {
            red: 200_u8,
            green: 0_u8,
            blue: 100_u8,
        }
    };
    snake
}

pub fn snake_moves(snake: &mut SnakeHead, direction: (i32, i32)) -> SnakeHead {
    snake.row = snake.row + direction.0 ;
    snake.column = snake.column + direction.1;

    let new_snake = SnakeHead {
        row: snake.row,
        column: snake.column,
        color: Cell {
            red: 200_u8,
            green: 0_u8,
            blue: 100_u8,
        }
    };

    new_snake
}

pub fn draw_grid_with_snake(mut grid: Grid, snake: &SnakeHead) -> Grid {
    let color = Cell {
        red: snake.color.red,
        green: snake.color.green,
        blue: snake.color.green,
    };

    grid.grid[snake.row as usize][snake.column as usize] = color;
    grid
}
}

types.rs


#![allow(unused_variables)]
fn main() {
pub struct SnakeHead {
    pub row: i32,
    pub column: i32,
    pub color: Cell,
}
}

Content of 'game in main.rs


#![allow(unused_variables)]
fn main() {
//outside the loop initialize snake
let mut snake = snake::snake_init();

for event in events.poll_iter() {
    match event {
        Event::KeyDown {
            keycode: Some(Keycode::Up),
            ..
        } => {
            direction.0 = -1;
            direction.1 = 0;
        }

        Event::KeyDown {
            keycode: Some(Keycode::Down),
            ..
        } => {
            direction.0 = 1;
            direction.1 = 0;
        }

        Event::KeyDown {
            keycode: Some(Keycode::Left),
            ..
        } => {
            direction.1 = -1;
            direction.0 = 0;
        }

        Event::KeyDown {
            keycode: Some(Keycode::Right),
            ..
        } => {
            direction.1 = 1;
            direction.0 = 0;
        }

        Event::Quit { .. }
        | Event::KeyDown {
            keycode: Some(Keycode::Escape),
            ..
        } => break 'game,

        _ => continue 'game,
    }
}

snake = snake::snake_moves(&mut snake, direction);
grid = snake::draw_grid_with_snake(grid, &snake);
lib::display_frame(&mut canvas, &grid, &columns, &rows, &cell_width);
}

How to Continue from Here

Congratulations, if you made it this far!

You don't have a running snake game yet, but you learned almost everything you need to know to continue on your own.

Things you can do to continue with the game:

Implement

  • a function that clears the grid, so the moving square does not leave a trace.

  • a function that deals with the square moving out of the canvas by either

    • wrapping (the square moves out of the canvas on one side then enters it on the other side again) or

    • restarting the game.

  • the snack for the snake.

  • the snake growing when eating the snack.

  • the snake biting itself leading to a restart of the game.

Things you can do to continue in the future:

  • Form a regular study group!

  • Go to a local Rust MeetUp to get help or learn new things.

  • Check out the book The Rust Programming Language to learn more and get a deeper and systematic understanding of the language.

  • Check out Rust by Example that illustrates various Rust concepts with running code examples.

  • Do Rustlings exercises.

  • Become a RustBridge Teaching Assistant the next time! You don't need to understand everything and you can learn so much when you're explaining things to others.