Creating Talks with TalkBuilder

You can build dialogue graphs programmatically using the TalkBuilder.

[ⓘ] The TalkBuilder is also used under the hood to build the graphs from the asset files.

If you need to generate procedurally dialogue graphs, or you just don't like the asset files, you can use this approach. Otherwise use the talk.ron files explained in the Getting Started chapter.

Simple Usage

Depending on your needs, building a dialogue graph via code can be more or less verbose. A simple, linear, conversation such as:

graph LR
    A((Start)) --> B[Say]
    B --> C[Say]

can be built with just a few lines of code:

#![allow(unused)]
fn main() {
let talk_builder = Talk::builder().say("Hello").say("World");
let talk_commands = commands.talks();
commands.spawn_talk(talk_builder);
}

To actually spawn the entities with the relationships, you pass the TalkBuilder to the Commands::spawn_talk method, which will prepare a Command to be added to the command queue.

The command, when applied, will first spawn the main parent entity of the graph with the Talk component. Then add a start node with StartNode component (the entry point of the graph) and finally spawn entities for each say, choose etc.

Usually the builder will connect the entities linearly based on the concatenated methods, with the only exception being the choose method which is used for branching. In the example above you would have 3 entities each in a relationship with the next one (start -> say -> say), all children of the main Talk entity.

You can check out all the methods that the builder provides in the API docs.

Build Branching Conversations

The simplest example would be a conversation with just 1 choice node:

graph LR
    A((Start)) --> B[Say]
    B --> C[Choice]
    C --> D[Say]
    C --> E[Say]
#![allow(unused)]
fn main() {
let talk_builder = Talk::builder();

talk_builder.say("How are you?")
    .choose(vec![
        ("I'm fine", Talk::builder().say("I'm glad to hear that")), 
        ("I'm not fine", Talk::builder().say("I'm sorry to hear that")), 
    ]);
}

The choose method expects a vector of tuples. The first element is the text field of the choice (to be displayed) and the second is the branch of the conversation, which is another TalkBuilder instance.

Multiple Branches

To make the example a bit more complex, let's say we have another choice in a branch:

graph LR
    A((Start)) --> B[Say]
    B --> C[Choice]
    C --> D[Say]
    C --> E[Say]
    E --> F[Choice]
    F --> G[Say]
    F --> H[Say]
#![allow(unused)]
fn main() {
let talk_builder = Talk::builder();

let happy_branch = Talk::builder().say("I'm glad to hear that");
let sad_branch = Talk::builder()
    .say("Why?")
    .choose(vec![
        ("Jk, I'm fine", Talk::builder().say("Aight")), 
        ("I want an editor!", Talk::builder().say("Me too :("))
    ]);

talk_builder.say("How are you?")
    .choose(vec![("I'm fine", happy_branch), ("I'm not fine", sad_branch)]);
}

It's easy to keep branching but it can get quite verbose and hard to read.

It is recommended to use the asset files for more complex conversations, but this can be useful if you want to quickly give some lines of texts to an item, or an NPC, or you are generating the conversation procedurally.

Connecting Nodes Manually

You can connect nodes manually with the connect_to method. But you will need to have the node to connect to.

If for some reason we need a loop like this:

graph LR
    A((Start)) --> B[Say]
    B --> C[Say]
    C --> B
#![allow(unused)]
fn main() {
let mut talk_builder = Talk::builder().say("Hello");

// grab latest node
let node_a = talk_builder.last_node_id();

talk_builder ? talk_builder.say("World").connect_to(node_a);
}

The node method returns an identifier of the node, and we can use it to do manual connections. Note you cannot create one node loops since currently self referential relationships are not supported.

You can also chain multiple connect_to calls to connect multiple nodes to the same node.

Branching and Manual Connections

Suppose we want to build this conversation:

graph LR
    A((Start)) --> B[Say]
    B --> C[Say]
    C --> D[Choice]
    D --> E[Say]
    D --> F[Say]
    F --> B

Situations like this are somewhat common in games. You are talking to an NPC where only one choice lets you continue and the others are just some flavour text or some extra lore.

