Writing conversations
A conversation is a named list of lines. play() runs one by name, the way AnimationPlayer.play() runs an animation by name.
Where they live
Typed into the node, they are stored in the scene — zero files. Saving one as a .tres is a later choice, not a starting requirement. See Shared library.
extends Node
## Docs: /guide/quick-start — the whole surface needed to say two lines.
func _ready() -> void:
_build_without_files()
#region play
# Nothing here needs a .tres file. The conversation was typed in the TalkKit
# workspace, and this is the entire runtime surface.
func greet() -> void:
$NPCTalkKit.play(&"greeting")
#endregion
## Builds in code what the TalkKit workspace builds by hand, so this file runs.
func _build_without_files() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
var smith := TalkSpeaker.new()
smith.display_name = "Torvald"
talk.speakers = [smith]
var box := _instant_box()
talk.dialogue_boxes = [box]
var conversation := TalkConversation.new()
conversation.conversation_id = &"greeting"
conversation.default_speaker = smith
conversation.default_dialogue_box = box
var first := TalkLine.new()
first.text = "The forge runs hot today."
var second := TalkLine.new()
second.text = "Come back when you need steel."
conversation.lines = [first, second]
talk.conversations = [conversation]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var spoken: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))
greet()
if not talk.is_running():
failures.append("quick_start: play() did not start the conversation")
talk.advance()
talk.advance()
if spoken.size() != 2:
failures.append("quick_start: expected two lines, got %s" % str(spoken))
if talk.is_running():
failures.append("quick_start: the conversation should have ended")
return failures
## Typing speed lives on the dialogue box now, so turning it off for a scripted
## run means handing the node a box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateText parameters
Write in a line and fill it at playback:
extends Node
## Docs: /guide/conversations — {{placeholders}} filled at playback.
func _ready() -> void:
_build()
#region play
# {{player_name}} is replaced when the line is shown. TalkKit never reads game
# state: it renders what you hand it.
func greet(player_name: String) -> void:
$NPCTalkKit.play(&"greeting", {"player_name": player_name})
#endregion
#region unknown
# A key with no value is left written as-is and warned about, rather than
# silently emptied — an authoring mistake you can see beats one you cannot.
func greet_without_a_name() -> void:
$NPCTalkKit.play(&"greeting") # renders: Welcome back, {{player_name}}.
#endregion
func _build() -> void:
var box := _instant_box()
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [box]
var line := TalkLine.new()
line.text = "Welcome back, {{player_name}}."
var conversation := TalkConversation.new()
conversation.conversation_id = &"greeting"
conversation.default_dialogue_box = box
conversation.lines = [line]
talk.conversations = [conversation]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var spoken: Array[String] = []
talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))
greet("Alex")
if spoken.is_empty() or spoken[0] != "Welcome back, Alex.":
failures.append("parameters: substitution failed, got %s" % str(spoken))
talk.stop()
spoken.clear()
greet_without_a_name()
if spoken.is_empty() or not spoken[0].contains("{{player_name}}"):
failures.append("parameters: an unknown key must stay visible, got %s" % str(spoken))
talk.stop()
# The source line must never be mutated: it may be shared by other NPCs.
if talk.conversations[0].lines[0].text != "Welcome back, {{player_name}}.":
failures.append("parameters: the authored line was mutated by substitution")
return failures
## Typing speed lives on the dialogue box now, so turning it off for a scripted
## run means handing the node a box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateMerge order, later wins:
default_paramson the node- the dictionary passed to
play()
Three rules worth knowing:
- A key with no value stays visible.
renders as written and logs a warning, rather than silently emptying — a mistake you can see beats one you cannot. - One pass, no recursion. A value containing
is not expanded again. This is not a template language. - The authored line is never modified. Substitution renders into a copy, because the line may be a shared resource other NPCs are using.
extends Node
## Docs: /guide/conversations — {{placeholders}} filled at playback.
func _ready() -> void:
_build()
#region play
# {{player_name}} is replaced when the line is shown. TalkKit never reads game
# state: it renders what you hand it.
func greet(player_name: String) -> void:
$NPCTalkKit.play(&"greeting", {"player_name": player_name})
#endregion
#region unknown
# A key with no value is left written as-is and warned about, rather than
# silently emptied — an authoring mistake you can see beats one you cannot.
func greet_without_a_name() -> void:
$NPCTalkKit.play(&"greeting") # renders: Welcome back, {{player_name}}.
#endregion
func _build() -> void:
var box := _instant_box()
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [box]
var line := TalkLine.new()
line.text = "Welcome back, {{player_name}}."
var conversation := TalkConversation.new()
conversation.conversation_id = &"greeting"
conversation.default_dialogue_box = box
conversation.lines = [line]
talk.conversations = [conversation]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var spoken: Array[String] = []
talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))
greet("Alex")
if spoken.is_empty() or spoken[0] != "Welcome back, Alex.":
failures.append("parameters: substitution failed, got %s" % str(spoken))
talk.stop()
spoken.clear()
greet_without_a_name()
if spoken.is_empty() or not spoken[0].contains("{{player_name}}"):
failures.append("parameters: an unknown key must stay visible, got %s" % str(spoken))
talk.stop()
# The source line must never be mutated: it may be shared by other NPCs.
if talk.conversations[0].lines[0].text != "Welcome back, {{player_name}}.":
failures.append("parameters: the authored line was mutated by substitution")
return failures
## Typing speed lives on the dialogue box now, so turning it off for a scripted
## run means handing the node a box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateTalkKit never reads game state. It renders what you hand it.
Saying something with no authoring at all
For a throwaway line — a tutorial hint, a debug message — skip authoring entirely:
$NPCTalkKit.say("The gate is barred.")
$NPCTalkKit.say(["One.", "Two."], {"name": "Alex"})The conversation is built on the fly and discarded; nothing is added to the node.
Per-line options
Most lines need nothing but text. Select an exceptional line and use the right-hand detail pane. Add override… reveals the complete task-oriented groups; active exceptions remain visible and each has its own Reset:
| Field | For |
|---|---|
speaker_mode, speaker | Inherit the conversation's speaker, None for a narrator line, or a different character speaking this line — name, face and voice together |
display_name_override | a different name only: "???" before a reveal. The face and the voice stay whoever is speaking |
visual_variant | one of the speaker's named faces, such as angry |
characters_per_second | slowing one dramatic line down; 0 inherits |
event_id, event_payload | telling the game something — events |
event_blocking | pausing for an answer — branching |
dialogue_box | one line in a different box |
placement | one line somewhere else — a bubble over whoever interrupts |
voice_clip | a recorded voice line, played as the line starts |
line_id | matching on a specific line from host code |
line_id is optional. It exists for host code and localisation keys, not because TalkKit needs it.
The right pane also renders the selected line with the real runtime renderer. ▶ Play plays the conversation there from the selected line — typewriter, transitions and voice, one line after another — and ■ Stop or ↓ Next holds it still again. At medium widths the pane becomes a drawer so line text keeps useful space. Line checkboxes enable bulk Speaker, Dialogue Box, Placement, duplicate and delete; the drag handle and Alt+↑/↓ both reorder.