Building the Rogue WebAssembly Game

Back in the days before the Internet filled up with pictures of kittens, animated memes, and ubiquitous social networking, university students had to walk three miles uphill in the snow to computer labs so they could play games on monochrome monitors tethered to Unix servers.

One of these games was an incredible creation called Rogue.[19] Remember when I suggested that constraints are often good for innovation? Rogue is a fantastic example of that. Fed up with text games that you could only play once, Rogue’s creators managed to let players hack and slash their way through procedurally generated dungeons in a simple 80 column by 24 row terminal.

For this code sample, you’ll be taking a JavaScript library for creating Rogue-like games and building a game with it. The point of this exercise isn’t to build a game (though that is a fun side effect), but rather to illustrate some possible ways for WebAssembly to interact with your JavaScript code and third-party JavaScript libraries. We want to examine strategies for spreading code and logic across the boundary between Rust (WebAssembly) and JavaScript.

It’s important to prepare for our decision about where to draw those boundaries and how to spread the code to be wrong. We will make an attempt, see how it plays, see what the code looks like, and then decide how to refactor it from there. Perfect software isn’t created, it comes from iteration and the deliberate choice of an imperfect starting point.

In the Rogue WebAssembly game, your objective is to find and open all of the treasure chests. Inside one of these chests is a WebAssembly module. You will have to find this module before the dreaded Rust Borrow Checker captures you!

Getting Started with Rot.js

Before getting started, you might want to take a few minutes to familiarize yourself with the Rot.js[20] library. You don’t need to become an expert. Just take a look at some of the basic documentation. In short, Rot.js injects a virtual 80x24 (you can resize it) terminal window into an HTML canvas. With that in place, you can use methods like draw to place characters on the map, and hook up a turn-based actor system. This library has far more functionality than you’ll use for this simple example (but plenty of goodies to play with if you want to add on later).

You can get started by making a new copy of the better “Hello, World” from the previous section. I called my new directory roguewasm, but you can call yours whatever you like. Make sure you’ve got a build script that builds the wasm module and invokes the wasm-bindgen CLI. You can do this with a shell script or with changes to package.json. You won’t need to add a reference to Rot.js for npm. Instead, add that reference to the index.html file, where it will be clear that this is a traditional, client-side JavaScript dependency. There are other ways to rely on these kinds of dependencies, but this isn’t a JavaScript book so I will steer clear of them.

The game screen consists of the canvas area managed by Rot.js, a header for the game title, and a sidebar that will serve as an area to display the player’s statistics. Here’s a very simple index.html file that uses CSS flex grid to create those regions (it may be difficult to tell, but I am not a designer):

 <html>
 
 <head>
  <meta content=​"text/html;charset=utf-8"​ http-equiv=​"Content-Type"​ />
  <title>Rogue WebAssembly</title>
  <script src=​"https://cdn.jsdelivr.net/npm/rot-js@2/dist/rot.js"​></script>
  <style>
  .row {
  display: flex;
  }
 
  .row_cell {
  flex: 1
  }
  </style>
 </head>
 
 <body>
  <div class=​"row"​>
  <div class=​"row_cell"​ style=​"text-align:center;"​>
  <h1>Rogue WebAssembly</h1>
  </div>
  </div>
  <div class=​"row"​>
  <div class=​"row_cell"​ id=​"rogueCanvas"​>
 
  </div>
  <div class=​"row_cell"​ id=​"statsContainer"​ style=​"padding:15px;"​>
  <div class=​"row_cell"​ style=​"text-align:center;"​>
  <h2>Stats</h2>
  </div>
  <p>
  <span>
  <b>HitPoints:</b>
  </span>&nbsp;
  <span id=​"hitpoints"​>0</span>&nbsp;/&nbsp;
  <span id=​"max_hitpoints"​>0</span>
  </p>
  <p>
  <span>
  <b>Moves:</b>
  </span>&nbsp;
  <span id=​"moves"​>0</span>
  </p>
  </div>
  </div>
 
  <script src=​'./bootstrap.js'​></script>
 </body>
 
 </html>

