Dose Response ported to WebAssembly!

Rust now supports compiling to WebAssembly (wasm). That means you can write Rust code, compile it and run it directly in the browser.

I planned to wait with looking into it once everything settles down. But then richardanaya wrote this very simple “roguelike” example in rust+wasm. I looked at it, and it was short and clear. Enough to understand what was going on.

And so I figured maybe I should take a look at what would it take to run Dose Response on wasm.

This post documents a process of doing that.

I had no idea what to expect. Dose Response is not a huge game, but it was not written with the browser in mind and while the game is rather small, it is not a trivial project.

I wasn’t even sure it was possible to actually do this at this time without a considerable rewrite (which I wasn’t going to do).

As a warning, there’s a certain amount of cargo-culting involved in this effort. My webdev (js, css, html) knowledge is a bit rusty, I knew next to nothing about wasm, not to mention these fancy modern graphics options such as the Canvas 2D API and WebGL. And it’s not like I’m an FFI/low-level expert either.

There were a few things going for this effort, though:

The game has a support for conditional compilation and switchable backends already Turns out, we don’t have a lot of direct dependencies (but a bunch of transitive ones) All the dependencies we do have are in Rust The graphics is just a grid of characters on the screen – no need to figure out how to do images etc. The game is single-threaded

The Plan

Set Rust up for wasm compilation Compile Dose Response if it turns out I can’t get it to compile, don’t even mess with the rest Figure out how to handle the game loop Figure out input handling Figure out graphics

Ideally, we would get to a minimal JavaScript core that will call out to Rust for everything without having to make huge changes to our Rust codebase.

Prerequisites

We need rustup and for now, Rust nightly. I’d switched to Nightly months ago because I tried impl Trait and didn’t want to go back. So that’s all good.

We need to get the latest nightly and add the wasm target:

$ rustup update nightly $ rustup target add wasm32-unknown-unknown --toolchain=nightly

Compilation

Let’s try to compile it! (this will fail for sure)

We’ll disable all the optional bits – that includes all graphics backends – so the result would be unplayable even if it did compile. There’s no chance we’ll be able to compile the windowing or opengl dependencies, though, so let’s not even try:

$ cargo +nightly build --release --target wasm32-unknown-unknown --no-default-features

A bunch of stuff failed to update. I ran cargo update and tried again to make sure I’m using the latest bits. That could have caused additional breakage (but it didn’t this time) and it did actually get rid of some of the errors.

But not all of them.

Crate investigation

Now with all the parallel compilation, etc. going around, it was hard to figure out which crates didn’t work and why. So I’ve created a new test project and added my dependencies one by one:

$ cd ~/tmp $ cargo new --vcs git --bin wasm $ cd wasm/ $ cargo +nightly build --release --target wasm32-unknown-unknown --no-default-features Compiling wasm v0.1.0 (file:///home/thomas/tmp/wasm) Finished release [optimized] target(s) in 1.59 secs

Okay, it compiles when there are no dependencies! (this is a basic but important thing to establish)

So now I just added the Dose Response dependencies one by one and tried to compile them to wasm:

bitflags: no problem clap: didn’t compile because atty didn’t compile. That’s okay, we won’t be parsing the command line arguments in the browser rand: didn’t compile – this is a problem. We depend heavily on rand time: didn’t compile. I’m only using this for the Duration struct, so I think I should be able to switch to std::time . Been meaning to anyway. serde, serde_derive, serde_json: compiled no problem

So, Clap is not a big deal, Time shoudln’t be either, but Rand is a huge problem.

I went to look at their repo, thinking about filing an issue when I discovered that the master has fixed this already, it’s not just released on crates.io yet.

So I tried rand = { git = "https://github.com/rust-lang-nursery/rand" } and it compiles /

Note there appears to be a new version of rand published now, so this should be all good. I haven’t tested it yet.

So we need to go back to Dose Response, make clap optional and replace the time crate with std::time . And use the master version of rand .

Compiling clap conditionally

We’ll add a new cargo feature called cli , turn it on by default and delegate our commandline handling there.

So in Cargo.toml in the [features] section:

default = ["opengl", "cli"] cli = ["clap"]

And we change the dependency from clap = "2.20.1" to clap = { version = "2.20.1", optional = true } .

Compiling with cargo check works. Here’s what happens when you disable the default features though:

$ cargo check --no-default-features Compiling dose-response v0.4.3 (file:///home/thomas/code/dose-response) error[E0463]: can't find crate for `clap` --> src/main.rs:6:1 | 6 | extern crate clap; | ^^^^^^^^^^^^^^^^^^ can't find crate error: aborting due to previous error error: Could not compile `dose-response`.

The code needs to handle the cli feature conditionally, too.

All of our clap usage is contained in src/main.rs . First, let’s import the crate only when the cli feature is enabled:

#[cfg(feature = "cli")] extern crate clap;

Okay, now it fails when we try to actually use it:

$ cargo check --no-default-features Compiling dose-response v0.4.3 (file:///home/thomas/code/dose-response) error[E0432]: unresolved import `clap` --> src/main.rs:189:9 | 189 | use clap::{App, Arg, ArgGroup}; | ^^^^ Maybe a missing `extern crate clap;`?

There’s a lot of ways to do this, but I’m just going to move all that logic to a separate function and then compile that out.

#[cfg(feature = "cli")] fn process_cli_and_run_game( display_size: point::Point, world_size: point::Point, map_size: i32, panel_width: i32, default_background: color::Color, title: &str, update: engine::UpdateFn<State>, ) { use clap::{App, Arg, ArgGroup}; let matches = App::new(title) .author("Tomas Sedovic <tomas@sedovic.cz>") .about("Roguelike game about addiction") .arg( ... ) .get_matches(); let state = if let Some(replay) = matches.value_of("replay") { ... }; ... }

And another one for when the cli feature is not enabled:

#[cfg(not(feature = "cli"))] fn process_cli_and_run_game( _display_size: point::Point, _world_size: point::Point, _map_size: i32, _panel_width: i32, _default_background: color::Color, _title: &str, _update: engine::UpdateFn<State>, ) { unimplemented!() }

This one is empty for now, but we will use it when running wasm. And the main function is now just:

process_cli_and_run_game(display_size, world_size, map_size, panel_width, color::background, title, game::update);

So with this, both cargo check and cargo check --no-default-features succeed, though the latter shows a lot of unused code warnings.

Of course if we try to run it with cargo run --no-default-features it, we’ll hit the unimplemented! macro. Normal cargo run continues to work though.

https://github.com/tomassedovic/dose-response/commit/6a16a6a556faf9fe7c71543e33cde9acc4447a5f

Replacing the time crate with std::time

I’m hoping this will be mostly straightforward, because at least in theory, everything we use from the time crate should just be in std::time now.

Let’s remove time from Cargo.toml , compile and try to just replace use time with use std::time everywhere.

That, sadly, didn’t quite work out.

We’re using two imports from the crate: Duration and PreciseTime . std::time seems to use Instant in place of PreciseTime .

The time::now function we use also doesn’t exist, but Instant::now seems to be the equivalent.

Next, Duration::milliseconds is called Duration::from_millis now.

Duration::zero is Duration::new(0, 0) .

Most of the remaining errors were about missing num_milliseconds and num_microseconds methods. These, sadly, are not in the standard library, so I just took their implementations from the time crate:

/// The number of nanoseconds in a microsecond. const NANOS_PER_MICRO: u32 = 1000; /// The number of nanoseconds in a millisecond. const NANOS_PER_MILLI: u32 = 1000_000; /// The number of microseconds per second. const MICROS_PER_SEC: u64 = 1000_000; /// The number of milliseconds per second. const MILLIS_PER_SEC: u64 = 1000; /// The number of seconds in a minute. pub fn num_milliseconds(duration: Duration) -> u64 { let secs_part = duration.as_secs() * MILLIS_PER_SEC; let nanos_part = duration.subsec_nanos() / NANOS_PER_MILLI; secs_part + nanos_part as u64 } pub fn num_microseconds(duration: Duration) -> Option<u64> { if let Some(secs_part) = duration.as_secs().checked_mul(MICROS_PER_SEC) { let nanos_part = duration.subsec_nanos() / NANOS_PER_MICRO; return secs_part.checked_add(nanos_part as u64) } None }

I’ve created a new module called util and put them in there. Every project should have a util module.

The final missing piece is the strftime function.

Turns out, there is one place where we use the date/time functionality as opposed to just tracking time duration. For every game, we create a file that captures the game’s seed and keyboard input so we can replay it later. That file has the current date and time in its name.

This is mostly used for debugging - if we get an unexpected panic or a weird behaviour, we can just replay the game and inspect it.

It’s not necessary for wasm (figuring out how to do file-based IO is a whole other kettle of fish) and so we can conditionally compile that out, too.

https://github.com/tomassedovic/dose-response/commit/386ef64717c8961dcd79165c505e1d96d8bdceca

Making replays optional

We need to get the current date & time and then format it.

There doesn’t seem to be a way to do this in the standard library, so I could just keep using time for that, but according to its repo, the crate is now deprecated and they recommend switching to chrono.

We’ll to make the replay functionality optional and start using chrono for literally one line of code. YAY

Let’s a new optional dependency: chrono = { version = "0.4.0", optional = true } and a new feature to Cargo.toml:

default = ["opengl", "cli", "replay"] replay = ["chrono"]

Next, we add the crate in ( main.rs ):

#[cfg(feature = "replay")] extern crate chrono;

Next to fix the compilation error we replace this:

let timestamp = format!( "{}.{:03}", time::strftime("%FT%H-%M-%S", &cur_time).unwrap(), (cur_time.tm_nsec / 1000000) );

with this:

use chrono::prelude::*; let local_time = Local::now(); // Timestamp in format: 2016-11-20T20-04-39.123. We can't use the // colons in the timestamp -- Windows don't allow them in a path. let timestamp = local_time.format("%FT%H-%M-%S%.3f");

To make replay optional, I needed to mark the generate_replay_path function with #[cfg(feature = "replay")] and update its return value from PathBuf to Option<PathBuf> .

And create stub that returns None for the case when the replay feature is not enabled:

#[cfg(not(feature = "replay"))] pub fn generate_replay_path() -> Option<PathBuf> { None }

And finally fix the compilation errors by handling the changed return value in the code that actually creates the file.

That was not actually difficult, though it turns out the whole reeplay file handling is a bit convoluted – I’ll have to go back and clean that up at some point.

Anyway, we can compile with and witohut default features and make replay still works!

https://github.com/tomassedovic/dose-response/commit/f0d387bc4c86b81f9677881e8a2bd8c8ee2fcf09

Using rand from git

We talked about this before, the currently-published version of the rand crate does not compile under wasm (NOTE: it probably does now), so we need to use the one from master:

rand = { git = "https://github.com/rust-lang-nursery/rand" }

cargo check works, so does cargo check --no-default-features and running the game and replaying it (to verify the random numbers are stable) does as well.

https://github.com/tomassedovic/dose-response/commit/d1c25ad2dc3d1a6a6475f9be28935ba39f1757b9

Compiling dose response with wasm again

So all of this was just updating our dependencies. None of this is necessarily a wasted effort (I planned to switch back to std::time at some point anyway), but the reason for doing it now is so we can do:

$ cargo +nightly build --release --target wasm32-unknown-unknown --no-default-features

IT COMPILES!

Now what?

The resulting file is in target/wasm32-unknown-unknown/release/dose-response.wasm and is about 62kb.

We need to put it inside a webpage somehow and then hook the input and display methods to it.

Browser

So before we do that, let’s run the game without the default features to see what happens (it should panic at the not-implemented main section).

$ cargo run --no-default-features

Which it does.

So let’s try to do the same in a webpage – I’m interested to see what a wasm panic looks like. If the browser doesn’t report anything wrong, we’ll have to tread really carefully.

Creating the webpage

So as the first step, let’s just literally copy the entire webpage from the rust-roguelike repo and run it as is:

https://github.com/richardanaya/rust-roguelike/blob/fcf6235d46894c8890baa0f9fbf95f490da7d73a/index.html

I expect this to fail because our repo does not have the roguelike.wasm file this webpage looks for.

$ wget 'https://raw.githubusercontent.com/richardanaya/rust-roguelike/master/index.html' $ python -m SimpleHTTPServer

(tip: python -m SimpleHTTPServer or python3 -m http.server will serve your current directory over the port 8000)

Going to http://localhost:8000/ loads the webpage (good) and the Python http server shows that it’s trying to load /roguelike.wasm (also good):

$ python -m SimpleHTTPServer Serving HTTP on 0.0.0.0 port 8000 ... 127.0.0.1 - - [13/Dec/2017 09:43:52] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [13/Dec/2017 09:43:52] code 404, message File not found 127.0.0.1 - - [13/Dec/2017 09:43:52] "GET /roguelike.wasm HTTP/1.1" 404 -

The developer console says:

Failed to load resource: the server responded with a status of 404 (File not found) localhost/:30 Uncaught (in promise) CompileError: WasmCompile: Wasm decoding failed: expected magic word 00 61 73 6d, found 3c 68 65 61 @+0 at fetch.then.then.bytes (http://localhost:8000/:30:28) at <anonymous>

Fantastic!

Loading the dose response binary

So now I’ll edit the index.html file to replace fetch('roguelike.wasm') with fetch('target/wasm32-unknown-unknown/release/dose-response.wasm') .

And reload the page.

It loads, the web server shows no error and the dev console says:

Uncaught (in promise) RuntimeError: unreachable at wasm-function[48]:1259 at wasm-function[1]:38 at wasm-function[3]:1 at wasm-function[13]:3 at wasm-function[4]:464 at wasm-function[178]:7 at <anonymous>

We do get panics!

Now I want to be 100% sure it’s actually displaying the panic from running our code and not something else related to say loading the wasm code.

So let’s replace unimplemented! in process_cli_and_run_game with: panic!("Hello World!"); .

We need to rebuild the file and reload the page.

Sadly, that produced an identical output. And trying to compile the debug mode (as opposed to the release one we’ve used so far):

$ cargo +nightly build --target wasm32-unknown-unknown --no-default-features --verbose

failed with:

error: Could not compile `dose-response`. Caused by: process didn't exit successfully: `rustc --crate-name dose_response src/main.rs --crate-type bin --emit=dep-info,link -C debuginfo=2 -C metadata=b2748c2fb6d01c53 --out-dir /home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps --target wasm32-unknown-unknown -L dependency=/home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps -L dependency=/home/thomas/code/dose-response/target/debug/deps --extern serde=/home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps/libserde-30f0f596b5a4d830.rlib --extern bitflags=/home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps/libbitflags-d9f60170e8604972.rlib --extern serde_json=/home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps/libserde_json-fbd6e26bf609ef03.rlib --extern serde_derive=/home/thomas/code/dose-response/target/debug/deps/libserde_derive-3f9c1af9677e8a6e.so --extern rand=/home/thomas/code/dose-response/target/wasm32-unknown-unknown/debug/deps/librand-5892cb4bb9295a9c.rlib` (signal: 11, SIGSEGV: invalid memory reference)

Sigh.

There’s another way to test this. If we just remove the panic altogether, the program should run and exit immediately – without any errors.

Hmmm:

(index):40 Uncaught (in promise) TypeError: results.instance.exports.start is not a function at fetch.then.then.then.results ((index):40) at <anonymous>

Okay, that’s different than the previous one and it appears to be a javascript error rather then a rust/wasm error.

Time to look at what’s actually going on in index.html :

.then(results => { results.instance.exports.start(width,height); document.body.addEventListener("keydown",function(e){ console.log("Key Pressed:"+e.keyCode); results.instance.exports.key_down(e.keyCode); }) });

It appears to be calling a Rust function called start which we don’t have. I’ve also only now noticed that the rust-roguelike repo is set up as a library, not a binary.

But one step at a time. Let’s create the start function:

#[no_mangle] pub extern "C" fn start(width: i32, height: i32) -> () { // TODO: run the game here }

The no_mangle bit will make sure that the compiler will export the function with the name you specified. Similarly, extern "C" will make sure this uses the stable C ABI rather than Rust’s default one which could change. Same thing has to happen if you want to call your Rust function from C, Python or whatever.

Recompile and reload.

Aha! No errors!

Pressing a key results in:

Key Pressed:37 (index):43 Uncaught TypeError: results.instance.exports.key_down is not a function at HTMLBodyElement.<anonymous> ((index):43)

Which is because the line immediately after calling start sets up a keyboard listener that tries to call another Rust function called key_down . Which we don’t have either.

That’s okay for now.

Before we go ahead, I tried putting println!("Hello World!") in start just in case it happened to hook up to the browser’s console.

It did not. Oh well.

I also tried to set #[no_mangle] on main (to avoid having another entry point), but that didn’t compile. Worth looking into later.

For now though, it appears that:

When we include the webassembly file, it gets executed We can Rust from the browser by using results.instance.exports

That means we could either just start the game with main as usual, or we can do nothing in main and run the game from javascript by calling the start function.

This may have implications on the kind of game loop we’re going to do.

For now, I’ll just go with the former approach as it’s identical to what we do everywhere else.

That is: we remove the start function for now, remove the body of the final then in the webpage and take it from there.

So here’s the HTML I’ll be working with for now:

<body> </body> <script> var width = 80; var height = 60; var squareSize = 10; var c = document.createElement('canvas'); c.width = width*squareSize; c.height = height*squareSize; document.body.append(c); var ctx = c.getContext('2d'); ctx.textAlign = "center"; ctx.font = '12px arial'; fetch('target/wasm32-unknown-unknown/release/dose-response.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, { env: { } })) .then(results => { console.log("The game has finished."); }); </script>

(I’ve left the canvas bit from rust-roguelike in because I’ll need that later anyway)

If you set main.rs to exit cleanly, the console will print The game has finished . If you panic, you’ll see an error instead.

https://github.com/tomassedovic/dose-response/commit/2b3bd82bac354cca37eef672bd435c9f97b1e595

Game loop

We’ll have to create a new graphics backend ( wasm ) and set up the game loop there.

The way I’d like to do this is (like with all the other backends) is to run the game loop fully inside Rust (in the graphics backend), call out to JS to render what we want and to read the keyboard input.

I’m not sure whether you can pass structs around (my guess would be no) so the data processing might get a little nasty, but the bigger worry is this:

Normally, the game runs in a loop that keeps yielding to the operating system when idle. Basically, each frame we process the game logic, draw stuff on the screen and then wait until the next frame is ready. This lets the OS handle input events that we receive next frame.

I don’t know how to “yield” from WebAssembly and my guess would be that if Rust just runs an infinite loop, the browser will lock up.

Now, my default browser (chromium) has been designed with multiprocessing in mind from the beginning. Each tab runs in a single process so they’re isolated from each other.

So I have no doubts that this will bring down everything.

But let’s try it! What happens if we run an infinite loop?

In process_cli_and_run_game :

loop { println!("y"); }

Running this normally cargo run --no-default-features --release shows that it does not get optimised away, so let’s load it up in the browser!

Surprisingly, the browser still seems to respond normally, but looking at the console, it prints no message (implying the program is still running) and htop shows one of my CPUs pegged to 100% and tied to a chromium-browser tab. And when I close the tab, it promptly goes away.

So that’s all good.

I wonder what happens if I do the same thing in JavaScript.

for(;;) { console.log("y") }

That does actually lock up the whole browser and peg all my CPUs.

Thanks for not letting us down, chromium!

How do javascript games implement game loops, then? My guess would be something like document.setInterval(update, dt) or something which iirc does actually yield the control back to the browser.

Searching for “game loop in javascript” (this is top notch gamedev process right here) yields this MDN document:

https://developer.mozilla.org/en-US/docs/Games/Anatomy

Which actually suggests using window.requestAnimationFrame . Fair enough.

So let’s see what happens there:

function update() { window.requestAnimationFrame(update); console.log("y"); } update();

It works! The browser is responsive, the CPU usage did come up (all CPUs, not just one), but not crazily so, and the console is printing our lovely messages.

But this inverts the control (the game loop is now in javascript rather than rust), and it means now I’d have to call the update function from javascript to rust. Which means writing more JS.

It’s also complicated because we can’t just be calling the Rust game::update from JS – it requires the game State struct which (you guessed it) holds all the game state.

So I guess we have to keep running the Rust game in a loop somehow and pass data from the JS to it or something?

UGH.

I tried running thread::sleep in wasm, but that panics.

Basically, this is a difference between using plain webassembly which has zero to no runtime and empscripten which does actually bring in a runtime.

And everything on the web seems to be just using emscripten even with wasm.

The rust-roguelike repo keeps all the gamestate in a static variable. I’m not keen about that, but it should work. So we would keep the game loop in JS, but all it would do is call a function that would load the game State from a static , run game::update and then display the results somehow.

So okay, we’ll drive the game loop from javascript. Rust will expose two functions: initialise where we set the static data up and update where we process the game loop.

So tentatively, this is our new javascript code:

results.instance.exports.initialise(); console.log("The game is initialised."); function update() { window.requestAnimationFrame(update); console.log(results.instance.exports.update()); } update();

And in main.rs we create two functions:

#[no_mangle] pub extern "C" fn initialise() { // NOTE: at our current font, the height of 43 is the maximum // value for 1336x768 monitors. let map_size = 43; let panel_width = 20; let display_size = (map_size + panel_width, map_size).into(); // NOTE: 2 ^ 30 let world_size = (1_073_741_824, 1_073_741_824).into(); let title = "Dose Response"; let state = State::new_game( world_size, map_size, panel_width, display_size, false, // exit-after None, // replay file false, // invincible ); }

(this is copied straight from main and is how we would set the game up. No statics yet, we’ll deal with that later)

and:

#[no_mangle] pub extern "C" fn update() { // TODO update a frame here }

When we run this, we get a panic /o



Now this took me a while to figure out (by commenting the code out lines by line), but eventually, I tracked it to two calls of rand::thread_rng .

Calling thread_rng just straight-up panics and I think this comes down to the lack of any real OS/runtime in wasm.

Now in basically everything in Dose Response uses a seeded random generator. We want to be able to do a perfect replay.

However, to generate the initial seed, we grab a value from rand::thread_rng . Another option would be to get the seed from the system time.

If we don’t generate a seed that’s different every time, the game map will be the same on every play through!

But for now, to test things out, let’s just hardcode the seed:

//let seed = rand::random::<u32>(); let seed = 1234;

( rand::random uses thread_rng internally)

Next, when we generate a new game tile and it’s a tree (represented by the hash character: # ), we will assign it a random colour to provide a little visual variety.

Since this had no effect on the gameplay, we just grabbed a random one (in level.rs ):

let options = [color::tree_1, color::tree_2, color::tree_3]; *rand::thread_rng().choose(&options).unwrap()

Again, this will need to be handled properly, but to move forward, let’s just replace it with:

color::tree_1

After those two fixes, we can initialise the game without panics /.

Let’s try to run the game update function.

For now, we’ll just put this code in initialise , right after we define the state just to make sure we’re good to go:

let dt = std::time::Duration::new(0, 0); let fps = 60; let keys: Vec<keys::Key> = vec![]; let mouse: engine::Mouse = Default::default(); let mut settings = engine::Settings{ fullscreen: false }; let mut drawcalls: Vec<engine::Draw> = vec![]; let result = game::update( &mut state, dt, display_size, fps, &keys, mouse, &mut settings, &mut drawcalls, );

(stubbing most of the arguments out for now)

Sadly, running the update panics too.

This time it’s with:

::std::time::Instant::now()

And I remember writing that a lot when we moved from time to std::time . And we do use use timers pervasively in the code. There’s probably a way of getting away with them, but for now, I’m just not that into it.

So that might be it for now.

(and I have indeed left it here for two days, but it kept bugging me – I really wanted to get this going – so I did a git grep Instant::now to see how much of a damage this would end up being and it turns out, the game code only uses this in a single place!)

So let’s just disable that for now.

I’ve added a new Cargo feature called web (with no optional crates) and then in timer.rs :

pub struct Stopwatch { #[cfg(not(feature = "web"))] start: Instant, } impl Stopwatch { pub fn start() -> Self { Stopwatch { #[cfg(not(feature = "web"))] start: Instant::now() } } pub fn finish(self) -> Duration { #[cfg(not(feature = "web"))] return Instant::now().duration_since(self.start); // TODO: make this work for the web as well! #[cfg(feature = "web")] return Duration::new(0, 0); }

And after that, the game update function panics no more!

So we need to figure out how to pass the state to the game. And I really don’t want to have it in the static section.

It turns out that we can just allocate State on the heap (which wasm does have available, else all our Vec allocations would have failed) and just pass the pointer to each update call.

Here’s the new initialise function:

#[no_mangle] pub extern "C" fn initialise() -> *mut State { let mut state = { // NOTE: at our current font, the height of 43 is the maximum // value for 1336x768 monitors. let map_size = 43; let panel_width = 20; let display_size: point::Point = (map_size + panel_width, map_size).into(); // NOTE: 2 ^ 30 let world_size: point::Point = (1_073_741_824, 1_073_741_824).into(); let _title = "Dose Response"; Box::new(State::new_game( world_size, map_size, panel_width, display_size, false, // exit-after None, // replay file false, // invincible )) }; Box::into_raw(state) }

Mostly identical, but we create a pointer (box) of the game state and then return it with into_raw .

That will cause Rust to forget about the memory (i.e. it will not attempt to drop it) and return a raw pointer we pass to javascript.

And, here’s the update function:

#[no_mangle] pub extern "C" fn update(state_ptr: *mut State) { #[allow(unsafe_code)] let mut state: Box<State> = unsafe { Box::from_raw(state_ptr) }; let dt = std::time::Duration::new(0, 0); let display_size = point::Point::new(0, 0); let fps = 60; let keys: Vec<keys::Key> = vec![]; let mouse: engine::Mouse = Default::default(); let mut settings = engine::Settings{ fullscreen: false }; let mut drawcalls: Vec<engine::Draw> = vec![]; let result = game::update( &mut state, dt, display_size, fps, &keys, mouse, &mut settings, &mut drawcalls, ); std::mem::forget(state); }

So we receive the raw pointer to State , reconstruct a Box from it using Box::from_raw ), call the game::update method on it and then instruct Rust to forget about it again with mem::forget so it doesn’t get dropped.

To make it all work, we now need to pass the pointer around in javascript.

First, create a global variable called gamestate_ptr :

var gamestate_ptr;

next, make sure it’s set when calling initialise :

gamestate_ptr = results.instance.exports.initialise();

and finally, pass it to update:

function update() { window.requestAnimationFrame(update); results.instance.exports.update(gamestate_ptr); }

Recompile & force refresh the browser window. Everything should still be working!

Graphics

Next, let’s do graphics. Yes, according to the initial plan, keyboard input was to be next, but I won’t believe what we’ve done actually works until I’ve seen it. Also, it’ll be easier to debug input if we can display the results.

The game::update function in Dose Response populates a Vec of “drawcalls” – structs that instruct the graphics backend what needs to be drawn. It’s just data the backend interprets.

In our opengl backend, we turn that into a list of vertices and upload those to the GPU.

I’d like to try doing something similar here because a) that’s how we do it in the other backends; b) it’s how I imagine using WebGL would work: JS to set up the webgl context and Rust/wasm to generate the vertices; and c) I’m interested to know how to pass more complex dynamic data back and forth.

This is in contrast with the rust-roguelike repo that just has a javascript draw_char function the Rust/wasm code calls for every glyph it wants to render.

Reading Vec from JavaScript

We can worry about handling structured data later. For now, let’s just create a Vec<u8> in Rust and see if we can read it from JS without copying the whole thing over.

There’s no way of passing arrays from wasm to js directly (that I know of), so we’ll resort to passing the pointer to the data and its length.

For now, we’ll just create it on every update call:

// TODO: put the data from real drawcalls here let js_drawcalls = vec![42; 10]; #[allow(unsafe_code)] unsafe { draw(js_drawcalls.as_ptr(), js_drawcalls.len()); }

draw is a function we’re going to write in JS to read the data (and ultimately display it on the page).

To compile this, we need to define draw as an external/ffi function:

extern { fn draw(nums: *const u8, len: usize); }

Next, we need to define the function in javascript.

Javascript functions that should be available in wasm go into the env object in the WebAssembly.instantiate call. It was empty for now so let’s put draw in:

.then(bytes => WebAssembly.instantiate(bytes, { env: { draw: function(ptr, len) { console.log("Called draw with ptr:", ptr, "len:", len); } } }))

Recompile and reload, we should see something like this:

Called draw with ptr: 1605016 len: 10

Okay so now we have JavaScript calling Rust and Rust calling JavaScript. How do we get JS to read Rust’s memory?

Turns out, the entire memory buffer is provided in results.instance.exports.memory.buffer after the wasm module is instantiated.

But to make that available to our draw function, we need store it in a global variable.

So, let’s create a variable that will store the wasm instance:

var wasm_instance;

(this is top level, outside of any function or then calls)

Then, set it as the first thing in the .then(results ⇒ ) before calling initialise :

.then(results => { wasm_instance = results.instance; gamestate_ptr = results.instance.exports.initialise(); console.log("The game is initialised."); ... });

And now we can access the memory buffer in our draw function.

We can use it to construct a Uint8Array pointing at the section we want:

draw: function(ptr, len) { console.log("Called draw with ptr:", ptr, "len:", len); memory = new Uint8Array(wasm_instance.exports.memory.buffer, ptr, len); console.log("memory:", memory); for(let n of memory.values()) { console.log(n); } }

So passing the memory buffer, pointer and length to Uint8Array will create a view of that memory. We can then use the values or entries methods to iterate over it (or just access it as a normal array).

Running this will print something like this:

Called draw with ptr: 1605016 len: 10 memory: Uint8Array(10) [42, 42, 42, 42, 42, 42, 42, 42, 42, 42] (10) 42

/

By the way, you might be tempted to store a reference to memory.buffer and use that directly. Don’t do that. As I’ve foolishly found out, if the browser has to allocate a new memory page (1 page is 64k) it will clear the existing memory, allocate the new one somewhere else and update memory.buffer so your view will be pointing to old, unused and empty section of the memory.

Make sure you use the fresh memory.buffer every time you create a view into it.

The next step is to populate this vector with actual stuff to draw!

Since we’ve dealt with lists of u8 , let’s keep doing that for now. Again, to my knowledge, there is no direct mapping between structs and JS objects in the wasm boundary.

We will ignore the other drawcalls right now and only focus on printing out the characters on the screen. Each character has it’s position ( x and y coordinates), a glyph and a colour (in RGB).

As luck would have it, each of our values fit in a u8 in practise!

The coordinates are roughly 50 x 80, all our glyphs are ASCII (and therefore 0-255) and our colour segments are already stored in u8 .

So! We’ll read the drawcalls returned by our update function and for each push 6 values onto the js_drawcalls vector:

// Each "drawcall" will be 6 u8 values: x, y, char, r, g, b let mut js_drawcalls = Vec::with_capacity(drawcalls.len() * 6); for dc in &drawcalls { match dc { &engine::Draw::Char(point, glyph, color) => { assert!(point.x >= 0 && point.x < 255); assert!(point.y >= 0 && point.y < 255); assert!(glyph.is_ascii()); js_drawcalls.push(point.x as u8); js_drawcalls.push(point.y as u8); js_drawcalls.push(glyph as u8); js_drawcalls.push(color.r); js_drawcalls.push(color.g); js_drawcalls.push(color.b); } _ => {} // TODO } }

We’ve sprinkled in a few asserts just in case, but like I said, this should all just work.

Now we need to unpack this in javascript:

if(len % 6 != 0) { throw new Error("The drawcalls vector must have a multiple of 6 elements!"); } for(let i = 0; i < len; i += 6) { let x = memory[i + 0]; let y = memory[i + 1]; let glyph = String.fromCharCode(memory[i + 2]); let r = memory[i + 3]; let g = memory[i + 4]; let b = memory[i + 5]; console.log("x:", x, "y:", y, "glyph:", glyph, "color:", [r, g, b]); }

We do a sanity check on the supplied length and then read the position, character and colour from the vector and print them out.

A compile and refresh later, the characters printed out on the console consist of . , # , % and finally a @ – exactly what we expect /.

Now let’s draw them on the canvas.

Drawing on the canvas

We have to update the width and height vars to match what we normally use:

var width = 63; var height = 43;

And then copy the canvas drawing lines from the rust-roguelike repo to our draw function:

ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.clearRect(x * squareSize, y * squareSize, squareSize, squareSize); ctx.fillText(glyph, x*squareSize + squareSize / 2, y*squareSize + squareSize / 2);

\o/

\o/

\o/

\o/

\o/

\o/ \o/ \o/

It actually works. For real.

It doesn’t look good because the font is different and too small and the background colour is white. But it works.

Wow.

Awesome.

https://github.com/tomassedovic/dose-response/commit/10524333b2c80994833d9a6fdd300421fb625626

So the rest is mostly just keyboard handling and then a ton of polish.

Keyboard input

We need to read keys from javascript and pass them to the update function. The way all the other backends work is they queue up all the keypresses and then submit the array to update.

The “engine” is not multi-threaded or evented or whatever.

So let’s record the keys in javascript first.

We’ll create another global for storing the keys:

var pressed_keys = [];

And then in the final block we put this:

document.addEventListener('keydown', (event) => { console.log(event); pressed_keys.push(event); });

This will run the closure every time a key is pushed. We print it out and add it to our queue.

Now we can refresh the page, press some keys and see what they’re made of in the dev console.

Now in Dose Response, we’re interested in the what key was pressed and whether any of the shift, ctrl or alt were down at the time.

So ideally, we’d put something like this to our update JS function:

for(let key of pressed_keys) { wasm_instance.exports.key_pressed(key.key, key.ctrlKey, key.altKey, key.shiftKey); } pressed_keys = [];

(expecting we’d have a corresponding key_pressed function in rust)

Now that’s all well and good, but the event.key value is a string, not a number. E.g. pressing the Down key, it says ArrowDown .

There is a code value in event.keyCode , but MDN says that it’s deprecated and that I should use the key value:

https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent

I’d rather not use deprecated stuff for new projects so key with its string value it is. Not to mention keyCode is apparently system-dependent so it could differ between browsers or even their versions. And while I know that never happens in web development, one can never be too sure.

I looked into passing a string to Rust, but basically, it involves asking Rust to allocate a bit of memory for the string, copying the string there and passing a pointer to it.

So instead I decided to build a look up table and pass an integer which is trivial:

const keymap = { ArrowUp: 0, ArrowDown: 1, ArrowLeft: 2, ArrowRight: 3 };

And then in the update function:

for(let key of pressed_keys) { var key_code = -1; if(key.key in keymap) { key_code = keymap[key.key]; } wasm_instance.exports.key_pressed( gamestate_ptr, key_code, key.ctrlKey, key.altKey, key.shiftKey); } pressed_keys = [];

I also pass the gamestate pointer in, so we can add add the keys to our State struct.

The rust part is pretty straightforward:

#[no_mangle] pub extern "C" fn key_pressed( state_ptr: *mut State, external_code: i32, ctrl: bool, alt: bool, shift: bool ) { #[allow(unsafe_code)] let mut state: Box<State> = unsafe { Box::from_raw(state_ptr) }; let code = match external_code { 0 => Some(keys::KeyCode::Up), 1 => Some(keys::KeyCode::Down), 2 => Some(keys::KeyCode::Left), 3 => Some(keys::KeyCode::Right), _ => None, }; if let Some(code) = code { state.keys.push(keys::Key { code, alt, ctrl, shift}); } std::mem::forget(state); }

We do the same from_raw/forget dance for our state pointer and then we just convert the “key code” to our KeyCode struct and push the key to state.keys .

And amazingly, when I compile it and refresh the page, I can move around!

https://github.com/tomassedovic/dose-response/commit/2741873ff6e34c88b4a5929ca477fc8dea877c7b

The game is done!

Well, not quite.

Not quite fixing time

The first thing I noticed is that when I walk to a dose (represented by a blue i ), the game hangs.

Now I happen to know that when you use a dose, we play an explosion effect animation and don’t let you move until the animation is finished (basically to prevent that a monster would seemingly move into an explosion). I’ll probably want to rework that, but for now it is what it is.

And in our update code, we’re not passing the real elapsed time, but rather a Duration::new(0, 0) .

Which means the animation never finishes, which means the player can’t move.

yea I know. dum

So anyway, a quick fix is just replace that with Duration::new(1, 0) i.e. pretend that a full second passed between each update.

Ultimately, we’ll have to pass the correct value there, though.

After that, the game is somewhat playable: we don’t have the keys to e.g. eat food and we don’t see the explosion animation or any text or UI, but we can use doses, pick stuff up and fight monsters.

There is one other issue I’ve noticed though:

When the player goes to the edge of the screen, the game normally re-centers the view around the player. That’s not happening here – the display just stays where it is.

I’m guessing that’s another animation/timing issue but it might also to do with the effectively disabled stopwatch we did earlier.

https://github.com/tomassedovic/dose-response/commit/fd8c6ffebbbbd2c80f58da0e2bc22ef47d2ea31b

So the next steps:

Pass in the remaining keys Implement the remaining drawcalls Detect and pass in the right elapsed time value Fix the thread_rng & stopwatch code we had to compile out

Remaining keys

I’m not happy about having to maintain a two/way mapping between the keys – in javascript and in rust – and I could use a build.rs script to generate both. In fact, I may end up doing just that at some point.

But for now, I’ll just do it manually because it’s faster and there’s a ton of other stuff we’ll want to clean up later anyway. This whole effort is about getting to a working proof of concept as quickly as possible.

I’m not going to paste the code here, because it’s just two big lookup tables:

https://github.com/tomassedovic/dose-response/commit/18fd5aab1836520284a4c2c124a158f48f238666

The code is brittle (any rearranging of the KeyCode enum would mess it up), but it does actually seem to work.

We’re also handling the numpad differently. For most keys, we want to use event.key because it prints out the your keyboard layout says you pressed rather than the physical key on the keyboard.

This is useful e.g. for the Dvorak users. If they press e to eat food, they want to do it where their layout says e is not where it’s on QWERTY.

But event.key reports the same value for both the numpad and alphanum keys. Dose Response distinguishes between the two however – numpad is for movement and alphanum for using items in the inventory.

So for those we need to look at event.code .

Now, this would be a perfect place for a lightweight JavaScript library. Something that would look at a key event in whatever browser it runs – whether it uses the new key string values or the old keyCode ones – and convert them to a numerical code that is consistent across all browsers.

I’m sure something like that exists and I’ll probably go look it up and switch to it later on.

Anyway though, THIS WORKS. I can play the game using the numpad.

And while verifying that things such as eating the food work properly is hard (because of the missing graphical information), it does actually seem to function fine.

Missing drawcalls

Let’s add the missing drawcalls and show everything we’re supposed to.

This will also be fiddly because our Rust drawcalls are actually enums of various sizes.

It might actually be prudent to use some sort of binary encoding that has library support for JS and rust.

But for now, it’s the quick&dirty solution time!

We can turn our text rendering to existing char drawcalls so let’s add that first.

Rendering text

The Draw::Text enum is not very sophisticated – it just takes a starting position, text to print out and a colour.

No alignment, font, style or anything fancy like that. The text looks actually horrid (being tied to a grid does that) and it’s something I’ll want to change at some point.

Anyway we can just go through all the characters and turn them into “js drawcalls”.

&engine::Draw::Text(start_pos, ref text, color) => { for (i, glyph) in text.char_indices() { let pos = start_pos + (i as i32, 0); assert!(pos.x >= 0 && pos.x < 255); assert!(pos.y >= 0 && pos.y < 255); assert!(glyph.is_ascii()); js_drawcalls.push(pos.x as u8); js_drawcalls.push(pos.y as u8); js_drawcalls.push(glyph as u8); js_drawcalls.push(color.r); js_drawcalls.push(color.g); js_drawcalls.push(color.b); } }

Basically just copy/pasted the code for Draw::Char .

And that works! We’re getting some weird text artifacts, but they seem to be something on the canvas side.

https://github.com/tomassedovic/dose-response/commit/8d0e939dbdc70334c5e72cf5b753ae5d51dc51ca

We should probably be clearing it or something.

Yep, that was it. Putting this in the draw js function worked:

ctx.clearRect(0, 0, width * squareSize, height * squareSize);

I mean, “worked”. It still looks bad.

Rendering rectangles

Now let’s draw a rectangle. It gets a top-left position, size and colour.

We can pass that information to the JS and call clearRect on the canvas context, but actually, I’m now interested how far can we take just using the existing code. So let’s just generate a bunch more empty characters to fill:

&engine::Draw::Rectangle(top_left, dimensions, color) => { if dimensions.x > 0 && dimensions.y > 0 { let rect = rect::Rectangle::from_point_and_size(top_left, dimensions); for pos in rect.points() { assert!(pos.x >= 0 && pos.x < 255); assert!(pos.y >= 0 && pos.y < 255); js_drawcalls.push(pos.x as u8); js_drawcalls.push(pos.y as u8); js_drawcalls.push(0); js_drawcalls.push(color.r); js_drawcalls.push(color.g); js_drawcalls.push(color.b); } } }

(note that this will probably be less efficient than passing just the rect info and colour)

Luckily, we already have a Rectangle struct that lets us iterate over its points, etc. Why is engine::Draw::Rectangle using two Point`s instead of a `Rectangle ? Because the struct came later and I haven’t updated the code yet.

We will want to have a special “glyph” to denote that we want to fill a rectangle with colour rather than clearing it and drawing a text there.

We’ll use 0 which is nonexistent in Dose Response’s texts.

We will update our canvas drawing function a little. First, we’ll set glyph to null if we get zero, just to make that explicit:

var glyph = null; if(memory[i + 2] != 0) { glyph = String.fromCharCode(memory[i + 2]); }

And then the drawing is now:

if(glyph === null) { ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); } else { ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillText(glyph, x * squareSize + squareSize / 2, y * squareSize + squareSize / 2); }

And voilà! We can see the bar in the top-right corner that shows our state of the mind /.

The background position and size seems a little off, but we’ll figure that out later.

Setting background

So next we implement the Background drawcall. This one is used for the explosion animation as well as highlighting the area where a dose is too strong to resist and the player character has to get to it.

By now, our drawcall implementation pattern should be clear: re-use the existing JS code as much as we can.

Basically, this should be the same thing as the Rectangle call except only do it for a single cell.

&engine::Draw::Background(pos, color) => { assert!(pos.x >= 0 && pos.x < 255); assert!(pos.y >= 0 && pos.y < 255); js_drawcalls.push(pos.x as u8); js_drawcalls.push(pos.y as u8); js_drawcalls.push(0); js_drawcalls.push(color.r); js_drawcalls.push(color.g); js_drawcalls.push(color.b); }

And that works /.

We still don’t see the explosion animation on using a dose, but we do see the irresistible area.

https://github.com/tomassedovic/dose-response/commit/12225b377068e7774dad3069b04043681e8ac18f

A slightly better time fix

The animation is easy to fix: it’s because of our still broken dt value we’re passing to game::update . The animations are shorter than 1 second so they don’t show up.

Changing it to Duration::from_millis(100) makes them show up.

https://github.com/tomassedovic/dose-response/commit/13a8851341b023e96e384e1264a494ce6c5454b4

(step on the blue i to see it)

Sorting the drawcalls

Now we’re almost there! We have one drawcall to implement (screen fading), but before we get to it, there’s one really obvious thing going on here: our drawcalls are unsorted and they overlap with each other.

It’s been visible before (if you looked at a food (the % sign), you could still see the dot . for an empty tile showing through on the same space) but with the explosions and irresistible areas, it’s really obvious:

This is because the drawcalls can theoretically come in any order, but our game assumes that there can only be one glyph on a cell. Similarly, setting a background shouldn’t actually overwrite a glyph that’s been set previously.

So we just need to sort them. This is actually super easy because we had to do literally the same thing in the opengl/glium backend.

First I’ve moved it out of the glium backend to make it available everywhere:

https://github.com/tomassedovic/dose-response/commit/f8acebc2c47cf3e37126d30436d41e4a68619228

And then just called it in wasm:

engine::sort_drawcalls(&mut drawcalls, 0..);

And with that, the ordering-based graphical issues are gone.

https://github.com/tomassedovic/dose-response/commit/4a1107cbae50819c77739f106f0a64bbb269bbcd

We still have the weird rectangular issues, but you should know the mantra now: later.

Adding screen fade

Now the final drawcall: Fade . This is a very important effect that does…

Screen fading!

When you get deeper and deeper into the withdrawal, the game gets darker and darker. When you lose, the game briefly fades to white, red or black (depending on the cause of your failure) before showing the game over screen.

The fact that it’s literally harder to see (and notice some monsters) when being in a withdrawal is one of the earliest design choices I made and I’m still absolutely sticking with it.

We need to figure out how to do that on the canvas and then figure out how to pass that data through (because that does not fit into the other drawcalls that well).

I wonder if I can draw a rectangle over the entire screen with a nonzero alpha and implement fading that way?

Putting this in the JS draw function after we’ve gone through our drawcalls:

// Test fading ctx.fillStyle = "rgba(255, 0, 0, 0.5)"; ctx.fillRect(0, 0, width * squareSize, height * squareSize);

It works! The whole game is still visible, but it’s covered in a translucent red tinge. Spoooooooky!

So yea that’s how we do it. Now how to pass the fade values through.

Since our “graphics data pipeline” has worked so well so far, I’d still like to keep using it as is.

If there were more things like fade that don’t map to characters easily, I’d say it’s time to stop, but this is literally the final drawcall.

So we need to pass in the RGB values as usual (the fade colour), the alpha (fade amount – this is normally a f32 but can be converted to a byte) and we need to indicate that this is indeed a fade not a character render.

So let’s say this: if both x and y equal to 255 (the highest position value, never used in the game), we’ll do a fade and treat the “glyph” as alpha.

yea yea messy and whatever.

So in draw :

if(x == 255 && y == 255) { let alpha = memory[i + 2] / 255; // convert the "alpha" to <0, 1> ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; ctx.fillRect(0, 0, width * squareSize, height * squareSize); } else if(glyph === null) { ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); } else { ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillText(glyph, x * squareSize + squareSize / 2, y * squareSize + squareSize / 2); } }

We’ve just added another branch that will take the alpha (stored as a byte value so between 0 and 255 ), convert it to a float between 0 and 1 by using this super clever thing called math and fills the entire screen with it.

Now for the Rust part:

&engine::Draw::Fade(fade, color) => { assert!(fade >= 0.0); assert!(fade <= 1.0); // NOTE: (255, 255) position means fade js_drawcalls.push(255); js_drawcalls.push(255); // NOTE: fade value/alpha is stored in the glyph js_drawcalls.push(((1.0 - fade) * 255.0) as u8); js_drawcalls.push(color.r); js_drawcalls.push(color.g); js_drawcalls.push(color.b); }

And it works!

So that’s all drawcalls done!

https://github.com/tomassedovic/dose-response/commit/4eff11c4f1a6a644603401f6c2fd6dc31543eafe

Passing the correct time delta

The window.requestAnimationFrame function apparently passes a timestamp to the update function. Let’s use it!

Changing our js update function signature to:

function update(timestamp) { ... }

The timestamp is the total number of elapsed milliseconds. We want to calculate how much elapsed since the last time we called update so we want to store the timestamp of the previous frame:

var previous_frame_timestamp = 0; function update(timestamp) { window.requestAnimationFrame(update); let dt = timestamp - previous_frame_timestamp; previous_frame_timestamp = timestamp; console.log(dt); ... }

So we store the previous timestamp, calculate the delta time, print it out and update the previous frame timestamp.

When I run this, I get a steady stream of 16-ish milliseconds, i.e. 60 FPS.

Dose Response will be a Real PC Game For Real PC Men.

Let’s pass that value to Rust now by adding it to the wasm update call:

results.instance.exports.update(gamestate_ptr, dt);

And in Rust, change our update function to:

#[no_mangle] pub extern "C" fn update(state_ptr: *mut State, dt_ms: u32) { #[allow(unsafe_code)] let mut state: Box<State> = unsafe { Box::from_raw(state_ptr) }; let dt = std::time::Duration::from_millis(dt_ms as u64); ... let result = game::update( &mut state, dt, display_size, fps, &keys, mouse, &mut settings, &mut drawcalls, );

i.e. we’ve added the dt_ms argument to our update function

Now I’m not a member of the PC MASTER RACE so I honestly can’t say whether it’s any good, but the animations do feel a little smoother and hey, I’ve just noticed that the screen scrolls correctly when we get to the edge.

https://github.com/tomassedovic/dose-response/commit/61c5025da235683dfaf39046381e65a193ffc87a

And that’s basically it! The game is now fully playable although it looks like ass.

And there are the a few time and randomness things we’ve commented out in order to proceed with the rest of the game. Now’s a good time to resolve them:

Random seeds Random tree colours Instant::now in timer::Stopwatch

Random seeds

We need to generate the seed in javascript and pass it to rust.

Javascript/browser has Math.random which returns a float in the <0, 1> range.

Let’s make it available in Rust by adding this to our env object alongside draw:

env: { random: Math.random, draw: function(ptr, len) { // ... } }

Next, add it to the extern block in main.rs:

extern { fn draw(nums: *const u8, len: usize); fn random() -> f32; }

Then add a new random_seed function with conditional implementation for wasm and non-wasm:

#[cfg(not(feature = "web"))] pub fn random_seed() -> u32 { rand::random::<u32>() } #[cfg(feature = "web")] pub fn random_seed() -> u32 { #[allow(unsafe_code)] // NOTE: this comes from `Math.random` and returns a float in the <0, 1> range: let random_float = unsafe { ::random() }; (random_float * ::std::u32::MAX as f32) as u32 }

And finally use it in state.rs ’ new_game instead of the hardcoded value:

let seed = util::random_seed();

And now the game is different every time you play it!

I think ultimately, it would be good to move anything that’s talking to the outside world (like randomness, getting current time, display, input etc.) to isolated modules and keeping everything else “pure” not in the functional sense but rather being isolated from the OS.

We’re kind of close to being there – and wasm actually helped us to discover the areas where we weren’t – but yea code like this should probably not be tucked away in utils.

Anyways, another thing to do later.

So that’s random seeds done.

https://github.com/tomassedovic/dose-response/commit/fdbc828503b8cd0583852f4ca732e1f9455555b3

Fixing starting a new game

Before we get to the other two options I’ve realised something though: we’re never testing the “start a new game after the existing one” functionality in wasm. That’s because it’s mapped to the F5 key which means “reload this page” in most browsers.

So let’s remap it to the “N” key. Although we have to be careful because that key is also used for movement.

So we update the message in render.rs :

let keyboard_text = "[N] New Game [Q] Quit";

And then in game.rs update:

if state.keys.matches_code(KeyCode::F5) || state.endgame_screen_visible && state.keys.matches_code(KeyCode::N) { ... }

I want to keep the F5 there for starting a new game while playing the current one. Eventually, we’ll probably add a menu of some sort instead.

https://github.com/tomassedovic/dose-response/commit/0195b2763b4a486478daf4c49cabdc55c6c76ba1

And guess what! Losing a game and pressing N doesn’t work under wasm but it does in native build. So we need to fix this!

This is most likely because the wasm update function ignores the result of the game::update – which is what controls whether a new game should be started.

And indeed, we take a result but then don’t do anything with it:

let result = game::update( &mut state, ... &mut drawcalls, );

So yea let’s take a look at the result and restart the game if we get a new state:

match result { game::RunningState::Running => {} game::RunningState::NewGame(new_state) => { *state = new_state; } game::RunningState::Stopped => {}, }

Easy peasy.

What’s more, it works :-)

https://github.com/tomassedovic/dose-response/commit/6707b535ac34bc15a310c7f74cd273b3a6f16a16

Random tree colours

With that little distraction out of the way, let’s look at our random trees!!

Problem is this code in level::Tile::new :

let options = [color::tree_1, color::tree_2, color::tree_3]; //*rand::thread_rng().choose(&options).unwrap() color::tree_1

So I’m not entirely sure what to do here: there’s nothing in the function signature ( pub fn new(kind: TileKind) → Tile ) to let us produce a random-like effect – say if we got the tile’s position or something there, we could at least do something like x * y % 3 or something.

We could implement our own thread_rng and we can even do it using the generate_seed fn from earlier.

But also, in this game runaway randomness is kind of dangerous because our replays rely on everything being completely deterministic. That wasn’t an issue here because the colour is a purely cosmetic thing. But still. I’d like to make this more explicit now.

That kind of decision doesn’t belong in the Tile::new function anyway.

I’ve replaced it with just:

impl Tile { pub fn new(kind: TileKind) -> Tile { let color = match kind { TileKind::Empty => color::empty_tile, TileKind::Tree => color::tree_1, }; Tile { kind, fg_color: color, } } }

And we’ll handle the randomness somewhere else.

The tiles are being created in generators/forrest.rs in the generate_map function. So let’s create the different colours there:

let mut tile = Tile::new(kind); if tile.kind == TileKind::Tree { let options = [color::tree_1, color::tree_2, color::tree_3]; tile.fg_color = *one_off_rng.choose(&options).unwrap(); }

This is where all of our procedural generation happens so it really is the place to do this kind of stuff.

Also, if we ever want to do say different kinds of forrets (maybe it’s autumn or maybe we’re in a tundra and the colours are different), it makes sense to have that code here anyway.

I’ve also decided to use a different random generator for this cosmetic stuff. It’s called a one_off_rng (couldn’t think of a better name – the old programming joke is in full effect) – basically this is the RNG we want to use for things we don’t want to affect the replay.

(author’s note 2 days later: throwavay_rng !)

The tree colour doesn’t matter much anyway because it’s all static, but if we decided to say insert some randomness into our animations or something, we’d want that anyway. So here goes.

And that means we’re now passing two RNGs to the generate_map fn:

fn generate_map<R: Rng, G: Rng>(rng: &mut R, one_off_rng: &mut G, map_size: Point, player_pos: Point) -> Vec<(Point, Tile)>

and we actually have create the new one. In Chunk::new :

let mut one_off_rng = chunk.rng.clone();

We just create it there and pass it in. Nothing special. Eventually, I may want to make that part of the Chunk or even World or whatever. But because no one cares about this RNG, it can be whatever wherever.

And that makes our trees marginally more interesting /

https://github.com/tomassedovic/dose-response/commit/8184ab306f0373b9a136750482e25e7bb740d27a

Implementing Stopwatch

Okay, onto the Stopwatch thing with Instant::now . No idea what that’s for because all the animations and timing-based things seem to be working fine.

…

Oh.

It’s only used for counting how long the game update and render portions took each frame. The game prints some stats of the slowest frames etc. at the end for me for when I want to tune the performance later on.

We don’t need that for wasm. I mean, we’ll probably want some wasm profiling later on but for now there’s no way of printing that data anyway, so whatever.

We are done with the Rust port!!

Fixing the game look

The game still doesn’t look right – I suspect the text position is not quite right – but that’s on the canvas js side of things.

Here’s a screenshot of the native build:

And here’s the WebAssembly version:

We need to fix that.

Let’s render something we know what it’s supposed to look like at a known place and see what actually happens.

Put this after the engine::sort_drawcalls (so we know it shows up):

drawcalls.push(engine::Draw::Background((0, 0).into(), color::Color{r: 255, g: 0, b: 0})); drawcalls.push(engine::Draw::Char((0, 0).into(), 'X', color::Color{r: 0, g: 0, b: 0})); drawcalls.push(engine::Draw::Background((1, 1).into(), color::Color{r: 255, g: 255, b: 255})); drawcalls.push(engine::Draw::Char((1, 1).into(), 'X', color::Color{r: 0, g: 0, b: 0}));

We’ll draw a black X on a red background on the [0, 0] coordinates and another one on a white background at [1, 1] .

And the letters are definitely not positioned correctly, but I can’t tell whether the backgrounds are of the correct size and shape.

So I took a screenshot and inspected it in GIMP.

The tiles are indeed square and of correct size and position. It’s the text that’s off.

Fixing glyph position

I just copied that bit of rendering from rust-roguelike and it looks like it might not work for us. Also, I’ve changed the font size.

Anyway here’s how each character was rendered:

ctx.fillText(glyph, x * squareSize + squareSize / 2, y * squareSize + squareSize / 2);

So I’m guessing this + squareSize / 2 bit at the end is the problem.

When I removed it from both coordinates the text went a lot further up and to the left almost outside the correct coordinates. So there clearly is a need for this, it’s just that the half tile size fudge is not quite correct.

Looking at the ctx.fillText documentation didn’t help much, so it seems I’m going to have to fudge this too for now.

Just in case this is font-dependent though, I’d like to make sure we use the right font, first. So I don’t have to redo it when I make the switch.

So let’s set the font now!

We already have the font under fonts/mononoki-Regular.ttf so it’s just a matter of loading it up in the CSS:

@font-face { font-family: 'mononoki'; font-style: normal; font-weight: 400; src: local('mononoki'), url('fonts/mononoki-Regular.ttf') format('truetype'); }

And then using it in the canvas:

ctx.font = '16px mononoki';

And looks like the position is the same. So let’s do some trial and error!

In the end, this is what I went with:

let x_fudge = 8; let y_fudge = 13; ctx.fillText(glyph, x * squareSize + x_fudge, y * squareSize + y_fudge);

I’ve also compared it with the non-web dose response and changed the font size to 14 . I still don’t understand how font sizes work when the same number can mean wildly different things but whatever. 14 looks close to the non-web version.

https://github.com/tomassedovic/dose-response/commit/acc7f3688b6f7abc728db0d2d7534fcc90d39d9b

Fixing rectangles

And there’s one final thing: our rectangles seem to be 1 tile wider and taller.

Which is actually a misunderstanding between our Rectangle struct we used to render these and what the Draw::Rectangle drawcall means.

A rect::Rectangle with size 1, 1 has four tiles, whereas our drawcall would make that a single tile. That’s actually a bug in rect::Rectangle now that I think about it (I mean clearly 2x2 does not have a 1x1 size). So let’s fix that.

https://github.com/tomassedovic/dose-response/commit/5f6c04af284af313a4b70126aac3a0c2719d02c0

And after that, it’s all done.

I’ve played it a bunch and it all seems to be working fine. So that’s awesome.

Wrapping up

I’ve merged it to master and now I’ll go clean up this stream-of-consciousness document.

Oh actually, I’ve been testing it all in Chromium, should try Firefox, too.

The initial load is taking forever – about 30 seconds to cromium’s 2ish. Dunno why – but after that, everything seems to be working smoothly, too.

I thought it might be the initial map generation, but restarting the game once everything loaded was fine, too.

So idk.

Anyway, here’s a list of things I think would make sense to look at going forward:

Clean up main.rs all our wasm code is there

but every other backend is in engine/

also, how about isolating all OS code (threads, time, randomness) to the backends? Reduce allocation costs we pre-allocate the js_drawcalls vec but underestimate its size

vec but underestimate its size we don’t pre-allocate drawcalls at all

at all we should persist and clear both vectors rather than creating them every frame Don’t hardcode the display and font size in js Use a solid library for getting key events or at least generate the key code mapping in build.rs Mouse support Look at optimizing our canvas use https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas Investigate WebGL Look at target = "wasm" for conditional compilation I forgot that you can actually inspect the target triple for conditional compilation

It’s quite possible we don’t need the web Cargo feature at all Switch to the latest release rand crate – I think it contains the wasm support now

Parting thoughts:

This was not that terrible, though knowing what to expect would have been nicer

Achieved minimal disruption of the Rust codebase

The JS stuff is pretty small outside of keymapping

Dose Response (intentionally) does not use singletons or any other fancy stuff – everything is in the game State and just passed around. This was super helpful here. In fact, the few places where that wasn’t the case (file IO, thread_rng and Instant::now ) actually hurt us

and just passed around. This was super helpful here. In fact, the few places where that wasn’t the case (file IO, and ) actually hurt us Passing nontrivial stuff back and forth is kind of messy – try to minimise that as much as you can. Or maybe use proper message passing/encoding.

The keyboard handling on js side could definitely use a nice little library

Barring any future issues, I’ll try to make all my future projects wasm-compatible from the beginning

Oh and one last thing!

Since it is now possible to play this in the browser, I’ve created a website that lets you do that.

Click here to play Dose Response in the browser!