Making Tetris in Rust and Wasm (Part 2)

Concurrency, Keyboard Handling, and the Game Loop


So you've gotten a taste for programming? Took a bite from the power of unbounded creation and now you can't stop? Want to learn more than just the bare minimum? Come in, come in, this is the internet, there's always an empty seat here. We've got some delicious new programming concepts for you today. The first a tool wielded comedians everywhere and the second is a favorite for out of work actors: timing, and callbacks.


1 - Timing

Everything takes time. Your computer doesn't display that tetris block immediately, there's some small interval between when we call draw_piece and when that block prints to the screen. People take time too. There's some small interval between when light hits our eyes and when our brain transforms that into an image, about 100 milliseconds if we go from eyes closed to eyes open. Movies are filmed at 24 frames per second, this means our eyes can interpret up to 250ms of movement that we didn't actually see! We're going to run our game at 60fps.


lib.rs from the end of the last article. We've imported the package 
              'gloo-timers' and now call 'Interval::new()' in our run function
              to loop over a closure 60 times every second. There's also Cargo.toml
              that adds the gloo-timers package. You can find the source
              at src/source/tetris_4/create_game_loop.rs and
              src/source/tetis_4/create_game_loop.toml

I used the gloo-timers crate for this part. We could use straight web-sys, Javascript does have a set_timeout function that's implemented by web-sys, but set_timeout_with_callback_and_timeout_and_arguments_0 is really long and anyway, it's hard to pass functions to web-sys.

So add gloo-timers to your Cargo.toml and import it in your src/lib.rs. Then create a Timeout. Like with every crate, you can find the documentation for gloo-timers on crates.io, the central hub for all Rust crates. Let's take a look at the Interval API. It takes our millisecond delay and a FnOnce, a Closure.

Milliseconds is a bit of math. We want to render our game at 60 frames per second and there are 1000 milliseconds in a second. We want to call game_loop every 1000/60 milliseconds. Comes out to 16ms after integer division. Closure syntax in Rust is the move || { }, with the code you want to run stuffed in between the curly braces.

Ok - now we've got a 60fps game loop. Let's do some animation.


We now pass a changing 'tetronimo_depth' variable to our game loop to move the piece every interval.
              Source can be found on my github at src/source/tetris_4/animate_game_loop.rs

And the result


Firefox gif showing the compiled Wasm. Tetris piece falls 10 pixels every 16ms and previous drawings
              remain on the screen.

...Way too fast and it's this long purple line but hey, we've got an animation!

2 - Clear the board and make room for Game State

Ok let's take another look at that gif.


Same gif as the previous image. Firefox showing the compiled Wasm. Tetris
              piece falls 10 pixels every 16ms and previous drawings remain on the screen.

Ah what a beautiful gif. An elegant gif. One of those gifs that just doesn't get old. Made using ScreenToGif. It's got an MSI installer so you know it's legit. .msi is like the Zune of installer formats. Just as legit as an iPod but nowhere near the street cred of using apt-get install. .exes are Android.

So we've got two problems. The first problem is our animation moves too fast. At level 0 a Tetris piece should fall one step every 48 frames so we need to implement some kind of speed control. The second problem is the blur. We're not erasing the old Tetris piece before drawing a new one so we just keep drawing pieces on top of each other.


Two files lib.rs and game.rs. lib.rs we create a GameState struct with two fields,
              frame_count and ycoord. Every frame we increment clear the board, draw our piece, 
              and increment frame_count. We increment ycoord every 48 frames. In game.rs we have
              a clear_board function that draws a black rectangle over our whole canvas. The source
              can be found on my github at src/source/tetris_4/game_state.rs

We do a couple things here so let's start at the top of src/lib.rs and work our way down.

First is this new GameState struct. This is our first time using a struct so the syntax might be confusing but they're not too complicated. Like everything else in programming, structs are there to improve our code organization. In this case we're separating the frame count from the tetris piece's height but we still want to group them together. We could do something like game_loop(&mut frame_count, &mut ycoord);. There's nothing wrong with doing it that way, due to Rust's focus on zero-cost abstractions both implementations are the same speed. But when we start adding the score, and the level, and the piece's horizontal position, and all the pieces that are already on the board, well, it's nice to have them all contained in one place.

So after we create the GameState and pass it to the game_loop we add that speed control. I decided I wanted my tetronimo to fall once every 48 frames so we add in our limiter. If the frame_count is 48, move the piece down one.

