The toolkit

game-toolkit

Small, focused Rust crates for 2D and 3D game jams.

v0.11.3 EUPL-1.2

Implement init, update and render, and ship. A batched 2D painter (sprites, shapes, text, tilemaps, optional vello vectors) and instanced, depth-tested 3D meshes draw into the same frame, so a 3D scene gets a 2D HUD for free. Native and the browser from the same code.

Cargo.toml
[dependencies]
game-toolkit = "0.11"

Play

Try it here.

Built for the browser from v0.11.3. Click into a running demo for keyboard input.

Cookie D'oh! title screen: a marquee reading Accept All Cookies over a cookie conveyor, with a cookie consent banner offering Accept All and a greyed-out Reject.

Cookie D'oh!

A C64/Amiga-era arcade game about accepting every cookie the Internet throws at you. A complete game on the toolkit, native and web from the same code.

  • Keyboard play
  • Gamepad / touch supported
Cheesed! title painting: a caped mouse holds a glowing cheese aloft on a crag, a huge cat lunging behind her and a castle under lightning in the distance.

Cheesed!

A complete game on the toolkit: a thrilling action adventure in DOS-era dress, with procedural music from title-screen metal to a music-box minuet. The Mouse. The Cheese. The Destiny.

Lave in the explorer: a green and blue planet with ice caps, and its data sheet: Rich Agricultural, Dictatorship, Human Colonials, famous for its vast rain forests and the Lavian tree grub.

Elite galaxies

The galaxy generator of Elite (1984) rebuilt in Rust, faithful to the 6502 original: eight galaxies of 256 systems from three 16-bit seeds, flown through in a 320×200 explorer drawn on the CPU.

  • ← → previous / next system
  • ↑ ↓ galactic hyperdrive
  • Tab galactic chart
  • Space pause rotation
Driving through a curve past billboards, with traffic ahead

Rust Racer

An OutRun-style pseudo-3D racer, a port of Jake Gordon’s javascript-racer onto the toolkit, with the simulation running in a sillyecs world: traffic, collisions, lap times and parallax as systems.

  • Arrows / WASD drive
  • Gamepad supported
  • M music on/off

Hello, web

The same code natively and in the browser: a sprite, text, a sound on click, looping synth music and the first gamepad.

How it’s made: the whole program →
  • Click / Space sound
  • M music on/off
  • Gamepad left stick

Indexed colour

Everything drawn with palette indices on a 160×120 canvas; a waterfall cycles and day crossfades into night.

How it’s made: the whole program →
  • Space day ↔ night

Game of Life on the GPU

A compute kernel steps the grid in ping-pong buffers and paints a storage texture; a readback ring counts the cells without ever waiting. Needs WebGPU: on WebGL2 it says so.

How it’s made: the whole program →
  • Mouse draw cells
  • R / C reseed / clear
  • Space pause

Features

What it does.

One frame, 2D and 3D

Sprites, SDF shapes, glyphon text and atlas tilemaps through one Painter; instanced meshes with depth testing under it. Draw order is the order you call.

Browser from the same code

wgpu on WebGPU with a WebGL2 fallback, WebAudio, the Gamepad API and touch. Assets preload from a manifest so loading stays synchronous.

Sound that just works

kira playback that degrades to muted without a device, plus synthie chiptune music and effects behind the synth feature.

Pull only what you use

Core, gfx, input, audio, assets, Aseprite, ECS, egui overlays and GPU compute are separate crates behind one umbrella and its features.

Jam templates

cargo generate a 2D or 3D starter with assets, a web build and an agent guide to the whole API already in place.

GPU compute

WGSL kernels over a grid, ping-pong storage buffers, a readback ring that never blocks, storage textures you draw like sprites, and a submitter that keeps simulations off the frame. Behind the compute feature.

Post and palettes

Virtual resolutions, a CRT filter, custom WGSL effects and indexed colour with palette cycling, for the retro look without fighting the GPU.

Code

What it looks like.

src/main.rs
use game_toolkit::prelude::*;

struct Game1;

impl Game for Game1 {
    fn init(_ctx: &mut Context) -> Result<Self> {
        Ok(Self)
    }

    fn update(&mut self, ctx: &mut Context, _dt: f32) {
        if ctx.input.key_pressed(Key::Escape) {
            ctx.quit();
        }
    }

    fn render(&mut self, ctx: &mut Context, frame: &mut Frame) {
        let mut p = frame.painter(&mut ctx.gfx);
        p.clear([0.1, 0.1, 0.15, 1.0]);
        p.text([24.0, 24.0], "hello, jam", 28.0, [1.0, 1.0, 1.0, 1.0]);
    }
}

fn main() -> Result<()> {
    run::<Game1>(AppConfig {
        title: "my-jam".into(),
        width: 800,
        height: 600,
        ..Default::default()
    })
}

More

The rest of the family.