Making Tetris in Rust and Wasm
Part 1: The Tetris Video Framework
Part 3: Architecting the Tetris Video Framework
Hello and welcome one and all to Robert Snakard’s World Famous Tetris Video Framework! Step right up and enjoy this Rust library of epic proportions! Only here will you see Results tried and returned, Arcs and RefCells used without measure, and who could forget the illustrious Piece museum!
In our main file you’ll find the request_animation_frame loop. Trust me, this little bit of recursion is one you won’t want to miss. You’ll be coming back again and again and again and again. To the left you’ll find our App module which performs unwrap after unwrap to create a beautiful CanvasRenderingContext and to the right is the Game module. While its small we promise it will grow bigger in size as we add more rules to the game.
So please folks. Sit back, relax, and enjoy this edu-tainment today!
1 - Creating a new project
We’re finally starting our project now. We’ve done the background research. We know how to write HTML, we know how to write Rust, now let’s make something useful.
You’ve done this before. Create a new Cargo project, initialize your git repository (if things aren’t working double check your ssh keys!), and push your first commit to master. Oh, and install wasm-pack.
We’ll use this, combined with
We want to add two dependencies to our project. The
wasm-bindgen crate and the
canvas.getContext(). Speaking of:
This is our
src/lib.rs. The blue imports our dependencies, the purple sets up our DOM, and the green calls fillRect. Notice the
#[wasm_bindgen(start)]. This lets us compile with wasm-pack instead of cargo so we can import our package as an ES6 module.
Wasm-pack compiles our package into the pkg directory where we have a host of files.
The only important ones are the
.js one and
.wasm one. You use them like so:
tetris.js is glue code. It includes code to access memory, pass strings, and import/export functions.
tetris_bg.wasm is our binary. We import the glue code so it’s accessible for everyone, then init our binary. When we call
init will call whatever function we labelled as
#[wasm_bindgen(start)]. This is how we will enter our webapp. Now, let’s see if it runs.
2 - Laying A Foundation
So what does a game of Tetris require? We need some way to draw pieces, we need some way to create pieces, we need some way to move pieces, and we need some way to detect collisions. Today we’re focusing on the “draw pieces” side of things but we’ll draw a blueprint for everything.
And then focus on
You can do the folding with zim fold,
3 - Beginning Construction
Now we ask ourselves a question. Where do we start? In this case, let’s start with what we already have. I’ve moved our
hello_world code into
draw_piece() and added comments. I like to organize my code into lots of small messes so refactoring is easier.
Ok. But we still need to draw a piece, and drawing the piece is a very key part of Tetris.
I’ll admit, I actually got a little bit of analysis paralysis at this part. If we want to draw a piece we have to know what a piece is. And depending on how you look at it a piece can be a lot of different things. Now I did a lot of research on this part trying to find the best way to represent a piece. I settled on an array.
IMPORTANT: ARRAYS START AT 0!!
Which is great and all, but using an array means we have to traverse it. And traversing arrays is an entire category of programming all by itself. We’ll just use a for loop. It may be slower, but it’s straightforward and easy enough to understand.
We’ve got our for loop, which starts at 0 (the first item in the list), and ends one before the length, which leaves us with a bit of math. Since you’ve played around with fillRect you’ll know we have to set the location of the square and then the size. The size can be pretty much anything, set it first though cause we also use it in our location. To set the location, take a look at the way we organized our
0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16 spots: 4 rows, 4 in a row. Now
4x4=16 so we know to go backwards we’ll need to divide. Let’s checkout spot number 6. We start at 0, count left to right, and hit this one. Row 1, Index 2.
0, 1, 0, 0, 1, 1, 1, 0 0, 0, 0, 0, 0, 0, 0, 0,
What’s 6 divided by 4? 1 remainder 2.
You can try the math for any index in the array, 3/4 = 0R3, 13/4 = 3R1. As long as you start counting at 0 the math will always work out to be your graphical coordinates. The xcoord uses
%, the remainder operator, and the ycoord uses
/, the divide-and-truncate operator. It’s one of those beautiful coincidences in mathematics.
4 - Finally pitch the tent
Now the whole point of architecting and modularizing and organizing your code is to make it portable. We want to organize the code into bite-sized chunks so we can create a sane API and call them where they’re needed. We currently have a
draw_piece() function. When I call it I’d like to specify which piece I’m drawing, how it’s rotated, and where on the tetris board it’s located, so let’s do that.
And then in another file named
I won’t make you define that whole thing, you can find the source on my github. What we’ve done though is define every piece and each of their rotations in a file named
src/pieces.rs. Now we can import them just like we did with our dependencies.
And then use our function parameters.
And while we’re at it, let’s split our code into
And reorganize the whole thing altogether!
So what did we do?
We reorganized. I decided to split the game into two parts. The
game module, which consists of everything we do when playing the game, and the
enums to the pieces file, they just seemed to fit there best.
A lot of organizing is arbitrary, like I could have put
app. I mean it does use the
CanvasRenderingContext! The thing is, it also uses the
PIECE array and I had to decide whether I wanted
web-sys to bleed into
pieces to bleed into
app. I chose the former.
You’re always going to have tradeoffs when programming and re-architecturing sucks so oftentimes it will be on you to decide if the duct-tape fix is safe enough. In one of these articles I’ll show you how to make a wrapper and we’ll wrap
ctx with a nice pretty bow, but for now it’s exposed to the world. Because it’s easier that way.