Skip to content

joaovictoralencar/hellodev-questsystem

Repository files navigation

HelloDev Quest System

A data-driven quest system for Unity games. Designers assemble quests from reusable components; programmers extend the system with new task types and conditions.

Assembly Structure

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.

Getting Started

1. Install the Package

Via Package Manager (Local):

  1. Open Unity Package Manager (Window > Package Manager)
  2. Click "+" > "Add package from disk"
  3. Navigate to this folder and select package.json

Dependencies:

  • com.hellodev.utils
  • com.hellodev.saving
  • com.hellodev.events
  • com.hellodev.conditions
  • com.hellodev.ids
  • com.unity.localization

2. Set Up the QuestManager

  1. Create an empty GameObject named "QuestManager"
  2. Add the QuestManager component
  3. Add your Quest_SO assets directly to the QuestManager's Quests Database list in the inspector

3. Create Your First Quest

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 DisplayName and TaskDescription

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 Tasks list
  • Configure localized DisplayName and Description

Step 3: Add Quest to QuestManager

  • Select your QuestManager in the scene
  • Add your quest to the Quests Database list

4. Use in Code

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

5. Subscribe to Events

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}");
}

6. Try the Example

See BasicQuestExample/README.md for instructions on setting up the example with UI and debug tools.

Features

Quest Graph Editor (Unity 6.2+)

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 metadata
  • StageNode - Quest stages with task groups (multi-capacity input)
  • TaskNode - Individual tasks (int, bool, string, etc.)
  • TaskGroupNode - Groups of tasks with completion mode
  • ChoiceNode - Player choice branches
  • TransitionNode - Configurable stage transitions with triggers and conditions
  • ConditionGateNode - Conditional flow control
  • RewardNode - Quest rewards
  • WorldFlagSetNode - Set world flags on completion

Requirements:

  • Unity 6.2 or later
  • Graph Toolkit package (com.unity.graph-toolkit)

Core System

  • 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

Task Types

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

Branching & Player Choices

  • 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

World State Flags

  • 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.conditions for WorldFlagEventModifier*_SO classes)

Conditions

  • 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

Quest Chains

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

QuestLines (Story Arcs)

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)

Rewards

  • QuestRewardType_SO - Abstract base for custom reward types
  • RewardInstance - Pairs reward type with amount
  • Auto-distribution on quest completion

Events

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

Event-Driven Conditions (Generic Event Pattern)

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 OnMonsterKilled event instead of OnGoblinKilled, 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.

Installation

Via Package Manager (Local)

  1. Open Unity Package Manager
  2. Click "+" > "Add package from disk"
  3. Navigate to this folder and select package.json

Usage

Creating a Quest (Designer)

  1. Create > HelloDev > Quest System > Scriptable Objects > Quest
  2. Set DevName and localized DisplayName/Description
  3. Add Task_SO references to the tasks list
  4. (Optional) Configure start/failure conditions
  5. (Optional) Add rewards

Creating Tasks

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 in Code

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;

Creating Custom Task Types

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

Creating Custom Reward Types

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!");
    }
}

Creating Quest Chains

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

  1. Create > HelloDev > Quest System > Conditions > Quest State Condition
  2. Set "Quest To Check" to Quest A
  3. Set "Target State" to Completed
  4. Set "Comparison Type" to Equals
  5. Add this condition to Quest B's Start Conditions

Branching Paths (Quest C requires Quest A OR Quest B):

  1. Create two ConditionQuestState_SO assets (one for each prerequisite quest)
  2. Create a CompositeCondition_SO with LogicType = OR
  3. Add both quest state conditions to the composite
  4. Add the composite to Quest C's Start Conditions

Locked Content (Quest D requires Quest A AND Quest B):

  1. Create two ConditionQuestState_SO assets
  2. Create a CompositeCondition_SO with LogicType = AND
  3. Add both conditions to the composite
  4. Add the composite to Quest D's Start Conditions

Exclusive Paths (Quest E only if Quest A NOT failed):

  1. Create a ConditionQuestState_SO for Quest A
  2. Set "Target State" to Failed
  3. Set "Comparison Type" to Equals
  4. Enable "Is Inverted" on the condition
  5. Add to Quest E's Start Conditions

Available States to Check:

  • NotStarted - Quest has never been added
  • InProgress - Quest is currently active
  • Completed - Quest was completed successfully
  • Failed - 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);
}

