Introduction to Bevy Talks

[ⓘ] Be aware that bevy_talks's API is still undergoing revisions (with possibly big architectural changes). Feedback on its ergonomics and developer experience (DX) is highly appreciated.

This [Bevy][bevy] plugin provides a way to create dialogues and conversations in your game as graphs.

You can imagine a Talk between the player and NPCs as a directed graph where each node is an action that can be performed such as saying a line, joining/leaving the conversation, or a choice the player can make.

The most common action is text being displayed on the screen, and a simple Talk is just a sequence of texts forming a conversation between actors.

You can have multiple entities each with their own Talk graph. Or you can make a VN-like game with one single big dialogue graph in the game.

[!NOTE] A more in-depth documentation is being slowly written as an mdbook here! Help is appreciated :)

Actions and Actors

Talks are made up of actions that are translated into graph nodes. Actions can be defined either via the TalkBuilder (where you have more control over the dialogue graph with custom components and events) or with "talk.ron" asset files. With the latter, you are building dialogue nodes by passing Action data:

#![allow(unused)]
fn main() {
struct Action {
    /// The ID of the action.
    id: ActionId,
    /// The kind of action.
    action: NodeKind,
    /// The actors involved in the action.
    actors: Vec<ActorSlug>,
    /// Any choices that the user can make during the action.
    choices: Option<Vec<Choice>>,
    /// The text of the action.
    text: Option<String>,
    /// The ID of the next action to perform.
    next: Option<ActionId>,
}
}

It contains several fields that define the kind of action it can be, the relevant actors, text or choices and the next action to perform (where to go in the graph after).

The actors are quite simple right now. It is just the name and an identifier (the slug):

#![allow(unused)]
fn main() {
struct Actor {
    /// The name of the actor.
    name: String,
    /// The unique slug of the actor.
    slug: ActorSlug,
}
}

Having a well defined Talk with actions and actors will result in spawning a graph where all the nodes are entities. Each action will be an entity "node", and each actor is also an entity.

All the action nodes will be connected with each other with an aery relationship (called FollowedBy), following the graph structure given by the actions next and id fields, and each action with actors will result in the corresponding entity being connected with the actors entities with another aery relationship (called PerformedBy).

The Parent Talk

All the node entities in the graph will be a child of a main entity that represents the Talk itself, with the Talk component attached to it.

You can think of this parent Talk entity as it "encapsulates" the graph and you can use it to identify a dialogue graph. You will use it to send events to advance the dialogue.

Build Talks from talk.ron files

The above-mentioned ron assets files are used to create TalkData assets. They can be used to build dialogue graphs via bevy Commands.

The files must have the extension: talk.ron. Here's an example:

(
    actors: [
        ( slug: "bob", name: "Bob" ),
        ( slug: "alice", name: "Alice" )
    ],
    script: [
        ( id: 1, action: Talk, text: Some("Bob and Alice enter the room."), next: Some(2) ),
        ( id: 2, action: Join, actors: [ "bob", "alice" ], next: Some(3)),
        ( id: 3, actors: ["bob"], text: Some("Hello, Alice!"), next: Some(4) ), // without the action field, it defaults to Talk
        (
            id: 4,
            choices: Some([
                ( text: "Alice says hello back.", next: 5 ),
                ( text: "Alice ignores Bob.", next: 6 ),
            ])
        ),
        ( id: 5, text: Some("Bob smiles."), next: Some(7)), // without the actors field, it defaults to an empty vector
        ( id: 6, text: Some("Bob starts crying."), next: Some(7) ),
        ( id: 7, text: Some("The end.") ) // without the next, it is an end node
    ]
)

The plugin adds an AssetLoader for these ron files, so it's as easy as:

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

Then you can use Talk::builder() to create a TalkBuilder, which has the fill_with_talk_data method. You can retrieve the TalkData from the assets collection talks: Res<Assets<TalkData>>.

With the builder ready, you can use the Commands extension to spawn the dialogue graph in the world:

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_talks::prelude::*;

// We stored the previously loaded handle of a TalkData asset in this resource
#[derive(Resource)]
struct TalkAsset {
    handle: Handle<TalkData>,
}

