#bevy-plugin #graph-node #plugin #dialog #bevy #gamedev #dialogues

bevy_talks

一个用于为角色编写对话以及玩家选择的Bevy插件

6个版本 (破坏性更新)

0.5.0 2024年1月17日
0.4.0 2024年1月2日
0.3.1 2023年11月4日
0.3.0 2023年9月9日
0.1.1 2023年8月17日

#450 in 游戏开发

Download history 18/week @ 2024-04-04 4/week @ 2024-04-11

每月 56 次下载

MIT/Apache

125KB
2K SLoC

Bevy Talks

[!WARNING]
请注意,bevy_talks的API仍在修订中(可能进行重大的架构变更)。对其易用性和开发者体验(DX)的反馈非常欢迎。

Bevy插件提供了一种在游戏中以图的形式创建对话和对话的方式。

你可以想象玩家与NPC之间的对话作为一个有向图,其中每个节点都是一个可以执行的操作,如说出一段话、加入/离开对话或玩家可以做出的选择。

最常见的操作是在屏幕上显示文本,而一个简单的对话就是一个由演员之间的文本序列形成的对话。

你可以有多个实体,每个实体都有自己的对话图。或者你可以制作一个类似视觉小说的游戏,其中只有一个大对话图在游戏中。

[!NOTE] 正在逐步编写更深入的文档,作为一个mdbook!欢迎提供帮助 :)

动作和演员

对话由转换为图节点的动作组成。动作可以通过TalkBuilder(在这里你可以通过自定义组件和事件对对话图有更多控制)或“talk.ron”资产文件来定义。后者,你通过传递Action数据来构建对话节点。

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>,
}

它包含几个字段,定义了动作的类型、相关的演员、文本或选择以及要执行的下一条动作(在图中去哪里)。

演员目前相当简单。它只是名称和一个标识符(slug)

struct Actor {
    /// The name of the actor.
    name: String,
    /// The unique slug of the actor.
    slug: ActorSlug,
}

有一个良好定义的包含动作和演员的对话将导致生成一个所有节点都是实体的图。每个动作将是一个实体“节点”,每个演员也是一个实体。

所有动作节点将通过一种称为“跟随”(FollowedBy)的复杂关系相互连接,遵循由动作的“next”和"id"字段给出的图形结构。每个与演员相关的动作将导致相应的实体通过另一种称为“执行者”(PerformedBy)的复杂关系与演员实体连接。

父对话

图形中的所有节点实体都将是一个表示“对话”(Talk)本身的主要实体的子节点,并且将“对话”组件附加到它上面。

您可以将此父对话实体视为它“封装”了图形,您可以使用它来识别对话图形。您将使用它来发送事件以推进对话。

从 talk.ron 文件构建对话

上述提到的 ron 资产文件用于创建 TalkData 资产。它们可以通过 bevy 的 Commands 构建对话图形。

文件必须具有以下扩展名:talk.ron。以下是一个示例

(
    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
    ]
)

插件为这些 ron 文件添加了一个 AssetLoader,因此它非常简单

let handle: Handle<TalkData> = asset_server.load("simple.talk.ron");

然后,您可以使用 Talk::builder() 创建一个 TalkBuilder,它具有 fill_with_talk_data 方法。您可以从资产集合 talks: Res 中检索 TalkData

有了构建器,您可以使用命令扩展在世界上生成对话图形

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, ());
}

生成此对话图形将产生以下结果

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;

用法

除了构建对话图形外,您必须与它们交互。毕竟,所有节点都是具有组件的实体,因此您可以使用特殊组件 CurrentNode 进行查询,该组件跟踪当前节点。然后每个节点都可以有一个 TextNodeJoinNodeLeaveNodeChoiceNode 或您自己的自定义组件(通过构建器添加)。

另一种方法是以事件驱动的方式使用对话图形。插件在您根据其具有的组件移动到新节点时发送事件。具有 TextNode 的节点将发送 TextNodeEvent 事件,具有 ChoiceNode 的节点将发送 ChoiceEvent 事件,依此类推。您还可以添加自己的节点发射组件以自定义行为。

例如,要显示 TextNode 的文本,您只需监听 TextNodeEvent 事件

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

请注意,连接到节点的演员已注入到事件中,因此您不需要查询它们。

请求事件

这是来自对话图形到您的事件。还有另一个方向,您可以向对话图形发送请求(以推进对话)。

要移动到下一个动作

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

要跳转到特定动作(与选择一起使用)

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

还有一个有用的事件可以重新发送与节点关联的所有事件

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

在这些事件中,您传递具有 Talk 组件的实体,以及选择事件中的下一个节点实体。

查看 examples 文件夹以了解如何使用此插件。

  • simple.rs 展示了如何使用插件创建一个简单的、线性的对话。
  • choices.rs 展示了如何使用插件创建带有选择(图中的跳跃)的对话。
  • full.rs 展示了一个使用了所有动作类型的 Talk。
  • ingame.rs 展示了如何使用插件与多个你可以与之交互的 Talk。
  • custom_node_event.rs 展示了如何添加自己的事件发射组件来创建自定义节点。

路线图

一些我脑海中想得到的美好事物

  • 更多节点类型(已经取消节点类型,现在节点是具有组件的实体)
  • 广泛的文档/手动维基(添加了 mdbook,但总是在进行中...)
  • 可扩展的交互/触发系统(我的意思是我在使用事件,比这更解耦合是不可能的)
  • 使用内置的 bevy_ecs 关系(当我们有一天有它们时)
  • 对话 UI
  • 图形编辑器来创建资产文件
  • 语音行/声音支持
  • 支持其他资产格式(yarn?)
  • 更多示例
  • 使用 Fluent 进行本地化

Bevy 版本支持

bevy_talks 版本的兼容性

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

许可证

根据您的选择,双重许可以下任一项

贡献

除非您明确声明,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在工作中的任何贡献,将按上述方式双重许可,不附加任何其他条款或条件。

依赖关系

~22–55MB
~1M SLoC