Quest System
Lightweight quest tracker with ScriptableObject definitions, objectives, progress tracking, and reward callbacks.
How to Use
Create quest ScriptableObjects: Assets > Create > Quests > Quest Data
Define objectives (Collect 5 coins, Kill 3 enemies, Talk to NPC)
Attach QuestSystem to a persistent manager
Accept quest: QuestSystem.Instance.AcceptQuest(questData)
Report progress: QuestSystem.Instance.ReportProgress("kill_goblin")
Hook OnQuestCompleted to give rewards
Check quest status: QuestSystem.Instance.HasQuest(quest)
Features
- ScriptableObject quest definitions with name, description, and objectives
- Multiple objective types: Collect, Kill, Talk, Reach, and Custom
- Runtime progress tracking with per-objective counters
- UnityEvents for quest accepted, quest completed, and objective progress
- DontDestroyOnLoad singleton for persistence across scenes
- Abandon quest and completion-check utility methods
When to Use This
Built for RPGs, adventure games, and open-world titles that need quest tracking. Supports fetch quests, kill quests, and exploration objectives. Pair with Dialogue Trigger for NPC quest givers and JSON Save Utility to persist quest progress between sessions.
Common Mistakes
Each objective's objectiveId must be unique across all quests — ReportProgress matches by ID string, so duplicates will advance unintended objectives. The QuestSystem uses DontDestroyOnLoad, so don't place it on a scene-specific object. Quest ScriptableObjects are shared assets; the runtime QuestInstance tracks per-playthrough progress separately.
Source Code
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);
}
}