fn spawn(mut commands: Commands, talks: Res<Assets<TalkData>>, talk_asset: Res<TalkAsset>) {
    let talk = talks.get(&talk_asset.handle).unwrap();
    let talk_builder = TalkBuilder::default().fill_with_talk_data(simple_talk);

    // spawn the talk graph
    commands.spawn_talk(talk_builder, ());
}
}

Spawning that talk graph will result in this:

graph LR;
    A[Narrator Talks] --> B[Alice,Bob Join];
    B --> C[Bob Talks];
    C --> D[Choice];
    D --> E[Narrator Talks];
    D --> F[Narrator Talks];
    F --> G[Narrator Talks];
    E --> G;

Usage

Besides building dialogue graphs, at some point you have to interact with them. After all the nodes are entities with components, so you could just do queries using the special CurrentNode component that keeps track of the current node. Then each node could have a TextNode, JoinNode, LeaveNode, ChoiceNode or your own custom components (added via the builder).

Another way is to use a dialogue graph in an event-driven way. The plugin sends events every time you move to a new node based on the components it has. A node with a TextNode will send a TextNodeEvent event, a node with a ChoiceNode will send a ChoiceEvent event, and so on. You can also add your own node emitting components to customize the behaviour.

For example, to display the text of a TextNode you can simply listen to the TextNodeEvent event:

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

Note that the actors connected to the node are injected in the event, so you don't need to query them.

Request Events

That's the events from a dialogue graph to you. There is also the other direction so you can send requests to the dialogue graph (to advance the dialogue).

To move forward to the next action:

#![allow(unused)]
fn main() {
/// Event to request the next node in a `Talk`.
/// It requires an entity with the `Talk` component you want to update.
#[derive(Event)]
pub struct NextNodeRequest  {
    /// The entity with the `Talk` component you want to update.
    pub talk: Entity,
}
}

To jump to a specific action (used with choices):

#![allow(unused)]
fn main() {
/// An event to jump to some specific node in a graph. 
/// It requires an entity with the `Talk` component you want to update.
#[derive(Event)]
pub struct ChooseNodeRequest {
    /// The entity with the `Talk` component you want to update.
    pub talk: Entity,
    /// The next entity to go to.
    pub next: Entity,
}
}

There is also an useful event to re-send all the events associated to a node:

#![allow(unused)]
fn main() {
/// Event to request the current node to re-send all its events.
#[derive(Event)]
pub struct RefireNodeRequest {
    /// The entity with the `Talk` component you want to update.
    pub talk: Entity,
}
}

You pass the entity with the Talk component in these events, plus the next node entity in case of the choose event.

Check out the examples folder to see how to use the plugin.

  • simple.rs shows how to use the plugin to create a simple, linear conversation.
  • choices.rs shows how to use the plugin to create a conversation with choices (jumps in the graph).
  • full.rs shows a Talk where all the action kinds are used.
  • ingame.rs shows how to use the plugin with more than one talk you can interact with.
  • custom_node_event.rs shows how to add your own event emitting component to create a custom node.

Roadmap

Some nice-to-haves from the top of my head:

  • More node kinds (got rid of node kinds, now nodes are entities with components)
  • Extensive documentation/manual wiki (added an mdbook, but always in progress...)
  • Extensible Interaction/Trigger system (I mean I'm using events, more decoupled than this is impossible)
  • Use the built-in bevy_ecs relations (when one day when we will have them)
  • Dialogue UIs
  • Graphical editor to create the asset files
  • Voice lines/sound support
  • Support other asset formats (yarn?)
  • More examples
  • Localization with Fluent

Bevy Version Support

Compatibility of bevy_talks versions: | bevy_talks | bevy | | :-- | :-- | | main | 0.12 | | 0.5.0 | 0.12 | | 0.4.0 | 0.12 | | 0.3.1 | 0.12 | | 0.3.0 | 0.11 | | 0.2.0 | 0.11 | | 0.1.1 | 0.11 | | bevy_main | main |

License

Dual-licensed under either of

  • Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

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. :)

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!