Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bevy Integration

bevy-brink exposes the brink runtime as a Bevy plugin: compiled stories load as Assets, each live conversation is an entity with flow Components, and story-wide save state lives in a Resource. Output is delivered through observer events, and ink EXTERNAL functions bind to engine systems.

cargo add bevy-brink
use bevy::prelude::*;
use bevy_brink::{BrinkPlugin, BrinkFlowRequest};

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, BrinkPlugin::<()>::default()))
        .add_systems(Startup, start_story)
        .run();
}

fn start_story(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn(
        BrinkFlowRequest::<()>::builder()
            .story(assets.load("dialogue.inkb"))
            .build(),
    );
}

The plugin

BrinkPlugin<M> registers everything for one story instance: the fulfillment system that turns requests into live flows, the transcript refresher, the external-binding resolvers, and the locale machinery. It does not add an auto-advance system — most games drive advancement from input or game state, not every tick, so you write that step loop yourself (see Spawning & Driving Flows).

Adding BrinkPlugin<M> also pulls in BrinkAssetsPlugin (once) for the marker-free asset types and loaders. You can add BrinkAssetsPlugin on its own if you want the asset machinery without any marker plumbing (e.g. a headless asset-processing binary).

Markers: multiple concurrent stories

Every type is generic over a Send + Sync + 'static marker M (default ()). The marker monomorphizes the resources and components to distinct Bevy types with no runtime cost, so independent stories coexist in one app:

struct MainStory;
struct DreamSequence;

app.add_plugins((
    BrinkPlugin::<MainStory>::default(),
    BrinkPlugin::<DreamSequence>::default(),
));

Each marker gets its own BrinkGlobals<M> resource and BrinkFlow<M>/BrinkContext<M>/BrinkLocale<M> components. Use () unless you actually need this.

The story-asset bundle

A loaded story is a thin bundle (BrinkStoryAsset) of two labeled sub-assets:

AssetHoldsNotes
BrinkStoryAssethandles to the two belowwhat you load() and hand to a request
ProgramAssetthe immutable bytecode Program + initial_contextwhat the VM executes; initial_context is the fresh “new game” state
LineTablesAssetthe localizable line tablesthe swappable rendering data (base language, or a locale overlay)

The split exists so line tables can be swapped (locale changes, hot-reload) without touching the immutable program. You rarely touch the sub-assets directly — spawn a request with a Handle<BrinkStoryAsset> and let the fulfillment system wire everything up.

Loaders and file types

ExtensionLoaderProducesFeature
.inkbInkbLoaderBrinkStoryAsset (+ labeled #program, #line_tables)always
.inkInkLoaderBrinkStoryAsset, compiled at load time, hot-reloads on source changedev
.inklInklLoaderLocaleAsset (a locale overlay)always
.brktBrktLoaderTranscriptAsset (a saved playthrough)always

The dev cargo feature (on by default) adds the .ink source loader, which compiles ink at load time and hot-reloads the program when any file in the INCLUDE graph changes. Ship a release build without it to drop the compiler and load only pre-compiled .inkb/.inkl assets:

bevy-brink = { version = "*", default-features = false }

Where to go next

  • Spawning & Driving Flows — the request-component pattern, the flow components/resources, the step loop, observer events, transcripts.
  • External Functions — bind ink EXTERNALs to engine code (pure / command / world-query / async / task) and call ink from the engine.
  • Localization & Saves — runtime locale switching and .brkt transcript persistence.