A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Dialog System provides a comprehensive framework for implementing branching conversations, narrative interactions, and NPC dialogues in your Bevy game. Inspired by professional game dialog systems, it supports complex conversation trees with conditional branching, animated responses, sound effects, and integration with other game systems like quests and stats.
Key Features:
Module Location: src/dialog.rs
The Dialog System is built on a hierarchical structure:
Dialog flows are non-linear and support:
The main component attached to entities that can initiate dialog (NPCs, signs, devices).
Key Fields:
id: u32 - Unique identifier for this dialog contentscene_id: u32 - Scene identifier for multi-scene supportcomplete_dialogs: Vec<CompleteDialog> - Collection of conversation treescurrent_dialog_index: usize - Currently active dialog treeshow_owner_name: bool - Display the speaker’s nameactive: bool - Whether this dialog is currently runningin_process: bool - Whether the dialog is actively being displayeduse_animations: bool - Enable character animations during dialogdialogue_active_animation: String - Animation name for talking stateUsage Pattern:
DialogContent {
id: 1,
scene_id: 0,
complete_dialogs: vec![merchant_greeting_dialog, merchant_trade_dialog],
current_dialog_index: 0,
show_owner_name: true,
active: false,
use_animations: true,
dialogue_active_animation: "Talk".to_string(),
..default()
}
Represents a complete conversation tree with all its nodes and configuration.
Key Fields:
Dialog Nodes:
id: u32 - Unique identifiername: String - Descriptive name for organizationnodes: Vec<DialogNode> - All dialog nodes in this conversationPlayback Control:
play_without_pausing: bool - Allow gameplay to continue during dialogplay_automatically: bool - Auto-advance dialog without player inputpause_player_actions: bool - Disable player actions during dialogpause_player_movement: bool - Disable player movement during dialogcan_use_input_for_next: bool - Allow input to advance dialogshow_full_on_input: bool - Skip text animation on inputText Animation:
show_word_by_word: bool - Display text one word at a timeword_speed: f32 - Seconds between words (default: 0.5)show_letter_by_letter: bool - Display text one character at a timeletter_speed: f32 - Seconds between characters (default: 0.03)Distance Management:
stop_on_distance: bool - Stop dialog if player moves too farmax_distance: f32 - Maximum distance before stoppingrewind_on_stop: bool - Go back one node if interruptedTrigger Integration:
play_on_trigger_enter: bool - Start dialog when player enters trigger zoneConfiguration Example:
CompleteDialog {
id: 1,
name: "Merchant Greeting".to_string(),
nodes: vec![/* dialog nodes */],
play_without_pausing: false,
play_automatically: true,
pause_player_movement: true,
show_letter_by_letter: true,
letter_speed: 0.03,
stop_on_distance: true,
max_distance: 5.0,
..default()
}
Represents a single line of dialog or decision point in the conversation.
Speaker Information:
id: u32 - Unique node identifiername: String - Node name for debuggingspeaker_name: String - Name displayed in UI (e.g., “Guard”, “Merchant”)content: String - The actual dialog textChoice Management:
choices: Vec<DialogChoice> - Available player responsesshow_previous_on_options: bool - Show previous dialog when displaying choicesis_end: bool - Marks this as the final nodeTiming Control:
delay_to_show: f32 - Delay before showing this line (seconds)delay_to_next: f32 - Delay before auto-advancing (seconds)use_next_button: bool - Require button press to continueAudio/Visual:
use_sound: bool - Play sound effectsound_path: Option<String> - Path to sound assetanimation_name: Option<String> - Animation to triggeranimation_delay: f32 - Delay before playing animationanimation_on_player: bool - Apply animation to player instead of NPCState Management:
disable_after_select: bool - Prevent this node from being shown againjump_to_if_disabled: Option<u32> - Alternate node if this one is disableddisabled: bool - Current disabled stateDialog Completion:
set_next_complete_dialog: bool - Switch to next CompleteDialog after this nodeset_new_complete_dialog: bool - Switch to specific CompleteDialognew_complete_dialog_id: Option<u32> - Target CompleteDialog IDEvent Integration:
remote_trigger_name: Option<String> - Name of trigger to activateactivate_remote_trigger: bool - Enable trigger activationExample Node:
DialogNode {
id: 1,
name: "Greeting".to_string(),
speaker_name: "Guard".to_string(),
content: "Halt! State your business.".to_string(),
choices: vec![
DialogChoice {
content: "I'm just passing through.".to_string(),
target_dialog_id: 2,
..default()
},
DialogChoice {
content: "I have a letter from the Captain.".to_string(),
target_dialog_id: 10,
use_stat_condition: true,
stat_name: Some("HasLetter".to_string()),
bool_stat_value: true,
..default()
},
],
use_next_button: false,
delay_to_show: 0.5,
animation_name: Some("Alert".to_string()),
use_sound: true,
sound_path: Some("guard_halt.ogg".to_string()),
..default()
}
Represents a player response option with conditional logic and branching.
Basic Properties:
id: u32 - Unique choice identifiername: String - Internal name for debuggingcontent: String - Text shown to the playertarget_dialog_id: u32 - Next dialog node when selectedRandom Branching:
use_random_dialog_id: bool - Enable random target selectionuse_random_range: bool - Pick random ID from numeric rangerandom_range: (f32, f32) - Min/max for random selectionrandom_id_list: Vec<u32> - List of possible target IDsState Management:
disable_after_select: bool - Hide this choice after usedisabled: bool - Current disabled stateConditional Display:
use_stat_condition: bool - Show choice based on stat checkstat_name: Option<String> - Name of stat to checkstat_is_amount: bool - Check numeric value (true) or boolean (false)min_stat_value: f32 - Minimum required value for numeric statsbool_stat_value: bool - Required boolean valueavailable: bool - Computed availability based on conditionsEvent Integration:
remote_trigger_name: Option<String> - Trigger to activate on selectionactivate_remote_trigger: bool - Enable trigger activationExample Choices:
// Simple choice
DialogChoice {
id: 1,
content: "Tell me more.".to_string(),
target_dialog_id: 5,
..default()
}
// Stat-gated choice (requires high Charisma)
DialogChoice {
id: 2,
content: "[Charisma 7] Persuade the guard.".to_string(),
target_dialog_id: 20,
use_stat_condition: true,
stat_name: Some("Charisma".to_string()),
stat_is_amount: true,
min_stat_value: 7.0,
..default()
}
// Random outcome choice
DialogChoice {
id: 3,
content: "Try your luck at the wheel.".to_string(),
use_random_dialog_id: true,
random_id_list: vec![30, 31, 32], // win, lose, jackpot
..default()
}
// One-time choice with trigger
DialogChoice {
id: 4,
content: "Accept the quest.".to_string(),
target_dialog_id: 100,
disable_after_select: true,
activate_remote_trigger: true,
remote_trigger_name: Some("StartQuest_Delivery".to_string()),
..default()
}
Component attached to the player (or any entity that can engage in conversations) to manage dialog state.
Core State:
enabled: bool - Whether the system is activecurrent_dialog_content: Option<DialogContent> - Active conversationprevious_dialog_content: Option<DialogContent> - Last conversation (for history)current_dialog_index: usize - Current node index in the conversationdialog_active: bool - Whether a dialog is currently displayeddialog_in_process: bool - Whether dialog is actively progressingPlayback Settings:
play_without_pausing: bool - Allow gameplay during dialogplay_automatically: bool - Auto-advance without inputcan_use_input_for_next: bool - Allow input to advanceshow_full_on_input: bool - Skip animation on inputText Animation:
show_word_by_word: bool - Enable word-by-word displayshow_letter_by_letter: bool - Enable letter-by-letter displaytext_showing_part_by_part: bool - Currently animating textDistance Management:
stop_on_distance: bool - Enable distance checkingmax_distance: f32 - Maximum conversation distancerewind_on_stop: bool - Rewind one node on interruptionText Alignment:
use_custom_text_alignment: bool - Enable custom UI positioningDisplay State:
current_dialog_line: String - Currently displayed textprevious_dialog_line: String - Previous dialog textTiming:
last_dialog_start_time: f32 - Timestamp of last dialog startAnimation:
current_character_animator: Option<Entity> - Entity with animatoruse_animations: bool - Enable animation systemplaying_character_animation: bool - NPC animation activeplaying_player_animation: bool - Player animation activeDialog choices can trigger quest events through remote triggers or direct quest system calls.
Pattern: Quest Acceptance Dialog
// In DialogNode
DialogNode {
content: "Will you help me find my lost cat?".to_string(),
choices: vec![
DialogChoice {
content: "Yes, I'll help you.".to_string(),
target_dialog_id: 10,
activate_remote_trigger: true,
remote_trigger_name: Some("AcceptQuest_LostCat".to_string()),
..default()
},
DialogChoice {
content: "Sorry, I'm busy.".to_string(),
target_dialog_id: 11,
..default()
},
],
..default()
}
// In a custom system
fn handle_dialog_triggers(
dialog_systems: Query<&DialogSystem>,
mut quest_logs: Query<&mut QuestLog>,
) {
// Listen for remote trigger "AcceptQuest_LostCat"
// Add quest to player's quest log
}
Dialog choices can check player stats to determine availability.
Pattern: Skill-Based Dialog
DialogChoice {
content: "[Intelligence 8] Identify the artifact.".to_string(),
target_dialog_id: 50,
use_stat_condition: true,
stat_name: Some("Intelligence".to_string()),
stat_is_amount: true,
min_stat_value: 8.0,
..default()
}
// System to update choice availability
fn update_dialog_choice_availability(
dialog_systems: Query<&DialogSystem>,
stats: Query<&Stats>,
) {
// Check player stats against choice requirements
// Update choice.available field
}
Dialog is typically triggered through the interaction system.
Pattern: NPC Interaction
// On NPC entity
commands.spawn((
DialogContent {
complete_dialogs: vec![greeting_dialog],
..default()
},
Interactable {
interaction_text: "Talk to Merchant".to_string(),
interaction_type: InteractionType::Talk,
..default()
},
// Other components...
));
// Custom system to start dialog on interaction
fn start_dialog_on_interact(
mut interaction_events: ResMut<InteractionEventQueue>,
dialog_contents: Query<&DialogContent>,
mut dialog_systems: Query<&mut DialogSystem>,
) {
for event in interaction_events.0.drain(..) {
if let Ok(content) = dialog_contents.get(event.target) {
if let Ok(mut system) = dialog_systems.get_mut(event.source) {
system.current_dialog_content = Some(content.clone());
system.dialog_active = true;
system.current_dialog_index = 0;
}
}
}
}
The system supports two types of text animation for creating typewriter effects:
Word-by-Word Animation:
CompleteDialog {
show_word_by_word: true,
word_speed: 0.5, // 0.5 seconds between words
show_full_on_input: true, // Skip animation on player input
..default()
}
Letter-by-Letter Animation:
CompleteDialog {
show_letter_by_letter: true,
letter_speed: 0.03, // 30ms between characters
show_full_on_input: true,
..default()
}
Implementation Note: The text animation requires a custom UI system that reads DialogSystem.text_showing_part_by_part and gradually reveals the text in DialogSystem.current_dialog_line.
Create unpredictable outcomes by randomizing dialog paths:
Random from List:
DialogChoice {
content: "Open the mystery box.".to_string(),
use_random_dialog_id: true,
random_id_list: vec![100, 101, 102, 103], // Different outcomes
..default()
}
Random from Range:
DialogChoice {
content: "Spin the wheel of fortune.".to_string(),
use_random_dialog_id: true,
use_random_range: true,
random_range: (200.0, 250.0), // Will pick a random node ID in this range
..default()
}
Use Cases:
Prevent conversations from continuing when the player moves away:
CompleteDialog {
stop_on_distance: true,
max_distance: 5.0, // Stop if player moves 5 units away
rewind_on_stop: true, // Go back one node instead of closing
..default()
}
Behavior:
max_distance, dialog is interruptedrewind_on_stop is true, player returns to previous nodeSynchronize character animations with dialog:
NPC Animation:
DialogNode {
content: "Look at this strange artifact!".to_string(),
animation_name: Some("Point".to_string()),
animation_delay: 0.5, // Wait 0.5s before playing
animation_on_player: false,
..default()
}
Player Animation:
DialogNode {
content: "[You nod in agreement]".to_string(),
animation_name: Some("Nod".to_string()),
animation_on_player: true,
..default()
}
Continuous Dialog Animation:
DialogContent {
use_animations: true,
dialogue_active_animation: "Talk".to_string(), // Loop while dialog is active
..default()
}
Prevent dialog nodes or choices from being repeated:
Disable Node After Selection:
DialogNode {
content: "Take this sword. I won't need it anymore.".to_string(),
disable_after_select: true,
jump_to_if_disabled: Some(999), // Go here if player talks again
..default()
}
Disable Choice After Selection:
DialogChoice {
content: "Tell me about the old war.".to_string(),
target_dialog_id: 50,
disable_after_select: true, // Choice won't appear again
..default()
}
Use Cases:
NPCs can have multiple conversation trees that change based on game state:
DialogContent {
complete_dialogs: vec![
first_meeting_dialog, // Index 0: Initial greeting
regular_dialog, // Index 1: Standard interactions
quest_active_dialog, // Index 2: While quest is active
quest_complete_dialog, // Index 3: After quest completion
],
current_dialog_index: 0,
..default()
}
// Switch dialogs based on game state
DialogNode {
set_new_complete_dialog: true,
new_complete_dialog_id: Some(2), // Switch to quest_active_dialog
..default()
}
Trigger external game events from dialog:
Quest Start Trigger:
DialogChoice {
content: "I accept your quest.".to_string(),
target_dialog_id: 100,
activate_remote_trigger: true,
remote_trigger_name: Some("StartQuest_DragonSlayer".to_string()),
..default()
}
Door Unlock Trigger:
DialogNode {
content: "Here's the key to the castle.".to_string(),
activate_remote_trigger: true,
remote_trigger_name: Some("UnlockCastleDoor".to_string()),
..default()
}
Implementation Pattern:
// Custom resource to track triggers
#[derive(Resource, Default)]
struct DialogTriggerQueue {
triggers: Vec<String>,
}
// System to process triggers
fn process_dialog_triggers(
mut trigger_queue: ResMut<DialogTriggerQueue>,
mut door_query: Query<&mut Door>,
mut quest_logs: Query<&mut QuestLog>,
) {
for trigger_name in trigger_queue.triggers.drain(..) {
match trigger_name.as_str() {
"UnlockCastleDoor" => {
// Unlock door logic
}
"StartQuest_DragonSlayer" => {
// Add quest to log
}
_ => warn!("Unknown dialog trigger: {}", trigger_name),
}
}
}
Step 1: Create Dialog Nodes
use bevy_allinone::prelude::*;
let greeting_node = DialogNode {
id: 1,
name: "Greeting".to_string(),
speaker_name: "Merchant".to_string(),
content: "Welcome to my shop! What can I do for you?".to_string(),
choices: vec![
DialogChoice {
id: 1,
content: "Show me your wares.".to_string(),
target_dialog_id: 2,
..default()
},
DialogChoice {
id: 2,
content: "Goodbye.".to_string(),
target_dialog_id: 999,
..default()
},
],
use_next_button: false,
..default()
};
let shop_node = DialogNode {
id: 2,
name: "Shop".to_string(),
speaker_name: "Merchant".to_string(),
content: "Take a look around.".to_string(),
is_end: true,
..default()
};
let end_node = DialogNode {
id: 999,
name: "End".to_string(),
speaker_name: "Merchant".to_string(),
content: "Come back anytime!".to_string(),
is_end: true,
..default()
};
Step 2: Create Complete Dialog
let merchant_dialog = CompleteDialog {
id: 1,
name: "Merchant Conversation".to_string(),
nodes: vec![greeting_node, shop_node, end_node],
pause_player_movement: true,
show_letter_by_letter: true,
letter_speed: 0.03,
..default()
};
Step 3: Attach to NPC
fn spawn_merchant(mut commands: Commands) {
commands.spawn((
Name::new("Merchant"),
DialogContent {
id: 1,
complete_dialogs: vec![merchant_dialog],
show_owner_name: true,
..default()
},
Interactable {
interaction_text: "Talk to Merchant".to_string(),
interaction_type: InteractionType::Talk,
..default()
},
// Transform, mesh, collider, etc.
));
}
Create a conversation with multiple paths and consequences:
// Opening node
let investigation_start = DialogNode {
id: 1,
speaker_name: "Detective".to_string(),
content: "So, where were you on the night of the murder?".to_string(),
choices: vec![
DialogChoice {
content: "I was home alone.".to_string(),
target_dialog_id: 10, // Suspicious path
..default()
},
DialogChoice {
content: "I was at the tavern with friends.".to_string(),
target_dialog_id: 20, // Alibi path
..default()
},
DialogChoice {
content: "[Lie] I don't remember.".to_string(),
target_dialog_id: 30, // Caught lying path
use_stat_condition: true,
stat_name: Some("Deception".to_string()),
min_stat_value: 5.0,
..default()
},
],
..default()
};
// Suspicious path
let suspicious_response = DialogNode {
id: 10,
speaker_name: "Detective".to_string(),
content: "Home alone? No witnesses? That's... convenient.".to_string(),
choices: vec![
DialogChoice {
content: "I live alone. What do you want from me?".to_string(),
target_dialog_id: 11,
..default()
},
],
animation_name: Some("Suspicious".to_string()),
..default()
};
// Alibi path
let alibi_response = DialogNode {
id: 20,
speaker_name: "Detective".to_string(),
content: "The tavern, you say? I'll need those names.".to_string(),
choices: vec![
DialogChoice {
content: "Sure, I'll give you their names.".to_string(),
target_dialog_id: 21,
activate_remote_trigger: true,
remote_trigger_name: Some("ProvideAlibi".to_string()),
..default()
},
],
..default()
};
Integrate dialog with the quest system:
let quest_offer = DialogNode {
id: 1,
speaker_name: "Village Elder".to_string(),
content: "Our village is being terrorized by bandits. Will you help us?".to_string(),
choices: vec![
DialogChoice {
id: 1,
content: "I'll deal with the bandits.".to_string(),
target_dialog_id: 10,
activate_remote_trigger: true,
remote_trigger_name: Some("AcceptQuest_Bandits".to_string()),
disable_after_select: true,
..default()
},
DialogChoice {
id: 2,
content: "I need more information first.".to_string(),
target_dialog_id: 20,
..default()
},
DialogChoice {
id: 3,
content: "I can't help right now.".to_string(),
target_dialog_id: 999,
..default()
},
],
..default()
};
let quest_accepted = DialogNode {
id: 10,
speaker_name: "Village Elder".to_string(),
content: "Thank you! The bandits hideout is in the northern forest.".to_string(),
is_end: true,
set_new_complete_dialog: true,
new_complete_dialog_id: Some(2), // Switch to "quest active" dialog
..default()
};
// System to handle quest trigger
fn handle_quest_triggers(
mut trigger_queue: ResMut<DialogTriggerQueue>,
mut quest_logs: Query<&mut QuestLog>,
) {
for trigger in trigger_queue.triggers.drain(..) {
if trigger == "AcceptQuest_Bandits" {
for mut log in quest_logs.iter_mut() {
log.active_quests.push(Quest {
id: 101,
name: "Bandit Problem".to_string(),
description: "Clear the bandit camp in the northern forest.".to_string(),
status: QuestStatus::InProgress,
..default()
});
}
}
}
}
Create dialog choices that appear based on character stats:
let negotiation = DialogNode {
id: 1,
speaker_name: "Guard Captain".to_string(),
content: "The toll is 50 gold. Pay or turn back.".to_string(),
choices: vec![
DialogChoice {
content: "Here's the gold.".to_string(),
target_dialog_id: 10,
..default()
},
DialogChoice {
content: "[Charisma 8] Surely we can come to an arrangement?".to_string(),
target_dialog_id: 20,
use_stat_condition: true,
stat_name: Some("Charisma".to_string()),
stat_is_amount: true,
min_stat_value: 8.0,
..default()
},
DialogChoice {
content: "[Intimidation 6] Let me through, or else.".to_string(),
target_dialog_id: 30,
use_stat_condition: true,
stat_name: Some("Intimidation".to_string()),
stat_is_amount: true,
min_stat_value: 6.0,
..default()
},
DialogChoice {
content: "I'll come back later.".to_string(),
target_dialog_id: 999,
..default()
},
],
..default()
};
Change NPC dialog based on game state:
// Create multiple dialog trees
let first_meeting = CompleteDialog {
id: 1,
name: "First Meeting".to_string(),
nodes: vec![/* first meeting nodes */],
..default()
};
let knows_player = CompleteDialog {
id: 2,
name: "Knows Player".to_string(),
nodes: vec![/* friendly dialog */],
..default()
};
let quest_active = CompleteDialog {
id: 3,
name: "Quest Active".to_string(),
nodes: vec![/* quest-related dialog */],
..default()
};
// Setup NPC with multiple dialogs
commands.spawn((
DialogContent {
complete_dialogs: vec![first_meeting, knows_player, quest_active],
current_dialog_index: 0, // Start with first meeting
..default()
},
// Other components
));
// System to switch dialogs based on game state
fn update_npc_dialog_state(
mut dialog_contents: Query<&mut DialogContent>,
quest_logs: Query<&QuestLog>,
) {
for mut content in dialog_contents.iter_mut() {
// Check if player has active quest
for log in quest_logs.iter() {
if log.active_quests.iter().any(|q| q.id == 101) {
content.current_dialog_index = 2; // Quest active dialog
} else if log.completed_quests.len() > 0 {
content.current_dialog_index = 1; // Knows player
}
}
}
}
dialog_active is truedisable_after_selectstop_on_distance is enabledprevious_dialog_content for contextA merchant with different greetings based on player reputation:
let shop_dialog = CompleteDialog {
nodes: vec![
DialogNode {
id: 1,
speaker_name: "Merchant".to_string(),
content: "Welcome back, valued customer!".to_string(),
choices: vec![
DialogChoice {
content: "Show me your goods.".to_string(),
target_dialog_id: 2,
..default()
},
],
use_stat_condition: true,
stat_name: Some("Reputation".to_string()),
min_stat_value: 10.0,
..default()
},
// Low reputation variant
DialogNode {
id: 1,
content: "What do you want?".to_string(),
// ... different tone for low reputation
..default()
},
],
..default()
};
NPC that sells information for gold:
let info_node = DialogNode {
id: 1,
speaker_name: "Informant".to_string(),
content: "I know things... for a price.".to_string(),
choices: vec![
DialogChoice {
content: "[Pay 100 Gold] Tell me about the secret passage.".to_string(),
target_dialog_id: 10,
use_stat_condition: true,
stat_name: Some("Gold".to_string()),
min_stat_value: 100.0,
activate_remote_trigger: true,
remote_trigger_name: Some("PayGold_100".to_string()),
..default()
},
DialogChoice {
content: "I'll come back when I have the gold.".to_string(),
target_dialog_id: 999,
..default()
},
],
..default()
};
NPC with random riddles and outcomes:
let riddle_node = DialogNode {
id: 1,
speaker_name: "Sphinx".to_string(),
content: "Answer my riddle correctly, and you may pass. Fail, and face the consequences.".to_string(),
choices: vec![
DialogChoice {
content: "I accept your challenge!".to_string(),
use_random_dialog_id: true,
random_id_list: vec![10, 11, 12], // Different riddles
..default()
},
DialogChoice {
content: "I'll pass, thanks.".to_string(),
target_dialog_id: 999,
..default()
},
],
..default()
};
Non-interactive dialog that plays automatically:
let cutscene = CompleteDialog {
play_automatically: true,
pause_player_actions: true,
pause_player_movement: true,
show_letter_by_letter: true,
letter_speed: 0.03,
nodes: vec![
DialogNode {
id: 1,
speaker_name: "Narrator".to_string(),
content: "Many years ago, in a distant land...".to_string(),
delay_to_next: 3.0,
use_next_button: false,
..default()
},
DialogNode {
id: 2,
speaker_name: "Narrator".to_string(),
content: "A great evil awakened.".to_string(),
delay_to_next: 3.0,
is_end: true,
..default()
},
],
..default()
};
Problem: Dialog doesn’t activate when interacting with NPC.
Solutions:
DialogContent is attached to the NPC entitycomplete_dialogs vector is not emptycurrent_dialog_index is valid (less than complete_dialogs.len())Problem: Dialog node shows but no choices appear.
Solutions:
choices vector is populateddisabled flag on choicesProblem: Dialog doesn’t progress to next node.
Solutions:
use_next_button settingtarget_dialog_id points to valid nodeProblem: Text appears instantly instead of animating.
Solutions:
show_word_by_word or show_letter_by_letter is truetext_showing_part_by_part flag is being setProblem: Choices with stat conditions never appear.
Solutions:
stat_is_amount matches your stat type (numeric vs boolean)available flag is being updated by systemsProblem: Dialog continues even when player moves far away.
Solutions:
stop_on_distance is truemax_distance is set to reasonable valueThe Dialog System uses placeholder event handlers due to Bevy 0.18 EventReader compatibility issues. In production, you should:
Example Implementation:
#[derive(Resource, Default)]
struct DialogEventQueue {
start_events: Vec<StartDialogEvent>,
next_events: Vec<NextDialogEvent>,
choice_events: Vec<SelectDialogChoiceEvent>,
close_events: Vec<CloseDialogEvent>,
}
All dialog components support serialization through Serde for:
Example JSON Dialog:
{
"id": 1,
"name": "Merchant Greeting",
"nodes": [
{
"id": 1,
"speaker_name": "Merchant",
"content": "Welcome!",
"choices": [
{
"content": "Hello",
"target_dialog_id": 2
}
]
}
]
}
Changed<DialogSystem>) to minimize UI updatesPotential improvements to the Dialog System:
src/dialog.rsDialogPluginbevy_allinone::prelude::*Components:
DialogContent - Attached to NPCs/objectsDialogSystem - Attached to player/conversing entityData Structures:
CompleteDialog - Full conversation treeDialogNode - Individual dialog lineDialogChoice - Player response optionKey Features: