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
Source code
More details
Installing some prerequisites
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
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
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>
Test WebAssembly locally
Install basic-http-server
cargo install basic-http-server
Run basic-http-server with WebAssembly
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.
FROM nginx:stable-alpine
COPY wasm /usr/share/nginx/html
Building the container
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.
docker run -p 8080:80 pinball2d
Push to DockerHub
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.
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"
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'
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
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)