Visual Novel
Branching narrative where choices shape the story — your decisions write the ending.
"The most powerful game mechanic is the one that makes you put down the controller and think."
Prerequisites
| Module | What You Used From It |
|---|---|
| Module 01 - Pong | Basic game loop, input handling, and rendering. Visual novels are architecturally simple but require clean state management. |
Week 1: History & Design Theory
The Origin
Phoenix Wright: Ace Attorney (2001), directed by Shu Takumi at Capcom, demonstrated that a visual novel could be built around active deduction rather than passive reading. While earlier visual novels like Snatcher (1988) and Tokimeki Memorial (1994) had established branching narrative and character relationship systems, Phoenix Wright added a courtroom mechanic where the player had to examine evidence, identify contradictions in testimony, and present the right piece of evidence at the right moment. This transformed the visual novel from a genre where the player simply selected dialog options into one where the player reasoned through problems, making choices feel consequential not because of branching paths but because they tested comprehension.
How the Genre Evolved
Doki Doki Literature Club (2017): Dan Salvato's free game used the visual novel format's conventions against the player, subverting expectations in ways that required the medium's tropes to function. It manipulated save files, broke the fourth wall by addressing the player directly, and corrupted its own interface. It demonstrated that the visual novel's data-driven structure — where story lives in script files and the engine merely renders them — could itself become a narrative device when the "script" appears to malfunction.
13 Sentinels: Aegis Rim (2019): Vanillaware's George Kamitani created a visual novel with 13 playable protagonists whose stories interleaved across multiple timelines. The player chose which character's route to pursue and in what order, with each route revealing information that recontextualized the others. It proved that the order in which the player experienced story beats changed their meaning, making the route system itself a form of storytelling.
What Makes Visual Novels "Great"
The core design insight of the visual novel is that anticipation of consequences is more engaging than the consequences themselves. When a player faces a choice — "Do you tell her the truth?" — the power of that moment lives not in the branching code but in the seconds the player spends deliberating. The best visual novels design choices where the player genuinely does not know what will happen, where they care enough about the characters to agonize, and where the consequences may not reveal themselves for hours. This delayed consequence loop is uniquely powerful: it transforms reading into an active, anxious experience.
The Essential Mechanic
Making choices that branch the story, where consequences may not be immediately apparent — the player reads, deliberates, decides, and lives with outcomes that reveal themselves over time.
Week 2: Build the MVP
What You're Building
A short branching story (10-20 story beats) with at least 3 meaningful choices leading to at least 2 distinct endings. The story is defined in a data file (not hardcoded), rendered with character sprites that change expression, a typewriter text effect, and background scene transitions. The player can save and load their progress.
This module is 2D. No engine is required.
Core Concepts (Must Implement)
1. Branching Narrative as a Directed Graph
The story is a directed graph where each node is a story beat and each edge represents a transition. Choice nodes have multiple outgoing edges, one per option. The graph may converge (branches rejoin), diverge (branches split permanently), or both.
story = {
"start": {
type: "text",
speaker: "narrator",
text: "The letter arrived on a Tuesday. You recognized the handwriting.",
next: "choice_open_letter"
},
"choice_open_letter": {
type: "choice",
text: "The envelope sits on your desk.",
options: [
{ text: "Open it immediately.", next: "open_letter",
set_flag: "opened_eagerly" },
{ text: "Leave it for later.", next: "ignore_letter",
set_flag: "ignored_letter" },
]
},
"open_letter": {
type: "text",
speaker: "protagonist",
text: "My hands trembled as I tore it open...",
next: "letter_contents"
},
"ignore_letter": {
type: "text",
speaker: "narrator",
text: "You set it aside. By evening, curiosity won.",
next: "letter_contents" // Branches reconverge
},
...
"ending_good": {
type: "ending",
text: "You made it through. Together.",
ending_name: "The Bridge Between"
}
} Why it matters: The directed graph is the fundamental architecture of the visual novel. Defining the story as data rather than code means writers can author content independently of programmers, and the engine can be reused for any story.
Click on choices to navigate the story. The node graph on the right shows story beats. Your current node is yellow, visited nodes are green, and unreachable dead-end branches gray out. Reach an ending to see which path you took!
2. Text Rendering / Typewriter Effect
Text appears character by character rather than all at once, creating a reading rhythm that mimics natural pacing. The player can click to instantly complete the current text block, then click again to advance.
class TypewriterText:
def __init__(self):
self.full_text = ""
self.visible_chars = 0
self.chars_per_second = 30
self.timer = 0.0
self.complete = false
self.pauses = {} # char_index -> pause_duration
def set_text(self, text):
self.full_text = text
self.visible_chars = 0
self.timer = 0.0
self.complete = false
self.pauses = {}
for i, char in enumerate(text):
if char == '.': self.pauses[i] = 0.3
elif char == ',': self.pauses[i] = 0.15
elif char == '!': self.pauses[i] = 0.25
def update(self, dt):
if self.complete: return
self.timer += dt
while self.timer >= 1.0 / self.chars_per_second:
self.timer -= 1.0 / self.chars_per_second
self.visible_chars += 1
if self.visible_chars in self.pauses:
self.timer -= self.pauses[self.visible_chars]
if self.visible_chars >= len(self.full_text):
self.complete = true
break
def skip_to_end(self):
self.visible_chars = len(self.full_text)
self.complete = true Why it matters: The typewriter effect controls pacing, which is everything in a text-driven game. A dramatic revelation delivered all at once loses impact. Character by character, with pauses at punctuation, the text breathes.
Text appears character by character. Use the speed slider to adjust. The character sprite on the left changes expression based on the mood of the text. Click anywhere to skip the current text or advance to the next line.
3. Character Sprite and Expression System
Characters are displayed as layered sprites on screen. Each character has a set of expression variants (happy, sad, angry, surprised). The story data specifies which characters are visible and which expression to display at any beat.
class CharacterSprite:
def __init__(self, name, base_position):
self.name = name
self.position = base_position # "left", "center", "right"
self.expressions = {} # expression_name -> image
self.current_expression = "neutral"
self.visible = false
self.alpha = 0.0
def set_expression(self, expression_name):
if expression_name in self.expressions:
self.current_expression = expression_name
def show(self, fade_duration=0.3):
self.visible = true
self.fade_target = 1.0
self.fade_speed = 1.0 / fade_duration
def hide(self, fade_duration=0.3):
self.fade_target = 0.0
self.fade_speed = 1.0 / fade_duration Why it matters: Character sprites are the visual anchor. Expression changes synchronized with dialog text create the illusion of a reacting, emotional character. A single sprite swap from "neutral" to "surprised" at the right moment communicates more than a paragraph.
4. Choice System with Consequence Tracking
Choices set flags that affect future story branches. Some consequences are immediate, while others are delayed (a flag set in chapter 1 determines an outcome in chapter 5).
class ChoiceTracker:
def __init__(self):
self.flags = {}
self.choice_history = []
self.affinity = {} # Character relationship scores
def make_choice(self, choice_data):
self.choice_history.append({
"node": choice_data["node_id"],
"chosen": choice_data["option_index"],
"text": choice_data["option_text"]
})
if "set_flag" in choice_data:
self.flags[choice_data["set_flag"]] = true
if "affinity" in choice_data:
for character, delta in choice_data["affinity"].items():
self.affinity[character] = self.affinity.get(character, 0) + delta
def evaluate_condition(self, condition):
if " AND " in condition:
parts = condition.split(" AND ")
return all(self.evaluate_condition(p.strip()) for p in parts)
if condition.startswith("affinity:"):
parts = condition.split()
char_name = parts[0].split(":")[1]
operator = parts[1]
value = int(parts[2])
actual = self.affinity.get(char_name, 0)
if operator == ">=": return actual >= value
return self.flags.get(condition, false) Why it matters: Delayed consequences are the soul of the visual novel. If every choice had immediate, obvious results, the player would simply optimize. When consequences are delayed, the player must make choices based on values — "What would I actually do?" rather than "Which option gives the best reward?"
5. Multiple Endings / Route System
The game has multiple distinct endings determined by accumulated choices and flags. An ending selection function evaluates the player's history to determine which ending to show.
ENDINGS = {
"ending_together": {
name: "The Bridge Between",
conditions: "told_elena_truth AND affinity:elena >= 3",
priority: 1,
text: "You cross the bridge side by side..."
},
"ending_alone_good": {
name: "A New Morning",
conditions: "!told_elena_truth AND showed_courage",
priority: 2,
text: "You stand at the bridge alone, but at peace..."
},
"ending_alone_bad": {
name: "Midnight, Alone",
conditions: null, # Default/fallback ending
priority: 99,
text: "The bridge is empty. The letter blows into the river..."
}
}
def determine_ending(choice_tracker):
sorted_endings = sorted(ENDINGS.values(), key=lambda e: e["priority"])
for ending in sorted_endings:
if ending["conditions"] is null:
return ending
if choice_tracker.evaluate_condition(ending["conditions"]):
return ending
return sorted_endings[-1] Why it matters: Multiple endings give choices weight retroactively. Even if the player does not replay, knowing that other endings exist validates the feeling that their choices mattered.
6. Scripting / Data-Driven Story Format
The entire story is defined in external data files (JSON, YAML, or a custom format), not hardcoded in the game logic. The engine reads these files and interprets them.
# story.json — the story as pure data
{
"nodes": {
"start": {
"type": "text",
"speaker": "narrator",
"text": "The letter arrived on a Tuesday.",
"background": "apartment_evening",
"characters": [],
"music": "quiet_piano",
"next": "choice_open_letter"
}
},
"characters": {
"elena": {
"display_name": "Elena",
"color": "#7EC8E3",
"expressions": {
"neutral": "elena_neutral.png",
"happy": "elena_happy.png",
"sad": "elena_sad.png"
}
}
}
} Why it matters: Separating story data from engine code is the architectural decision that makes visual novels practical. A hardcoded story cannot be written by non-programmers, cannot be easily tested in isolation, and cannot be swapped or modded.
7. Background / Scene Transitions
Background images change to establish location. Transitions between backgrounds use effects like crossfade, dissolve, or cut-to-black to create pacing and mood.
class BackgroundRenderer:
def __init__(self):
self.current_bg = null
self.next_bg = null
self.transition_type = "none"
self.transition_progress = 0.0
def change_background(self, new_bg_image, transition="crossfade"):
if self.current_bg is null:
self.current_bg = new_bg_image
return
self.next_bg = new_bg_image
self.transition_type = transition
self.transition_progress = 0.0
def update(self, dt):
if self.transition_type == "none": return
self.transition_progress += dt * self.transition_speed
if self.transition_progress >= 1.0:
self.current_bg = self.next_bg
self.next_bg = null
self.transition_type = "none" Why it matters: Backgrounds establish place and mood. The transition type communicates narrative pacing — a hard cut implies sudden change, a slow crossfade implies gentle passage of time.
Stretch Goals
- Save/load with multiple slots: Allow the player to save at any point and maintain multiple save files.
- Text log / backlog: A scrollable history of all text and choices the player has seen.
- Auto-advance mode: Automatically advance text after a configurable delay.
- Ending gallery: A screen that shows which endings the player has discovered.
MVP Spec
| Feature | Required |
|---|---|
| Story defined in data file (JSON, YAML, or DSL) | Yes |
| Story engine that traverses the narrative graph | Yes |
| Typewriter text rendering with skip | Yes |
| At least 2 character sprites with 3+ expressions each | Yes |
| Expression changes synchronized with story beats | Yes |
| At least 3 meaningful choices with flag-setting | Yes |
| At least 1 delayed consequence | Yes |
| At least 2 distinct endings | Yes |
| Background images with crossfade transitions | Yes |
| Speaker name and text styling per character | Yes |
| Save/load with multiple slots | Stretch |
| Text log / backlog | Stretch |
| Auto-advance mode | Stretch |
| Ending gallery | Stretch |
Deliverable
Submit your playable visual novel with source code, the story data file, and a narrative graph diagram showing all story nodes, choices, branches, and endings. Include a short write-up (300-500 words) answering: How did you design your choices so that the player feels their decisions matter? Describe one choice where the consequence is delayed and explain how you used flags to connect the choice to its eventual outcome.
Analogies by Background
These analogies map game dev concepts to patterns you already know.
For Backend Developers
| Game Dev Concept | Backend Analogy |
|---|---|
| Branching narrative graph | A workflow engine (Temporal, Step Functions) — nodes are workflow steps, edges are transitions, and the execution path depends on runtime conditions |
| Typewriter text rendering | Streaming HTTP responses — delivering data character by character rather than in a single payload, with the consumer able to request the full payload early (skip) |
| Character sprite/expression system | A template engine with partials — the base template (character) swaps in different partials (expressions) based on context variables |
| Choice system with consequence tracking | Event sourcing — every choice is an immutable event appended to a log, and current state is derived by replaying all events |
| Multiple endings / route system | A rules engine — ordered rules evaluated top-down, where the first rule whose conditions match produces the result |
| Data-driven story format | Configuration-as-code — runtime behavior is entirely determined by declarative configuration files |
| Background / scene transitions | Blue-green deployment — transitioning smoothly from one active state to another with a brief overlap period |
For Frontend Developers
| Game Dev Concept | Frontend Analogy |
|---|---|
| Branching narrative graph | A state machine for UI flow (XState) — each state renders different content, transitions are triggered by user actions |
| Typewriter text rendering | A CSS animation on width with overflow: hidden on a monospace element — or a JS interval that reveals characters one at a time |
| Character sprite/expression system | Conditional rendering of image components — a component receives an expression prop and renders the corresponding image |
| Choice system with consequence tracking | Redux action history — each choice dispatches an action that updates the store |
| Multiple endings / route system | A/B test resolution — evaluating conditions from a prioritized list of variants |
| Data-driven story format | CMS-driven content — the React app is the engine, the content comes from a headless CMS |
| Background / scene transitions | CSS transitions between background images — crossfade using overlapping absolutely-positioned divs |
For Data / ML Engineers
| Game Dev Concept | Data / ML Analogy |
|---|---|
| Branching narrative graph | A decision tree with stateful traversal — each internal node splits on a player choice, leaf nodes are endings |
| Typewriter text rendering | Streaming inference — a language model generating tokens one at a time, with the option to batch-return remaining tokens |
| Character sprite/expression system | Feature visualization — displaying different visual representations of a data entity depending on active features |
| Choice system with consequence tracking | Feature store updates — each choice writes a feature to the store, and downstream models read from it |
| Multiple endings / route system | Model selection based on metadata — evaluating a ranked list of candidate models against the current data profile |
| Data-driven story format | Config-driven pipelines — the pipeline code is generic, and config determines what runs |
| Background / scene transitions | Dataset blending — smoothly transitioning between two data distributions by interpolating weights |
Discussion Questions
- The paradox of choice: Research suggests that more options can lead to decision paralysis and less satisfaction. How many choices should a visual novel offer per chapter? How did you balance this in your MVP?
- Visible vs. invisible consequences: Some visual novels show you exactly what changed after a choice ("Elena will remember this"). Others hide consequences entirely. What are the design implications of each approach?
- The replay problem: Visual novels encourage replay to see different endings, but replaying means re-reading content. How can designers balance making each playthrough feel fresh?
- Story as data vs. story as code: Your MVP defines the story in a data file. What are the tradeoffs versus writing story logic directly in code?