Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Basics of Ownership: Rust Crash Course Lesson 2, Part 2

DZone's Guide to

Basics of Ownership: Rust Crash Course Lesson 2, Part 2

In this post, a software developer walks us through how to develop a browser-based game using Rust. Read on to become a Rustacean!

· Web Dev Zone ·
Free Resource

Start coding something amazing with our library of open source Cloud code patterns. Content provided by IBM.

Welcome back! If you missed Part 1 of this lesson, you can check it out here.

Bouncy

Enough talk, let’s fight! I want to create a simulation of a bouncing ball. 

Let’s step through the process of creating such a game together. I’ll provide the complete src/main.rs at the end of the lesson, but strongly recommend you implement this together with me throughout the sections below. Try to avoid copy-pasting, but instead type in the code yourself to get more comfortable with Rust syntax.

Initialize the Project

This part’s easy:

$ cargo new bouncy

If you cd into that directory and run cargo run, you’ll get output like this:

$ cargo run
   Compiling bouncy v0.1.0 (/Users/michael/Desktop/bouncy)
    Finished dev [unoptimized + debuginfo] target(s) in 1.37s
     Running `target/debug/bouncy`
Hello, world!

The only file we’re going to touch today is src/main.rs, which will have the source code for our program.

Define Data Structures

To track the ball bouncing around our screen, we need to know the following information:

  • The width of the box containing the ball.
  • The height of the box containing the ball.
  • The x and y coordinates of the ball.
  • The vertical direction of the ball (up or down).
  • The horizontal direction of the ball (left or right).

We’re going to define new datatypes for tracking the vertical and horizontal direction, and use u32s for tracking the position.

We can define VertDir as an enum. This is a simplified version of what enums can handle since we aren’t giving it any payload. We’ll do more sophisticated stuff later.

enum VertDir {
    Up,
    Down,
}

Go ahead and define a HorizDir as well; this tracks whether we’re moving left or right. Now, to track a ball, we need to know its xand y positions and its vertical and horizontal directions. This will be a struct since we’re tracking multiple values instead of (like an enum) choosing between different options.

struct Ball {
    x: u32,
    y: u32,
    vert_dir: VertDir,
    horiz_dir: HorizDir,
}

Define a Frame struct that tracks the width and height of the play area. Then tie it all together with a Game struct:

struct Game {
    frame: Frame,
    ball: Ball,
}

Create a New Game

We can define a method on the Game type itself to create a new game. We’ll assign some default width and height and initial ball position.

impl Game {
    fn new() -> Game {
        let frame = Frame {
            width: 60,
            height: 30,
        };
        let ball = Ball {
            x: 2,
            y: 4,
            vert_dir: VertDir::Up,
            horiz_dir: HorizDir::Left,
        };
        Game {frame, ball}
    }
}

Challenge: Rewrite this implementation to not use any let statements.

Notice how we use VertDir::Up; the Up constructor is not imported into the current namespace by default. Also, we can define Game with frame, ball instead of frame: frame, ball: ball since the local variable names are the same as the field names.

Bounce

Let’s implement the logic of a ball to bounce off of a wall. Let’s write out the logic:

  • If the x value is 0, we’re at the left of the frame, and therefore we should move right.
  • If y is 0, move down.
  • If x is one less than the width of the frame, we should move left.
  • If y is one less than the height of the frame, we should move up.
  • Otherwise, we should keep moving in the same direction.

We’ll want to modify the ball, and take the frame as a parameter. We’ll implement this as a method on the Ball type.

