Developing Quest Chains and Narrative Design
A quest system doesn't break in script writing—it breaks in state management. A quest with three objectives, two possible endings, and dependency on three other quests—that's minimum 12 state flags that need correct initialization, updating, and checking. If this logic is scattered across PlayerPrefs, hardcode, and random if checks in NPC scripts—the system will break on edge cases.
Quest System Architecture
A quest is a data object with identifier, list of objectives (QuestObjective[]), and current state (QuestState). States: Locked, Available, Active, ObjectivesComplete, Completed, Failed. Transitions only via QuestManager, never directly.
QuestManager is a singleton (or service locator) with Dictionary<string, QuestData> by quest ID. Methods: StartQuest(id), CompleteObjective(questId, objectiveId), FailQuest(id). Each call publishes OnQuestStateChanged(QuestData) event—UI, NPC controllers, analytics subscribe.
QuestData ScriptableObject holds quest static data: name, description, objective list with text and type (KillObjective, CollectObjective, ReachLocationObjective, TalkObjective), prerequisite quest ID list. Runtime quest state lives separately—in QuestRuntimeData, serialized to save file.
Separation of static and runtime is key. ScriptableObject for quest doesn't change in play mode; QuestRuntimeData lives only in memory and in saves. This prevents accidental data mutation in editor during testing.
Chains and Dependencies
A quest chain is a DAG (directed acyclic graph) of quests where each subsequent quest has prerequisites—list of quests that must be Completed before unlock. QuestManager checks prerequisites on StartQuest() attempt and on any state change automatically updates Locked → Available for unlocked quests.
Cyclic dependencies (quest A requires B, quest B requires A)—bug to catch in Editor script on asset save, not runtime. QuestDependencyValidator : AssetPostprocessor traverses graph via DFS and logs error on cycle found.
Narrative Design: Integrating Story with Gameplay
Narrative design is not "write good text." It's story integration into mechanics. Best narrative moments in games work because mechanic and narrative speak of one thing. In Papers, Please the document-checking mechanic is the narrative about conformism and moral choice. In Celeste platform difficulty is a metaphor for fighting anxiety.
Narrative pillars are three to five theses describing emotional story essence. Each quest, dialog, and mechanic is checked against these pillars. If a quest doesn't work for any pillar—why is it there?
Information revelation timing is a narrative tool heavily affecting quest design. Player learns something important in action moment, not before. "Kill the traitor"—trivial quest. "Find the mayor's killer" → player gathers evidence → ending realizes it was their mentor—this is narrative via gameplay.
Quest Objective Types and Implementation
KillObjective is simplest: subscribe to EnemyDeath(EnemyType) event, increment counter, check count >= required. Problem emerges on scene change: if counter lives in MonoBehaviour on quest object that unloads—data is lost. Counter must live in QuestRuntimeData, persistent.
CollectObjective is tied to InventorySystem: subscribe to OnItemAdded(ItemDefinition, quantity), check match to needed item. Nuance: if item is quest item and can't be dropped, need isQuestItem flag and check in InventoryContainer.TryRemove().
ReachLocationObjective is trigger zone (BoxCollider isTrigger) with QuestTrigger component, publishing LocationReached(questId, objectiveId) on entry. Important: OnTriggerEnter doesn't fire if object teleports into zone—need additional position check on scene load.
Nonlinearity and Branching Endings
Branching quest ending requires both branches fully written and implemented—half of studios cut corners, making one "real" ending and one placeholder. Players feel it.
Technically: ending is determined by set of flags collected through quest (choiceA_taken, evidence_found, npc_alive). In QuestCompleteHandler, logic: if (flagSet.Contains("evidence_found") && flagSet.Contains("npc_alive")) → outcome = "JusticeEnding". Flags are strings in HashSet<string> inside QuestRuntimeData.
Timeline Guidelines
| Scale | Components | Timeframe |
|---|---|---|
| Single quest | 3–5 objectives, linear | 3–5 days |
| Quest chain | 5–10 quests, dependencies, simple branching | 2–4 weeks |
| Main story | 20–40 quests, nonlinearity, multiple endings | 2–4 months |
| Full narrative system | + tooling, editor, localization | 4–6 months |
Process
Design starts with quest graph in Miro or Articy:Draft—visualization of all dependencies. Then QuestData ScriptableObject is created for each quest with filled prerequisites. QuestManager code is written and tested before first content quest. Sounds like overhead, but saves weeks of fixes later.





