A Rust workspace of small, focused crates for 2D-first game jams. Drop in, implement
Game::init / update / render, and ship. Each subsystem is its own crate, so a jam
pulls only what it uses.
Built on wgpu, winit 0.30, kira, glyphon (cosmic-text), image, and bytemuck,
with optional egui tooling overlays and Aseprite asset loading.
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<()> {
env_logger::init();
run::<Game1>(AppConfig {
title: "my-jam".into(),
width: 800,
height: 600,
..Default::default()
})
}AppConfig::asset_root defaults to ./assets relative to the working directory. Binaries
that load files and want to run from anywhere should set it explicitly:
asset_root: std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets"),AppConfig also exposes msaa_samples (e.g. 4 for anti-aliasing; default 1) and
depth_format (Some(wgpu::TextureFormat::Depth32Float) to allocate a depth buffer;
default None). The built-in 2D pipelines never write depth, so enabling it is harmless
and is there to support depth-tested rendering.
Generate a project from a template with cargo-generate:
cargo generate --git https://github.com/sunsided/game-toolkit templates/jam --name my-jam
# or the 3D starter (spinning cube):
cargo generate --git https://github.com/sunsided/game-toolkit templates/jam-3d --name my-jamEach template is a minimal Game (window + Esc-to-quit + a drawn title) depending on
game-toolkit with the ui feature on, plus an assets/ directory. cd my-jam && cargo run.
| Crate | What it gives you |
|---|---|
game-toolkit-core |
App loop on winit 0.30 ApplicationHandler, the Game trait, Context, time, optional fixed timestep. |
game-toolkit-gfx |
wgpu init + surface management, sprite batcher, SDF circle/ring primitives, glyphon text, atlas tilemap, the Painter API. |
game-toolkit-input |
Keyboard, mouse and gamepads (via gilrs) with held / just-pressed / just-released semantics, hot-plug, and rumble. |
game-toolkit-audio |
Sound loading + playback on kira; degrades gracefully to muted when no device is available. Optional chiptune synthesis (synth feature, via synthie). |
game-toolkit-assets |
Asset path resolution + optional hot-reload watcher (notify). |
game-toolkit-aseprite |
Load native .aseprite files and exported PNG + JSON sheets into a GPU-ready SpriteSheet with animation playback. |
game-toolkit-ecs |
Small glue (component data, command queue) for the sillyecs compile-time archetype ECS. |
game-toolkit-ui |
egui overlay integration for debug tooling (feature-gated). |
game-toolkit |
Umbrella crate - depend on this one and use game_toolkit::prelude::*;. Features: ui, aseprite, ecs, synth, vector. |
The dependency direction is one-way: game-toolkit-aseprite depends on game-toolkit-gfx, never the
reverse, so the renderer stays unaware of asset formats.
Run any example with cargo run -p <package>.
| Run | Package | Shows |
|---|---|---|
01_window |
ex_01_window |
Minimal window with an animated clear color; Esc quits. |
02_sprite |
ex_02_sprite |
Load a PNG via load_texture, draw it plain, spinning, and tinted. |
03_primitives |
ex_03_primitives |
SDF circles, rings, lines, rectangles; F12 saves a screenshot. |
04_text |
ex_04_text |
Text rendering through glyphon. |
05_tilemap |
ex_05_tilemap |
Instanced atlas tilemap. |
06_egui |
ex_06_egui |
egui debug overlay (uses the ui feature). |
07_3d |
ex_07_3d |
Depth-tested perspective cubes (instanced meshes) with a 2D HUD on top. |
07_audio |
ex_07_audio |
Play a WAV (Space) and synthesized retro effects (1/2/3); runs muted with no device. |
08_hot_reload |
ex_08_hot_reload |
Edit assets/reload_me.png while it runs and watch the texture update live. |
09_aseprite |
ex_09_aseprite |
Load a native .aseprite, pack its frames into an atlas, and play a tagged animation. |
10_gamepad |
ex_10_gamepad |
Live gamepad overlay: sticks, buttons, stick-clicks, and rumble on A. |
11_ecs |
ex_11_ecs |
Bouncing particles driven by the sillyecs archetype ECS (build.rs codegen). |
12_vector |
ex_12_vector |
Vector graphics (filled circles, stroked path) via the optional vello backend. |
bouncing_ball |
bouncing_ball |
A faux-3D tumbling beachball shaded through the sprite batcher. |
game-toolkit-aseprite reads both Aseprite formats into the same SpriteSheet:
// Native binary: frames are flattened and packed into one atlas texture.
let sheet = SpriteSheet::load_aseprite(&mut ctx.gfx, "character.aseprite")?;
// Exported sprite sheet: a PNG atlas plus its Aseprite JSON sidecar.
let sheet = SpriteSheet::from_aseprite_json(&mut ctx.gfx, "sheet.png", "sheet.json")?;
// Play a tag (or `sheet.full_animation()` when a sheet has no tags).
let mut player = AnimationPlayer::new(&sheet, "walk").unwrap();
player.advance(&sheet, dt);
let (uv_min, uv_max) = player.current_uv(&sheet);Both the hash and array frames layouts of the exported JSON are accepted, and tag
directions (forward / reverse / ping-pong) drive playback.
tools/atlas-packer is an offline CLI that packs a directory of PNGs into one atlas:
cargo run -p atlas-packer -- --input sprites/ --output atlas.png --metadata atlas.json
# options: --max-size 2048 --padding 2It writes the atlas PNG plus an Aseprite-compatible JSON sidecar, so the result loads with
no extra code via SpriteSheet::from_aseprite_json(&mut ctx.gfx, "atlas.png", "atlas.json").
ctx.gfx.request_screenshot("shot.png") saves a PNG of the next presented frame, read back
from the surface with miniscreenshot-wgpu.
A small instanced static-mesh path renders under the 2D layers (which composite on top).
It needs a depth buffer (AppConfig::depth_format).
let cube = ctx.gfx.create_mesh(&vertices, &indices); // [MeshVertex] + [u16]
ctx.gfx.camera3d.eye = [0.0, 1.6, 6.0]; // perspective Camera3D
// each frame, via the painter:
let model = transform::mul(&transform::translation([x, 0.0, 0.0]), &transform::rotation_y(t));
p.mesh(cube, model, [0.4, 0.8, 0.45, 1.0]);Lighting is forward-unlit (Lambert from the vertex normal). glTF loading and a perspective follow-camera are future work.
The optional vector feature adds a vello backend
for high-quality 2D vector content. painter.vector(|scene| ...) hands the frame's
vello::Scene; vello renders it to an offscreen target that composites on top of the
sprite/primitive/text layers. It is compute-based and needs higher device limits, so it is
off by default.
use vello::{kurbo::{Affine, Circle}, peniko::{Color, Fill}};
p.vector(|scene| {
scene.fill(Fill::NonZero, Affine::IDENTITY, Color::from_rgb8(240, 140, 168), None,
&Circle::new((400.0, 300.0), 120.0));
});Common tasks run through go-task (Taskfile.dist.yaml):
task # list tasks
task ci # fmt check + clippy + tests
task run:3d # run an example (see task --list for example:* shortcuts)
task atlas -- --input sprites/ --output a.png --metadata a.json
task publish:dry # trial publish all crates in order (task publish releases for real)The generated jam templates ship their own Taskfile.yaml too.
First crates.io release: 0.1.0. The 2D runtime, input (keyboard / mouse / gamepad),
audio, assets, text, tilemap, egui overlay, Aseprite loading, optional depth/MSAA, a
first-cut 3D mesh path, ECS glue, the atlas-packer CLI, and cargo-generate jam templates
are all in place. Still pre-1.0, so the API may shift between minor versions; roadmap and
open workstreams live in the toolkit epic.
Licensed under the European Union Public Licence v. 1.2 (EUPL-1.2). See LICENSE, or the official text.