MASSLESS LTD.

Steps to Run a Rust+Bevy Project in the Browser with wasm-bindgen

22 November 2024

Steps to Run a Rust+Bevy Project in the Browser with wasm-bindgen

This article assumes you have basic familiarity with Rust, and want to figure out how to run your Bevy+Rust project in a web browser.

Follow these steps to get your Rust+Bevy project running in the browser using WebAssembly with the wasm-bindgen CLI.

0. Setup

Step 0.0: Create index.html File

Create an index.html file in your ./web directory with the following content:

<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      body {
        margin: 0;
        background: darkgrey;
        background-size: 400% 400%;
        animation: gradient 15s ease infinite;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;    
      }
      .loader {
        border: 16px solid #f3f3f3;
        border-radius: 50%;
        border-top: 16px solid #3498db;
        width: 120px;
        height: 120px;
        position: absolute;
        z-index : -999;
        -webkit-animation: spin 2s linear infinite;
        animation: spin 2s linear infinite;
      }
      
      @-webkit-keyframes spin {
        0% { -webkit-transform: rotate(0deg); }
        100% { -webkit-transform: rotate(360deg); }
      }
      
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
      </style>
  </head>
  <body>
    <div class="loader"></div>
    <script>
        // the following function keeps track of all AudioContexts and resumes them on the first user
        // interaction with the page. If the function is called and all contexts are already running,
        // it will remove itself from all event listeners.
        (function () {
            // An array of all contexts to resume on the page
            const audioContextList = [];

            // An array of various user interaction events we should listen for
            const userInputEventNames = [
                "click",
                "contextmenu",
                "auxclick",
                "dblclick",
                "mousedown",
                "mouseup",
                "pointerup",
                "touchend",
                "keydown",
                "keyup",
            ];

            // A proxy object to intercept AudioContexts and
            // add them to the array for tracking and resuming later
            self.AudioContext = new Proxy(self.AudioContext, {
                construct(target, args) {
                    const result = new target(...args);
                    audioContextList.push(result);
                    return result;
                },
            });

            // To resume all AudioContexts being tracked
            function resumeAllContexts(_event) {
                let count = 0;

                audioContextList.forEach((context) => {
                    if (context.state !== "running") {
                        context.resume();
                    } else {
                        count++;
                    }
                });

                // If all the AudioContexts have now resumed then we unbind all
                // the event listeners from the page to prevent unnecessary resume attempts
                // Checking count > 0 ensures that the user interaction happens AFTER the game started up
                if (count > 0 && count === audioContextList.length) {
                    userInputEventNames.forEach((eventName) => {
                        document.removeEventListener(eventName, resumeAllContexts);
                    });
                }
            }

            // We bind the resume function for each user interaction
            // event on the page
            userInputEventNames.forEach((eventName) => {
                document.addEventListener(eventName, resumeAllContexts);
            });
        })();
    </script>
    <script type="module">
        import init from '/out/<your-project-name-here>.js' 
        init();
    </script>
</body>
</html>

Remember to replace <your-project-name-here> with the actual project name as specified in your Cargo.toml file.

Step 0.1: Install wasm-bindgen CLI

First, install the latest version of the wasm-bindgen CLI by running:

cargo install -f wasm-bindgen-cli

If you run into issues, you may need to install a specific version. For example:

cargo install -f wasm-bindgen-cli --version 0.2.93

This version might be outdated, so it's best to keep things up to date.

Step 0.2: Add WebAssembly Target

In your project directory, add web support by running:

rustup target add wasm32-unknown-unknown

This step is required only once per project.

1. Build and Run the Project

Step 1.1: Build the Project for Web

To compile your project to WebAssembly, run:

cargo build --release --target wasm32-unknown-unknown

This needs to be done every time you want to build your project for the web.

Step 1.2: Generate WebAssembly Bindings

Next, generate the WebAssembly bindings by running:

wasm-bindgen --out-dir ./out/ --target web ./target/wasm32-unknown-unknown/release/<your_project_name>.wasm

Replace <your_project_name> with the actual project name as specified in your Cargo.toml file.

Step 1.3: Copy Output Directory

Copy the ./out directory to ./web/out by running:

cp -r ./out ./web/out

Step 1.4: Serve the Project Locally

You can now serve your project locally with:

npx serve web

Open localhost on the specified port to see your Rust+Bevy project running in the browser.

--

Step 2: Deploying

You can deploy your /web folder to a static hosting service such as Vercel, Netlify, Github Pages, AWS S3, or Render - for example.

Make sure you've included your /out directory in the web folder, along with the index.html code.