One of the great things about using Rust and bracket-lib
is that you can compile your games for the web. Rust natively supports compiling to WebAssembly (aka. WASM). With a little bit of automated work to map web assembly bindings to native JavaScript, you can run your games in your browser.
Here’s Flappy Dragon in your browser
WASM does come with some limitations:
- Threading works differently in a web browser, and was largely disabled in response to the Spectre line of vulnerabilities. This means you can’t take advantage of the multi-threading built into Legion (and similar ECS systems). Your game will run in a single thread.
- Not every browser runs WASM well. It works great in Chrome and Firefox, not so well in Safari.
- Large games can take a while to download.
- WASM doesn’t have a native filesystem, so loading assets from your webserver is difficult. This is alleviated by embedding your assets into your program.
- You need a web server. It can be running locally, but most browsers refuse to execute WASM programs loaded locally with
file:
links.
So with those caveats in mind, let’s get started making your Rust games work in your web browser.
Before you start: Run
cargo update
on projects you want to publish to the web. You need a recent version ofbracket-lib
for this article.
Install the Toolchain
Rust’s cargo
system natively supports cross-compilation. Cross-compilation allows you to compile programs for a platform other than the one you are currently using. For example, you can make Linux builds of your game without leaving Windows. WASM is supported as another platform, so you cross-compile your programs into WASM format. The official name of the WASM platform is: wasm32-unknown-unknown
.
The Rust install tool (rustup
) can install toolchains for you. Install wasm
support on your computer with the following command:
rustup target add wasm32-unknown-unknown
Rust will download and install the current wasm32-unknown-unknown
toolchain support for you.
Install Bindgen
Compiling to WebAssembly isn’t quite enough to make your programs work in your browser. You also need some JavaScript to connect your WASM program to the browser’s execution engine. You could create this by hand, but it’s really tedious. Instead, the wasm-bindgen
program can do the work for you.
Note that when you update your Rust setup, you’ll want to repeat this process to get the newest
wasm-bindgen
tool.
You can install bindgen with the following command:
cargo install wasm-bindgen-cli
Compilation
You can now compile your programs to WASM with the following command:
cargo build --target wasm32-unknown-unknown --release
This will download all of your dependencies, and compile them—alongside your program—into WASM format. The binaries will be optimized, and almost ready to use. You still have a couple of steps remaining: handling filesystem dependencies and making some HTML to actually run your program in a browser.
Embedding: WASM doesn’t have a File System
WASM doesn’t provide direct support for your filesystem. Overall, that’s a good thing—you don’t want random programs on the Internet gaining access to your private files! It does make life a little more difficult when you need to load resources to support your program. bracket-lib
helps get around this by providing an “embedding” system: you can embed resources directly into your program, including them at compilation time. This makes compilation take longer (and produces larger binaries), but it removes the need to create a web-based asset loading system.
bracket-lib
automatically embeds terminal8x8
and vga8x16
fonts for you. The basic bracket-lib
demos will work unchanged because of this, as will the ASCII version of Flappy Dragon. However, you probably need more than the built-in font files! These can be added with a two-step process.
In your main.rs
file, outside of the main
function, you can embed resources with the embedded_resource!
macro provided by bracket-lib
. This is a thin wrapper over the built-in include_bytes!
macro. The path you provide must be the path Cargo will see when it runs the build. For example, the following line of code embeds flappy32.png
from the FlappyBonus
project:
embedded_resource!(TILE_FONT, "../resources/flappy32.png");
This includes your file in the program’s compiled output. The last step is to tell bracket-lib
to include it as a resource. At the top of your main.rs
function, you need to link the resource (this adds it to bracket-lib
’s font system). Remove any ..
from your path: this step uses the final path stored in the resource build. For example, to link the flappy32.png
file we embedded a moment ago you would use:
fn main() -> BError {
link_resource!(TILE_FONT, "resources/flappy32.png");
let context = BTermBuilder::new()
.with_font("flappy32.png", 32, 32)
.with_simple_console(SCREEN_WIDTH, SCREEN_HEIGHT, "flappy32.png")
.with_fancy_console(SCREEN_WIDTH, SCREEN_HEIGHT, "flappy32.png")
.with_title("Flappy Dragon Enhanced")
.with_tile_dimensions(16, 16)
.build()?;
main_loop(context, State::new())
}
Building and Binding
Now that’s in place, build your project:
cargo build --target wasm32-unknown-unknown --release
Now create a directory called wasm-help
(off of your project directory, next to src
and in the same directory as Cargo.toml
). You don’t have to name it that, but the examples in this article will assume that you did so. It’s time to create the Javascript linking your program to the web browser. Run the following command (you may need to adjust the path to match your target
directory if you are using workspaces):
wasm-bindgen target\wasm32-unknown-unknown\release\flappy_bonus.wasm --out-dir .\wasm_help --no-modules --no-typescript
If you look in your wasm-help
directory, you’ll see that some files have appeared:
flappy_bonus.js
- containing JavaScript bindings for your program.flappy_bonus_bg.wasm
- containing your WASM compiled code.
All that remains is to create a webpage to execute your program.
Skeletal HTML
I typically copy/paste the following into a file named index.html
in the wasm-help
directory and edit the text to match the game name:
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
</head>
<body>
<p>The Flappy Bonus example from <a href="https://hands-on-rust.com/">Hands-on Rust</a>.</p>
<canvas id="canvas" width="640" height="480"></canvas>
<script src="./flappy_bonus.js"></script>
<script>
window.addEventListener("load", async () => {
await wasm_bindgen("./flappy_bonus_bg.wasm");
});
</script>
</body>
</html>
This file loads the JavaScript file created for you, and loads your WASM once the page has finished downloading. It creates a “canvas” on which the game can render. Upload your wasm-help
directory to a webserver (even a local one), and you can play your game in a browser.
Flappy Dragon - WASM Edition
The examples so far have shown you how to make flappy_bonus
run in a browser. All code changes and steps have been included. For completeness, here’s a link to the finished program. (LINK) See what changed?
- The
wasm-help
directory is ready to use. main.rs
now embeds and links the font file directly.- I’ve inclued a
.cmd
batch file to build it on windows.
Dungeon Crawler - WASM Edition
The Dungeon Crawler from Hands-on Rust requires a few more changes: Legion needs to be put into single-threaded mode, you need to embed your tile graphics, and the later chapters that use data-driven definitions need to load the data from embedded resources.
We’ll work on the loot_tables
example to get the finished game onto the web.
Single-Threaded Legion
Open your Cargo.toml
file and find the dependency for Legion
. Replace it with the following:
legion = { version = "=0.3.1", default-features = false, features = ["wasm-bindgen", "codegen"] }
This disables Legion’s multi-threading and enables compatibility with wasm-bindgen
.
Embedded Tile Graphics
Just like you did with Flappy, you need to embed the graphics files. In main.rs
(above fn main()
) include the following:
embedded_resource!(TILE_FONT, "../resources/dungeonfont.png");
At the top of fn main()
include:
link_resource!(TILE_FONT, "resources/dungeonfont.png");
Otherwise, your main
function is the same:
embedded_resource!(TILE_FONT, "../resources/dungeonfont.png");
fn main() -> BError {
link_resource!(TILE_FONT, "resources/dungeonfont.png");
let context = BTermBuilder::new()
.with_title("Dungeon Crawler")
.with_fps_cap(30.0)
.with_dimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT)
.with_tile_dimensions(32, 32)
.with_resource_path("resources/")
.with_font("dungeonfont.png", 32, 32)
.with_font("terminal8x8.png", 8, 8)
.with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(SCREEN_WIDTH*2, SCREEN_HEIGHT*2, "terminal8x8.png")
.build()?;
main_loop(context, State::new())
}
Notice how it is using terminal8x8
, but you don’t link it? It’s embedded by default in bracket-lib
.
Loading Data Files
In the Loot chapter of Hands-on Rust, you learn to use TOML files to define your data. Loading the definitions from disk offers great flexibility, but isn’t very WASM friendly—you need to embed the file in your program. Fortunately, Rust provides an include_bytes!
macro to embed files in your code. You can combine this with a from_bytes
function from the RON deserializer to include the file as part of your WASM build.
Open src/spawner/template.rs
and replace the load()
function:
const TEMPLATE_FILE : &[u8] = include_bytes!("../../resources/template.ron");
impl Templates {
pub fn load() -> Self {
ron::de::from_bytes(TEMPLATE_FILE).expect("Unable to load templates")
//let file = File::open("resources/template.ron")// (11)
// .expect("Failed opening file");
//from_reader(file).expect("Unable to load templates")// (12)
}
The template.ron
file will now be embedded in your WASM file, and the reader will load it from there.
Build the Game
Next, you need to follow the same steps you used for Flappy. Create a wasm-help
directory, and add an index.html
file to it:
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
</head>
<body>
<p>Loot Tables example from <a href="https://hands-on-rust.com/">Hands-on Rust</a>.</p>
<canvas id="canvas" width="640" height="480"></canvas>
<script src="./loot_tables.js"></script>
<script>
window.addEventListener("load", async () => {
await wasm_bindgen("./loot_tables_bg.wasm");
});
</script>
</body>
</html>
Then build your WASM file:
cargo build --target wasm32-unknown-unknown --release
Next, run wasm-bindgen
to connect Rust and JavaScript:
wasm-bindgen target\wasm32-unknown-unknown\release\loot_tables.wasm --out-dir .\wasm_help --no-modules --no-typescript
Now you can publish your wasm_help
directory to your webserver.
Here’s the Dungeon Crawler in your browser
Wrap-Up
WebAssembly is a great way to publish your games on the web. Rust has all the tools you need, and bracket-lib
is designed to help you with the process. Now you can publish your game, too. Be sure to send me a link if you do—I love to see what you come up with.