DEV Community

Gunstein Vatnar
Gunstein Vatnar

Posted on • Edited on

How I deployed Pinball2D to the Web using WebAssembly

Introduction

In my previous article I wrote a short introduction to how I built Pinball2D using Bevy and Rapier.
When I upgraded Pinball2D to Bevy version 0.6 I realized it should be easy to deploy the game to web using WebAssembly.
I was not disappointed.

Summarized, I did the following:

  • First I built and tested the WebAssembly locally.
  • Then I built a Docker container with Nginx webserver and the WebAssembly.
  • In the end I deployed the Docker container to an Ubuntu Linode server with docker-compose.
  • I also made some efforts to reduce the size of the WebAssembly. I managed to reduce the size by 50% with the Binaryen toolkit.

The finished result

https://pinball2d.vatnar.no/

Source code

More details

Installing some prerequisites

rustup target add wasm32-unknown-unknown

cargo install wasm-bindgen-cli
Enter fullscreen mode Exit fullscreen mode

Build WebAssembly

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

wasm-bindgen --out-name wasm_pinball2d --out-dir wasm/target --target web target/wasm32-unknown-unknown/release/pinball2d.wasm
Enter fullscreen mode Exit fullscreen mode

Add an index.html file

I put an index.html in the wasm-directory created above.
Index.html presents the WebAssembly and gives some additional information.
I had to add some javascript code to force focus to the canvas. Keypress events are only sent to the canvas when the canvas is in focus. Basically, the launcher and flippers doesn't work if the WebAssembly canvas is not in foucs. There might be more elegant solutions to this problem.

<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      body {
        background-color: black;
      }
      *:focus {
        outline: none;
      }
    </style>
  </head>
  <body>
    <h1 style="color:#009999;">Pinball2D</h1>
    <p style="font-size:14px; color:#009999; font-weight:bold; font-style:italic;">
        Launch ball: Space<br>
        Flippers: Left/Right arrow<br><br>
        <a href="https://github.com/gunstein/Pinball2D" target="_blank">https://github.com/gunstein/Pinball2D</a>
    </p>
  </body>
  <script type="module">
    import init from './target/wasm_pinball2d.js';
    async function run(){
        await init()

    }
    run();
  </script>

  <script>
    //Force focus to game-canvas.  
    const targetNode = document.documentElement || document.body;
    const config = {childList: true, subtree: true};

    const callback = function(mutationsList, observer) {
        for(let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                // Elements have changed
                let canvas = document.querySelector("canvas");
                if (canvas !== null)
                {
                    canvas.focus();
                    canvas.onblur = function() {
                    var me = this;
                    setTimeout(function() {
                    me.focus();
                    }, 50);
                }
                }
            }
        }
    };

    const observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
  </script>
</html>
Enter fullscreen mode Exit fullscreen mode

Test WebAssembly locally

Install basic-http-server

cargo install basic-http-server
Enter fullscreen mode Exit fullscreen mode

Run basic-http-server with WebAssembly

basic-http-server wasm
Enter fullscreen mode Exit fullscreen mode

Build a docker container

Dockerfile

The dockerfile is used to build a Docker container that is using Nginx as webserver. The wasm directory is copied into the image with the COPY command.

FROM nginx:stable-alpine
COPY wasm /usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

Building the container

docker build -t pinball2d .

docker tag pinball2d:latest gunstein/pinball2d:latest
Enter fullscreen mode Exit fullscreen mode

Test the container locally

It's easy to test the container locally before deploying.

docker run -p 8080:80 pinball2d
Enter fullscreen mode Exit fullscreen mode

Push to DockerHub

docker push gunstein/pinball2d:latest
Enter fullscreen mode Exit fullscreen mode

Deploy to server

I use docker-compose to deploy my Pinball2D container. Traefik, reverse proxy, makes certificate handling and other boring stuff much easier. I described more thorough about a similar deployment in this article.
Here's my docker-compose.yml file:

version: "3.8"

services:
  traefik:
    image: "traefik:v2.6"
    container_name: "traefik"
    command:
      # - "--log.level=DEBUG"
      # - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.email=my_email@email.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      # redirect port 80 -> 443
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
    ports:
      - "443:443"
      - "80:80"
      # - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  pinball2d:
    image: gunstein/pinball2d:latest
    container_name: "pinball2d"
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.pinball2d.rule=Host(`pinball2d.vatnar.no`)"
      - "traefik.http.routers.pinball2d.entrypoints=websecure"
      - "traefik.http.routers.pinball2d.tls.certresolver=myresolver"
      # use compression
      - "traefik.http.routers.pinball2d.middlewares=test-compress"
      - "traefik.http.middlewares.test-compress.compress=true"

Enter fullscreen mode Exit fullscreen mode

Reduce size of WebAssembly

After some testing of the WebAssembly I started to wonder if the size could be reduced. The default release build is approximately 15MB.
First I tried adding some options to Cargo.toml.
For example:

[profile.release]
opt-level = 'z'
Enter fullscreen mode Exit fullscreen mode

It didn't save that much for this WebAssembly. Around 1MB.

Then I installed the Binaryen toolkit and ran it with options to reduce size aggressively.

sudo apt install binaryen
wasm-opt -Oz -o wasm_pinball2d_bg_small.wasm wasm_pinball2d_bg.wasm
Enter fullscreen mode Exit fullscreen mode

That was successful, in my opinion.
The size of the WebAssembly was reduced with close to 50%.
(I'm not seeing the same size reduction after upgrading to Bevy version 0.7. It's more like 30% now, it seems, but I have not used much effort investigating this.)
Read more about WebAssembly shrinking here

Top comments (0)