Module 18

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

ModuleWhat You Used From It
Module 01 - PongBasic 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.

Demo: Branching Narrative Visualizer

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.

Demo: Typewriter Text with Expressions

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.

35 cps

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

MVP Spec

FeatureRequired
Story defined in data file (JSON, YAML, or DSL)Yes
Story engine that traverses the narrative graphYes
Typewriter text rendering with skipYes
At least 2 character sprites with 3+ expressions eachYes
Expression changes synchronized with story beatsYes
At least 3 meaningful choices with flag-settingYes
At least 1 delayed consequenceYes
At least 2 distinct endingsYes
Background images with crossfade transitionsYes
Speaker name and text styling per characterYes
Save/load with multiple slotsStretch
Text log / backlogStretch
Auto-advance modeStretch
Ending galleryStretch

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 ConceptBackend Analogy
Branching narrative graphA workflow engine (Temporal, Step Functions) — nodes are workflow steps, edges are transitions, and the execution path depends on runtime conditions
Typewriter text renderingStreaming 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 systemA template engine with partials — the base template (character) swaps in different partials (expressions) based on context variables
Choice system with consequence trackingEvent sourcing — every choice is an immutable event appended to a log, and current state is derived by replaying all events
Multiple endings / route systemA rules engine — ordered rules evaluated top-down, where the first rule whose conditions match produces the result
Data-driven story formatConfiguration-as-code — runtime behavior is entirely determined by declarative configuration files
Background / scene transitionsBlue-green deployment — transitioning smoothly from one active state to another with a brief overlap period

For Frontend Developers

Game Dev ConceptFrontend Analogy
Branching narrative graphA state machine for UI flow (XState) — each state renders different content, transitions are triggered by user actions
Typewriter text renderingA 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 systemConditional rendering of image components — a component receives an expression prop and renders the corresponding image
Choice system with consequence trackingRedux action history — each choice dispatches an action that updates the store
Multiple endings / route systemA/B test resolution — evaluating conditions from a prioritized list of variants
Data-driven story formatCMS-driven content — the React app is the engine, the content comes from a headless CMS
Background / scene transitionsCSS transitions between background images — crossfade using overlapping absolutely-positioned divs

For Data / ML Engineers

Game Dev ConceptData / ML Analogy
Branching narrative graphA decision tree with stateful traversal — each internal node splits on a player choice, leaf nodes are endings
Typewriter text renderingStreaming inference — a language model generating tokens one at a time, with the option to batch-return remaining tokens
Character sprite/expression systemFeature visualization — displaying different visual representations of a data entity depending on active features
Choice system with consequence trackingFeature store updates — each choice writes a feature to the store, and downstream models read from it
Multiple endings / route systemModel selection based on metadata — evaluating a ranked list of candidate models against the current data profile
Data-driven story formatConfig-driven pipelines — the pipeline code is generic, and config determines what runs
Background / scene transitionsDataset blending — smoothly transitioning between two data distributions by interpolating weights

Discussion Questions

  1. 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?
  2. 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?
  3. 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?
  4. 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?