How do we do that though? Well if we don't want that blur we first we have to clear the board. I put clear_board() in src/game.rs because again, while ctx is an app concept the board is not. Also we're using clear_board in conjunction with draw_piece and I like to group things that get used together. clear_board works by drawing a big black rect over everything. It's like painting over graffiti except there's no history to go back and look at 50 years later.

One last thing, note the &ctx. This was a mistake from my last post, we should always be borrowing ctx. If you don't, and try to move ctx instead you'll get an error, error[E0382]: use of moved value. This is due to Rust's goal of freeing memory you're not using anymore. When you compile your code Rust tries to figure out when you're done using a variable so it can delete it. If you let clear_board borrow ctx you're telling the compiler “I'm still using this”, but if instead you give it to clear_board then clear_board owns it, and it can be deleted when clear_board is done with it. If you found this paragraph interesting you can do more research googling Rust Lifetimes.


Firefox gif with tetris piece falling once every 48 frames

3 - Callbacks

Everyone loves callbacks. Call your mom on her birthday? On yours she calls you back. Find the career of your dreams? Of course you're getting a callback. Register a keydown event listener then push the Right Arrow key on your keyboard? You better hope you're getting a callback otherwise something isn't right with your code and debugging Javascript is a fate worse than death.


lib.rs with an event listener registered to a keyboard event. In the listener we call
              the Javascrip 'console.log' to print the event to the browser console. There's also
              a 'gloo-events' addition to the Cargo.toml. The source can be found on my github at
              src/source/tetris_4/log_event_listener.rs and src/source/tetris_4/log_event_listener.toml

The first thing we're going to do is create an EventListener. You can check out the docs for gloo-events here, to sum up our target is the browser Window, our event type is a keydown event, and our callback is a closure that passes the specific event as an argument. Then we log the event.

We log the event because Javascript objects are hard. There's a lot of documentation but a lot of it is wrong, a lot of it is out of date, and a lot of it is straight up confusing. MDN is a good bet but another easy strategy is just to print it out and let our browser do the heaving lifting for us.


Firefox window showing the browser console open and some events logged

Opening the browser console is different for every browser but ctrl+shift+i works in a surprising number of cases. After viewing it you can see, keycode for an ”ArrowLeft” string is 37, and so on. Let's add that to our game:


lib.rs again. We've created a function called move piece that gets called by the event listener.
              This function logs a different string depending on if we push the left or right arrow. The source
              can be found on my github at src/source/tetris_4/using_key_codes.rs

We're finally writing that move_piece function. We'll put it back in src/game.rs by the end of this article but it's here for simplicity's sake. It's much easier to show what I'm doing if I'm only working in one file.

A couple things to note. First the use of dyn_ref instead of dyn_into . If you take a look at the gloo EventListener docs you'll see the closure's function argument is an &Event, an event reference. Second is the match. Match is a great way to choose between lots of different options. Right now we've got two (plus the default) but we'll be adding more for rotating the tetronimo and various other features.

Don't forget to add KeyboardEvent to your Cargo.toml!

4 - Rc, RefCell, and Fighting the Borrow Checker

We've got our event listener working so that's all fine and dandy, but we still need to move the tetris piece. Let's try what we've been doing:


Edited move_piece to actually move the piece. It takes in a GameState reference same as game_loop does, 
              then increments/decrements state.xcoord depending on whether the left or right arrow is pressed. Source
              can be found on my github at src/source/tetris_4/shared_mut.rs

Seems like it will work but when we try to compile it

Screenshot of a Cargo compile error in the terminal. It reads 'error[E0382]: use of moved value `state`'

There's a lot to decode here. And it's all due to Rust's Ownership and Borrowing rules. And a little thing called synchronization.

Now I'm gonna sit ya'll down for a little lesson in concurrency here. A little lesson in hyper-cores and multi-threading. Close your eyes, sit back, and imagine, you've got the number 4. It's a nice pretty 4, all straight lines with that perfect right triangle hypotenuse to complete the package. It's also how much money is in your bank account. Now you're coming home from a hard day's work and you stop by the bank to deposit your paycheck. You made $5 making tin can telephones today. At the same time your Significant Other stops by the ATM to make a withdrawal. They need $2 for some clothes. I mean yea you'll enjoy them later but still! That's half your paycheck and you haven't even gotten home yet! Anyway those commands travel over the wires and reach the big bank datacenter at exactly the same time. From the north we've got a -2 command and from the south west a +5 command. Here's the kicker though, THE COMPUTER CAN”T DO MATH IN PLACE. It has to first read the value, do the math, then put it back.

Now nothing can happen at exactly the same time so let's assume your deposit gets there slightly sooner. So your deposit arrives, reads the $4, then your SO's withdrawal arrives! Your deposit hasn't done the math yet so the withdrawal reads the $4 as well. The computer does the math and writes $9 back (because 4+5=9), but then your SO's withdrawal does its math. Remember it also read a 4, and 4-2=2. So then the withdrawal writes $2 back where your balance goes, completely erasing the $5 you just deposited!

Yes it's a contrived example, it's also the classic contrived example. And it helps explain our Rust error above. We've got a game loop running 60 times every second and an event handler that triggers every time we hit a button. At some point there's going to be a collision and Rust is smart enough to know that. So how do we fix it?

Well Javascript is actually single threaded and has fake multithreading so we're gonna use Rc<RefCell<T>>. If we had real multithreading we'd use Arc<Mutex<T>>.


Same lib.rs as ref_mut except every &mut is replaced with an Rc<RefCell<. Source
              can be found on my github at src/sourc/tetris_4/ref_cell_state.rs

Starting from the top again first we import Rc and RefCell. These are the wrapper types we're using and it's important to note that neither of these abstractions are zero-cost. They both have a little bit of overhead in incrementing a reference counter so we want to use as few of them as possible.

We then create a new Rc<RefCell<GameState>> called state1 and we clone it. This clone() is Rc's clone. Remember how we were talking about borrowing earlier? And Rust tries to figure out when it can throw away our objects? Well our problem here is we're trying to give our GameState to both the event handler and the game loop. Normally you can't do that, if some scope owns an object, another scope can't own it as well. Rc is a magic workaround. Think of it like a portal. We put our GameState somewhere and wrap it up in a nice little RefCell box. We then create a portal to it with Rc. We can look through the portal, we can ask RefCell if we're allowed to borrow it, we can also clone the portal. Create as many copies of this portal as we need and pass them out to whoever wants them. You still need to call borrow_mut() before you can access it.

So what is borrow_mut()? Borrow mut is part of RefCell's API, and RefCell's job is to enforce Rust's Ownership rules at runtime (either lots of people borrow or one person borrow_muts). It's kind of like the police, as long as your code is correct everything's fine, but you break one rule and RefCell panic!()s.


Firefox gif showing the tetris piece moving side to side as I press the arrow keys

5 - Organizaion and Rotation

Let's finish up this post with a few extra features. And some code cleanup.


Show lib.rs and game.rs where we've moved GameState into game.rs. Also created an impl
              as well as moved game_loop and move_piece into the same file. Source can be found on my
              github at src/source/tetris_4/code_cleanup.rs

First thing we're going to do is move GameState into src/game.rs . I mean, GameState is the definitive object of our game. If we don't put it in the game module then why do we have a game module in the first place?

Moving GameState causes a cascading effect so that by the time our code compiles again you end up with the fully organized code above. We need some way to create a GameState object. The Rust convention is a new() static method. We need to make the GameState visible so we pub it. game_loop() now needs to access GameState's members but we don't want to pub every member in a struct so instead we move game_loop() to our game module and pub it. Same thing with move_piece().

Now we're left with one last thing. Our call to get_context() inside the game module. We could keep it in src/lib.rs and pass it in to game as a parameter. We could keep it in game_module() and import our app module into game. Both cause leaking. I like to keep lib small so I chose to leak app into game a bit.


Implemented clockwise and counterclockwise rotation functions for the Rotation enum. Add rotation functionality
              to GameState and the EventHandler. Source can be found on my github at src/source/tetris_4/rotation_increment.rs

If you take a look at src/pieces.rs we have this line *self = match *self {. This here assigns the result of the match to self and is special to Rust because everything in Rust is an expression. Every other bit of new code you've seen before so I'll leave figuring it out as an exercise for the reader.


Firefox gif with the piece falling and moving/rotating with the arrow keys I press
Copyright 2019© Snakard Group Inc
Email: robert.snakard@gmail.com
Phone: 703-994-3635