The next decision you need to make is the hardest: what code will be in your JavaScript (e.g., index.js) and what code will be in the WebAssembly module? The right answer isn’t always to put everything inside WebAssembly or Rust just because that’s what’s new and shiny. There are a couple of things that Rot.js does quite well, like implementing pathfinding and random dungeon generation, that you’re not going to want to reinvent in WebAssembly.

Instead, you’ll want to invoke that functionality from wherever it’s most appropriate. Using wasm-bindgen, you can allow JavaScript classes to manifest in Rust and you can let Rust structs with functions appear as classes in JavaScript. To get started, let’s work on the game engine’s core logic and see how much of it can be implemented in Rust.

Before moving on to creating the game engine, you might want to have a look at the Rot.js tutorial.[21] This tutorial walks you through creating the all-JavaScript game on which Rogue WebAssembly is based. Skimming through this might help provide some context as to what you are building in the next section.

Creating the Game Engine

The game engine is subservient to the JavaScript in index.js. If you took a look at the Rot.js documentation, you may have noticed that the random dungeon generation works by taking a callback parameter. While there are a number of dungeon types available, the one we’ll be using in this game is a digger.

Each time Rot.js digs a piece out of raw map material, it invokes the callback. This callback contains the x- and y-coordinates of the spot, and a value integer. The meaning of this field varies between dungeon types. In the case of Rogue WebAssembly, we only care about the 0-values (open space).

First you’re going to want to set up some code blocks to hold your imports and exports. Because I’m either clairvoyant or I’ve written this sample multiple times, I know that you’re going to need access to the alert and console.log JavaScript functions, as well as eventually a stats_updated function for notifying the UI when a player’s stats change:

 #​[macro_use]
 extern​ crate serde_derive;
 
 extern​ crate wasm_bindgen;
 use​ ​std​::​collections​::HashMap;
 use​ ​wasm_bindgen​::​prelude​::*;
 #​[wasm_bindgen]
 extern​ ​"C"​ {
 fn​ ​alert​(s: &str);
 
  #[wasm_bindgen(js_namespace = console)]
 fn​ ​log​(s: &str);
 
  #[wasm_bindgen(module = ​"./index"​)]
 fn​ ​stats_updated​(stats: JsValue);
 
 pub​ ​type​ Display;
 
  #[wasm_bindgen(method, structural, js_namespace = ROT)]
 fn​ ​draw​(this: &Display, x: i32, y: i32, ch: &str);
 
  #[wasm_bindgen(method, structural, js_name = draw, js_namespace = ROT)]
 fn​ ​draw_color​(this: &Display, x: i32, y: i32, ch: &str, color: &str);
 }

With the log function, notice that you can import functions from specific JavaScript namespaces (e.g., console). You can tell wasm-bindgen which JavaScript module contains the function you’re going to import, as we do with the stats_updated function.

Next is where some of this tooling starts to really shine. Rot.js contains a class called Display in the ROT namespace. By declaring the Display type inside the extern block, wasm-bindgen makes that type available to your code and generates everything necessary to communicate with it. Notice that we didn’t put a namespace qualifier on the Display type, only the functions. wasm-bindgen builds types from the functions, which do have a namespace qualifier.

You will want access to two overloads of the draw method: one that just renders a character in the default colors and the other that renders a character with an explicit color code. There’s a lot going on here, so make sure you spend a few minutes taking in all of the code generation happening on your behalf.

By using the structural and method keywords in the wasm_bindgen macro, we are telling the macro and JavaScript boilerplate to take the method accessed on the Display type and call the decorated function in the WebAssembly module.

If in JavaScript you had wanted to call ROT.Display.draw(4,5,"@"), you can invoke the Rust function display.draw(4,5,"@") where display is a Rust variable that behaves like a struct with methods. As you’ll see, your JavaScript code can pass in a reference to an initialized ROT.Display and your Rust code can use whatever methods on it you declare in your extern block.

