Part of these game systems:
intermediate Utilities

Quest System

Lightweight quest tracker with ScriptableObject definitions, objectives, progress tracking, and reward callbacks.

Unity 2022.3+ · 3.4 KB · QuestSystem.cs

How to Use

1

Create quest ScriptableObjects: Assets > Create > Quests > Quest Data

2

Define objectives (Collect 5 coins, Kill 3 enemies, Talk to NPC)

3

Attach QuestSystem to a persistent manager

4

Accept quest: QuestSystem.Instance.AcceptQuest(questData)

5

Report progress: QuestSystem.Instance.ReportProgress("kill_goblin")

6

Hook OnQuestCompleted to give rewards

7

Check quest status: QuestSystem.Instance.HasQuest(quest)

Source Code

QuestSystem.cs
C#
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;

/// <summary>
/// ScriptableObject quest definition.
/// </summary>
[CreateAssetMenu(menuName = "Quests/Quest Data")]
public class QuestData : ScriptableObject
{
    public string questName = "New Quest";
    [TextArea(2, 4)] public string description;
    public QuestObjective[] objectives;
    public int experienceReward;
}

/// <summary>
/// A single objective within a quest.
/// </summary>
[System.Serializable]
public class QuestObjective
{
    public string objectiveId;
    public string description;
    public ObjectiveType type;
    public int requiredAmount = 1;

    public enum ObjectiveType { Collect, Kill, Talk, Reach, Custom }
}

/// <summary>
/// Runtime quest instance tracking progress.
/// </summary>
[System.Serializable]
public class QuestInstance
{
    public QuestData data;
    public int[] objectiveProgress;
    public bool isComplete;

    public QuestInstance(QuestData questData)
    {
        data = questData;
        objectiveProgress = new int[questData.objectives.Length];
        isComplete = false;
    }

    public float GetProgress()
    {
        if (data.objectives.Length == 0) return 1f;
        int completed = 0;
        for (int i = 0; i < data.objectives.Length; i++)
        {
            if (objectiveProgress[i] >= data.objectives[i].requiredAmount)
                completed++;
        }
        return (float)completed / data.objectives.Length;
    }
}

/// <summary>
/// Quest manager. Tracks active quests and objective progress.
/// </summary>
public class QuestSystem : MonoBehaviour
{
    public static QuestSystem Instance { get; private set; }

    [Header("Events")]
    public UnityEvent<QuestData> OnQuestAccepted;
    public UnityEvent<QuestData> OnQuestCompleted;
    public UnityEvent<string, int> OnObjectiveProgress; // objectiveId, newProgress

    private List<QuestInstance> activeQuests = new List<QuestInstance>();
    private List<QuestData> completedQuests = new List<QuestData>();

    /// <summary>Currently active quests.</summary>
    public IReadOnlyList<QuestInstance> ActiveQuests => activeQuests;

    /// <summary>Completed quest data.</summary>
    public IReadOnlyList<QuestData> CompletedQuests => completedQuests;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    /// <summary>Accept a new quest.</summary>
    public bool AcceptQuest(QuestData quest)
    {
        if (HasQuest(quest) || IsQuestCompleted(quest))
            return false;

        QuestInstance instance = new QuestInstance(quest);
        activeQuests.Add(instance);
        OnQuestAccepted?.Invoke(quest);
        return true;
    }

    /// <summary>
    /// Report progress on an objective. Call this when player collects, kills, etc.
    /// </summary>
    public void ReportProgress(string objectiveId, int amount = 1)
    {
        // Iterate backwards to safely remove completed quests
        for (int q = activeQuests.Count - 1; q >= 0; q--)
        {
            var quest = activeQuests[q];
            if (quest.isComplete) continue;

            for (int i = 0; i < quest.data.objectives.Length; i++)
            {
                if (quest.data.objectives[i].objectiveId == objectiveId)
                {
                    quest.objectiveProgress[i] = Mathf.Min(
                        quest.objectiveProgress[i] + amount,
                        quest.data.objectives[i].requiredAmount
                    );

                    OnObjectiveProgress?.Invoke(objectiveId, quest.objectiveProgress[i]);
                    CheckQuestCompletion(quest);
                }
            }
        }
    }

    /// <summary>Check if a quest is currently active.</summary>
    public bool HasQuest(QuestData quest)
    {
        return activeQuests.Exists(q => q.data == quest);
    }

    /// <summary>Check if a quest has been completed.</summary>
    public bool IsQuestCompleted(QuestData quest)
    {
        return completedQuests.Contains(quest);
    }

    /// <summary>Get the runtime instance of an active quest.</summary>
    public QuestInstance GetQuestInstance(QuestData quest)
    {
        return activeQuests.Find(q => q.data == quest);
    }

    /// <summary>Abandon an active quest.</summary>
    public void AbandonQuest(QuestData quest)
    {
        activeQuests.RemoveAll(q => q.data == quest);
    }

    private void CheckQuestCompletion(QuestInstance quest)
    {
        for (int i = 0; i < quest.data.objectives.Length; i++)
        {
            if (quest.objectiveProgress[i] < quest.data.objectives[i].requiredAmount)
                return;
        }

        quest.isComplete = true;
        activeQuests.Remove(quest);
        completedQuests.Add(quest.data);
        OnQuestCompleted?.Invoke(quest.data);
    }
}