Imported from gamedev-skills/awesome-gamedev-agent-skills (
skills/genres/visual-novel/SKILL.md) via skills.sh. Install upstream withnpx skills add gamedev-skills/awesome-gamedev-agent-skills --skill visual-novel. Copyright stays with the author.
Visual Novel
A playbook for visual novels — the branching script, the presentation (text box, characters, backgrounds), choices, and the quality-of-life systems players expect (save anywhere, backlog, skip, auto). This is a compositional skill: it drives a dialogue engine and a UI layer. It does not re-teach the dialogue engine or UI nodes; it defines the script model and the player conveniences that make a VN pleasant to read.
When to use
- Use when the game is mostly reading branching text with character art and backgrounds: visual novel, dating sim, branching interactive fiction, story-choice game.
- Use when designing a choice/route structure, story flags, or VN conveniences (backlog, skip, auto-advance, save-anywhere).
When not to use: dialogue as one feature inside a larger game → rpg consuming
dialogue-systems. Card/board play → other genres. For the branching-script engine itself,
use dialogue-systems (Ink / Yarn Spinner).
If the project is Ren'Py (current stable 8.5.3): the engine already ships save-anywhere,
rollback, a history (backlog) screen, skip that stops at unseen text by default
(preferences.skip_unseen = False), auto-forward and a text-speed preference. Use and restyle
those through its screens and preferences; apply this skill's design guidance (routes, flags,
reactivity) rather than rebuilding the systems in Patterns 2–3.
Core loop
Read a line → advance → (at a branch) make a choice → the story branches on flags/choices → read on → reach an ending. The "game" is the shape of the branching and whether choices feel consequential; everything else is presentation and convenience.
Must-have systems
- Branching script — ordered lines + choices + jumps, with conditions and variables (Ink/Yarn).
- Text box — speaker name, body text, typewriter reveal, advance on click/key.
- Characters — sprites with expressions/poses, positions, show/hide transitions.
- Backgrounds + transitions — scene images, fades/dissolves.
- Choices — present options, gate some on flags, record the pick.
- Story state — flags/variables that branch the script and unlock content.
- Save/load (save-anywhere) — full script position + state; multiple slots; quick save.
- VN conveniences — backlog/history, skip (read text), auto-advance, text-speed setting.
- Audio — music per scene, SFX, optional voice clips.
Design knobs
| Knob | Effect | Notes |
|---|---|---|
| Text speed / instant | reading comfort | Always allow instant + a skip. |
| Auto-advance delay | hands-free reading | Tunable; pause on choices. |
| Skip scope | re-reading | Skip read text only by default. |
| Branch breadth/depth | replay value vs. cost | Branches multiply writing/art work. |
| Flag-gated content | reactivity | Lines/choices that check past decisions. |
| Route structure | story shape | Branch-and-merge vs. distinct routes (refs). |
| Choice visibility | fairness | Show locked choices vs. hide them. |
| Backlog length | convenience | Keep enough to re-read recent context. |
Patterns
1. Script as data the engine walks
# Pseudocode. Lines, choices, and jumps as data — usually authored in Ink/Yarn and stepped
# through by that runtime. The engine asks the script for "the next thing to show".
node = script.current()
if node.kind == "line":
show_text(node.speaker, node.text) # wait for advance input
elif node.kind == "choice":
options = [o for o in node.options if condition_met(o.condition, flags)] # gate by flags
show_choices(options) # wait for selection
elif node.kind == "set":
flags[node.var] = eval_expr(node.expr, flags)
script.advance(selected_option_or_none)
2. Typewriter reveal + advance (skippable)
# Pseudocode. Lay out the whole line once, then reveal it; a press first completes the line.
def show_text(speaker, text):
name_label.text = speaker
body_label.text = text # full text set up front: wrapping is decided now
shown = 0.0
while shown < visible_length(body_label): # count displayed characters, not markup tags
if advance_pressed(): # first press: reveal the whole line instantly
break
shown += chars_per_second * frame_dt()
body_label.visible_characters = int(shown)
wait_one_frame()
show_all_characters(body_label)
backlog.append((speaker, text)) # once, when the line is complete
wait_for_advance() # second press: go to the next line
Reveal with the label's visible-character count, not by slicing the string: slicing cuts
markup tags in half and re-wraps the text every frame, so a word starts on one line and jumps
to the next. Godot 4.7: RichTextLabel.visible_characters (or visible_ratio) with
visible_characters_behavior = TextServer.VC_CHARS_AFTER_SHAPING; the default
VC_CHARS_BEFORE_SHAPING leaves hidden text out of line breaking, so words still jump. Unity 6.3:
TMP_Text.maxVisibleCharacters (TextMeshPro, part of the uGUI package).
3. Choice sets a flag that branches later content
# Pseudocode. Choices write flags; later conditions read them — that is "reactivity".
def on_choice(option):
if option.set: flags[option.set] = True # e.g. flags["helped_npc"] = True
script.jump(option.target) # follow the branch
# Elsewhere, a line/choice/ending checks the flag:
if flags.get("helped_npc"): play_route("good_ending") else: play_route("neutral_ending")
Pitfalls / failure modes
- Save that only stores a checkpoint → VNs need save-anywhere. Persist the exact script position and all flags/variables (and seen-text data) so a load resumes the same line. Check what your runtime can restore: Ink saves the exact position, Yarn Spinner only the node (refs §4).
- Typewriter text that jumps between lines or shows raw tags → the reveal slices the string. Set the full text and raise the visible-character count (Pattern 2).
- Presentation logic baked into the script → unmaintainable. Keep content (text, choices) in the script and how it looks (sprites, transitions) in the engine layer.
- No skip/auto/backlog → readers feel trapped, especially on replays. These are expected baseline features, not extras.
- Skipping unread text → players miss content. Skip should fast-forward read text only.
- Choices with no consequence → branches that reconverge instantly feel fake. Set flags that visibly change later lines, choices, or endings.
- Combinatorial branch explosion → unshippable. Prefer branch-and-merge with a few flagged variations over fully distinct trees (refs).
- Lost reading context → no backlog to re-read the last lines. Keep a history buffer.
- Hardcoded language → no localization path. Keep text in data keyed for translation.
Composition (build it from these skills)
- Script engine:
dialogue-systems(Ink / Yarn Spinner) — branching, conditions, variables, localization hooks. - Presentation:
game-ui-uxfor text-box/choice-menu layout, scaling, and safe areas;godot-ui-controlfor the concrete text box, choice menu, name plate, and backlog UI. - Persistence:
save-systemsfor save-anywhere slots, seen-text/skip data, and settings. - Audio:
audio-designfor per-scene music, SFX, and voice playback. - Visuals: the engine animation/
Tweenskill for sprite/background transitions;shader-programmingfor dissolves. - Process:
prototype-fastto test the branch structure in plain text before adding art.
References
- For the branching data model, route structures (branch-and-merge vs. routes), flags/variables,
save-anywhere + backlog/skip data, and the content/presentation split, read
references/script-and-flow.md.