Creating Branching Quests (Player Choices)

Branching quests allow players to make meaningful choices that affect quest progression and the game world.

Stage-Based Quest Structure:

  1. Create > HelloDev > Quest System > Scriptable Objects > Quest
  2. Enable stage-based structure in the quest inspector
  3. Add stages with unique indices (0, 1, 10, 20, etc.)
  4. Configure transitions between stages

Player Choice Transitions:

  1. In a stage's transitions, set Trigger to PlayerChoice
  2. Enable IsPlayerChoice flag
  3. Set a unique ChoiceId (e.g., "combat_path", "diplomacy_path")
  4. Add optional conditions to gate certain choices
  5. Add WorldFlagsOnSelect to set world flags when the choice is made

Conditional Choices (e.g., Reputation Gates):

  1. Create a WorldFlagInt_SO for the reputation (e.g., GuardReputation)
  2. Create a ConditionWorldFlagInt_SO that checks reputation >= 20
  3. Add the condition to the choice's transition conditions
  4. 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:

  1. Create a condition that triggers on player action (e.g., buying an item)
  2. Add the condition to a choice transition
  3. 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}");

AAA Integration Patterns

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)

Bootstrap Support

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
}

Save/Load System

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".

  1. Create > HelloDev > Quest System > Save Slot Config
  2. 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);

Creating QuestLines

QuestLines group related quests into narrative arcs. They work alongside quest chains (execution dependencies).

Creating a QuestLine:

  1. Create > HelloDev > Quest System > Scriptable Objects > Quest Line
  2. Set DevName and localized DisplayName/Description
  3. Add Quest_SO references to the quests list (order matters for UI)
  4. (Optional) Set a Prerequisite Line (another questline that must complete first)
  5. (Optional) Add completion rewards

Adding QuestLine Prerequisites: Use ConditionQuestLineState_SO to unlock content after completing a questline:

  1. Create > HelloDev > Quest System > Conditions > Quest Line State Condition
  2. Set "QuestLine To Check" to the prerequisite questline
  3. Set "Target State" to Completed
  4. 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.

Chained Questlines

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:

  1. Open the dependent QuestLine_SO (e.g., "Act 2")
  2. Set "Prerequisite Line" to the required questline (e.g., "Act 1")
  3. The questline remains Locked until the prerequisite is Completed

Method 2: Condition-Based (Flexible) Use ConditionQuestLineState_SO for complex unlock requirements:

  1. Create > HelloDev > Quest System > Conditions > Quest Line State Condition
  2. Set "QuestLine To Check" to the prerequisite questline
  3. Set "Target State" to Completed
  4. 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 prerequisiteLine for simple sequential arcs
  • Use ConditionQuestLineState_SO when you need event-driven unlocking or complex conditions

API Reference

QuestManager

The QuestManager is the entry point for all quest operations. It manages quest lifecycle and provides events for game integration.

Configuration Properties

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

Quest Lifecycle Methods

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

Query Methods

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

QuestLine Lifecycle Methods

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

Events

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

QuestRuntime

The runtime representation of a quest. Access via QuestManager.GetActiveQuest(questSO).

Properties

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

Methods

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

Events

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

TaskRuntime

The runtime representation of a task.

Properties

Member Description
TaskId Unique GUID
Data Reference to Task_SO
CurrentState NotStarted, InProgress, Completed, Failed
Progress 0-1 completion percentage

Methods

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

ConditionQuestState_SO

An event-driven condition for creating quest chains. Checks if a quest is in a specific state.

Properties

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

Methods

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

Usage Example

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

QuestLineRuntime

The runtime representation of a questline. Access via QuestManager.GetQuestLine(questLineSO).

Properties

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

Events

Event Description
OnQuestLineStarted QuestLine started
OnQuestLineCompleted QuestLine completed
OnQuestLineUpdated Progress changed
OnQuestLineFailed QuestLine failed
OnQuestInLineCompleted A quest in the line completed

ConditionQuestLineState_SO

An event-driven condition for questline prerequisites. Checks if a questline is in a specific state.

Properties

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

Methods

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

Dependencies

Required

  • 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

Optional

  • Odin Inspector (for enhanced inspectors)

License

MIT License

About

Data-driven quest system for Unity games

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages