From 88d2a484beaa680741c3eaf353a205b210d52ad5 Mon Sep 17 00:00:00 2001 From: Giuseppe De Palma Date: Wed, 17 Jan 2024 21:18:53 +0100 Subject: [PATCH] Update documentation and rename add_component to with_component --- docs/src/builder/index.md | 104 ++++++++++++++++++++-------- docs/src/getting-started/index.md | 109 +++++++++++++++--------------- examples/custom_node_event.rs | 4 +- src/builder/build_command.rs | 2 +- src/builder/mod.rs | 10 +-- 5 files changed, 138 insertions(+), 91 deletions(-) diff --git a/docs/src/builder/index.md b/docs/src/builder/index.md index cf7302d..a41d59f 100644 --- a/docs/src/builder/index.md +++ b/docs/src/builder/index.md @@ -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 @@ -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 @@ -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. @@ -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)) ]); ``` @@ -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) ]); ``` @@ -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. \ No newline at end of file +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; +} +``` + +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::()`. + +### 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::();`. + +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! diff --git a/docs/src/getting-started/index.md b/docs/src/getting-started/index.md index ad7ab16..7345b2a 100644 --- a/docs/src/getting-started/index.md +++ b/docs/src/getting-started/index.md @@ -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: @@ -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`: @@ -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>) { // with Ref 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) { + 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) { + 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) { + 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) { + 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>, - mut next_action_events: EventWriter, - mut choose_action_events: EventWriter, - talks: Query<(Entity, &Talk)>, + mut next_action_events: EventWriter, + mut choose_action_events: EventWriter, + talks: Query>, + choices: Query<&ChoiceNode, With>, ) { - 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! diff --git a/examples/custom_node_event.rs b/examples/custom_node_event.rs index 938d830..f729f6b 100644 --- a/examples/custom_node_event.rs +++ b/examples/custom_node_event.rs @@ -13,7 +13,7 @@ struct DanceStart { fn main() { App::new() .add_plugins((DefaultPlugins, TalksPlugin)) - .register_node_event::() + .register_node_event::() // Register the component and event .add_systems(Startup, setup_talk) .add_systems( Update, @@ -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(), diff --git a/src/builder/build_command.rs b/src/builder/build_command.rs index a57c98b..1f67560 100644 --- a/src/builder/build_command.rs +++ b/src/builder/build_command.rs @@ -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(); diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 963d122..d89ff5d 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -306,7 +306,7 @@ impl TalkBuilder { /// #[reflect(Component)] /// struct MyComp; /// - /// let builder = TalkBuilder::default().empty_node().add_component(MyComp); + /// let builder = TalkBuilder::default().empty_node().with_component(MyComp); /// ``` pub fn empty_node(mut self) -> Self { let talk_node = BuildNode { @@ -317,7 +317,7 @@ impl TalkBuilder { self } - /// Add components attached to the latest added node. + /// Add a component to the latest added node. /// If you add a `NodeEventEmitter` component the node will automatically emit the relative event when reached. /// /// # Note @@ -326,7 +326,7 @@ impl TalkBuilder { /// /// # Panics /// If you call this method on an empty builder it will panic. - pub fn add_component(mut self, comp: C) -> Self { + pub fn with_component(mut self, comp: C) -> Self { match self.queue.back_mut() { None => panic!("You can't add a custom component to an empty builder"), Some(node) => node.components.push(Box::new(comp)), @@ -448,7 +448,7 @@ mod tests { #[rstest] fn add_component_on_last_node(talk_builder: TalkBuilder) { - let builder = talk_builder.say("hello").add_component(MyComp); + let builder = talk_builder.say("hello").with_component(MyComp); assert_eq!(builder.queue.len(), 1); assert_eq!(builder.queue[0].components.len(), 2); } @@ -456,6 +456,6 @@ mod tests { #[rstest] #[should_panic] fn add_component_on_empty_panics(talk_builder: TalkBuilder) { - talk_builder.add_component(MyComp); + talk_builder.with_component(MyComp); } }