#![allow(unused)]
fn main() {
let mut talk_builder = Talk::builder().say("Hello");

// grab latest node
let convo_start = talk_builder.last_node_id();

talk_builder = talk_builder
    .say("Hey")
    .choose(vec![
        ("Good Choice", Talk::builder().say("End of the conversation")),
        ("Wrong Choice", Talk::builder().say("Go Back").connect_to(convo_start))
    ]);
}

Connecting To The Same Node

Imagine you want to land on a node from multiple places like this:

graph LR
    A((Start)) --> B[Choice]
    B --> C[Say]
    C --> D[Choice]
    D --> E[Say]
    D --> F[Say]
    E --> F
    B --> F

You have an initial choice that can take the player to the end of the conversation, or go for some chat and then another choices which either goes to the end or passes by a talk node first.

You can think of that last talk node as its own branch that is pointed by multiple nodes.

#![allow(unused)]
fn main() {
let end_branch_builder = Talk::builder().say("The End"); // Create the end immediately
let end_node_id = end_branch_builder.last_node_id(); // <- grab the end node

// Create the good path
let good_branch = Talk::builder().say("something").choose(vec![
    ("Bad Choice", Talk::builder().connect_to(end_node_id.clone())),
    (
        "Another Good Choice", 
        Talk::builder().say("Before the end...").connect_to(end_node_id)
    ),
]);

let builder = Talk::builder().choose(vec![
    ("Good Choice", good_branch),
    // NB the builder is passed here. If we never add it and keep using connect_to
    // the end node would never be created
    ("Bad Choice", end_branch_builder) 
]);
}

Adding Actors to the mix

We saw the builder in action with just the say method, but we can also have actors say stuff. First we need to add the actors to the builder:

#![allow(unused)]
fn main() {
let mut talk_builder = Talk::builder()
    .add_actor("bob", "Bob")
    .add_actor("alice", "Alice");
}

Then we can use the actor_say method (or actors_say for multiple actors at once):

#![allow(unused)]
fn main() {
talk_builder = talk_builder.actor_say("bob", "Hello")
    .actor_say("alice", "Hi Bob");
}

The first argument is the actor slug. If the builder doesn't have an actor with that slug, it will panic when building. So always make sure to add the correct actors first. Also there is a actors_say method that takes a vector of actors slug.

Actors can also "join" or "leave" the conversation. For that there are the relative methods join and leave:

#![allow(unused)]
fn main() {
talk_builder = talk_builder.add_actor("bob", "Bob")
    .join("bob")
    .actor_say("bob", "Folks, it do be me.");
}

Node Event Emitters

The dialogue graph emits events when a node is reached. The way it does that is by using the NodeEventEmitter trait for the node components that implement it.

#![allow(unused)]
fn main() {
/// Trait to implement on dialogue node components to make them emit an event when reached.
#[bevy_trait_query::queryable]
pub trait NodeEventEmitter {
    /// Creates an event to be emitted when a node is reached.
    fn make(&self, actors: &[Actor]) -> Box<dyn Reflect>;
}
}

In case of say, choose, join and leave the builder will spawn an entity and add the TextNode, ChoiceNode, JoinNode and LeaveNode components respectively. Each of these components implement the NodeEventEmitter trait.

The idea is that you can create a Component, implement the trait so you can create an Event (optionally injecting the active actors) and then use that event to trigger some logic in your game.

You can check out the custom_node_event example to see how to implement custom events. You will see that there is also a macro to help you with that and that you need to register the component (and event) with the app.register_node_event::<C, T>().

Custom Node Components

Related to the previous section, you can also add any custom components to a node with the with_component method:

#![allow(unused)]
fn main() {
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct MyCustomComponent {
    pub some: bool,
}

talk_builder = Talk::builder().say("Hello").with_component(MyCustomComponent::default());
}

This will add the component to the node entity, but remember to register the component type first with app.register_type::<MyCustomComponent>();.

Going one step further, you can do a completely customized node by creating one empty first and then adding components to it:

#![allow(unused)]
fn main() {
let builder = Talk::builder().empty_node().with_component(MyCustomComponent::default());
}

You could create any kind of entity graph this way!