impl Ball {
    fn bounce(&mut self, frame: &Frame) {
        if self.x == 0 {
            self.horiz_dir = HorizDir::Right;
        } else if self.x == frame.width - 1 {
            self.horiz_dir = HorizDir::Left;
        }

        ...

Go ahead and implement the rest of this function.

Move

Once we know which direction to move in by calling bounce, we can move the ball one position. We’ll add this as another method within impl Ball:

fn mv(&mut self) {
    match self.horiz_dir {
        HorizDir::Left => self.x -= 1,
        HorizDir::Right => self.x += 1,
    }
    ...
}

Implement the vertical half of this as well.

Step

We need to add a method to Game to perform a step of the game. This will involve both bouncing and moving. This goes inside impl Game:

fn step(&self) {
    self.ball.bounce(self.frame);
    self.ball.mv();
}

There are a few bugs in that implementation which you’ll need to fix.

Render

We need to be able to display the full state of the game. We’ll see that this initial implementation has its flaws, but we’re going to do this by printing the entire grid. We’ll add a border, use the letter o to represent the ball, and put spaces for all of the other areas inside the frame. We’ll use the Display trait for this.

Let’s pull some of the types into our namespace. At the top of our source file, add:

use std::fmt::{Display, Formatter};

Now, let’s make sure we got the type signature correct:

impl Display for Game {
    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
        unimplemented!()
    }
}

We can use the unimplemented!() macro to stub out our function before we implement it. Finally, let’s fill in a dummy main function that will print the initial game:

fn main () {
    println!("{}", Game::new());
}

If everything is set up correctly, running cargo run will result in a “not yet implemented” panic. If you get a compilation error, go fix it now.

Top Border

Alright, now we can implement fmt. First, let’s just draw the top border. This will be a plus sign, a series of dashes (based on the width of the frame), another plus sign, and a newline. We’ll use the write! macro, range syntax (low..high), and a for loop:

write!(fmt, "+");
for _ in 0..self.frame.width {
    write!(fmt, "-");
}
write!(fmt, "+\n");

Looks nice, but we get a compilation error:

error[E0308]: mismatched types
  --> src/main.rs:79:60
   |
79 |       fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
   |  ____________________________________________________________^
80 | |         write!(fmt, "+");
81 | |         for _ in 0..self.frame.width {
82 | |             write!(fmt, "-");
83 | |         }
84 | |         write!(fmt, "+\n");
   | |                           - help: consider removing this semicolon
85 | |     }
   | |_____^ expected enum `std::result::Result`, found ()
   |
   = note: expected type `std::result::Result<(), std::fmt::Error>`
              found type `()`

