if you don’t know systems programming
Rust is a fast and memory-safe programming language with a strong type system and declarative memory management. It’s very focused on letting you write correct code, but can be hard to learn because of the restrictions it imposes to ensure this.
Rust also has really good error messages, well-written documentation, and excellent tools that make writing code and debugging easier. I like it because it helps me make sure that my program is doing exactly what I want it to. I hope you like it too!
Where do I write Rust? Rust has two language servers that attach to your editor to analyze your code while you write it, rls
and rust-analyzer
(most people use the second). If you’re not already attached to a text editor, VS Code’s rust-analyzer
extension is really good!
Where do I find documentation? Documentation for Rust’s standard library is at https://doc.rust-lang.org/std/index.html. Documentation for crates like macroquad is at https://docs.rs/crate_name.
What’s cargo? Cargo is Rust’s package manager and build system. It downloads crates from https://crates.io, runs projects, runs tests, generates documentation, manages dependencies, and more! You use it from the terminal—if you’re not familiar with it, there’s a tutorial linked on the last page.
I’m going to walk through an example of an extremely minimal 2D game using the library Macroquad. First, you want to make a new crate (a project) with cargo. In the terminal, navigate to the folder where you want to make your project, and run the command:
This will create a new folder in that directory, cool-game
. It will contain a src/
directory, where your Rust files will go, and a Cargo.toml file, where you’ll list the dependencies of the crate—in this case, just Macroquad (version 0.3.10).
At the end of the file, under [dependencies], add:
macroquad = "0.3.10"
and save the file.
Now, open the main.rs
file in the src/
folder and replace its contents with some setup code for Macroquad, taken from its documentation page: https://docs.rs/macroquad/0.3.10
use macroquad::prelude::*; // A
#[macroquad::main("cool game")] // B
async fn main() { // C
loop {
next_frame().await; // C
}
}
You can run this from the terminal with the command cargo run
, and a window with a black background should pop up.This isn’t particularly interesting, but Macroquad provides functions we can draw with:
loop {
clear_background(DARKBLUE);
draw_rectangle(350.0, 250.0, 100.0, 100.0, PINK);
next_frame().await;
}
Now when we run this there’ll be a blue background and a pink square in the middle.
We can add an if statement at the beginning of the loop to break out of it (then close the window) when the player presses the Q key:
enum Pet { Cat, Dog }
allows you to say that your pet is a Pet::Cat
or a Pet::Dog
, but nothing else.println!
is a macro (you can tell because of the !) used to format and print things to the consoleHere’s a for loop:
i
is an integer, so we use as
to cast between number types (floats to integers, etc)You can iterate over a range or a collection.
let positions = vec![100.0, 250.0, 400.0, 550.0]; // A, B
for x in positions.iter() {
draw_rectangle(x, 250.0, 100.0, 100.0, PINK);
}
let
. You don’t need to say what type it is unless it’s ambiguous (if the compiler gets confused and tells you to)vec!
is a macro that gives a shorthand to declare Vec
s, which are like arraysRust doesn’t allow null variables, and you have to define what value a variable holds before using it. For example, this won’t compile:
If you want a variable to maybe hold a value, you can use the enum Option
, and use a match statement to check if it’s Some(value)
or None
.
let maybe_positions = Some(vec![100.0, 250.0, 400.0, 550.0]);
match maybe_positions { // A
Some(positions) => {
// loop through the x positions in rects
for x in positions {
draw_rectangle(x, 250.0, 100.0, 100.0, PINK);
}
},
None => { /* do nothing */ }
}
Games are usually interactive, so let’s make a player character. We can define a struct—like a Java/Python class—to hold data about our game.
Rect
, which has an x and y position, a width, and a heightWe can implement functions for our Game
with an impl
block.
impl Game {
fn new() -> Self { // A
Self { player: Rect::new(50.0, 50.0, 25.0, 25.0) } // B
}
fn update(&mut self) {
if is_key_down(KeyCode::Up) {
self.player.y -= 1.0;
}
if is_key_down(KeyCode::Down) {
self.player.y += 1.0;
}
if is_key_down(KeyCode::Right) {
self.player.x += 1.0;
}
if is_key_down(KeyCode::Left) {
self.player.x -= 1.0;
}
}
fn draw(&self) { // C
clear_background(DARKBLUE);
draw_rectangle(self.player.x, self.player.y,
self.player.w, self.player.h, PINK);
}
}
Self
is shorthand for the type of the impl blockRect::new(...)
creates a rectangle at (50, 50) with width 25 and height 25this
like in Java; need to explicitly include self
parameter like in PythonThen replace the contents of our async main function with
let mut game = Game::new(); // A
loop {
if is_key_down(KeyCode::Q) {
println!("thanks for playing!");
break;
}
game.draw(); // B
game.update(); // B
next_frame().await;
}
self
parameter, call it with Type::function_name()
And we’ll have a pink square on a blue background that we can move with arrow keys. Yay :)
All variables in Rust are immutable by default, so we have to explicitly tell Rust that we want to be able to change it. This might seem weird and restrictive, but Rust is very cautious: it doesn’t want you accidentally modifying data that shouldn’t be changed.
Likewise, when defining our game’s update method, we had to add a mut keyword to let us modify the game state.
But what about the &
?
Every value in Rust can only owned by a single variable. For example, if we try to assign our game to another variable,
let mut game = Game::new();
{
let mut game2 = game;
// do things with game2
}
loop {
game.draw();
game.update();
}
This won’t compile because the game now belongs to game2
after assigning it again, so the first game
is now invalid. Rust doesn’t want multiple variables to own the same data at the same time. This has to do with how memory management works in Rust, but I won’t get into that here (on the final page there are links to people who explain it far better than I could).
But sometimes you want values to be accessible or modifiable from different locations. Here you would borrow game
with a reference using the &
symbol, which points to the data at a variable without moving it.
And this would compile! If you wanted to use game2
to modify game
, you would make the reference mutable:
You can have as many immutable references to the same value as you want, but only one mutable reference. Like ownership, Rust hates it when multiple things are able to modify the same data at the same time.
Every value can only ever have either:
(this list was stolen from CS181G (spring 2021) course notes by Prof Osborn. Thanks Prof Osborn)
Ownership and references are tricky concepts, so don’t worry if you don’t understand them at first. You’ll get it!
Now you have a tiny game and know some basics of Rust. Congrats!
Read the error messages!
Be patient!
You’ll be okay!!
Ferris image from https://rustacean.net // draws inspiration from the Rust book, Prof Osborn’s CS181G Rust intro notes, and Becca Turner’s RustConf 2020 talk
written by Cynthia Li // made with 🦀 💙