Make a game

Every original game on wasmga.me compiles to a single standalone .wasm binary with no custom JavaScript glue. Your module renders into a linear memory framebuffer and returns event flags each frame, while the host page handles display scaling, keyboard/touch/gamepad input, sound effects, save states, and leaderboards. You can build your own game targeting this same interface and run it directly in the test player.

See the game module format specification for the complete list of exports and imports supported by the host page.

Pick an engine

  • wasmgame-wg (Rust) provides resolution-independent anti-aliased vector shapes and SDF text rendering at the canvas's native device pixel resolution. Most of our newer games are built with it.
  • wasmgame-wg-c (C) is a set of freestanding, zero-libc single-file headers: wg.h provides a fixed-resolution framebuffer and bitmap font, while wgfb.h adds anti-aliased shape and text rendering.

Both SDKs are released under the MIT License (with the bundled Inter font licensed under the SIL Open Font License), and the source code for all of our original games is also MIT-licensed for reference.

Rust

Create a library crate configured as a cdylib with wasmgame-wg as its sole dependency:

[package]
name = "coins"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasmgame-wg = "0.1"

[profile.release]
panic = "abort"
lto = true

Implement the wg::Game trait and register your game struct with wg::game!. Here is a complete playable game in src/lib.rs:

//! Collect as many coins as you can in 30 seconds.
use wg::{hex, Game, Input, Rng, Screen};

/// Persistent game state: plain POD fields only, serialized byte-for-byte.
#[repr(C)]
#[derive(Clone, Copy)]
struct Save {
    version: u32,
    rng: Rng,
    you: [f32; 2],
    coin: [f32; 2],
    coins: i32,
    left_ms: i32,
}
unsafe impl wg::Plain for Save {}

struct Coins {
    s: Save,
}

impl Game for Coins {
    type Save = Save;
    const PLAN: (f32, f32) = (400.0, 300.0);

    fn new() -> Self {
        Coins { s: Save { version: 1, rng: Rng::new(1), you: [200.0, 150.0], coin: [80.0, 80.0], coins: 0, left_ms: 30_000 } }
    }
    fn save(&mut self) -> &mut Save {
        &mut self.s
    }
    fn start(&mut self, seed: u32) {
        self.s = Coins::new().s;
        self.s.rng = Rng::new(seed);
    }
    fn update(&mut self, input: &Input) -> u32 {
        if self.over() {
            return 0;
        }
        let step = input.dt as f32 * 0.2;
        if input.held & wg::LEFT != 0 { self.s.you[0] -= step; }
        if input.held & wg::RIGHT != 0 { self.s.you[0] += step; }
        if input.held & wg::UP != 0 { self.s.you[1] -= step; }
        if input.held & wg::DOWN != 0 { self.s.you[1] += step; }
        self.s.you = [self.s.you[0].clamp(10.0, 390.0), self.s.you[1].clamp(40.0, 290.0)];
        let mut ev = 0;
        let [dx, dy] = [self.s.you[0] - self.s.coin[0], self.s.you[1] - self.s.coin[1]];
        if dx * dx + dy * dy < 20.0 * 20.0 {
            self.s.coins += 1;
            self.s.coin = [self.s.rng.range(20, 380) as f32, self.s.rng.range(50, 280) as f32];
            ev |= wg::EV_HIT;
        }
        self.s.left_ms -= input.dt;
        if self.s.left_ms <= 0 {
            self.s.left_ms = 0;
            ev |= wg::EV_GAMEOVER;
        }
        ev
    }
    fn draw(&mut self, s: &mut Screen) {
        s.clear(hex(0x1d2433));
        s.text(&format!("Coins: {}", self.s.coins), 12.0, 20.0, 18.0, hex(0xf4efe6), 1.0);
        s.text_r(&format!("{}", (self.s.left_ms + 999) / 1000), 388.0, 20.0, 18.0, hex(0xf4efe6), 1.0);
        s.disc(self.s.coin[0], self.s.coin[1], 8.0, hex(0xffd166), 1.0);
        s.disc(self.s.you[0], self.s.you[1], 12.0, hex(0x5ec8e5), 1.0);
    }
    fn score(&self) -> u32 {
        self.s.coins as u32
    }
    fn over(&self) -> bool {
        self.s.left_ms == 0
    }
    fn loaded(&mut self) -> bool {
        self.s.version == 1 && self.s.left_ms >= 0
    }
}

