“WebAssembly: A Game-Changer in Web Development”

In this article, we want to share our experience creating a Snake game for the browser with WebAssembly and Rust.

You can find a live demo of our game here and the source code here.

What is WebAssembly?

WebAssembly is a platform-independent format for executable programs and is intended as a compilation target for programming languages. WebAssembly was designed to run in the browser and complement JavaScript in two ways:

enabling near-native performance for web apps like games, image and video editing, image recognition, and many others

enabling the execution of existing or new code written in languages other than JavaScript in the browser

WebAssembly (WASM) is also increasingly relevant for server-side computing, and Solomon Hykes, a co-founder of Docker, even wrote:

If WASM+WASI existed in 2008, we wouldn’t have needed to create Docker. […] Webassembly on the server is the future of computing.

In this article, we focus on the use case of creating a browser game.

Why Rust?

WebAssembly had an initial focus on C/C++, but now many programming languages can compile to WebAssembly.

As computationally intensive tasks are a common use case for WebAssembly, using it with a low-level programming language is a natural choice. What makes Rust specifically attractive is the balance between emphasis on performance and avoidance of pitfalls typically associated with low-level languages like C/C++.

According to the report, The State of WebAssembly 2022 Rust has become the most frequently used as well as most desired language for WebAssembly development, making it even more attractive as the ecosystem grows with the popularity.

Tools

We used the recommended Rust crates wasm-pack and wasm-bindgen for compiling our Rust code to WebAssembly and facilitating interoperability with JavaScript. These libraries build the WebAssembly module as well as corresponding JavaScript wrappers that abstract low-level details of the interactions between the JavaScript and Rust code. You can find a great getting started guide here.

The crate web-sys provided us with the wasm-bindgen based imports for the browser APIs. It comes with an extensive list of examples.

For fast iterations, we used cargo-watch for rebuilding on each change and devserver for hot reloading in the browser.

Game Structure

In the browser requestAnimationFrame is the recommended foundation of the main “loop” rendering the game at a suitable time between repaints

function onFrame() {

  requestAnimationFrame(onFrame);

  const canvas = document.getElementById(“game”).getContext

The translation to Rust with wasm-bindgen and web-sys is quite verbose as you can see in this requestAnimationFrame example and this canvas example. Web APIs rely on the dynamic typing and missing null safety of JavaScript and therefore simple JavaScript constructs like

const canvas = document.getElementById(“game”);

const ctx = canvas.getContext(“2d”);

become very cumbersome when written with the corresponding null-safe and statically typed Rust wrappers provided web-sys

let document = web_sys::window().unwrap().document().unwrap();

let canvas = document.get_element__id(“canvas”).unwrap();

let canvas: web_sys::HtmlCanvasElement = canvas

    .dyn_into::<web_sys::HtmlCanvasElement>()

    .map_err(|_| ())

    .unwrap();

let context = canvas

    .get_context(“2d”)

    .unwrap()

    .unwrap()

    .dyn_into::<web_sys::CanvasRenderingContext2d>()

    .unwrap();

Also, the JavaScript concept of callbacks sharing garbage collected state does not translate to Rust without some friction. Callbacks are represented Rust closures in web-sys. But the ownership concept of Rust makes it difficult to share the game state between the different closures used e.g. for repainting and handling user input.

WebAssembly is clear about not being intended as a replacement for JavaScript but as a complement. The general recommendation is to keep the interface between JavaScript and WebAssembly simple. Therefore, we decided to expose a single game state container as a struct with methods for handling the relevant events.

use wasm_bindgen::prelude::*;

use wasm_bindgen::Clamped;

use web_sys::{CanvasRenderingContext2d, ImageData};

#[wasm_bindgen]

pub struct Game {

    … // game state

#[wasm_bindgen]

impl Game {

    pub fn new() -> Game {

        Game {

            … // game 

    pub fn on_frame(&mut self, ctx: &CanvasRenderingContext2d) {

        … // update game state and re

    pub fn click(&mut self, x: i32, y: i32) {

        … // update game state

The corresponding JavaScript creates the state container after loading the WebAssembly module and takes care of registering event callbacks forwarding to the corresponding methods.

import init, { Game } from “./pkg/snake.js”;

const canvas = document.getElementById(“canvas”);

let game;

init().then(() => {

  game = Game.new();

  canvas.addEventListener(“click”, event => game.click(event));

  requestAnimationFrame(onFrame);

function onFrame() {

  requestAnimationFrame(onFrame);

  game.on_frame(canvas.getContext(“2d”));

Graphics

The browser API provides different approaches for rendering to an HTML canvas.

For fully leveraging hardware-accelerated graphics, WebGL provides a powerful API and this API can be used through web-sys bindings but requires taking care of low-level details.

Another approach is to draw the game graphics using primitives like rectangles and circles of the Canvas API. You can find an example here.

Here we decided in favor of rasterizing the game graphics into a Rust array representing the individual pixel colors. Converting this Rust array to an ImageData and drawing it to the canvas works with a few lines of code.

use wasm_bindgen::prelude::*;

use wasm_bindgen::Clamped;

use web_sys::{CanvasRenderingContext2d, ImageData};

#[wasm_bindgen]

impl Game {

    pub fn on_frame(&mut self, ctx: &CanvasRenderingContext2d) {

        … // update game state 

        let data = ImageData::new_with_u8_clamped_array_and_sh(

            Clamped(&self.world.screen),

            WIDTH,

            HEIGHT,

        )

        .expect(“should create ImageData from array”);

        ctx.put_image_data(&data, 0.0, 0.0)

            .expect(“should write array to context”);

By setting the image-rendering property of the canvas to pixelated the look supports the retro style of the game.

Game Logic with Rust

The central part of the game state is an array screen: [u8; SCREEN_SIZE] representing the color of each pixel on the screen, a growable array of coordinates snake: VecDeque<Coord> holding the current position of the snake, and a vector direction: Coord with the current movement direction.

pub struct Coord {

    pub x: i32,

    pub y: i32,

pub struct World {

    screen: [u8; SCREEN_SIZE],

    direction: Coord,

    snake: VecDeque<Coord>,

    alive: bool,

Based on this data structure, the high-level logic of the snake game can be implemented repeatedly calculating a new position of the snake head and extending the head to this position. Depending on the previous color of the pixel at the new head position we either

generate new food, because the old one was eaten

end the game, because the snake collided with itself

shorten the snake tail to move the snake forward one pixel

In Rust, this algorithm can be written like this.

impl World {

    pub fn on_frame(&mut self) {

        if self.alive {

            let new_head_pos = self.get_new_head();

            let color_at_new_head = self.get_color_at(&new_head_pos);

            self.extend_head_to(&new_head_pos);

atch color_at_new_head {

                Color::Food => self.create_food(),

                Color::Snake => self.die(),

                _ => self.shorten_tail

Result

How our snake game looks in action:

Ñ

Our snake game in action

Conclusion

In this article, we gave an overview of how to create a small game with WebAssembly and Rust. The mentioned tools make the implementation surprisingly easy and convenient. The main challenge ‰was to find a sensible separation of responsibility between the Rust core logic and the supporting JavaScript code.

This blog post is published Comsysto Reply GmbH.

 

 

 

Subscribe
Notify of
5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Miracle Isaiah

Great

Ekaette Asanga

Nice one

Loveth Noah

Nice

Oscar Akom

Beautiful

Innocent Malachy OKON

Utilizing clean and intuitive design elements, such as a well-structured navigation menu, ensures that visitors can easily explore the available plans, features, and benefits.

Scroll to Top