To create an instance of a Rot.js display and pass it to an instance of our game engine, we’re first going to need the Engine class. This is a struct we define in Rust (you’ll see it shortly), and, thanks to the wasm-bindgen macro, we can import it as though it was just another JavaScript class:

 import​ { Engine, PlayerCore } ​from​ ​'./jsint_roguewasm'​;

Here, roguewasm is a JavaScript file produced when we run the wasm-bindgen CLI tool. PlayerCore is another struct-exported-as-class that you’ll see shortly. The JavaScript code to create an instance of a Rot.js display and pass it to an instance of the game engine looks like this:

 // this.display = new ROT.Display();
 this​.display = ​new​ ROT.Display({ width: 125, height: 40 })
 document.getElementById(​"rogueCanvas"​).appendChild(​this​.display.getContainer());
 
 this​.engine = ​new​ Engine(​this​.display);

With some of that connective tissue set up, it’s time to create the game engine. The foundation of the game is map generation and map rendering, and Rot.js uses the dig callback to allow your game engine to produce the game map. Here’s the Rust code for the engine that handles the dig callback, updates its state, and renders an entire map:

 #​[wasm_bindgen]
 pub​ ​struct​ Engine {
  display: Display,
  points: HashMap<GridPoint, String>,
  prize_location: Option<GridPoint>,
 }
 
 #[wasm_bindgen]
 impl​ Engine {
  #[wasm_bindgen(constructor)]
 pub​ ​fn​ ​new​(display: Display) ​->​ Engine {
  Engine {
  display,
  points: ​HashMap​::​new​(),
  prize_location: None,
  }
  }
 
 pub​ ​fn​ ​on_dig​(&​mut​ ​self​, x: i32, y: i32, val: i32) {
 if​ val == 0 {
 let​ pt = GridPoint { x, y };
 self​.points​.insert​(pt, ​"."​​.to_owned​());
  }
  }
 
 pub​ ​fn​ ​draw_map​(&​self​) {
 for​ (k, v) in &​self​.points {
 self​.display​.draw​(k.x, k.y, &v);
  }
  }
 }

The Engine struct owns a reference to the following: a ROT Display instance, a hash that maps grid coordinates to renderable characters, and the location of the hidden WebAssembly module. The constructor illustrates an important point: Rust structs cannot be initialized with missing fields. To deal with something that could be missing like the prize location (you’ll write that code in a bit), we make use of Rust’s Option type.

on_dig adds the supplied grid coordinates to the points field. The draw_map function may look a little strange to you if you’re not used to Rust. In Rust, by default, for loops take ownership of the items over which they iterate. This means if you just casually loop over a collection, you can’t just dish out references to those items to other functions (because you no longer own them). If you don’t want to own (many in the community may also call this consuming) the collection, you can iterate over references to the items as indicated by the & sign.

Lastly, the draw_map function invokes the draw function on the Display instance. Rust knows that this opaque thing provided by the host (in our case, provided by JavaScript) has a three-parameter draw function on it because we specified that in our extern block.

With some map-related engine functions available on the Engine struct, you can write some code in a JavaScript Game class that invokes the Rot.js map digger:

 generateMap: ​function​ () {
 var​ digger = ​new​ ROT.Map.Digger();
 var​ freeCells = [];
 
 var​ digCallback = ​function​ (x, y, value) {
 if​ (!value) {
 var​ key = x + ​","​ + y;
  freeCells.push(key);
  }
this​.engine.on_dig(x, y, value);
  }
  digger.create(digCallback.bind(​this​));
 
 this​.generateBoxes(freeCells);
this​.engine.draw_map();
 
this​.player = ​this​._createBeing(Player, freeCells);
 this​.enemy = ​this​._createBeing(Checko, freeCells);
 },

Invoke the on_dig function on the Rust Engine struct.

Invoke the draw_map function on the Rust Engine struct.

Here this refers to the Game class instance and we call a utility function to create an instance of either a player or an enemy to put on the map.

The aging JavaScript syntax to add functions to the Game class is to avoid using translators and to keep the code samples as simple and portable as possible. Feel free to convert this to your favorite syntax and use Babel or its ilk to transpile the code.

