A data-driven quest system for Unity games. Designers assemble quests from reusable components; programmers extend the system with new task types and conditions.
The package is organized into modular assemblies:
| Assembly | Description | Optional |
|---|---|---|
HelloDev.QuestSystem |
Core quest system (Quests, Tasks, Stages, QuestLines, SaveLoad) | No |
HelloDev.QuestSystem.Tutorials |
Tutorial system with steps and sequencing | Yes |
HelloDev.QuestSystem.Achievements |
Achievement tracking and unlocking | Yes |
Optional assemblies can be excluded if you don't need them. Simply don't reference them in your assembly definitions.
Via Package Manager (Local):
- Open Unity Package Manager (Window > Package Manager)
- Click "+" > "Add package from disk"
- Navigate to this folder and select
package.json
Dependencies:
com.hellodev.utilscom.hellodev.savingcom.hellodev.eventscom.hellodev.conditionscom.hellodev.idscom.unity.localization
- Create an empty GameObject named "QuestManager"
- Add the QuestManager component
- Add your Quest_SO assets directly to the QuestManager's
Quests Databaselist in the inspector
Step 1: Create a Task
Right-click > Create > HelloDev > Quest System > Scriptable Objects > Tasks > Int Task
Configure:
- Set
DevName(e.g., "KillGoblins") - Set
RequiredCount(e.g., 5) - Configure localized
DisplayNameandTaskDescription
Step 2: Create a Quest
Right-click > Create > HelloDev > Quest System > Scriptable Objects > Quest
Configure:
- Set
DevName(e.g., "GoblinsBane") - Add your task to the
Taskslist - Configure localized
DisplayNameandDescription
Step 3: Add Quest to QuestManager
- Select your QuestManager in the scene
- Add your quest to the
Quests Databaselist
using HelloDev.QuestSystem;
using HelloDev.QuestSystem.Quests;
using UnityEngine;
public class QuestGiver : MonoBehaviour
{
[SerializeField] private Quest_SO goblinsBaneQuest;
public void GiveQuest()
{
// Add and start the quest immediately
QuestManager.Instance.AddAndStartQuest(goblinsBaneQuest);
}
}
public class GoblinEnemy : MonoBehaviour
{
[SerializeField] private Quest_SO goblinsBaneQuest;
public void OnDeath()
{
// Progress the quest when a goblin is killed
var quest = QuestManager.Instance.GetActiveQuest(goblinsBaneQuest);
quest?.CurrentTask?.IncrementStep();
}
}void Start()
{
QuestManager.Instance.QuestCompleted.AddListener(OnQuestCompleted);
QuestManager.Instance.QuestStarted.AddListener(OnQuestStarted);
}
void OnQuestCompleted(QuestRuntime quest)
{
Debug.Log($"Quest completed: {quest.Data.DevName}");
}
void OnQuestStarted(QuestRuntime quest)
{
Debug.Log($"Quest started: {quest.Data.DevName}");
}See BasicQuestExample/README.md for instructions on setting up the example with UI and debug tools.
A visual node-based editor for designing quests using Unity's Graph Toolkit.
Features:
- Visual design of quests, stages, task groups, and questlines
- Drag-and-drop node creation
- Subgraph support for reusable stage and task group templates
- Export graphs to ScriptableObject assets
- Validation with reachability analysis
Graph Types:
| Graph | Extension | Purpose |
|---|---|---|
| Quest Graph | .quest |
Design individual quests with stages and tasks |
| Stage Graph | .stage |
Reusable stage templates (subgraphs) |
| Task Group Graph | .taskgroup |
Reusable task group templates |
| QuestLine Graph | .questline |
Design questlines with quest references |
Node Types:
QuestStartNode- Entry point with quest metadataStageNode- Quest stages with task groups (multi-capacity input)TaskNode- Individual tasks (int, bool, string, etc.)TaskGroupNode- Groups of tasks with completion modeChoiceNode- Player choice branchesTransitionNode- Configurable stage transitions with triggers and conditionsConditionGateNode- Conditional flow controlRewardNode- Quest rewardsWorldFlagSetNode- Set world flags on completion
Requirements:
- Unity 6.2 or later
- Graph Toolkit package (
com.unity.graph-toolkit)
- QuestManager - Singleton managing quest and questline lifecycle, events, and state
- QuestRuntime / Quest_SO - Runtime quest instances with ScriptableObject data
- QuestLineRuntime / QuestLine_SO - Narrative grouping of related quests (story arcs)
- TaskRuntime / Task_SO - Abstract task system with typed implementations
- IntTask / TaskInt_SO - Counter-based tasks (collect 5 items, kill 10 enemies)
- BoolTask / TaskBool_SO - Boolean tasks (toggle a switch, trigger an event)
- StringTask / TaskString_SO - String matching tasks (enter password, find code)
- LocationTask / TaskLocation_SO - Location-based tasks (reach a waypoint)
- TimedTask / TaskTimed_SO - Timer-based tasks with countdown
- DiscoveryTask / TaskDiscovery_SO - Discovery tasks (find hidden items)
- Stage-based branching - Quests can branch based on player choices or conditions
- PlayerChoice transitions - Present choices to players (UI, dialogue, physical actions, etc.)
- Implicit choices - Choices can be made through actions (buying items, entering areas, etc.)
- Choice tracking - All branch decisions are recorded for save/load and analytics
- Event-driven - OnChoicesAvailable, OnChoiceMade, OnChoiceAvailabilityChanged
- WorldFlagBool_SO - Boolean flags for binary state (met_king, chose_evil_path)
- WorldFlagInt_SO - Integer flags for numeric state (reputation, kill_count)
- ConditionWorldFlagBool_SO - Check boolean flags in conditions
- ConditionWorldFlagInt_SO - Check integer flags with comparisons (>=, <, ==, etc.)
- Event Modifiers - Set flags when events fire (see
com.hellodev.conditionsforWorldFlagEventModifier*_SOclasses)
- Start conditions - Control when quests become available
- Failure conditions - Quest-level failure triggers
- Global task failure conditions - Fail current task when met
- Task conditions - Task completion triggers via event-driven conditions
- ConditionQuestState_SO - Quest chains (Quest B requires Quest A completed)
- ConditionQuestLineState_SO - QuestLine prerequisites (unlock content after completing a questline)
- ConditionWorldFlagBool_SO - Check boolean world state flags
- ConditionWorldFlagInt_SO - Check integer world state flags
- Sequential chains - Quest B starts only after Quest A completes
- Branching paths - Quest C requires Quest A OR Quest B (CompositeCondition with OR)
- Locked content - Quest D requires Quest A AND Quest B (CompositeCondition with AND)
- Exclusive paths - Quest E only available if Quest A NOT failed (IsInverted)
A QuestLine is a narrative grouping of related quests that together tell a complete story. Unlike quest chains (execution dependencies), a QuestLine is a thematic container.
AAA Examples:
- Skyrim: "Companions Questline", "Thieves Guild Questline"
- Witcher 3: Story "threads" within narrative phases
- Cyberpunk 2077: Character arcs (Panam's arc, Judy's arc)
Features:
- Groups quests belonging to the same storyline
- Tracks overall progress across all contained quests (0-100%)
- Completion rewards when all quests in the line are done
- Optional prerequisite questlines (unlock Questline B after completing Questline A)
- Configurable failure behavior (fail line if any quest fails)
- Works alongside quest chains (not replacing them)
- QuestRewardType_SO - Abstract base for custom reward types
- RewardInstance - Pairs reward type with amount
- Auto-distribution on quest completion
All major state changes fire events for UI and game integration:
- QuestManager:
QuestAdded,QuestStarted,QuestCompleted,QuestFailed,QuestUpdated - QuestManager (QuestLines):
QuestLineAdded,QuestLineStarted,QuestLineCompleted,QuestLineFailed,QuestLineUpdated - Quest:
OnQuestUpdated,OnAnyTaskUpdated,OnAnyTaskCompleted,OnAnyTaskFailed - QuestLine:
OnQuestLineStarted,OnQuestLineCompleted,OnQuestLineUpdated,OnQuestInLineCompleted - Task:
OnTaskUpdated,OnTaskCompleted,OnTaskFailed
CRITICAL: Events should be GENERIC, conditions should be SPECIFIC.
The quest system uses a pattern where:
- Events are generic and reusable (e.g.,
OnMonsterKilled,OnNPCDialogue,OnLocationAttacked) - Conditions hold the specific expected values (e.g., which monster ID, which NPC ID, which location ID)
Why this pattern?
- Reduces event proliferation - One
OnMonsterKilledevent instead ofOnGoblinKilled,OnOrcKilled,OnSkeletonKilled - Enables designer flexibility - Create new monster/NPC/location types without code changes
- Simplifies game code integration - Raise the same event with different IDs
Correct approach:
Event: OnMonsterKilled (GameEventID_SO) - generic, reusable
Condition: SO_Condition_Event_ID_GoblinKilled - references OnMonsterKilled + GoblinId
Condition: SO_Condition_Event_ID_OrcKilled - references OnMonsterKilled + OrcId
Wrong approach:
Event: OnGoblinKilled (GameEventBool_SO) - specific, not reusable
Event: OnOrcKilled (GameEventBool_SO) - specific, not reusable
Standard generic events:
| Event | Type | Purpose |
|---|---|---|
OnMonsterKilled |
GameEventID_SO |
Any monster killed |
OnNPCDialogue |
GameEventID_SO |
Dialogue with specific NPC |
OnNPCKilled |
GameEventID_SO |
Any NPC killed |
OnEnemyAlert |
GameEventID_SO |
Enemy spots player (stealth) |
OnLocationAttacked |
GameEventID_SO |
Location under attack |
OnItemCollected |
GameEventID_SO |
Item collected/recovered |
OnItemDestroyed |
GameEventID_SO |
Item destroyed |
OnFindLocation |
GameEventID_SO |
Player reaches location |
OnItemDiscovered |
GameEventID_SO |
Item/clue discovered |
Game Code Integration:
// Monster kill handler - uses generic event
public class MonsterKillHandler : MonoBehaviour
{
[SerializeField] private ID_SO monsterId; // Goblin, Orc, etc.
[SerializeField] private GameEventID_SO onMonsterKilled;
public void OnDeath()
{
onMonsterKilled.Raise(monsterId); // Same event, different ID
}
}See BasicQuestExample/Docs/EventIntegrationGuide.md for complete integration documentation.
- Open Unity Package Manager
- Click "+" > "Add package from disk"
- Navigate to this folder and select
package.json
- Create > HelloDev > Quest System > Scriptable Objects > Quest
- Set DevName and localized DisplayName/Description
- Add Task_SO references to the tasks list
- (Optional) Configure start/failure conditions
- (Optional) Add rewards
Create > HelloDev > Quest System > Scriptable Objects > Tasks > Int Task
Create > HelloDev > Quest System > Scriptable Objects > Tasks > Bool Task
Create > HelloDev > Quest System > Scriptable Objects > Tasks > String Task
using HelloDev.QuestSystem;
using HelloDev.QuestSystem.Quests;
using HelloDev.QuestSystem.Tasks;
// Subscribe to quest events
QuestManager.Instance.QuestCompleted.AddListener(OnQuestCompleted);
// Get active quests
var activeQuests = QuestManager.Instance.GetActiveQuests();
// Get a specific quest and work with it
var quest = QuestManager.Instance.GetActiveQuest(questSO);
quest?.IncrementCurrentTask(); // Progress current task
quest?.CurrentTask?.IncrementStep(); // Same effect, more explicit
// Access quest properties
var tasks = quest.Tasks;
var currentTask = quest.CurrentTask;
var progress = quest.CurrentProgress;// 1. Create the runtime task
using HelloDev.QuestSystem.Tasks;
using HelloDev.QuestSystem.ScriptableObjects;
public class TimedTaskRuntime : TaskRuntime
{
private readonly TaskTimed_SO _timedData;
public float TimeRemaining { get; private set; }
public TimedTaskRuntime(TaskTimed_SO data) : base(data)
{
_timedData = data;
TimeRemaining = data.Duration;
}
public override float Progress => 1f - (TimeRemaining / _timedData.Duration);
protected override void CheckCompletion(TaskRuntime task)
{
if (TimeRemaining <= 0) CompleteTask();
}
public override void ForceCompleteState() => TimeRemaining = 0;
public override bool OnIncrementStep() { CompleteTask(); return true; }
public override bool OnDecrementStep() { return false; }
}
// 2. Create the ScriptableObject data
[CreateAssetMenu(menuName = "HelloDev/Quest System/Tasks/Timed Task")]
public class TaskTimed_SO : Task_SO
{
[SerializeField] private float duration = 60f;
public float Duration => duration;
public override TaskRuntime GetRuntimeTask() => new TimedTaskRuntime(this);
public override void SetupTaskLocalizedVariables(LocalizedString localizedString, TaskRuntime task)
{
// Add localization variables (e.g., {current}, {required})
// Called BEFORE assigning to LocalizeStringEvent.StringReference
}
}using HelloDev.QuestSystem.ScriptableObjects;
[CreateAssetMenu(menuName = "HelloDev/Quest System/Rewards/Gold Reward")]
public class GoldRewardType_SO : QuestRewardType_SO
{
public override void GiveReward(int amount)
{
// Add gold to player inventory
PlayerInventory.Instance.AddGold(amount);
Debug.Log($"Received {amount} gold!");
}
}Quest chains allow you to create prerequisites between quests. Use ConditionQuestState_SO to check if a quest is in a specific state.
Sequential Chain (Quest B requires Quest A completed):
- Create > HelloDev > Quest System > Conditions > Quest State Condition
- Set "Quest To Check" to Quest A
- Set "Target State" to
Completed - Set "Comparison Type" to
Equals - Add this condition to Quest B's Start Conditions
Branching Paths (Quest C requires Quest A OR Quest B):
- Create two
ConditionQuestState_SOassets (one for each prerequisite quest) - Create a
CompositeCondition_SOwithLogicType = OR - Add both quest state conditions to the composite
- Add the composite to Quest C's Start Conditions
Locked Content (Quest D requires Quest A AND Quest B):
- Create two
ConditionQuestState_SOassets - Create a
CompositeCondition_SOwithLogicType = AND - Add both conditions to the composite
- Add the composite to Quest D's Start Conditions
Exclusive Paths (Quest E only if Quest A NOT failed):
- Create a
ConditionQuestState_SOfor Quest A - Set "Target State" to
Failed - Set "Comparison Type" to
Equals - Enable "Is Inverted" on the condition
- Add to Quest E's Start Conditions
Available States to Check:
NotStarted- Quest has never been addedInProgress- Quest is currently activeCompleted- Quest was completed successfullyFailed- Quest was failed
// Programmatic check
var questStateCondition = ScriptableObject.CreateInstance<ConditionQuestState_SO>();
// Configure via inspector, or check programmatically:
if (QuestManager.Instance.IsQuestCompleted(prerequisiteQuest))
{
QuestManager.Instance.AddAndStartQuest(nextQuest);
}Branching quests allow players to make meaningful choices that affect quest progression and the game world.
Stage-Based Quest Structure:
- Create > HelloDev > Quest System > Scriptable Objects > Quest
- Enable stage-based structure in the quest inspector
- Add stages with unique indices (0, 1, 10, 20, etc.)
- Configure transitions between stages
Player Choice Transitions:
- In a stage's transitions, set
TriggertoPlayerChoice - Enable
IsPlayerChoiceflag - Set a unique
ChoiceId(e.g., "combat_path", "diplomacy_path") - Add optional conditions to gate certain choices
- Add
WorldFlagsOnSelectto set world flags when the choice is made
Conditional Choices (e.g., Reputation Gates):
- Create a
WorldFlagInt_SOfor the reputation (e.g., GuardReputation) - Create a
ConditionWorldFlagInt_SOthat checks reputation >= 20 - Add the condition to the choice's transition conditions
- The choice only appears if the condition is met
World Consequences: Each choice can modify world flags when selected:
- Bool flags: Set true/false (e.g., "ChoseCombatPath = true")
- Int flags: Set, Add, or Subtract values (e.g., "Reputation += 10")
Implicit Choices (Action-Based): Choices can be made through player actions instead of explicit menus:
- Create a condition that triggers on player action (e.g., buying an item)
- Add the condition to a choice transition
- When the player performs the action, the choice auto-selects
Example: The Merchant's Dilemma (see BasicQuestExample)
Stage 0: Talk to Merchant
→ auto-transition to Stage 1
Stage 1: The Choice (presents 3 options)
→ [Combat] Confront Bandits → Stage 10 (sets ChoseCombat flag)
→ [Diplomacy] Negotiate → Stage 20 (sets ChoseDiplomacy flag)
→ [Lawful] Report to Guards → Stage 30 (requires Guard Rep >= 20, sets ChoseLawful flag)
Stage 10: Combat Path (defeat bandits) → Stage 100
Stage 20: Diplomacy Path (negotiate) → Stage 100
Stage 30: Lawful Path (report to guards) → Stage 100
Stage 100: Return to Merchant (resolution, terminal)
Using Branching in Code:
// Get available choices at current stage
var choices = quest.GetAvailableChoices();
foreach (var choice in choices)
{
Debug.Log($"Choice: {choice.TransitionLabel} (ID: {choice.ChoiceId})");
if (!choice.AreConditionsMet())
Debug.Log(" [Locked - conditions not met]");
}
// Select a choice
quest.SelectChoiceById("combat_path");
// Subscribe to choice events
quest.OnChoicesAvailable += (q, choices) => ShowChoiceUI(choices);
quest.OnChoiceMade += (q, choice) => Debug.Log($"Player chose: {choice.ChoiceId}");The branching and world state systems are designed to support AAA-style quest patterns found in games like Skyrim, Witcher 3, Mass Effect, and Cyberpunk 2077.
Pattern 1: Cross-Quest Consequences (Witcher 3 Style) Choices in one quest affect other quests:
Quest A: Choose to save or sacrifice the village
→ Sets WorldFlag: VillageSaved = true/false
Quest B: (starts later)
→ Start condition: ConditionWorldFlagBool_SO checks VillageSaved
→ If saved: Village welcomes you, new merchants available
→ If sacrificed: Village is ruins, hostile NPCs
Pattern 2: Reputation Gates (Skyrim Style) Faction standing unlocks dialogue and quest options:
WorldFlagInt: GuardReputation (0-100)
Quest: The Merchant's Dilemma
→ "Report to Guards" choice requires GuardReputation >= 20
→ Choosing this path grants +10 reputation
Pattern 3: Branching Narrative Paths (Mass Effect Style) Major story decisions tracked across the entire game:
WorldFlagBool: ChoseParagonPath, ChoseRenegadePath
WorldFlagInt: ParagonScore, RenegadeScore
Quest choices add to scores and set flags
Future quests check flags for dialogue variations
Ending quests check cumulative scores
Pattern 4: Implicit Choices (Cyberpunk 2077 Style) Player actions make choices without explicit menus:
Stage 1: Approach the deal
→ [Buy drugs] triggered by purchasing illegal items
→ [Call police] triggered by phone call action
→ [Attack] triggered by combat initiation
Game systems raise events, quest conditions detect and auto-select choices
Pattern 5: Dynamic Availability (Living World) World flags control which content is available:
WorldFlagBool: DragonDefeated
WorldFlagInt: GuildRank
Quest A: Only available if DragonDefeated = true
Quest B: Only available if GuildRank >= 5
Quest C: Only available if both conditions met
Combining Patterns: All patterns work together - a single quest can:
- Check world flags to gate availability (Pattern 5)
- Present reputation-gated choices (Pattern 2)
- Set world flags that affect other quests (Pattern 1)
- Track choices for narrative endings (Pattern 3)
- Respond to player actions (Pattern 4)
The quest system supports Unity's bootstrap pattern for controlled initialization order via IBootstrapInitializable from com.hellodev.utils.
Components with Bootstrap Support:
| Component | Priority | Description |
|---|---|---|
QuestManager |
150 | Game Systems layer |
QuestSaveManager |
200 | Persistence layer |
SaveSystemSetup |
250 | Data Loading layer |
Standalone Mode (Default):
Each component self-initializes in Unity's lifecycle when selfInitialize = true.
Bootstrap Mode:
Set selfInitialize = false on each component, then use GameBootstrap to control initialization order.
// Components implement IBootstrapInitializable
public class QuestManager : MonoBehaviour, IBootstrapInitializable
{
public bool SelfInitialize => initializeOnAwake;
public int InitializationPriority => 150;
public bool IsInitialized => _isInitialized;
public Task InitializeAsync() { /* ... */ }
public void Shutdown() { /* ... */ }
}QuestAutoAddMode:
Controls how quests are auto-added from the database on initialization:
| Mode | Description |
|---|---|
Disabled |
No auto-add. Quests added via gameplay (NPCs, events, etc.). Default for production. |
AllQuests |
Add all quests regardless of conditions. Useful for debugging. |
WithConditionsMet |
Only add quests whose start conditions are already met. |
// In QuestManager inspector, set "Auto Add Mode" dropdown
// Or check programmatically:
if (autoAddMode == QuestAutoAddMode.Disabled)
{
// Quests will only be added via AddQuest() calls
}The quest system includes a flexible save/load system that allows you to persist quest progress. The system uses SaveService from com.hellodev.utils for storage, so you can integrate with any save system (JSON files, cloud saves, Easy Save 3, etc.).
Quick Start:
using HelloDev.QuestSystem.SaveLoad;
using HelloDev.Saving;
// Setup at application startup (typically in a bootstrap script)
SaveService.SetProvider(new JsonSaveProvider("saves", ".sav", true));
// Save quest progress via locator
await questSaveLocator.SaveAsync("save_slot_1");
// Load quest progress
await questSaveLocator.LoadAsync("save_slot_1");
// Check if save exists
bool exists = await questSaveLocator.SaveExistsAsync("save_slot_1");
// Delete a save
await questSaveLocator.DeleteSaveAsync("save_slot_1");
// List all saves
string[] slots = await questSaveLocator.GetAllSaveSlotsAsync();Per-Slot Autosave:
The save system supports slot-based autosave, where each save slot gets its own autosave file. When playing on slot 1, autosaves go to "autosave-1"; when playing on slot 2, autosaves go to "autosave-2".
- Create > HelloDev > Quest System > Save Slot Config
- Reference the config in your save management code
// Programmatic slot management
slotConfig.SetActiveSlot(0); // Autosaves now go to "autosave-0"
slotConfig.SetActiveSlot(2); // Autosaves now go to "autosave-2"
// Get slot keys
string autoKey = slotConfig.CurrentAutosaveSlotKey; // "autosave-2"
string saveKey = slotConfig.CurrentManualSlotKey; // "save-2"
// When loading a different save
await questSaveLocator.LoadAsync("save-1");
slotConfig.SetActiveSlot(1); // Future autosaves go to "autosave-1"SaveSlotConfig_SO Properties:
| Property | Description |
|---|---|
MaxSlots |
Maximum number of save slots (configurable) |
CurrentSlotIndex |
Currently active slot index (-1 if none) |
HasActiveSlot |
True if a slot is active |
CurrentAutosaveSlotKey |
Returns "autosave-X" for current slot |
CurrentManualSlotKey |
Returns "save-X" for current slot |
Custom Save Provider:
Implement ISaveProvider from HelloDev.Saving to integrate with your preferred save system:
using HelloDev.Saving;
using System.Threading.Tasks;
// Example: Easy Save 3 integration
public class ES3SaveProvider : ISaveProvider
{
public Task<bool> SaveAsync<T>(string key, T data)
{
ES3.Save(key, data);
return Task.FromResult(true);
}
public Task<T> LoadAsync<T>(string key)
{
if (!ES3.KeyExists(key)) return Task.FromResult(default(T));
return Task.FromResult(ES3.Load<T>(key));
}
public Task<bool> ExistsAsync(string key) => Task.FromResult(ES3.KeyExists(key));
public Task<bool> DeleteAsync(string key) { ES3.DeleteKey(key); return Task.FromResult(true); }
public Task<string[]> GetKeysAsync(string prefix = null) => Task.FromResult(Array.Empty<string>());
}
// Set provider at application startup
SaveService.SetProvider(new ES3SaveProvider());What Gets Saved:
- All quest states (active, completed, failed)
- Current stage and task progress
- Branch decisions (which choices were made)
- World flag values (boolean and integer)
- QuestLine progress
World Flags: For world flags to be saved, register them with the save manager:
// Option 1: Add to QuestSaveManager's worldFlagRegistry in the inspector
// Option 2: Register programmatically
QuestSaveManager.Instance.RegisterWorldFlag(myWorldFlag);Save Events:
QuestSaveManager.Instance.OnBeforeSave.AddListener(slotKey => Debug.Log($"Saving to {slotKey}..."));
QuestSaveManager.Instance.OnAfterSave.AddListener((slotKey, success) => Debug.Log($"Save {(success ? "succeeded" : "failed")}"));
QuestSaveManager.Instance.OnBeforeLoad.AddListener(slotKey => Debug.Log($"Loading from {slotKey}..."));
QuestSaveManager.Instance.OnAfterLoad.AddListener((slotKey, success) => Debug.Log($"Load {(success ? "succeeded" : "failed")}"));Manual Snapshots: For custom implementations, you can capture/restore snapshots directly:
// Capture current state
QuestSystemSnapshot snapshot = QuestSaveManager.Instance.CaptureSnapshot();
// Serialize to JSON
string json = JsonUtility.ToJson(snapshot);
// Later: deserialize and restore
var loaded = JsonUtility.FromJson<QuestSystemSnapshot>(json);
QuestSaveManager.Instance.RestoreSnapshot(loaded);QuestLines group related quests into narrative arcs. They work alongside quest chains (execution dependencies).
Creating a QuestLine:
- Create > HelloDev > Quest System > Scriptable Objects > Quest Line
- Set DevName and localized DisplayName/Description
- Add Quest_SO references to the quests list (order matters for UI)
- (Optional) Set a Prerequisite Line (another questline that must complete first)
- (Optional) Add completion rewards
Adding QuestLine Prerequisites:
Use ConditionQuestLineState_SO to unlock content after completing a questline:
- Create > HelloDev > Quest System > Conditions > Quest Line State Condition
- Set "QuestLine To Check" to the prerequisite questline
- Set "Target State" to
Completed - Add this condition to a quest's Start Conditions
Using QuestLines in Code:
using HelloDev.QuestSystem;
using HelloDev.QuestSystem.QuestLines;
using HelloDev.QuestSystem.ScriptableObjects;
// Add a questline to tracking
QuestManager.Instance.AddQuestLine(questLineSO);
// Subscribe to questline events
QuestManager.Instance.QuestLineCompleted.AddListener(OnQuestLineCompleted);
// Get active questlines
var activeLines = QuestManager.Instance.GetActiveQuestLines();
// Check progress
var line = QuestManager.Instance.GetQuestLine(questLineSO);
float progress = line.Progress; // 0.0 to 1.0
int completed = line.CompletedQuestCount;
int total = line.TotalQuestCount;
// Check state
bool isComplete = QuestManager.Instance.IsQuestLineCompleted(questLineSO);
bool isActive = QuestManager.Instance.IsQuestLineActive(questLineSO);QuestLine vs Quest Chain:
| Concept | Purpose | Mechanism |
|---|---|---|
| Quest Chain | Execution dependency | ConditionQuestState_SO in startConditions |
| QuestLine | Narrative grouping | QuestLine_SO containing multiple quests |
Both can be used together: a QuestLine can contain quests that have chain dependencies.
Questlines can be chained together, where completing one questline unlocks another. This is preferred over chaining the last quest of Questline A to the first quest of Questline B, as it provides cleaner narrative structure.
Method 1: Direct Prerequisite (Simple)
Set the prerequisiteLine field on the dependent questline:
- Open the dependent QuestLine_SO (e.g., "Act 2")
- Set "Prerequisite Line" to the required questline (e.g., "Act 1")
- The questline remains Locked until the prerequisite is Completed
Method 2: Condition-Based (Flexible)
Use ConditionQuestLineState_SO for complex unlock requirements:
- Create > HelloDev > Quest System > Conditions > Quest Line State Condition
- Set "QuestLine To Check" to the prerequisite questline
- Set "Target State" to
Completed - Add this condition to the first quest's Start Conditions in the dependent questline
Chaining Patterns:
| Pattern | Description | Implementation |
|---|---|---|
| Sequential | Questline B after Questline A | Set prerequisiteLine on B |
| Branching | Questline C requires A OR B | CompositeCondition (OR) with two ConditionQuestLineState_SO |
| Convergent | Questline D requires A AND B | CompositeCondition (AND) with two ConditionQuestLineState_SO |
| Exclusive | Questline E only if A NOT failed | ConditionQuestLineState_SO with IsInverted |
Example: Two-Act Story
QuestLine: Act1_TheGoblinThreat
├── Quest: Goblin's Bane
├── Quest: The Bandit's Employer
└── Quest: The Goblin Conspiracy
QuestLine: Act2_TheGreaterEvil (prerequisiteLine = Act1_TheGoblinThreat)
├── Quest: Shadows Unveiled
├── Quest: The Dark Council
└── Quest: Final Confrontation
Cross-Questline Quest Triggers: For finer control, the last quest of a questline can explicitly trigger the first quest of another:
// In quest completion handler
QuestManager.Instance.QuestCompleted.AddListener(quest => {
if (quest.QuestData == lastQuestOfAct1)
{
QuestManager.Instance.AddQuestLine(act2QuestLine);
}
});Best Practices:
- Use questline chaining for major narrative arcs (acts, chapters)
- Use quest chaining within a questline for sequential missions
- Use
prerequisiteLinefor simple sequential arcs - Use
ConditionQuestLineState_SOwhen you need event-driven unlocking or complex conditions
The QuestManager is the entry point for all quest operations. It manages quest lifecycle and provides events for game integration.
| Property | Description |
|---|---|
Instance |
Singleton instance |
QuestsDatabase |
Read-only access to the quest database |
QuestLinesDatabase |
Read-only access to the questline database |
ActiveQuestCount |
Number of currently active quests |
CompletedQuestCount |
Number of completed quests |
FailedQuestCount |
Number of failed quests |
ActiveQuestLineCount |
Number of currently active questlines |
CompletedQuestLineCount |
Number of completed questlines |
| Method | Description |
|---|---|
AddQuest(Quest_SO) |
Add a quest; starts automatically if conditions met |
AddAndStartQuest(Quest_SO) |
Add and immediately start a quest (bypasses conditions) |
FailQuest(Quest_SO) |
Fail a quest |
RemoveQuest(Quest_SO) |
Remove a quest from active quests |
RestartQuest(Quest_SO) |
Restart a quest |
| Method | Description |
|---|---|
GetActiveQuest(Quest_SO) |
Get active quest runtime instance |
GetActiveQuests() |
Get all active quests (read-only) |
GetCompletedQuests() |
Get all completed quests (read-only) |
GetFailedQuests() |
Get all failed quests (read-only) |
IsQuestActive(Quest_SO) |
Check if quest is active |
IsQuestCompleted(Quest_SO) |
Check if quest is completed |
IsQuestFailed(Quest_SO) |
Check if quest has failed |
| Method | Description |
|---|---|
AddQuestLine(QuestLine_SO) |
Add a questline to tracking |
GetQuestLine(QuestLine_SO) |
Get active or completed questline |
GetActiveQuestLines() |
Get all active questlines (read-only) |
GetCompletedQuestLines() |
Get all completed questlines (read-only) |
IsQuestLineActive(QuestLine_SO) |
Check if questline is active |
IsQuestLineCompleted(QuestLine_SO) |
Check if questline is completed |
| Event | Description |
|---|---|
QuestAdded |
Fired when a quest is added |
QuestStarted |
Fired when a quest starts |
QuestCompleted |
Fired when a quest completes |
QuestFailed |
Fired when a quest fails |
QuestUpdated |
Fired when quest progress changes |
QuestRemoved |
Fired when a quest is removed |
QuestRestarted |
Fired when a quest is restarted |
QuestLineAdded |
Fired when a questline is added |
QuestLineStarted |
Fired when a questline starts |
QuestLineCompleted |
Fired when a questline completes |
QuestLineFailed |
Fired when a questline fails |
QuestLineUpdated |
Fired when questline progress changes |
The runtime representation of a quest. Access via QuestManager.GetActiveQuest(questSO).
| Member | Description |
|---|---|
QuestId |
Unique GUID |
QuestData |
Reference to Quest_SO |
CurrentState |
NotStarted, InProgress, Completed, Failed |
CurrentProgress |
0-1 completion percentage |
Tasks |
List of all runtime tasks |
CurrentTask |
First in-progress task (null if none) |
CurrentTasks |
All in-progress tasks (for parallel groups) |
TaskGroups |
List of task groups |
CurrentGroup |
Currently active task group |
| Method | Description |
|---|---|
StartQuest() |
Begin the quest |
CompleteQuest() |
Complete and distribute rewards |
FailQuest() |
Mark as failed |
ResetQuest() |
Reset and restart |
IncrementCurrentTask() |
Progress current task |
DecrementCurrentTask() |
Regress current task |
ForceComplete() |
Complete all remaining tasks |
| Event | Description |
|---|---|
OnQuestStarted |
Quest started |
OnQuestCompleted |
Quest completed |
OnQuestFailed |
Quest failed |
OnQuestRestarted |
Quest restarted |
OnQuestUpdated |
Progress changed |
OnAnyTaskStarted |
Any task started |
OnAnyTaskUpdated |
Any task updated |
OnAnyTaskCompleted |
Any task completed |
OnAnyTaskFailed |
Any task failed |
The runtime representation of a task.
| Member | Description |
|---|---|
TaskId |
Unique GUID |
Data |
Reference to Task_SO |
CurrentState |
NotStarted, InProgress, Completed, Failed |
Progress |
0-1 completion percentage |
| Method | Description |
|---|---|
StartTask() |
Begin the task |
IncrementStep() |
Progress the task |
DecrementStep() |
Regress the task |
CompleteTask() |
Force complete |
FailTask() |
Mark as failed |
ResetTask() |
Reset to initial state |
An event-driven condition for creating quest chains. Checks if a quest is in a specific state.
| Property | Description |
|---|---|
QuestToCheck |
The quest whose state will be checked |
TargetState |
The state to compare against (NotStarted, InProgress, Completed, Failed) |
ComparisonType |
How to compare: Equals or NotEquals |
IsInverted |
Inherited from Condition_SO - inverts the result |
| Method | Description |
|---|---|
Evaluate() |
Returns true if condition is met |
SubscribeToEvent(Action) |
Subscribe to quest state changes |
UnsubscribeFromEvent() |
Unsubscribe from events |
ForceFulfillCondition() |
Debug: Force-trigger the callback |
// The condition automatically subscribes to QuestManager events:
// - QuestStarted
// - QuestCompleted
// - QuestFailed
// - QuestRestarted
// - QuestAdded
// When the tracked quest changes state, the condition re-evaluates
// and fires the callback if the condition becomes true.The runtime representation of a questline. Access via QuestManager.GetQuestLine(questLineSO).
| Member | Description |
|---|---|
QuestLineId |
Unique GUID |
Data |
Reference to QuestLine_SO |
CurrentState |
Locked, Available, InProgress, Completed, Failed |
Progress |
0-1 completion percentage |
CompletedQuestCount |
Number of completed quests in the line |
TotalQuestCount |
Total number of quests in the line |
IsComplete |
True if all quests are completed |
IsAvailable |
True if questline can be started |
IsInProgress |
True if at least one quest has started |
IsFailed |
True if questline has failed |
NextQuest |
Next incomplete quest in the line |
FirstQuest |
First quest in the line |
| Event | Description |
|---|---|
OnQuestLineStarted |
QuestLine started |
OnQuestLineCompleted |
QuestLine completed |
OnQuestLineUpdated |
Progress changed |
OnQuestLineFailed |
QuestLine failed |
OnQuestInLineCompleted |
A quest in the line completed |
An event-driven condition for questline prerequisites. Checks if a questline is in a specific state.
| Property | Description |
|---|---|
QuestLineToCheck |
The questline whose state will be checked |
TargetState |
The state to compare against (Locked, Available, InProgress, Completed, Failed) |
ComparisonType |
How to compare: Equals or NotEquals |
IsInverted |
Inherited from Condition_SO - inverts the result |
| Method | Description |
|---|---|
Evaluate() |
Returns true if condition is met |
SubscribeToEvent(Action) |
Subscribe to questline state changes |
UnsubscribeFromEvent() |
Unsubscribe from events |
ForceFulfillCondition() |
Debug: Force-trigger the callback |
- com.hellodev.utils (1.5.0+) - Includes GameContext for bootstrap integration
- com.hellodev.saving (1.1.0+) - Unified save system
- com.hellodev.events (1.1.0+)
- com.hellodev.conditions (1.7.0+) - Includes world flag event modifiers
- com.hellodev.ids (1.1.0+)
- com.unity.localization
- Odin Inspector (for enhanced inspectors)
MIT License