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
assetloads.repfiles, and reports parse errors as asset errors.pluginis the plugin and its system sets.runneris theDialogueRunnercomponent driving one dialogue.messageis everything the game and a runner send each other.callis commands, functions, the conversion of their arguments, and the subjects and featureshasasks 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