Now that the map rendering and storage of grid points is taken care of, it’s time to move on to adding players, enemies, and the cherished hidden treasure: the WebAssembly module.

Adding Players, Enemies, and Treasure

As you’ve seen, one strategy for separating the implementation between Rust and JavaScript is to assign responsibilities by expertise or to avoid limitations. For example, it makes a lot of sense for the top-level JavaScript to initiate the game and any dependencies and then call into the WebAssembly module for whatever remains.

In this section, you’re going to add support for the player, an enemy, and obtaining the treasure that might be hidden within the boxes (* on the map canvas). Here you’ll see another strategy for separating the two worlds: encapsulation.

Using encapsulation allows you to create a class called Player in JavaScript, and then have a private member inside that class that’s an instance of the Rust-based player core. With this strategy, it becomes easy to have JavaScript handle things like keyboard input and configure the callbacks for use with Rot.js, all while deferring logic and other processing to the internal WebAssembly module.

This also lets you have a Player and Enemy class both share the functionality of the Rust-exported PlayerCore class. The JavaScript player class will handle subscribing to the key down browser event, exposing the Rot.js scheduler callback function act, and exposing property queries and self-rendering functions. Let’s take a look at the player abstraction in JavaScript:

 var​ Player = ​function​ (x, y) {
 this​._core = ​new​ PlayerCore(x, y, ​"@"​, ​"#ff0"​, Game.display);
 this​._core.draw();
 }
 
 Player.prototype.act = ​function​ () {
  Game.rotengine.lock();
  window.addEventListener(​"keydown"​, ​this​);
 }
 
 Player.prototype.handleEvent = ​function​ (e) {
 var​ keyMap = {};
  keyMap[38] = 0;
  keyMap[33] = 1;
  keyMap[39] = 2;
  keyMap[34] = 3;
  keyMap[40] = 4;
  keyMap[35] = 5;
  keyMap[37] = 6;
  keyMap[36] = 7;
 
 var​ code = e.keyCode;
 
 if​ (code == 13 || code == 32) {
  Game.engine.open_box(​this​._core, ​this​._core.x(), ​this​._core.y());
 return​;
  }
 
 /* one of numpad directions? */
 if​ (!(code ​in​ keyMap)) { ​return​; }
 
 /* is there a free space? */
 var​ dir = ROT.DIRS[8][keyMap[code]];
 var​ newX = ​this​._core.x() + dir[0];
 var​ newY = ​this​._core.y() + dir[1];
 
 if​ (!Game.engine.free_cell(newX, newY)) { ​return​; };
 
  Game.engine.move_player(​this​._core, newX, newY);
  window.removeEventListener(​"keydown"​, ​this​);
  Game.rotengine.unlock();
 }
 
 Player.prototype.getX = ​function​ () { ​return​ ​this​._core.x(); }
 
 Player.prototype.getY = ​function​ () { ​return​ ​this​._core.y(); }

The first interesting piece of JavaScript is this:

 this​._core = ​new​ PlayerCore(x, y, ​"@"​, ​"#ff0"​, Game.display);
 this​._core.draw();

This creates an instance of the PlayerCore class, which is actually a Rust struct you’ll write shortly that, through the power of wasm_bindgen, looks to JavaScript like an ordinary class. This constructor also takes an instance of a ROT.Display object, which gives the player core access to the map canvas.

If the key pressed by the player is a directional key, then Rot.js provides a convenience array (ROT.DIRS[8]) to help in computing the x- and y-coordinates of the direction indicated by a key press. In the following code, you can see the elements of the resulting direction array being added to the location state being managed by the player core:

 var​ dir = ROT.DIRS[8][keyMap[code]];
 var​ newX = ​this​._core.x() + dir[0];
 var​ newY = ​this​._core.y() + dir[1];

This is a perfect example of some code better left in JavaScript. Rot.js already has facilities for direction calculation and, as you’ll see, pathfinding, so there’s no need to reinvent those in Rust.