It says “considering removing this semicolon.” Remember that putting the semicolon forces our statement to evaluate to the unit value (), but we want a Result value. And it seems like the write! macro is giving us a Result value. Sure enough, if we drop the trailing semicolon, we get something that works:

    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/bouncy`
+------------------------------------------------------------+

You may ask: what about all of the other Result values from the other calls to write!? Good question! We’ll get to that a bit later.

Bottom Border

The top and bottom border are exactly the same. Instead of duplicating the code, let’s define a closure that we can call twice. We introduce a closure in Rust with the syntax |args| { body }. This closure will take no arguments, and so will look like this:

let top_bottom = || {
    write!(fmt, "+");
    for _ in 0..self.frame.width {
        write!(fmt, "-");
    }
    write!(fmt, "+\n");
};

top_bottom();
top_bottom();

First, we’re going to get an error about Result and () again. You’ll need to remove two semicolons to fix this. Do that now. Once you’re done with that, you’ll get a brand new error message. Yay!

error[E0596]: cannot borrow `top_bottom` as mutable, as it is not declared as mutable
  --> src/main.rs:88:9
   |
80 |         let top_bottom = || {
   |             ---------- help: consider changing this to be mutable: `mut top_bottom`
...
88 |         top_bottom();
   |         ^^^^^^^^^^ cannot borrow as mutable

The error message tells us exactly what to do: stick a mut in the middle of let top_bottom. Do that, and make sure it fixes things. Now the question: why? The top_bottom closure has captured the fmt variable from the environment. In order to use that, we need to call the write! macro, which mutates that fmt variable. Therefore, each call to top_bottom is itself a mutation. Therefore, we need to mark top_bottom as mutable.

There are three different types of closure traits: Fn, FnOnce, and FnMut. We’ll get into the differences among these in a later tutorial.

Anyway, we should now have both a top and bottom border in our output.

Rows

Let’s print each of the rows. In between the two top_bottom() calls, we’ll stick a for loop:

for row in 0..self.frame.height {
}

Inside that loop, we’ll want to add the left border and the right border:

write!(fmt, "|");
// more code will go here
write!(fmt, "|\n");

Go ahead and call cargo run, you’re in for an unpleasant surprise:

error[E0501]: cannot borrow `*fmt` as mutable because previous closure requires unique access
  --> src/main.rs:91:20
   |
80 |         let mut top_bottom = || {
   |                              -- closure construction occurs here
81 |             write!(fmt, "+");
   |                    --- first borrow occurs due to use of `fmt` in closure
...
91 |             write!(fmt, "|");
   |                    ^^^ borrow occurs here
...
96 |         top_bottom()
   |         ---------- first borrow used here, in later iteration of loop

Oh no, we’re going to have to deal with the borrow checker!

Fighting the Borrow Checker

Alright, remember before that the top_bottom closure capture a mutable reference to fmt? Well, that’s causing us some trouble now. There can only be one mutable reference in play at a time and top_bottom is holding it for the entire body of our method. Here’s a simple workaround in this case: take fmt as a parameter to the closure, instead of capturing it:

let top_bottom = |fmt: &mut Formatter| {

Go ahead and fix the calls to top_bottom, and you should get output that looks like this (some extra rows removed).

+------------------------------------------------------------+
||
||
||
||
...
+------------------------------------------------------------+

Alright, now we can get back to…

Columns

Remember that // more code will go here comment? Time to replace it! We’re going to use another for loop for each column:

for column in 0..self.frame.width {
    write!(fmt, " ");
}

Running cargo run will give you a complete frame, nice! Unfortunately, it doesn’t include our ball. We want to write a o character instead of space when column is the same as the ball’s x, and the same thing for y. Here’s a partial implementation:

let c = if row == self.ball.y {
    'o'
} else {
    ' '
};
write!(fmt, "{}", c);

There’s something wrong with the output (test with cargo run). Fix it and your render function will be complete!

The Infinite Loop

We’re almost done! We need to add an infinite loop in our main function that:

  • Prints the game.
  • Steps the game.
  • Sleeps for a bit of time.

We’ll target 30 FPS, so we want to sleep for 33ms. But how do we sleep in Rust? To figure that out, let’s go to the Rust standard library docs and search for sleep. The first result is std::thread::sleep, which seems like a good bet. Check out the docs there, especially the wonderful example, to understand this code.

fn main () {
    let game = Game::new();
    let sleep_duration = std::time::Duration::from_millis(33);
    loop {
        println!("{}", game);
        game.step();
        std::thread::sleep(sleep_duration);
    }
}

There’s one compile error in this code. Try to anticipate what it is. If you can’t figure it out, ask the compiler, then fix it. You should get a successful cargo run that shows you a bouncing ball.

Problems

There are two problems I care about in this implementation:

  • The output can be a bit jittery, especially on a slow terminal. We should really be using something like the curses library to handle double buffering of the output.
  • If you ran cargo run before, you probably didn’t see it. Run cargo clean and cargo build to force a rebuild, and you should see the following warning:
warning: unused `std::result::Result` which must be used
  --> src/main.rs:88:9
   |
88 |         top_bottom(fmt);
   |         ^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled

I mentioned this problem above: we’re ignoring failures coming from the calls to the write! macro in most cases, but throwing away the Result using a semicolon. There’s a nice, single character solution to this problem. This forms the basis of proper error handling in Rust. However, we’ll save that for another time. For now, we’ll just ignore the warning.

Complete Source

You can find the complete source code for this implementation as a GitHub gist. Reminder: it’s much better if you step through the code above and implement it yourself.

I’ve added one piece of syntax we haven’t covered yet in that tutorial, at the end of the call to top_bottom. We’ll cover that in much more detail next week.

Code something amazing with the IBM library of open source blockchain patterns. Content provided by IBM.

Topics:
web dev ,rust ,web application development ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}