This repository contains a basic multiplayer game inspired by Achtung, die Kurve!, written in Rust using Bevy Engine (v0.18) and Avian Physics. The goal of the project was to explore network programming when real-time movement is involved.
⬆️ Play the local multiplayer build on itch.io. A signalling server for the online multiplayer build is not currently deployed.
- Implementation of Achtung, die Kurve! (also known as Curve Fever)
- Local multiplayer (up to 5 players)
- Online multiplayer (up to 8 players)
- Native and browser-friendly online multiplayer (plus a standalone signalling server)
- You can mix and match local and online players in the same game
- Touch controls for mobile devices
- Cross-platform (Linux, Windows, WebAssembly)
- Two compile-time backends, selected via Cargo feature flags:
online_renet— native only; UDP transport viabevy_renetwith anetcodeprotocolonline_matchbox— native and WASM; WebRTC data channels viabevy_matchbox, required for browser builds
- Both backends use a client-server topology: one instance acts as host, all others connect as clients
- Wire data is serialised with
postcardand sent over three channels:Unreliable,ReliableUnordered, andReliableOrdered - The matchbox backend brokers initial WebRTC connections through a WebSocket signalling server
(
mooplas_signalling_server, see README); ICE uses Google's public STUN (stun.l.google.com:19302) - The WASM implementation supports room joining: the host generates an 8-character room ID and shares it with clients; clients enter it in the join menu to connect
SIGNALLING_SERVER_URLis baked in at build time (defaults tows://localhost:3536); for native development the signalling server starts embedded in the host process
Online multiplayer:
Lobby with touch controls enabled:
In-game:
You can run this project in any way you like, but I have set things up to make it easy to develop using JetBrains RustRover. For this, you'll need:
direnv- Any Direnv integration plugin e.g. https://plugins.jetbrains.com/plugin/15285-direnv-integration
nix
This way, you'll just need to direnv allow in the project directory after which all prerequisites (incl. Rust, Cargo,
all Bevy dependencies, etc.) will be available to you. The JetBrains plugin will ensure that the environment is
available to your IDE and you can run the project from there (vs cargo build and cargo run in the terminal).
RustRover will always fail to sync the project when you open it because it doesn't wait for direnv. Just re-sync
immediately after the failure and it will work.
Did RustRover forget where the Rust standard library is again? Run the below and update the path in the settings:
find /nix/store -type d -name rust_lib_srccargo sweep is your friend and comes with the Flake. For example, the below will delete all build artefacts that
are older than 7 days:
cargo sweep -t 7To clean everything except for the latest build:
cargo sweep --stamp
<Insert any number of cargo build, cargo test, etc. commands>
cargo sweep --file
Upgrade the flake by running nix flake update --flake . in the repository's base directory.
Note
Browser builds intended for online multiplayer should use the Matchbox backend (--features online_matchbox) and a
reachable signalling server URL.
- Run (already included in the Flake if using Nix):
rustup target add wasm32-unknown-unknown
- Set
RUSTFLAGSenvironment variable:- Linux:
export RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg=getrandom_backend=\"wasm_js\""
- Windows:
$env:RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg=getrandom_backend=`"wasm_js`""
- Linux:
- Set
SIGNALLING_SERVER_URLwhen building for production. If omitted, the build falls back tows://localhost:3536, which is useful for native/local development but not for deployed HTTPS browser builds.- Linux:
export SIGNALLING_SERVER_URL="wss://signal.example.com"
- Windows:
$env:SIGNALLING_SERVER_URL="wss://signal.example.com"
- Linux:
- Make sure you have Node.js with
serveinstalled
Then you can build the WASM file:
- Build the WASM file:
For local development builds you can omit
SIGNALLING_SERVER_URL='ws://localhost:3536' RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg=getrandom_backend="wasm_js"' cargo build --target wasm32-unknown-unknown --release --manifest-path mooplas_game/Cargo.toml --package mooplas_game --bin mooplas_game --no-default-features --features online_matchbox
SIGNALLING_SERVER_URLand the game will usews://localhost:3536. - Clean the
/www/publicdirectory and copy the game's assets over:- Linux:
./scripts/clean-mooplas-files.sh ./scripts/copy-assets.sh
- Windows:
./scripts/clean-wasm-files.ps1 ./scripts/copy-assets.ps1
- Linux:
- Run
wasm-bindgento generate the JS bindings and move all relevant files to the/www/publicdirectory:- Linux:
wasm-bindgen --out-dir ./www/public --target web ./target/wasm32-unknown-unknown/release/mooplas_game.wasm
- Windows:
wasm-bindgen.exe --out-dir ./www/public --target web ./target/wasm32-unknown-unknown/release/mooplas_game.wasm
- Linux:
- Zip the WASM file if needed (e.g. for itch.io):
zip -r mooplas.zip ./www/public
You can optimise the WASM file (from the Unofficial Bevy Cheat Book):
# Optimize for size (z profile).
wasm-opt -Oz -o output.wasm input.wasm
# Optimize for size (s profile).
wasm-opt -Os -o output.wasm input.wasm
# Optimize for speed.
wasm-opt -O3 -o output.wasm input.wasm
# Optimize for both size and speed.
wasm-opt -O -ol 100 -s 100 -o output.wasm input.wasmFinally, to run the game in your browser locally, run the below and paste the URL copied to your clipboard into your browser:
npx serve ./www/public



