Skip to content

Commit

Permalink
Update documentation and rename add_component to with_component
Browse files Browse the repository at this point in the history
  • Loading branch information
giusdp committed Jan 17, 2024
1 parent 406faa2 commit 88d2a48
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 91 deletions.
104 changes: 74 additions & 30 deletions docs/src/builder/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,21 @@ graph LR
can be built with just a few lines of code:

```rust,no_run
let talk_builder = Talk::builder().say("Hello").say(bob, "World");
let talk_builder = Talk::builder().say("Hello").say("World");
let talk_commands = commands.talks();
talk_commands.spawn_talk(talk_builder, ());
commands.spawn_talk(talk_builder);
```

To actually spawn the entities with the relationships, you pass the `TalkBuilder` to the `TalkCommands::spawn_talk` method, which
will prepare a `Command` to be added to the command queue.
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 `NodeKind::Start` which will act as the entry point of the graph and finally spawn entities for each `say`, `choose` etc.
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.

With `say` the builder will connect the entities linearly. 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.
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](https://docs.rs/bevy_talks/latest/bevy_talks/builder/struct.TalkBuilder.html).

### Build Branching Conversations

The builder normally just chains the nodes one after the other as you call the methods. If, instead, you need to connect a node to multiple other nodes (e.g. a choice node) you'll have to start branching.

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

```mermaid
Expand All @@ -54,12 +51,12 @@ let talk_builder = Talk::builder();
talk_builder.say("How are you?")
.choose(vec![
("I'm fine".to_string(), Talk::builder().say("I'm glad to hear that")),
("I'm notfine".to_string(), Talk::builder().say("I'm sorry to hear that")),
("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 effectively is another `TalkBuilder` instance.
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

Expand All @@ -80,24 +77,21 @@ graph LR
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".to_string(), happy_branch.clone()),
("I want an editor!".to_string(), Talk::builder().say("Me too :("))
]);
talk_builder.say("How are you?")
let sad_branch = Talk::builder()
.say("Why?")
.choose(vec![
("I'm fine".to_string(), happy_branch),
("I'm not fine".to_string, sad_branch),
("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)]);
```

As you can see, it's easy to keep branching the conversation and you can also reuse branches. The problem with this approach is that it can get quite verbose and hard to read.
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.
Expand Down Expand Up @@ -151,8 +145,8 @@ let convo_start = talk_builder.last_node_id();
talk_builder = talk_builder
.say("Hey")
.choose(vec![
("Good Choice".to_string(), Talk::builder().say("End of the conversation")),
("Wrong Choice".to_string(), Talk::builder().say("Go Back").connect_to(convo_start))
("Good Choice", Talk::builder().say("End of the conversation")),
("Wrong Choice", Talk::builder().say("Go Back").connect_to(convo_start))
]);
```

Expand Down Expand Up @@ -181,18 +175,18 @@ 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".to_string(), Talk::builder().connect_to(end_node_id.clone())),
("Bad Choice", Talk::builder().connect_to(end_node_id.clone())),
(
"Another Good Choice".to_string(),
"Another Good Choice",
Talk::builder().say("Before the end...").connect_to(end_node_id)
),
]);
let builder = Talk::builder().choose(vec![
("Good Choice".to_string(), good_branch),
("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".to_string(), end_branch_builder)
("Bad Choice", end_branch_builder)
]);
```

Expand All @@ -214,5 +208,55 @@ 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.
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`:

```rust,no_run
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.

```rust,no_run
/// 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`](https://github.com/giusdp/bevy_talks/blob/main/examples/custom_node_event.rs) 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:

```rust,no_run
#[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:

```rust,no_run
let builder = Talk::builder().empty_node().with_component(MyCustomComponent::default());
```

You could create any kind of entity graph this way!
109 changes: 56 additions & 53 deletions docs/src/getting-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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:

```toml
bevy_talks = "0.4"
bevy_talks = "0.5"
```

or just run:
Expand All @@ -30,7 +30,7 @@ Just go to the next section :(

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

If you want to do it via code, checkout the next chapter here: [Creating Talks with TalkBuilder](#builder).
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](#builder).

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

Expand Down Expand Up @@ -181,92 +181,95 @@ fn spawn_talk(
) {
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

let mut talk_commands = commands.talks(); // commands is extended with the TalkCommands
talk_commands.spawn_talk(talk_builder, ()); // spawn the talk graph
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. So we can just query for that entity and use the Talk component to get the data we need to display.
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 right now, so you can use whatever you want to display the dialogue.
A quick way is to query for the Talk component and print the current node to the console:
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.

```rust
/// Print the current talk node (if changed) to the console.
fn print(talk_comps: Query<Ref<Talk>>) { // with Ref<Talk> we get access to change detection
for talk in &talk_comps {
if !talk.is_changed() || talk.is_added() {
continue;
}

let actors = &talk.current_actors;

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

match talk.current_kind {
NodeKind::Talk => println!("{speaker}: {}", talk.current_text),
NodeKind::Join => println!("--- {actors:?} enters the scene."),
NodeKind::Leave => println!("--- {actors:?} exit the scene."),
NodeKind::Choice => {
println!("Choices:");
for (i, choice) in talk.current_choices.iter().enumerate() {
println!("{}: {}", i + 1, choice.text);
}
}
_ => (),
};
fn print_join(mut join_events: EventReader<JoinNodeEvent>) {
for join_event in join_events.read() {
println!("--- {:?} enters the scene.", join_event.actors);
}
}
```

The Talk component has several fields that you can use to get the data of the current node of the dialogue graph.
fn print_leave(mut leave_events: EventReader<LeaveNodeEvent>) {
for leave_event in leave_events.read() {
println!("--- {:?} exit the scene.", leave_event.actors);
}
}

Here we are using the `current_kind` field to check what kind of node we are in and then print the text, the actors or the choices.
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);
}
}
}
```

If the current node has no actors (checked with `current_actors`) we default to "Narrator".
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 printed the talk, but we can't interact with it to move forward (or pick a choice).
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 a 2 events that you can use: `NextActionRequest` and `ChooseActionRequest`. They both need the entity with the `Talk` component you want to update, and for the `ChooseActionRequest` you also need to provide the entity of the next action to go to.
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.

```rust
/// 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<NextActionRequest>,
mut choose_action_events: EventWriter<ChooseActionRequest>,
talks: Query<(Entity, &Talk)>,
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, talk) = talks.single(); // let's grab our talk entity

if talk.current_kind == NodeKind::Choice { // if it's the choice node, let's check the input
if input.just_pressed(KeyCode::Key1) {
let next_ent = talk.current_choices[0].next; // choose first choice
choose_action_events.send(ChooseActionRequest::new(talk_ent, next_ent));
} else if input.just_pressed(KeyCode::Key2) {
let next_ent = talk.current_choices[1].next; // choose second choice
choose_action_events.send(ChooseActionRequest::new(talk_ent, next_ent));
}
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;
}

if input.just_pressed(KeyCode::Space) { // otherwise just try to move forward
next_action_events.send(NextActionRequest(talk_ent));
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 ChooseActionRequest event we have access to the current choices in the Talk component. Each choice has a `next` (and a `text` used in the print system) field with the entity of the next action to go to. So we just grab that and send the event.
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 `CurrentNode`s and you will have to filter them).

## That's it!

Expand Down
4 changes: 2 additions & 2 deletions examples/custom_node_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct DanceStart {
fn main() {
App::new()
.add_plugins((DefaultPlugins, TalksPlugin))
.register_node_event::<DanceStart, DanceStartEvent>()
.register_node_event::<DanceStart, DanceStartEvent>() // Register the component and event
.add_systems(Startup, setup_talk)
.add_systems(
Update,
Expand All @@ -32,7 +32,7 @@ fn setup_talk(mut commands: Commands) {
commands.spawn_talk(
Talk::builder()
.say("Oh lord he dancing")
.add_component(DanceStart {
.with_component(DanceStart {
moves: vec![
"dabs".to_string(),
"whips".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion src/builder/build_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ mod integration_tests {
app.update();
let builder = TalkBuilder::default()
.say("Hello There")
.add_component(TestComp);
.with_component(TestComp);

BuildTalkCommand::new(app.world.spawn_empty().id(), builder).apply(&mut app.world);
app.update();
Expand Down
Loading

0 comments on commit 88d2a48

Please sign in to comment.