Skip to main content

Crate bevy_replique

Crate bevy_replique 

Source
Expand description

Replique dialogues, as a Bevy plugin.

replique parses and runs the dialogue; this crate wires it into Bevy. A .rep file is an asset, a running dialogue is an entity, and the game talks to it with messages.

§A dialogue

Dialogues are plain text. Lines, choices and jumps make up most of a file; [let] holds state, [if] and [while] shape what gets played, and anything between [ and ] inside a line is evaluated and inserted where it stands. assets/dialogue/shop.rep:

:= shop
Merchant: Welcome! You have [gold()] coins.

[if gold() >= 10]
    Merchant: The lantern is yours.
    >> buy("lantern")
[else]
    Merchant: Come back when you can pay.

[let $bells = 3]
[while $bells > 0]
    [let $bells = $bells - 1]
    >> await ring_bell()

-> Thanks!
    Merchant: Any time.
-> Say nothing
    Merchant: ...
---

gold() and buy(...) are the game’s own, registered as Bevy systems — see Commands and functions below. The full format is documented in replique.

§Setup

Add RepliquePlugin, load a dialogue, spawn a DialogueRunner for it and send it a StartDialogue. The asset does not have to be loaded yet: the runner holds the start pending until it is.

use bevy::prelude::*;
use bevy_replique::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(RepliquePlugin)
        .add_systems(Startup, setup)
        .add_systems(Update, show_line)
        .run();
}

fn setup(
    mut commands: Commands,
    assets: Res<AssetServer>,
    mut start: MessageWriter<StartDialogue>,
) {
    let dialogue: Handle<RepliqueDialogue> = assets.load("dialogue/shop.rep");
    let runner = commands.spawn(DialogueRunner::new(dialogue)).id();

    start.write(StartDialogue {
        runner,
        node: "start".into(),
    });
}

/// Show every line, then ask the runner to carry on.
fn show_line(
    mut lines: MessageReader<DialogueLine>,
    mut resume: MessageWriter<ResumeDialogue>,
) {
    for line in lines.read() {
        match &line.speaker {
            Some(speaker) => println!("{speaker}: {}", line.text),
            None => println!("{}", line.text),
        }

        resume.write(ResumeDialogue {
            token: line.token,
            input: ResumeInput::Advance,
        });
    }
}

§Driving a dialogue

A runner suspends every time it needs the game, and reports why: DialogueLine for a line, DialogueChoices for a choice point, DialogueCommand for a >> command, DialogueFinished at the end.

Each of them carries a DialogueToken, which a ResumeDialogue has to hand back to move the dialogue on — ResumeInput::Advance after a line or a command, ResumeInput::Select after choices. A runner mints a new token every time it moves, so an answer that arrives late (a second press on a line, an animation ending after the player skipped it) is dropped instead of advancing whatever the dialogue has reached in the meantime.

Because every message names its runner, several dialogues can run at once.

The #tags of a line or of a choice, as in Alice: #angry Give it back!, come with it in DialogueLine::tags and DialogueChoice::tags.

§Commands and functions

A dialogue reaches into the game in two ways. A >> command(...) does something and may leave the dialogue waiting; a [function(...)] answers something and never can. Both are plain Bevy systems, taking their arguments already typed through In:

use bevy_replique::prelude::*;

#[derive(Resource, Default)]
struct Flags(HashSet<String>);

/// `>> set_flag("met_alice", true)`
#[replique_command]
fn set_flag(In((flag, on)): In<(String, bool)>, mut flags: ResMut<Flags>) {
    if on {
        flags.0.insert(flag);
    } else {
        flags.0.remove(&flag);
    }
}

/// `Alice: You have [gold()] coins.`
#[replique_function]
fn gold(In(()): In<()>, flags: Res<Flags>) -> i64 {
    flags.0.len() as i64
}

app.add_dialogue_command(set_flag)
    .add_dialogue_function(gold);

A command that means to make the dialogue wait takes the token off DialogueCall and hands it to whatever will end the wait. See call::command for that, and call::function for what a function may return.

§Subjects and features

has asks the game a yes-or-no question without calling anything: [if Alice has Angry] is whether the entity named Alice holds the component registered as Angry. A RepliqueSubject names the entity, register_replique_feature names the component:

use bevy_replique::prelude::*;

#[derive(Component)]
struct Angry;

app.register_replique_feature::<Angry>("Angry");

// `[if Alice has Angry]` is now true, until the component is removed.
app.world_mut().spawn((RepliqueSubject::new("Alice"), Angry));

A name the game was never given stops the dialogue with an error instead of answering false, so that a typo does not quietly pick the [else]. See call::features for subjects that only some dialogues can see, and for features whose value a dialogue can read, as in Alice.Mana.

§Modules

  • asset loads .rep files, and reports parse errors as asset errors.
  • plugin is the plugin and its system sets.
  • runner is the DialogueRunner component driving one dialogue.
  • message is everything the game and a runner send each other.
  • call is commands, functions, the conversion of their arguments, and the subjects and features has asks about.

§Examples

The examples directory has five runnable ones, from a minimal UI to custom commands, functions, features and several dialogues at once:

cargo run -p bevy_replique --example bevy_simple

Modules§

asset
call
What a dialogue calls into the game, and what it learns about the call.
message
Messages exchanged between the dialogue runner and the game.
plugin
prelude
runner