GithubHelp home page GithubHelp logo

giusdp / bevy_talks Goto Github PK

View Code? Open in Web Editor NEW
52.0 1.0 5.0 2.43 MB

A Bevy plugin to write dialogues for your characters, together with player choices.

Home Page: https://giusdp.github.io/bevy_talks/

License: Apache License 2.0

Rust 100.00%
bevy gamedev plugin visual-novel dialogue-system dialogues writing

bevy_talks's Introduction

Bevy Talks

Warning

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

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

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:

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:

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:

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:

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

/// 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:

/// 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

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.

bevy_talks's People

Contributors

dependabot[bot] avatar giusdp avatar manankarnik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

bevy_talks's Issues

Conditional choices

Library looks snazzy, was wanting to give it a shot but had a feature request. It'd be nice to be able to conditionally include choices. A use case being new dialogue option existing if the player is currently on a quest.

Potentially related to #41, though it'd be nice if it could also be done within the asset files.

API to build dialogue graphs programmatically

Besides the asset files that describe dialogue graphs, it can be useful to have some builder pattern API to create dialogue graphs and add nodes via code so they can be build at runtime.

Split talk script files

Right now there is one asset file used to create a dialogue graph. It contains both the actors vector and the script actions vector.
The actions also contain the data needed to create the actual graph (ids and next ids), together with other data (the kind of action, actor, text...).

We could instead take a different approach and split this asset file into 3 assets.

  • Actors asset file that acts as a DB of the characters in a game

  • A file with just the dialogue lines without graph metadata.

    • It can be useful to build dialogue graphs procedurally. If we have just the pool of dialogue lines we can pick them and make a graph at runtime.
    • The dialogue lines should have some metadata so they can be grouped together, especially important to indicate which lines are player choices.
    • It is also useful to build graphs with just 1 node that can be added to game items as their description.
  • The graph file that operates just with the ids and puts together actors id, dialogue ids to form a node and connect them together.

    • The non-talk actions (such as Join and Leave) can be used here to indicate when an actor enters/exits the conversation.

Nodes emitting events architecture

After the dialogues as entity-graph refactor it would be nice to move away from the single point of access approach (the parent entity with the Talk component). Instead an user should be able to interact with the nodes directly (by making CurrentNode component public so it can be queried).

The Talk parent is still needed to be able to differentiate between multiple dialogue graphs and identify which dialogue graph to advance with the NextActionRequest and ChooseActionRequest events.

We can add events from the dialogue graph to the outside (the other direction) so that it is possible to react to the changes in the dialogue graph (just moving from one node to another). Having direct access to the nodes and having the nodes send events would make the dialogue graphs way more flexible and customizable.

I have a working demo using bevy-trait-query and this trait:

#[bevy_trait_query::queryable]
pub trait NodeEventEmitter {
    /// Creates an event to be emitted when a node is reached.
    fn make(&self, actors: &[Entity]) -> Box<dyn Reflect>;
}

Moving to a node with text will result in an event being sent that I defined as:

/// Emitted when a text node is reached.
#[derive(Event, Reflect, Default, Clone)]
#[reflect(Event)]
pub struct TextNodeEvent {
    /// The text from the node.
    pub text: String,
    /// The actors from the node.
    pub actors: Vec<Entity>,
}

Now a "Text Node" in a dialogue graph is a component:

/// Component to mark a dialogue node as a text node containing some text.
#[derive(Component, Default, Debug)]
pub struct TextNode(pub String);

impl NodeEventEmitter for TextNode {
    fn make(&self, actors: &[Entity]) -> Box<dyn Reflect> {
        Box::from(TextNodeEvent {
            text: self.0.clone(),
            actors: actors.to_vec(),
        })
    }
}

Note that the "actors" are automatically injected by the plugin.
This is a complete implementation of a component for a dialogue node that will enable the node to send an event when reached.

Also the Join, Leave and Choice nodes are defined as components with the relative events attached.
For the users it will unlock the possibility to add any custom component to nodes that can optionally emit events (the TalkBuilder will need to be updated for that).

Reactive to events instead of checking the Talk entity fields also simplify the API. Now you can have multiple smaller systems that just listen to the events (join, leave, text, choice + custom ones)

This needs some cleaning and testing + all the boilerplate to enable events attached to components can be stuffed inside a macro.

Refactor Dialogue Graph

Currently the graph that models a conversation is implemented with petgraph and stored into a component Talk.
This also keeps track of the start_node and current_node (these are NodeIndex).

There are also some helper components (CurrentText, CurrentActor, CurrentNodeKind...) that can be queried, but they are kinda useless since you just need to query for Talk and you can access all the info you need.

It would be interesting to try to model the graph as a bunch of entities:

  • Each node as an entity with several components (text, nodekind, next...)
  • the "Next" component contains the Entity of the next node.
  • We can group these components in a Bundle to quickly create nodes.

We could have a root Entity that represents the entire dialogue graph (with some marker component). Then each children entity has the bundle of components to make it a "TalkNode". They each have the "Next" component so we know where it points to.

  • It becomes it easy to despawn the entire dialogue graph cause we just need to despawn_recursively() the root entity.

Perhaps we could use aery to add relationships between entities.
Instead of using "Next" component to have a pointer to the next dialogue, we can put the 2 TalkNode entities in a relationship? We should investigate this.

The relationship can also be useful to tie together a TalkNode with the Actors.
We ca have all the actors of a dialogue graph as their own entities (with the appropiate components) connected to each TalkNode in a relationship.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.