This repository is an example project using the power of Web Assembly and the WebAssembly Component Model to create a hot reloadable game development experience.
rustup target add wasm32-wasi && cargo install wasm-tools
- Install Just
- In one terminal window run
just watch
to start compiling the game assembly on every change - In another terminal window, run
just hotreload
- Make a change in
game/src/lib.rs
to some text or a color, save the file, and watch the logic - Click to increment the counter, and hotreload with another change to see that the state survives
- Run
just run
to run the project without wasmtime or hotreloading
The project is split up into two crates:
- Game (A simple library) - Contains all of the core logic describing the scene to be drawn, how to interact with keyboard/mouse, etc.
- Launcher (A macroquad game binary) - A uncommonly changed shell which forwards UX interactions and draws to the screen as requested
The launcher contains a feature flag hotreload
which changes how the launcher consumers the game crate:
Without hotreload
the Launcher
has a direct hard library dependency on Game
and nothing special happens.
With hotreload
however the Launcher loads up a web assembly packaged version of the launcher crate via wasmtime. A simple file watcher then waits for the assembly file to change, and then reloads it before processing the next frame. Before this reload, the entire state of the game is serialized via Serde which is then restored inside the new web assembly instance.
There is a WebAssembly Component Interface file (wit/interface.wit
) which contains a simple stateless interface to a portion of macroquad. Each frame is passed the state of the mouse and keyboard and returns a vector of draw instructions. These instructions are then executed within the launcher host.
As we want to arbitrarily reload the game, the global state of the graphics stack and window must not be lost. This is why the Game does not directly use macroquad.
Note: It would be possible to have the game web assembly directly call exposed interfaces to invoke macroquad, but it was decided to use a set of commands to keep the FFI boundary from being too "chatty".
hot-lib-reloader-rs is an impressive crate, but in my experience was never stable enough to actually save me much time. About 1 in 5 times my projects would crazy, so I had to setup a relaunch script, which would occasionally go haywire and need to be manually killed.
Any use of any thread local storage, or function pointers (dyn traits) would caused it to misbehave. This applied significant design pressure to how the project needed to be laid out to be "binary reload friendly".
So far WASM hot reloading has been completely stable in my experience.
The launcher binary uses a feature flag to enable hot reload goodness. Unfortunately it is not possible to remove a dependency, so there is an "unnecessary" hard link from game to launcher even in the hotreload case. The launcher is written to never use the game dependency in hotreload contexts, but if it did those portions would not be hotreloaded (or you would get a compile error).
It is possible to use two inverse feature flags (hotreload and direct), but that is discouraged behavior. I have not looked into this much yet. In the past the crate hierarchy was more complex and feature unification prevented from being an option.
As the WIT interface used between the worlds is code generated, it is not always trivial to see the final rust interface, as you can not jump to definition. The rust-analyzer: Expand macro recursively at caret
feature can be useful, but it is not an ideal user experience.