wg::game!(Coins);

Compile it to WebAssembly:

rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
# target/wasm32-unknown-unknown/release/coins.wasm

Games built with wasmgame-wg delegate their title, pause, and game-over overlays to the host page, so the module itself only needs to draw active gameplay. For examples of setup options, level selection, post-game statistics, and pointer input, see wgdemo.

C

Here is the same game written in C using wg.h's 256×224 framebuffer. Because it omits the optional menu exports, it renders its own game-over prompt and restarts when Start is pressed:

/* Collect as many coins as you can in 30 seconds. */
#include "wg.h"

static struct {
  u32 version, rng;
  i32 x, y, cx, cy, coins, left_ms;
} S;

WG_EXPORT(wg_init) void wg_init(u32 seed) {
  S.version = 1;
  S.rng = seed;
  S.x = W / 2; S.y = H / 2;
  S.cx = 40; S.cy = 60;
  S.coins = 0;
  S.left_ms = 30000;
}

WG_EXPORT(wg_frame) u32 wg_frame(u32 buttons, i32 dt) {
  u32 ev = 0;
  if (S.left_ms > 0) {
    if (buttons & B_LEFT) S.x -= 2;
    if (buttons & B_RIGHT) S.x += 2;
    if (buttons & B_UP) S.y -= 2;
    if (buttons & B_DOWN) S.y += 2;
    if (S.x < 0) S.x = 0;
    if (S.x > W - 8) S.x = W - 8;
    if (S.y < 16) S.y = 16;
    if (S.y > H - 8) S.y = H - 8;
    i32 dx = S.x - S.cx, dy = S.y - S.cy;
    if (dx * dx + dy * dy < 64) {
      S.coins++;
      S.cx = 8 + xorshift(&S.rng) % (W - 24);
      S.cy = 24 + xorshift(&S.rng) % (H - 40);
      ev |= WG_EV_HIT;
    }
    S.left_ms -= dt;
    if (S.left_ms <= 0) { S.left_ms = 0; ev |= WG_EV_GAMEOVER; }
  } else if (buttons & B_START) {
    wg_init(S.rng);
  }
  char num[12];
  clear(RGB(29, 36, 51));
  text(4, 4, "COINS", 1, RGB(244, 239, 230));
  text(40, 4, itoa10(S.coins, num), 1, RGB(244, 239, 230));
  text(W - 20, 4, itoa10((S.left_ms + 999) / 1000, num), 1, RGB(244, 239, 230));
  fill(S.cx, S.cy, 8, 8, RGB(255, 209, 102));
  fill(S.x, S.y, 8, 8, RGB(94, 200, 229));
  if (!S.left_ms) text_center(H / 2, "TIME UP! PRESS START", 1, RGB(244, 239, 230));
  return ev;
}

WG_EXPORT(wg_score) i32 wg_score(void) { return S.coins; }
WG_EXPORT(wg_state) void *wg_state(void) { return &S; }
WG_EXPORT(wg_state_size) i32 wg_state_size(void) { return sizeof S; }
WG_EXPORT(wg_state_loaded) i32 wg_state_loaded(void) { return S.version == 1 && S.left_ms >= 0; }

Compile it directly with Clang (no external linker or sysroot required):

git clone https://github.com/wasmga-me/wasmgame-wg-c
clang -I wasmgame-wg-c --target=wasm32 -O2 -mbulk-memory \
  -ffreestanding -fno-builtin -nostdlib -Wl,--no-entry \
  -o coins.wasm coins.c

Play it

Open the test player and select your compiled .wasm binary. It runs with the same keyboard bindings, touch controls, gamepad support, save-state slots, and crash diagnostics as listed games on the site. After recompiling, click Use other files to load your updated build.