If the key pressed isn’t a movement key, but it’s instead either the carriage return (code 13) or the space bar (code 32), then the player will try to open a box. This code defers to the WebAssembly module by invoking the Rust open_box function on the Engine struct.

With the player’s basic behavior defined, it’s time to create the enemy. In this game, the player’s arch-nemesis is the cruel, evil, heartless borrow checker, a villain responsible for ensuring your code can never be compiled unless it is memory safe! Let’s call him Checko.

 // Checko the Borrow Checker! Run away!
 var​ Checko = ​function​ (x, y) {
 this​._core = ​new​ PlayerCore(x, y, ​"B"​, ​"red"​, Game.display);
 this​._core.draw();
 
  Checko.prototype.act = ​function​ () {
 var​ x = Game.player.getX();
 var​ y = Game.player.getY();
 
 var​ passableCallback = ​function​ (x, y) {
 return​ Game.engine.free_cell(x, y);
  }
 var​ astar = ​new​ ROT.Path.AStar(x, y, passableCallback, { topology: 4 });
 
 var​ path = [];
 var​ pathCallback = ​function​ (x, y) {
  path.push([x, y]);
  }
  astar.compute(​this​._core.x(), ​this​._core.y(), pathCallback);
 
  path.shift();
 if​ (path.length <= 1) {
  Game.rotengine.lock();
  alert(​"Game over - you were captured by the Borrow Checker!!"​);
  } ​else​ {
  x = path[0][0];
  y = path[0][1];
  Game.engine.move_player(​this​._core, x, y);
  }
  }
 }

Where the player’s act callback (invoked by the Rot.js scheduler) handles keyboard input and subsequent movement, Checko the Borrow Checker’s act callback uses Rot.js’s A-star pathfinding to compute a path to the player. It then finds the first step in that path and moves in that direction. If the path indicates that the player is about to be caught by Checko, the game is over. (Game.rotengine.lock() stops all schedulers.)

Again, you can see where the PlayerCore struct is maintaining Checko’s current position. These coordinates are passed as initialization parameters to the A-star pathfinding algorithm. This calculation requires a callback, where the boolean indicator of whether a coordinate is traversable is deferred to the free_cell function on the Engine struct (dots and asterisks are traverseable):

 pub​ ​fn​ ​free_cell​(&​self​, x: i32, y: i32) ​->​ bool {
 let​ g = GridPoint { x, y };
 match​ ​self​.points​.get​(&g) {
 Some​(v) ​=>​ v == ​"."​ || v == ​"*"​,
  None ​=>​ ​false​,
  }
 }

Earlier in this section, you saw some code at the top of the lib.rs file that looked like this:

 #​[macro_use]
 extern​ crate serde_derive;

This is a reference to the serde crate (serde refers to serialization/de-serialization). Serde is one of the most commonly used Rust crates in the entire ecosystem and contains functionality for manually performing raw serialization as well as macros for automatically deriving serialization and de-serialization implementations for your structs and enums.

As a reminder, the stats_updated function provided by the browser host to be invoked by our game engine is declared as follows:

 #​[​wasm_bindgen​(module = ​"./index"​)]
 fn​ ​stats_updated​(stats: JsValue);

When the game engine invokes the stats_updated callback, it’s sending a raw JSON value rather than attempting to marshal a struct that exists on both sides of the barrier. This makes the stats notification faster and consume less resources because you don’t need the generated boilerplate to make the Stats struct appear as a JavaScript class.

For this kind of serialization code to compile, you’ll need to customize your Cargo.toml slightly so that the wasm-bindgen reference includes serialization support:

 [package]
 name = ​"roguewasm"
 version = ​"0.1.0"
 authors = [​"Your Name <[email protected]>"​]
 
 [lib]
 crate-type = ​["cdylib"]
 
 [dependencies]
 serde = ​"^1.0.59"
 serde_derive = ​"^1.0.59"
 
 [dependencies.wasm-bindgen]
 version = ​"^0.2"
 features = ​["serde-serialize"]

