🦀 Deploying your Rust WASM Game
In this post, we see you how you can get going on deploying your Rust WASM game. Often, WASM games will just need an HTML page to be embedded in, and a public server to make the page available across the Internet. We saw in a recent post on Rust game engines, that many engines currently offer the option to output your game in WASM, with (Bevy, Fyrox and Macroquad included).
Here, we will use an open-source Macroquad game written to demo Entity Component Systems, using Shipyard, a Rust ECS mentioned in a recent Rust ECS post.
Shuttle Rust App Hosting Service
That leaves us with a web server and deployment service to pick! Axum is currently a popular Rust Web framework choice, and to keep things completely Rust, we shall deploy using Shuttle. Shuttle offer Rust app hosting and have a free tier, which should be just fine for our game.
Now we know the approach, why don’t we get started?
🧱 What we're Building
We will build the Square Eater game, which was originally written to demo ECSs. You should be able to follow along easily enough, with your own Rust game, though, even if it’s not built using Macroquad. Let me know if you run in difficulties with other game engines!
We will create three binaries from the project:
- one just to run the game locally, natively or build to WASM;
- another as a local Axum server for development and to test the webpage locally; and
- a copy of the Axum server binary configured to deploy and run on Shuttle.
Shuttle has excellent Axum support, and the code for running the Axum server locally and on Shuttle is almost identical, so we will put that shared code in a library and reference it from the two server output versions we create.
⚙️ Cargo.toml
To add the three binaries in a single project, we can use the Cargo targets feature. The Cargo.toml
file will look like this:
[lib]
path = "src/lib.rs"
name = "shared"
[[bin]]
path = "src/bin/server.rs"
name = "local-server"
[[bin]]
path = "src/bin/shuttle.rs"
name = "square-eater"
[[bin]]
path = "src/bin/main.rs"
name = "game"
Here we have the library and three binaries, mentioned earlier. If you match the name of the Shuttle binary to your Shuttle project name, (line 16
), Shuttle will automatically build and deploy that binary. The project name needs to be unique across Shuttle, as it forms the domain Shuttle uses to server the project from.
📂 Project Structure
Here, we are just using some open-source code for the game, and it can fit into a single source file. We place that code in src/bin/main.rs
, and can run the game locally using:
cargo run --bin game
With game
matching the binary name used in Cargo.toml
, above. The code we use is provided as an example of using the Shipyard Rust ECS. Paste the main.rs
square_eater code from the repo into src/bin/main.rs
in your project.
For this to run, we need to add the following dependencies in Cargo.toml
:
[package]
name = "square_eater"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
name = "shared"
# TRUNCATED...
[[bin]]
path = "src/bin/main.rs"
name = "game"
[build-dependencies]
askama = "0.12.1"
[dependencies]
macroquad = "0.4.4"
miniquad = "0.3.12"
sapp-wasm = "=0.1.26"
shipyard = { version = "0.6.2", default-features = false, features = ["proc", "std"] }
miniquad
and sapp-wasm
are not, strictly, needed to run the game locally, but used in the next section, when we build the game to WASM.
🎮 Building a Macroquad the Game as WASM
See the macroquad docs for full details on building a WASM game.
If this is the first time you are generating WASM with Rust on your machine, add the WASM output toolchain:
rustup target add wasm32-unknown-unknown
Then, you can run a WASM release build with:
cargo build --bin game --release --target wasm32-unknown-unknown
This will output the WASM code to target/wasm32-unknown-unknown/release/game.wasm
. Copy that file to a new public
folder in the project root directory; we will use it when we create the web page next.
Adding a Web Page
We will serve the WASM file and HTML page from the public
directory. Add an index.html
file there with this content:
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link
rel="alternate icon"
href="/favicon.ico"
type="image/png"
sizes="16x16"
/>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="192x192" />
<link rel="mask-icon" href="/favicon.svg" />
<title>Square Eater</title>
<style>
@font-face{font-display:swap;font-family:Josefin Sans;font-style:normal;font-weight:700;src:url(/fonts/josefin-sans-v32-latin-700.woff2)format("woff2")}@font-face{font-display:swap;font-family:Fira Mono;font-style:normal;font-weight:400;src:url(/fonts/fira-mono-v14-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:Fira Mono;font-style:normal;font-weight:700;src:url(/fonts/fira-mono-v14-latin-700.woff2)format("woff2")}:root{--font-size-1:1.125rem;--font-size-2:1.406rem;--font-size-3:1.758rem}*,:after,:before{box-sizing:border-box}*{margin:0}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}body{background-color:oklch(.68,.03,261.69);color:oklch(.3,.04,279.69);font:var(--font-size-1)/1.75 Fira Mono;width:100%;max-width:100%;display:flex}main{width:100%;max-width:640px;margin-block-start:4rem;margin-inline:auto}h1,h2{margin-block:3rem 1.5rem}h1{font:var(--font-size-3)/1.3 Josefin Sans}h2{font:var(--font-size-2)/1.3 Josefin Sans}p{margin-block:0 1rem;margin-inline:0;padding:0}canvas{width:640px;max-width:100%;height:auto;margin-block:3rem 1.5rem;padding:0;overflow:hidden}
</style>
</head>
<body>
<main>
<h1>Square Eater</h1>
<canvas id="glcanvas" tabindex="1"></canvas>
<h1>How to play</h1>
<p>
Eat as many squares as you can before the swarm gets you! Eat yellow
squares to repel the swarm.
</p>
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script>
load("game.wasm");
</script>
</main>
</body>
</html>
I have inlined the CSS here, and you can use Rust-based tooling like Lightning CSS to minify and bundle CSS here. You might also want to create a Rust build script to generate the HTML from a template, using the askama
crate (works a little like Jinja).
Remember to add any favicons referenced in the HTML rel tags to the public folder.
🚧 Local Development and Testing Server using Axum
We just need Axum to run as a static file server, and can set this up in a few source files. Create src/bin/server.rs
with this content:
use shared::app;
#[tokio::main]
async fn main() {
let listen_address = "0.0.0.0:8000";
let listener = tokio::net::TcpListener::bind(listen_address).await.unwrap();
println!("Local http://{listen_address}/");
axum::serve(listener, app()).await.unwrap();
}
This references the shared library, mentioned before. We can create it now, adding a src/lib.rs
file:
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "game")] { } else {
mod routes;
use axum::{routing::get, Router};
use routes::health_check;
use tower_http::services::{ServeDir, ServeFile};
pub fn app() -> Router {
Router::new()
.route("/health_check", get(health_check))
.fallback_service(
ServeDir::new("public").not_found_service(ServeFile::new("public/index.html")),
)
}
}
}
This will serve files in the public
folder from the /
route, and just serve the index.html
file if the requested path is not found. See Auxm docs for more sophisticated setup.
Finally, we can add the mentioned health_check
route in src/routes/health_check.rs
:
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde::Serialize;
#[derive(Serialize)]
struct Health {
healthy: bool,
}
pub async fn health_check() -> impl IntoResponse {
let health = Health { healthy: true };
(StatusCode::OK, Json(health))
}
And, include that file in the source tree by adding src/routes/mod.rs
:
pub mod health_check;
pub use health_check::health_check;
As a last step, here, we need to update Cargo.toml
with Axum dependencies. We only use those dependencies when building the local or Shuttle binaries, not the game. For that reason, we make them optional, and will update the game build command to skip them.
Here is the final Cargo.toml
:
[package]
name = "square_eater"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
name = "shared"
[[bin]]
path = "src/bin/server.rs"
name = "local-server"
[[bin]]
path = "src/bin/shuttle.rs"
name = "square-eater"
[[bin]]
path = "src/bin/main.rs"
name = "game"
[dependencies]
axum = { version = "0.7.4", optional = true }
cfg-if="1.0.0"
macroquad = "0.4.4"
miniquad = "0.3.12"
sapp-wasm = "=0.1.26"
serde = { version = "1.0.196", features = ["derive"] }
shipyard = { version = "0.6.2", default-features = false, features = ["proc", "std"] }
shuttle-axum = { version = "0.38.0", optional = true }
shuttle-runtime = { version = "0.38.0", optional = true }
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"], optional = true }
tower-http = { version = "0.5.1", features = ["fs", "trace"], optional = true }
[features]
default = ["shuttle"]
game = []
local_server = [
"dep:axum",
"dep:tokio",
"dep:tower-http",
]
shuttle = [
"dep:axum",
"dep:shuttle-axum",
"dep:shuttle-runtime",
"dep:tokio",
"dep:tower-http",
]
I also added the shuttle dependencies, which we use in the next section.
To build just the game, now, we can run:
cargo build --bin game --release --target wasm32-unknown-unknown --features=game --no-default-features
And to test the local server:
cargo run --bin local-server --features=local_server
Try that command, and jump to http://127.0.0.1:8000
in your browser, and you should see your game, in-browser.
🚀 Shuttle Configuration and Deploy
To get going with Shuttle, you will need the CLI app installed locally. To install, run:
cargo install cargo-shuttle
This will probably take a few minutes to download and compile.
Next, create a Shuttle.toml
file in the project root directory, with the following content:
name = "square-eater" # REPLACE with your own project name
Then, we need to add the source code for the shuttle binary file, which Shuttle will run when they deploy your app. This file will is tiny, because we are using the shared Axum-based library, created earlier:
use shared::app;
#[shuttle_runtime::main]
async fn main() -> shuttle_axum::ShuttleAxum {
Ok(app().into())
}
Shuttle Deploy
To deploy, first create a project, from the Terminal (remember your project name has to be unique for this to work):
cargo shuttle project start
Then deploy:
cargo shuttle deploy
The binary will compile on Shuttle servers, and you will see output as it compiles. Your game site should now be accessible from the URL https://YOUR-PROJECT-NAME.shuttleapp.rs
!
🙌🏽 Deploying your Rust WASM Game: Wrapping Up
In this post on deploying your Rust WASM game, we saw how you can deploy a Macroquad WASM game with Shuttle and Axum. In particular, we saw:
- how to Cargo targets to build multiple binaries;
- how to serve a static file server with Axum; and
- the process for deploying a Rust app to Shuttle.
I hope you found this useful. You can find the full source code for the project in a Rodney Lab GitHub repo. Please share links for WASM games you create. Also, let me know if there is anything I could improve in this content to make it easier to follow, or improve your experience.
🙏🏽 Deploying your Rust WASM Game: Feedback
If you have found this post useful, see links below for further related content on this site. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on X (previously Twitter) and also, join the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Game Dev as well as Rust and C++ (among other topics). Also, subscribe to the newsletter to keep up-to-date with our latest projects.