How I deployed Pinball2D to the Web using WebAssembly
Tags: rust, webassembly, gamedev, bevy
Published: 2022-02-04 20:43:54

## Introduction
In my previous [article](https://dev.to/gunstein/a-simple-2d-pinball-game-made-with-rust-bevy-and-rapier-3hh0) 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/](https://pinball2d.vatnar.no/)

## Source code
* [Sourcecode for Pinball2D on Github](https://github.com/gunstein/Pinball2D)

## More details
### Installing some prerequisites
```Bash
rustup target add wasm32-unknown-unknown

cargo install wasm-bindgen-cli
```

### Build WebAssembly
```Bash
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
```

### 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
<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>
``` 

### Test WebAssembly locally
#### Install basic-http-server 
```Bash
cargo install basic-http-server
```
#### Run basic-http-server with WebAssembly
```Bash
basic-http-server wasm
```

### 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.
```Dockerfile
FROM nginx:stable-alpine
COPY wasm /usr/share/nginx/html
```
#### Building the container
```Bash
docker build -t pinball2d .

docker tag pinball2d:latest gunstein/pinball2d:latest
```
#### Test the container locally
It's easy to test the container locally before deploying.
```Bash
docker run -p 8080:80 pinball2d
```
#### Push to DockerHub
```Bash
docker push gunstein/pinball2d:latest
```

### 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](https://dev.to/gunstein/how-i-put-my-demo-into-production-with-docker-compose-and-traefik-5553).
Here's my docker-compose.yml file:
```yaml
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"

```
### 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:
```bash
[profile.release]
opt-level = 'z'
```
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.
```bash
sudo apt install binaryen
wasm-opt -Oz -o wasm_pinball2d_bg_small.wasm wasm_pinball2d_bg.wasm
```
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](https://rustwasm.github.io/docs/book/reference/code-size.html)