Run Your Rust Games in a Browser: Hands-on Rust Bonus Content

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 of bracket-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.

Share