Now let’s write the code for the player core and show some of the other important structs like Stats and GridPoint:

 #​[​derive​(Serialize)]
 pub​ ​struct​ Stats {
 pub​ hitpoints: i32,
 pub​ max_hitpoints: i32,
 pub​ moves: i32,
 }
 
 #[derive(PartialEq, Eq, PartialOrd, Clone, Debug, Hash)]
 struct​ GridPoint {
 pub​ x: i32,
 pub​ y: i32,
 }
 
 #[wasm_bindgen]
 pub​ ​struct​ PlayerCore {
» loc: GridPoint,
  moves: i32,
  display: Display,
  hp: i32,
  max_hp: i32,
  icon: String,
  color: String,
 }
 
 #[wasm_bindgen]
 impl​ PlayerCore {
  #[wasm_bindgen(constructor)]
 pub​ ​fn​ ​new​(x: i32, y: i32, icon: &str,
  color: &str, display: Display) ​->​ PlayerCore {
  PlayerCore {
  loc: GridPoint { x, y },
  display,
  moves: 0,
  max_hp: 100,
  hp: 100,
  icon: icon​.to_owned​(),
  color: color​.to_owned​(),
  }
  }
 
 pub​ ​fn​ ​x​(&​self​) ​->​ i32 {
 self​.loc.x
  }
 
 pub​ ​fn​ ​y​(&​self​) ​->​ i32 {
 self​.loc.y
  }
 
 pub​ ​fn​ ​draw​(&​self​) {
  &​self
  .display
 .draw_color​(​self​.loc.x, ​self​.loc.y, &​self​.icon, &​self​.color);
  }
 
 pub​ ​fn​ ​move_to​(&​mut​ ​self​, x: i32, y: i32) {
 self​.loc = GridPoint { x, y };
 self​​.draw​();
 
 self​.moves += 1;
 self​​.emit_stats​();
  }
 
 pub​ ​fn​ ​emit_stats​(&​self​) {
 let​ stats = Stats {
  hitpoints: ​self​.hp,
  max_hitpoints: ​self​.max_hp,
  moves: ​self​.moves,
  };
»stats_updated​(​JsValue​::​from_serde​(&stats)​.unwrap​());
  }
 
 pub​ ​fn​ ​take_damage​(&​mut​ ​self​, hits: i32) ​->​ i32 {
 self​.hp = ​self​.hp ​-​ hits;
 self​​.emit_stats​();
 self​.hp
  }
 }

The first arrow shows that every instance of a PlayerCore maintains the current location, icon, and icon color of the player (or enemy) that it supports. The second highlight shows internal state being converted into a struct that will be serialized as a raw JSON value and sent to the JavaScript host.

I’m not just showing my preference for encapsulation over inheritance with this implementation, but also illustrating a good pattern for hiding the seams between JS and WebAssembly. To make sure you’ve got a fully working version of the game to play with, download it from this book’s resources. In the project directory, type:

 $ ​​npm​​ ​​install
 ...
 $ ​​npm​​ ​​run​​ ​​serve
 
 >​​ ​​@​​ ​​serve​​ ​​/home/kevin/Code/Rust/wasmbook/khrust/Book/code/jsint_roguewasm
 >​​ ​​webpack-dev-server
 
 ℹ 「wds」: Project is running at http://localhost:8080/
 ℹ 「wds」: webpack output is served from /
 ℹ 「wdm」: Hash: 54b422da54fec6fc085e
 Version: webpack 4.16.3
 
 ...

With your WebAssembly module compiled and processed by wasm-bindgen, you can now open localhost:8080 and you should be able to play Rogue WebAssembly and see a page that looks similar to this:

images/roguewasm/roguewasm_screenshot.png

Congratulations—you’ve just created a micro version of one of the most classic and influential games of all time using WebAssembly and Rust! Before continuing to the next section of this chapter, you should, as usual, take a moment to bask in the glory of your own success and play the game. Keep an eye out for things that work, things that feel awkward, and what you like and don’t like about it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.7.224