HullMain menu
Game shell for Bevy 0.19.1

Hull, the game shell for Bevy

Start your Bevy game at gameplay, not at the settings menu.

The engine main menu in the ui_gallery example: title, tagline, three menu items with the first selected by an accent bar, a stats card and key hints
Real capture, same build, one flag changedui_gallery, default theme
Pick an accentOrange, Shroud’s theme

Why

Every Bevy game begins by rebuilding the same shell: a main menu, a settings screen that saves, Esc to pause, volume sliders, quality presets, a way to test it. Hull ships that shell, so the first commit is gameplay.

The crate is called engine today. Hull is a working name.

Options

Inside the shell

Shell

  • App bootstrapWindow or headless, CLI flags, data and asset folders.engine::app(GameInfo::new(id, title))
    One call
  • App statesBoot, MainMenu, InGame. Games hang their own sub-states under InGame.
    Built in
  • SettingsCore settings plus a typed section for your game. Saved as JSON with atomic writes; a corrupt file is set aside, not fatal.
    Persisted
  • Settings screenEach FieldDef becomes a row: toggle, slider, choice, cards or swatches. Reset tab restores defaults.
    Generated
  • Input and cursorBindings, mouse look, cursor capture that lets go on menus.
    Built in
  • AudioSoundLibrary with variants, volume categories, muting for test runs.
    Built in
  • Graphics qualityLow, Medium, High and Ultra presets for AA, ambient occlusion, bloom and shadows.
    4 presets
  • SavesDataDir JSON helpers with the same atomic writes as settings.
    Built in

UI kit

  • ThemeRe-brand the whole kit with one accent colour. Barlow and Barlow Condensed are embedded.Theme::default().with_accent(Color::srgb(1.0, 0.78, 0.22))
    with_accent
  • WidgetsToggle, slider, choice, cards and swatches. Keyboard and mouse.
    5 kinds
  • Screen stackEsc pauses, Back pops. The game pauses when the window loses focus.
    Built in
  • Main and pause menusItems, a side panel and a pause header, all from one MenuConfig.
    MenuConfig
  • Toasts and FPS counterPost a toast from any system, even in headless runs.
    Built in

Play

  • FPS controllerKinematic collide-and-slide, mouse look, crouch, sprint, jump.
    feature fps
  • Chase cameraThird-person follow for any entity. Looks ahead, orbits, pulls in front of walls.
    ChaseCamera
  • MeshKitLow-poly vertex-coloured meshes from Bevy primitives. One mesh per object type.
    MeshKit
  • Physicsavian3d 0.7, paused with the game. Re-exported as engine::avian3d.
    feature physics
  • Sun and shadowsSunSetup: sun angle, shadow distance and cascades for arenas or open maps.
    SunSetup

Render

  • ClassificationAnything that moves, has physics or animates is an actor at full detail. The rest becomes scenery.
    Automatic
  • Distance detailSmall props lose shadows, then fade. Big pieces cross-fade to a flat far copy.
    On
  • StreamingScenery cells spawn around the camera with hysteresis. Colliders are never streamed.
    SceneryStream
  • Frame pacingMenus and pause redraw at 30 Hz, 8 Hz unfocused, so the GPU idles.
    On
  • Idle skipA frame where nothing visible changed is simulated but not drawn.
    Opt-in

Devtools

  • Control port and MCPTests and AI agents drive the game: keys, mouse, state, screenshots.
    Dev only
  • Offscreen screenshotsA hidden window renders to an image, so captures work behind a fullscreen app.
    Dev only
  • Perf overlay and session logCPU, GPU, memory and frame times per span, plus a log of state changes.
    Dev only
  • Metrics windowF4 opens a second window with live charts and run history tied to git commits.
    F4
  • Ship checkpackage.sh refuses to package if any of the ashlar debug crates behind the devtools is still in the tree.
    Enforced
src/main.rs
use engine::prelude::*;

fn main() {
    engine::app(
        GameInfo::new("orbs", "Orbs").tagline("Collect them all."),
    )
    .add_plugins(MyGamePlugin)
    .run();
}
Cargo.toml
[dependencies.engine]
path = "../engine"
default-features = false
features = ["physics", "fps"]

[dependencies]
bevy = { version = "=0.19.1", default-features = false }

[features]
# cargo run / cargo test keep every debug tool
default = ["dev"]
dev = ["engine/devtools", "engine/trace"]

Your main.rs is the whole setup

engine::app builds the window, the app states, settings, menus and pause. Your plugin is the game. Ship builds drop every debug tool with --no-default-features.

Settings are data: each FieldDef becomes a row on the generated screen, saved to settings.json.

Start a game
Proof

Two games, one shell

An indoor shooter and a driving game. Same settings screen, same tabs, same presets, same key hints. Courier changed the accent with one line.

Shroud's video settings: quality presets with High selected in orange, field of view slider, window mode, VSync and UI scale
Shroud, Settings, Video
Courier's video settings: the same screen and rows, with High selected in yellow over a valley backdrop
Courier, Settings, Video
Theme::default().with_accent(Color::srgb(1.0, 0.78, 0.22))
Render budget

Drawn on a budget, with zero code

meshes drawn
from 40,231to 480
40k-prop city, overview
ms p95 frame
from 4.54to 3.07
same view
GPU busy in menus
from 85 to 92%to 21 to 32%
frame pacing

Apple M5 Pro, 1920×1080, High quality, vsync off. Before: every optimisation off. After: the defaults.

The test city from above with every optimisation off: blocks and thousands of props out to the horizon
Budget off: 40,231 meshes drawn
The same view with the defaults: near props unchanged, far props faded, far blocks flattened
Budget on: 480 meshes drawn

Near the camera nothing changes. Far props fade out in a dithered cross-fade, so nothing pops, and far blocks become flat copies. GPU occlusion culling was tried too and left off: it was 3 to 15% slower in every pose measured.

Read the render budget
Devtools

Every run, against the last one

The metrics window: large charts for frame time, CPU, GPU and memory, rows of keys with sparklines, and a runs table comparing this run with three earlier runs, changes in green when better and red when worse
metrics_demo after four runs of commit 330d7d5--metrics-window
Two live charts from the metrics window: frame time at 6.56 ms and process CPU at 182%
Live charts, redrawn 4 times a second
The runs table: this run against earlier runs of commit 330d7d5, frame time p95 down 34% in green, CPU up 65% in red
Runs table: green better, red worse

Press F4 in a dev build and a second window charts frame time, CPU, GPU and memory, then keeps a table of past runs tied to the git commit. Green is better, red is worse, dim is within 3%.

A control port and an MCP server let tests and AI agents drive the game: send keys, read state, take offscreen screenshots. Every engine capture on this site was taken that way.

  • Ship builds--no-default-features removes all of it. The ashlar debug crates the devtools are built on are not even in the dependency tree, and the packaging script refuses to package if one slipped in.
    Enforced
  • Clean exitsA pipelined-rendering deadlock on exit was found and fixed: clean exits went from 22 of 30 runs to 30 of 30.
    30 / 30