Getting Started

In this tutorial we will nstall Bevy Talks and do a quick overview on how to build and spawn a dialogue in your game.

Content

1. Installation

This plugin is compatible with Bevy 0.12 and is available on crates.io. To install it, add the following line to your Cargo.toml file:

bevy_talks = "0.5"

or just run:

cargo add bevy_talks

2. Open the editor

That we don't have yet... one day... one can dream...

Just go to the next section :(

3. Create a talk

You have two ways to create a dialogue: via code or via a file.

If you want to do it via code and perhaps add custom stuff to your dialogue graphs, checkout the next chapter here: Creating Talks with TalkBuilder.

Otherwise, let's create a talk.ron file in your assets folder, let's call it hello.talk.ron:

(
  actors: [],
  script: []
)

These files are made of two parts: the actors and the script. The actors are just a list of names and slugs (the identifier for an actor) and the script if a list of actions that can be performed (talk, choice, join and leave).

3.1 Talking

Let's add an actor (replace the actors field with this):

actors: [
    ( slug: "bob", name: "Bob" )
],

Now let's add a talk action:

script: [
    ( id: 1, action: Talk, text: Some("Hello!"),  actors: [ "bob" ], )
]

An action needs to have an id so it can be referenced by other actions. The action field is the type of action, in this case Talk. It is not mandatory, if missing defaults to Talk. The text field is the text that will be displayed in the dialogue box and needs to be wrapped in Some when present. Finally, the actors field is a list of slugs of the actors performing the action. If missing, defaults to an empty list.

3.2 Joining

We could also add a Join action before Bob starts talking to model the fact that he enters the room:

script: [
    ( id: 1, action: Join, actors: [ "bob" ], next: Some(2) ),
    ( id: 2, action: Talk, text: Some("Hello!"), actors: [ "bob" ], )
]

We had to add a next field to the Join action to tell the plugin which action to go to next. If missing, defaults to None and the dialogue will end.

3.3 Leaving

Now let's send Bob away after his line:

script: [
    ( id: 1, action: Join, actors: [ "bob" ], next: Some(2) ),
    ( id: 2, text: Some("Hello!"), actors: [ "bob" ], next: Some(3)),
    ( id: 3, action: Leave, actors: [ "bob" ] )
]

Notice we can remove the action field from the Talk action, since it defaults to Talk.

3.4 Choices

The plugin also supports player choices. This results in branching because the talk continues with the action chosen by the player.

script: [
    ( id: 1, action: Join, actors: [ "bob" ], next: Some(2) ),
    ( id: 2, text: Some("Hello!"), actors: [ "bob" ], next: Some(3) ),
    ( id: 3, action: Choice, choices: Some([
        (text: "Hi Bob", next: 5), 
        (text: "I'm Alice's BF.", next: 4)
    ])),
    ( id: 4, action: Leave, actors: [ "bob" ] ),
    ( id: 5, text: Some(":)"), actors: [ "bob" ] ),
]

We added a Choice action with two choices. In each choice the text field is the text that you can display associated with a choice, and the next field is the id of the action to go to next if the player chooses that option.

We also don't really need the action field for the Choice action. If the choice vector is defined, it defaults to Choice.

Notice that we didn't add the next field to the last two actions. Any of the two choices will end the dialogue.

3.5 The Complete Talk

Here's the full talk.ron file:

(
    actors: [
        ( slug: "bob", name: "Bob" ),
    ],
    script: [
        ( id: 1, action: Join, actors: [ "bob" ], next: Some(2) ),
        ( id: 2, text: Some("Hello!"), actors: [ "bob" ], next: Some(3) ),
        ( id: 3, action: Choice, choices: Some([
            (text: "Hi Bob", next: 5), 
            (text: "I'm Alice's BF.", next: 4)
        ])),
        ( id: 4, action: Leave, actors: [ "bob" ] ),
        ( id: 5, text: Some(":)"), actors: [ "bob" ], next: Some(2) ),
    ]
)

3.5.1 Loops

If you want to loop back, just use the next field:

script: [
    ( id: 1, action: Join, actors: [ "bob" ], next: Some(2) ),
    ( id: 2, text: Some("Hello!"), actors: [ "bob" ], next: Some(3) ),
    ...
    ( id: 5, text: Some(":)"), actors: [ "bob" ], next: Some(2) ),
]

4. Spawning the talk in your game

Now that we have a talk, let's add it to our game. To load the asset:

#![allow(unused)]
fn main() {
let h: Handle<TalkData> = asset_server.load("hello.talk.ron");
}

That creates a TalkData asset. We need to store that handle so we can retrieve the actual TalkData and use it to spawn the action entities in the world:

#![allow(unused)]
fn main() {
#[derive(Resource)]
struct MyTalkHandle(Handle<TalkData>);

fn load_talks(mut commands: Commands, server: Res<AssetServer>) {
    let h: Handle<TalkData> = server.load("hello.talk.ron");
    commands.insert_resource(MyTalkHandle(h));
}
}

Now that we have the system that loads the talk, we need to spawn it in the world. We can do that in another system:

#![allow(unused)]
fn main() {
fn spawn_talk(
    mut commands: Commands,
    talks: Res<Assets<TalkData>>,
    talk_handle: Res<MyTalkHandle>,
) {
    let my_talk = talks.get(&talk_handle.0).unwrap();
    let talk_builder = TalkBuilder::default().fill_with_talk_data(my_talk); // create a TalkBuilder with the TalkData
    commands.spawn_talk(talk_builder, ()); // spawn the graph with a commands extension
}
}

Alright! Now we have just spawned a graph of entities where each action is an entity with their own components. The actions performed by actors are also connected to the actors entities (just Bob in our case).

The entire graph is a child of a main entity with the Talk component, you can use it to identify the graph in the world.

5. Displaying the talk

The plugin doesn't provide any UI system right now, so you can use whatever you want to display the dialogue. A dialogue graph sends you events everytime you move to a new node, so you can create small systems that listen to the different events.

#![allow(unused)]
fn main() {
fn print_text(mut text_events: EventReader<TextNodeEvent>) {
    for txt_ev in text_events.read() {
        let mut speaker = "Narrator";
        if !txt_ev.actors.is_empty() {
            speaker = &txt_ev.actors[0];
        }
        println!("{speaker}: {}", txt_ev.text);
    }
}

fn print_join(mut join_events: EventReader<JoinNodeEvent>) {
    for join_event in join_events.read() {
        println!("--- {:?} enters the scene.", join_event.actors);
    }
}

fn print_leave(mut leave_events: EventReader<LeaveNodeEvent>) {
    for leave_event in leave_events.read() {
        println!("--- {:?} exit the scene.", leave_event.actors);
    }
}

fn print_choice(mut choice_events: EventReader<ChoiceNodeEvent>) {
    for choice_event in choice_events.read() {
        println!("Choices:");
        for (i, choice) in choice_event.choices.iter().enumerate() {
            println!("{}: {}", i + 1, choice.text);
        }
    }
}
}

The basics events are the TextNodeEvent, JoinNodeEvent, LeaveNodeEvent and ChoiceNodeEvent. They all have the actors field to quickly access the actor names. In case of no actors (empty vector) we're defaulting to "Narrator".

6. Interacting with the talk

We spawned and are listening to the talk events, but we can't interact with it to move forward (or pick a choice).

To do that, the plugin has another kind of events: the "Request" events that you can send. Here the 2 that we will use: NextNodeRequest and ChooseNodeRequest. They both need the entity with the Talk component you want to update, and for the ChooseNodeRequest you also need to provide the entity of the next action to go to.

#![allow(unused)]
fn main() {
/// Advance the talk when the space key is pressed and select choices with 1 and 2.
fn interact(
    input: Res<Input<KeyCode>>,
    mut next_action_events: EventWriter<NextNodeRequest>,
    mut choose_action_events: EventWriter<ChooseNodeRequest>,
    talks: Query<Entity, With<Talk>>,
    choices: Query<&ChoiceNode, With<CurrentNode>>,
) {
    let talk_ent = talks.single();

    if input.just_pressed(KeyCode::Space) {
        next_action_events.send(NextNodeRequest::new(talk_ent));
    }

    // Note that you CAN have a TextNode component and a ChoiceNode component at the same time.
    // It would allow you to display some text beside the choices.
    if choices.iter().count() == 0 {
        return;
    }

    let choice_node = choices.single();

    if input.just_pressed(KeyCode::Key1) {
        choose_action_events.send(ChooseNodeRequest::new(talk_ent, choice_node.0[0].next));
    } else if input.just_pressed(KeyCode::Key2) {
        choose_action_events.send(ChooseNodeRequest::new(talk_ent, choice_node.0[1].next));
    }
}
}

To grab the Talk entity for the events is pretty easy, just query for it.

For the ChooseNodeRequest event we need access to the possible choices if the current node has the ChoiceNode component. To grab them we can do a query on the special CurrentNode that is attached only to the current node entity in a graph (note that if you have multiple dialogue graphs you will have multiple CurrentNodes and you will have to filter them).

That's it!

The tutorial was based on the "full" example code in the examples folder. Also checkout the other examples, in particular the ingame one where 2 dialogue graphs are spawned and set as children (actually the Talk parent entity) of 2 interactable entities.

Next Steps

The plugin is being developed slowly but steady. Many many things are still missing or being experimented with. Hopefully in the following years as Bevy will shape up to a 1.0 this plugin will be the best dialogue system for it. :)