A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Quest System is a structural framework for managing player objectives, narrative progression, and mission state. It allows for the creation of linear or branching quest lines, tracking objectives in real-time, and persisting progress across sessions.
The Quest System in Bevy All-in-One is designed to be data-driven and event-based. Instead of hardcoding quest logic into systems, quests are defined as data structures (Quest structs) that can be loaded from files (JSON/Ron) or defined in code.
The system is decoupled from the specific gameplay mechanics it tracks. It uses a generic “Objective” model where progress is updated via events, allowing it to integrate seamlessly with the Combat, Interaction, and Inventory systems without direct dependencies.
QuestStation components allow any entity (NPC, Signboard, Item) to become a quest giver.The system operates on a Provider-Consumer model:
QuestStation): Holding the definition of a quest. When interacted with, it offers the quest to the player.QuestLog): Attached to the player. It stores the instances of active and completed quests.QuestEventQueue): A central resource that processes state changes.QuestStation.QuestLog and if the quest is new.Quest data is cloned from the Station into the Player’s QuestLog as an “Active Quest”.QuestSystem listens to events, updates specific objectives, and checks for completion.QuestThe blueprint for a mission. This struct is used both in defining the quest (on a Station) and tracking it (in the Log).
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct Quest {
pub id: u32,
pub name: String,
pub description: String,
pub objectives: Vec<Objective>,
pub status: QuestStatus,
pub rewards_description: String,
}
id: Unique identifier. Crucial for serialization and lookups.objectives: List of steps required to finish the quest.status: Current state (see QuestStatus enum).rewards_description: Text description of what the player gets (e.g., “500 Gold”). Note: Actual reward logic is handled by specific systems, this is for display.ObjectiveA single unit of work within a quest.
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct Objective {
pub name: String, // Short title (e.g., "Find the Key")
pub description: String, // Detailed info (e.g., "It is hidden in the cave.")
pub status: QuestStatus, // State of this specific objective
}
QuestLogThe central component for player progression. It separates current tasks from history.
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component)]
pub struct QuestLog {
pub active_quests: Vec<Quest>,
pub completed_quests: Vec<Quest>,
}
active_quests: Quests currently being tracked. Systems iterate over this list to check for updates.completed_quests: Archived history. Useful for checking prerequisites (“Has player finished Quest X?”).QuestStationAttaches to an entity to make it a quest giver.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub struct QuestStation {
pub quest: Quest,
}
interaction_system detects clicks on entities with this component and delegates to the quest handler.QuestStatus (Enum)Represents the state of a Quest or Objective.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
pub enum QuestStatus {
NotStarted,
InProgress,
Completed,
Failed,
}
QuestEventQueue (Resource)A workaround for Bevy’s event frame-delay limitations in certain schedules, ensuring events are processed reliably.
#[derive(Resource, Default)]
pub struct QuestEventQueue(pub Vec<QuestEvent>);
QuestStation.QuestLog, status flips to InProgress.QuestEvent::ObjectiveCompleted(quest_id, objective_index).handle_quest_events system catches this and marks the specific objective as Completed.update_quest_status system runs every frame (or tick).active_quests.quest.objectives.iter().all(|o| o.status == Completed), the Quest itself is marked Completed.Completed, the quest is moved from active_quests list to completed_quests list.// Create the objective
let find_sword = Objective {
name: "Retrieve the Blade".into(),
description: "Find the rusted sword in the old ruins.".into(),
status: QuestStatus::NotStarted,
};
// Create the quest
let starter_quest = Quest {
id: 101,
name: "A New Beginning".into(),
description: "Prove your worth by finding a weapon.".into(),
objectives: vec![find_sword],
status: QuestStatus::NotStarted,
rewards_description: "Old Iron Sword".into(),
};
// Spawn the Quest Giver NPC
commands.spawn((
Name::new("Elder Marcus"),
QuestStation {
quest: starter_quest
},
// Add visual components (Mesh, Transform...)
));
To progress a quest from another system (e.g., when an item is picked up):
fn handle_sword_pickup(
// ... query for pickup events ...
mut quest_events: ResMut<QuestEventQueue>,
) {
if player_picked_up_sword {
// Trigger completion of Objective 0 for Quest 101
quest_events.0.push(QuestEvent::ObjectiveCompleted(101, 0));
}
}
handle_quest_interactions)This internal system bridges the Interaction System and Quest System.
InteractionEvent events.QuestStation component.QuestLog to ensure the player doesn’t already have the quest (Active or Completed).QuestEvent::Started.The Quest System is built with serde support. Every struct (Quest, Objective, QuestLog, QuestStatus) derives Serialize and Deserialize.
This is critical for the Save System. When the game is saved:
QuestLog component is serialized along with the Player entity.active_quests contains the full state of objectives, no external “Quest Database” lookup is strictly necessary to restore state, ensuring robust saves even if quest definitions change in patches (though relying on ID lookups is safer for long-term support).JSON Example of a QuestLog:
{
"active_quests": [
{
"id": 101,
"name": "A New Beginning",
"status": "InProgress",
"objectives": [
{
"name": "Find Sword",
"status": "Completed"
},
{
"name": "Return to Elder",
"status": "InProgress"
}
]
}
],
"completed_quests": []
}
While the Quest struct looks linear (a list of objectives), branching can be achieved via the Dialogue System or event listeners.
Scenario: A player can either Kill the Goblin King OR Negotiate Peace.
handle_dialogue_choice, if Player chooses “Fight”, trigger QuestEvent::Started(Quest_Kill_ID).QuestEvent::Started(Quest_Peace_ID).QuestLog for completed_quests containing specific IDs.Displaying the quest journal is a matter of querying the QuestLog.
fn update_quest_ui(
quest_log_query: Query<&QuestLog, Changed<QuestLog>>,
mut ui_text_query: Query<&mut Text, With<QuestJournalText>>,
) {
if let Ok(log) = quest_log_query.get_single() {
let mut text = String::from("Active Quests:\n");
for quest in &log.active_quests {
text.push_str(&format!("- {} [{:?}]\n", quest.name, quest.status));
for obj in &quest.objectives {
let mark = if obj.status == QuestStatus::Completed { "[x]" } else { "[ ]" };
text.push_str(&format!(" {} {}\n", mark, obj.name));
}
}
// Update UI component
for mut ui_text in ui_text_query.iter_mut() {
ui_text.0 = text.clone();
}
}
}
To create a quest where objectives unlock sequentially (e.g., “Find Key” THEN “Open Door”), you typically handle the logic in your event system.
Pattern:
hidden flag if you extend the struct).Objective 0 completes -> Trigger a TutorialPopup or Dialog that gives a hint for Objective 1.const file or Enum for Quest IDs to avoid magic numbers.const QUEST_TUTORIAL_START: u32 = 100;QuestStation setup if possible. Look up strings by ID from a localization file.QuestStation or manage state in a custom NPCBrain system.active_quests and completed_quests in the inspector.QuestStation?InteractionEvent firing? (See Interaction System docs).QuestEvent::ObjectiveCompleted(id, index).Quest or Objective struct fields? serde might fail to deserialize old save files if fields are missing.#[serde(default)] for new fields to maintain backward compatibility.