Customise the dialogue box style
Four routes, from "change one colour" to "draw the whole thing myself". Start at the top and stop as soon as it looks right.
1. Pick a template
extends Node
## Docs: /guide/dialogue-box — what the box is and how it looks.
func _ready() -> void:
_build()
#region template
# Three templates ship with the addon. None bundle a font or a texture, so
# they inherit whatever font your project already uses.
func use_a_template(talk: NPCTalkKit) -> void:
# A typed array, because that is what the node carries. Assigning a bare
# `[...]` through an untyped `$NPCTalkKit` fails at run time, not compile.
var boxes: Array[TalkBoxTemplate] = [
preload("res://addons/npc_talkkit/templates/terminal.tres")]
talk.dialogue_boxes = boxes
#endregion
#region share
# A style is an ordinary Resource. Save one as a .tres, assign it to twenty
# NPCs, edit it once, and all twenty change. Godot's Make Unique lets one of
# them diverge. There is no TalkKit-specific sharing mechanism to learn.
func share_one_template(npcs: Array[NPCTalkKit], template: TalkBoxTemplate) -> void:
var boxes: Array[TalkBoxTemplate] = [template]
for npc in npcs:
npc.dialogue_boxes = boxes
#endregion
#region tweak
# Styles hold appearance only — no target, no NodePath, no per-character data.
# That is what makes sharing safe.
func warm_up_the_box(template: TalkBoxTemplate) -> void:
# The Panel's Base Appearance is a plain Godot StyleBoxFlat.
var panel := template.base.style as StyleBoxFlat
panel.bg_color = Color(0.14, 0.09, 0.06, 0.96)
panel.border_color = Color(0.82, 0.58, 0.28)
panel.set_corner_radius_all(18)
template.text_size = 21
template.open_transition = "slide"
template.transition_duration = 0.22
#endregion
#region chain
# Two levels, and only two:
#
# line.dialogue_box → conversation.default_dialogue_box
#
# A conversation says once which box paints it. A line says so only when it is
# the exception — a flashback, an interruption — and the line after it goes
# back to the default without being told to.
func use_another_box_for_one_line(line: TalkLine, flashback: TalkBoxTemplate) -> void:
line.dialogue_box = flashback
func use_another_box_for_a_conversation(conversation: TalkConversation, warning: TalkBoxTemplate) -> void:
conversation.default_dialogue_box = warning
#endregion
#region theme
# Already have a UI theme? Assign it and the box stops overriding fonts, sizes
# and text colours, so your theme wins. The Panel, padding and layers always
# apply: a Theme styles the text, never removes the frame.
func follow_the_project_theme(template: TalkBoxTemplate, theme: Theme) -> void:
template.theme = theme
#endregion
#region nine_slice
# A frame from an asset pack: the Panel's Base Appearance becomes a nine-slice.
# Scale keeps pixel art crisp; padding follows the frame's margins unless set
# to Custom.
func use_a_pixel_frame(template: TalkBoxTemplate, frame: Texture2D) -> void:
template.base.visual_source = TalkBoxLayer.Source.NINE_SLICE
var panel := template.base.style as StyleBoxTexture
panel.texture = frame
panel.set_texture_margin_all(8)
template.base.scale = 3
template.base.filter = CanvasItem.TEXTURE_FILTER_NEAREST
#endregion
#region layers
# Layers decorate the Panel, the Speaker Name and the Dialogue Visual. Each
# draws a native resource: here an AtlasTexture region in a corner, and a
# StyleBoxFlat name box behind the name.
func add_an_ornament_and_a_name_box(template: TalkBoxTemplate, ornaments: Texture2D) -> void:
var gem := AtlasTexture.new()
gem.atlas = ornaments
gem.region = Rect2(0, 0, 16, 16)
var corner := TalkBoxLayer.new()
corner.name = "Top Left Ornament"
corner.position = TalkBoxLayer.Position.TOP_LEFT
corner.depth = TalkBoxLayer.Depth.ABOVE
corner.visual_source = TalkBoxLayer.Source.IMAGE
corner.texture = gem
corner.scale = 2
var name_box := TalkBoxLayer.new()
name_box.name = "Name Box"
name_box.slot = TalkBoxLayer.Slot.SPEAKER_NAME
name_box.depth = TalkBoxLayer.Depth.BEHIND
name_box.visual_source = TalkBoxLayer.Source.FLAT_STYLE
(name_box.style as StyleBoxFlat).bg_color = Color(0.48, 0.18, 0.11)
name_box.padding = Vector4(8, 2, 8, 2)
template.name_placement = TalkBoxTemplate.NamePlacement.TOP_EDGE
template.layers.append(corner)
template.layers.append(name_box)
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
use_a_template(talk)
if talk.dialogue_boxes.is_empty():
failures.append("dialogue box: the preset did not load")
elif talk.dialogue_boxes[0].font != null or talk.dialogue_boxes[0].base.style is not StyleBoxFlat:
failures.append("dialogue box: a shipped template must bundle no font and no texture")
var style := _instant_box()
warm_up_the_box(style)
var painted := style.build_panel_style() as StyleBoxFlat
if painted == null or painted.get_corner_radius(CORNER_TOP_LEFT) != 18:
failures.append("dialogue box: the tweaks did not reach the generated StyleBox")
# A theme must actually win, or the escape hatch is decoration.
var label := RichTextLabel.new()
follow_the_project_theme(style, Theme.new())
style.apply_to(PanelContainer.new(), label)
if label.has_theme_font_size_override("normal_font_size"):
failures.append("dialogue box: paint overrides must stand down under a theme")
label.free()
# The resolution chain, end to end.
var conversation := TalkConversation.new()
conversation.conversation_id = &"tale"
var plain := TalkLine.new()
plain.text = "One."
var flashback := TalkLine.new()
flashback.text = "Two."
conversation.lines = [plain, flashback]
conversation.default_dialogue_box = style
talk.conversations = [conversation]
var owned: Array[TalkBoxTemplate] = [style]
talk.dialogue_boxes = owned
var memory := _instant_box()
use_another_box_for_one_line(flashback, memory)
talk.play(&"tale")
if talk._box.effective_template() != style:
failures.append("dialogue box: the conversation default should paint the first line")
talk.advance()
if talk._box.effective_template() != memory:
failures.append("dialogue box: a line override should paint its own line")
talk.stop()
# And the line after an override goes back to the default on its own.
var third := TalkLine.new()
third.text = "Three."
conversation.lines = [plain, flashback, third]
talk.play(&"tale")
talk.advance()
talk.advance()
if talk._box.effective_template() != style:
failures.append("dialogue box: the line after an override must return to the default")
talk.stop()
var warning := _instant_box()
use_another_box_for_a_conversation(conversation, warning)
talk.play(&"tale")
if talk._box.effective_template() != warning:
failures.append("dialogue box: changing the conversation default should repaint it")
talk.stop()
var framed := _instant_box()
var art := ImageTexture.create_from_image(Image.create_empty(24, 24, false, Image.FORMAT_RGBA8))
use_a_pixel_frame(framed, art)
if not framed.drawn_panel_style() is StyleBoxTexture or framed.effective_padding() != Vector4(24, 24, 24, 24):
failures.append("dialogue box: a nine-slice frame should pad by its margins at ×3")
add_an_ornament_and_a_name_box(framed, art)
if framed.layers_for(TalkBoxLayer.Slot.PANEL).size() != 1 or framed.layers_for(TalkBoxLayer.Slot.SPEAKER_NAME).size() != 1:
failures.append("dialogue box: the ornament and the name box should land on their slots")
var one := NPCTalkKit.new()
var two := NPCTalkKit.new()
share_one_template([one, two], style)
if one.dialogue_boxes != two.dialogue_boxes:
failures.append("dialogue box: sharing must hand both NPCs the same resource")
one.free()
two.free()
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 templateThree ship with the addon, each a different kind of box: classic_panel (a panel with a portrait), speech_bubble (a bubble with a tail) and terminal (text only, instant).
None bundle a font or a texture, so they inherit whatever font your project uses. Everything else, from colour to transitions, is yours to change.
2. Customize from what you see
Open TalkKit → Dialogue Boxes. Choose a working preset from + New, or select the current box and use Actions → Duplicate before changing it. The right pane is a live runtime preview, so a warmer colour is visible immediately.
extends Node
## Docs: /guide/dialogue-box — what the box is and how it looks.
func _ready() -> void:
_build()
#region template
# Three templates ship with the addon. None bundle a font or a texture, so
# they inherit whatever font your project already uses.
func use_a_template(talk: NPCTalkKit) -> void:
# A typed array, because that is what the node carries. Assigning a bare
# `[...]` through an untyped `$NPCTalkKit` fails at run time, not compile.
var boxes: Array[TalkBoxTemplate] = [
preload("res://addons/npc_talkkit/templates/terminal.tres")]
talk.dialogue_boxes = boxes
#endregion
#region share
# A style is an ordinary Resource. Save one as a .tres, assign it to twenty
# NPCs, edit it once, and all twenty change. Godot's Make Unique lets one of
# them diverge. There is no TalkKit-specific sharing mechanism to learn.
func share_one_template(npcs: Array[NPCTalkKit], template: TalkBoxTemplate) -> void:
var boxes: Array[TalkBoxTemplate] = [template]
for npc in npcs:
npc.dialogue_boxes = boxes
#endregion
#region tweak
# Styles hold appearance only — no target, no NodePath, no per-character data.
# That is what makes sharing safe.
func warm_up_the_box(template: TalkBoxTemplate) -> void:
# The Panel's Base Appearance is a plain Godot StyleBoxFlat.
var panel := template.base.style as StyleBoxFlat
panel.bg_color = Color(0.14, 0.09, 0.06, 0.96)
panel.border_color = Color(0.82, 0.58, 0.28)
panel.set_corner_radius_all(18)
template.text_size = 21
template.open_transition = "slide"
template.transition_duration = 0.22
#endregion
#region chain
# Two levels, and only two:
#
# line.dialogue_box → conversation.default_dialogue_box
#
# A conversation says once which box paints it. A line says so only when it is
# the exception — a flashback, an interruption — and the line after it goes
# back to the default without being told to.
func use_another_box_for_one_line(line: TalkLine, flashback: TalkBoxTemplate) -> void:
line.dialogue_box = flashback
func use_another_box_for_a_conversation(conversation: TalkConversation, warning: TalkBoxTemplate) -> void:
conversation.default_dialogue_box = warning
#endregion
#region theme
# Already have a UI theme? Assign it and the box stops overriding fonts, sizes
# and text colours, so your theme wins. The Panel, padding and layers always
# apply: a Theme styles the text, never removes the frame.
func follow_the_project_theme(template: TalkBoxTemplate, theme: Theme) -> void:
template.theme = theme
#endregion
#region nine_slice
# A frame from an asset pack: the Panel's Base Appearance becomes a nine-slice.
# Scale keeps pixel art crisp; padding follows the frame's margins unless set
# to Custom.
func use_a_pixel_frame(template: TalkBoxTemplate, frame: Texture2D) -> void:
template.base.visual_source = TalkBoxLayer.Source.NINE_SLICE
var panel := template.base.style as StyleBoxTexture
panel.texture = frame
panel.set_texture_margin_all(8)
template.base.scale = 3
template.base.filter = CanvasItem.TEXTURE_FILTER_NEAREST
#endregion
#region layers
# Layers decorate the Panel, the Speaker Name and the Dialogue Visual. Each
# draws a native resource: here an AtlasTexture region in a corner, and a
# StyleBoxFlat name box behind the name.
func add_an_ornament_and_a_name_box(template: TalkBoxTemplate, ornaments: Texture2D) -> void:
var gem := AtlasTexture.new()
gem.atlas = ornaments
gem.region = Rect2(0, 0, 16, 16)
var corner := TalkBoxLayer.new()
corner.name = "Top Left Ornament"
corner.position = TalkBoxLayer.Position.TOP_LEFT
corner.depth = TalkBoxLayer.Depth.ABOVE
corner.visual_source = TalkBoxLayer.Source.IMAGE
corner.texture = gem
corner.scale = 2
var name_box := TalkBoxLayer.new()
name_box.name = "Name Box"
name_box.slot = TalkBoxLayer.Slot.SPEAKER_NAME
name_box.depth = TalkBoxLayer.Depth.BEHIND
name_box.visual_source = TalkBoxLayer.Source.FLAT_STYLE
(name_box.style as StyleBoxFlat).bg_color = Color(0.48, 0.18, 0.11)
name_box.padding = Vector4(8, 2, 8, 2)
template.name_placement = TalkBoxTemplate.NamePlacement.TOP_EDGE
template.layers.append(corner)
template.layers.append(name_box)
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
use_a_template(talk)
if talk.dialogue_boxes.is_empty():
failures.append("dialogue box: the preset did not load")
elif talk.dialogue_boxes[0].font != null or talk.dialogue_boxes[0].base.style is not StyleBoxFlat:
failures.append("dialogue box: a shipped template must bundle no font and no texture")
var style := _instant_box()
warm_up_the_box(style)
var painted := style.build_panel_style() as StyleBoxFlat
if painted == null or painted.get_corner_radius(CORNER_TOP_LEFT) != 18:
failures.append("dialogue box: the tweaks did not reach the generated StyleBox")
# A theme must actually win, or the escape hatch is decoration.
var label := RichTextLabel.new()
follow_the_project_theme(style, Theme.new())
style.apply_to(PanelContainer.new(), label)
if label.has_theme_font_size_override("normal_font_size"):
failures.append("dialogue box: paint overrides must stand down under a theme")
label.free()
# The resolution chain, end to end.
var conversation := TalkConversation.new()
conversation.conversation_id = &"tale"
var plain := TalkLine.new()
plain.text = "One."
var flashback := TalkLine.new()
flashback.text = "Two."
conversation.lines = [plain, flashback]
conversation.default_dialogue_box = style
talk.conversations = [conversation]
var owned: Array[TalkBoxTemplate] = [style]
talk.dialogue_boxes = owned
var memory := _instant_box()
use_another_box_for_one_line(flashback, memory)
talk.play(&"tale")
if talk._box.effective_template() != style:
failures.append("dialogue box: the conversation default should paint the first line")
talk.advance()
if talk._box.effective_template() != memory:
failures.append("dialogue box: a line override should paint its own line")
talk.stop()
# And the line after an override goes back to the default on its own.
var third := TalkLine.new()
third.text = "Three."
conversation.lines = [plain, flashback, third]
talk.play(&"tale")
talk.advance()
talk.advance()
if talk._box.effective_template() != style:
failures.append("dialogue box: the line after an override must return to the default")
talk.stop()
var warning := _instant_box()
use_another_box_for_a_conversation(conversation, warning)
talk.play(&"tale")
if talk._box.effective_template() != warning:
failures.append("dialogue box: changing the conversation default should repaint it")
talk.stop()
var framed := _instant_box()
var art := ImageTexture.create_from_image(Image.create_empty(24, 24, false, Image.FORMAT_RGBA8))
use_a_pixel_frame(framed, art)
if not framed.drawn_panel_style() is StyleBoxTexture or framed.effective_padding() != Vector4(24, 24, 24, 24):
failures.append("dialogue box: a nine-slice frame should pad by its margins at ×3")
add_an_ornament_and_a_name_box(framed, art)
if framed.layers_for(TalkBoxLayer.Slot.PANEL).size() != 1 or framed.layers_for(TalkBoxLayer.Slot.SPEAKER_NAME).size() != 1:
failures.append("dialogue box: the ornament and the name box should land on their slots")
var one := NPCTalkKit.new()
var two := NPCTalkKit.new()
share_one_template([one, two], style)
if one.dialogue_boxes != two.dialogue_boxes:
failures.append("dialogue box: sharing must hand both NPCs the same resource")
one.free()
two.free()
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 templateTo reuse it, choose Actions → Save as Shared…. It writes the .tres and replaces the collection entry with the shared resource. Assign that box to other NPCs; edit it once and they all change. Make Unique lets one diverge.
Preset or blank?
Start from a preset for incremental customization. Choose Blank Custom only when you deliberately want the script defaults.
3. A hand-drawn frame
Set Panel › Appearance to Nine-slice Texture, choose the image and set its margins. The margins are drawn as guide lines on the image. Scale keeps pixel art crisp, and padding follows the frame's margins unless you choose Custom.
extends Node
## Docs: /guide/dialogue-box — what the box is and how it looks.
func _ready() -> void:
_build()
#region template
# Three templates ship with the addon. None bundle a font or a texture, so
# they inherit whatever font your project already uses.
func use_a_template(talk: NPCTalkKit) -> void:
# A typed array, because that is what the node carries. Assigning a bare
# `[...]` through an untyped `$NPCTalkKit` fails at run time, not compile.
var boxes: Array[TalkBoxTemplate] = [
preload("res://addons/npc_talkkit/templates/terminal.tres")]
talk.dialogue_boxes = boxes
#endregion
#region share
# A style is an ordinary Resource. Save one as a .tres, assign it to twenty
# NPCs, edit it once, and all twenty change. Godot's Make Unique lets one of
# them diverge. There is no TalkKit-specific sharing mechanism to learn.
func share_one_template(npcs: Array[NPCTalkKit], template: TalkBoxTemplate) -> void:
var boxes: Array[TalkBoxTemplate] = [template]
for npc in npcs:
npc.dialogue_boxes = boxes
#endregion
#region tweak
# Styles hold appearance only — no target, no NodePath, no per-character data.
# That is what makes sharing safe.
func warm_up_the_box(template: TalkBoxTemplate) -> void:
# The Panel's Base Appearance is a plain Godot StyleBoxFlat.
var panel := template.base.style as StyleBoxFlat
panel.bg_color = Color(0.14, 0.09, 0.06, 0.96)
panel.border_color = Color(0.82, 0.58, 0.28)
panel.set_corner_radius_all(18)
template.text_size = 21
template.open_transition = "slide"
template.transition_duration = 0.22
#endregion
#region chain
# Two levels, and only two:
#
# line.dialogue_box → conversation.default_dialogue_box
#
# A conversation says once which box paints it. A line says so only when it is
# the exception — a flashback, an interruption — and the line after it goes
# back to the default without being told to.
func use_another_box_for_one_line(line: TalkLine, flashback: TalkBoxTemplate) -> void:
line.dialogue_box = flashback
func use_another_box_for_a_conversation(conversation: TalkConversation, warning: TalkBoxTemplate) -> void:
conversation.default_dialogue_box = warning
#endregion
#region theme
# Already have a UI theme? Assign it and the box stops overriding fonts, sizes
# and text colours, so your theme wins. The Panel, padding and layers always
# apply: a Theme styles the text, never removes the frame.
func follow_the_project_theme(template: TalkBoxTemplate, theme: Theme) -> void:
template.theme = theme
#endregion
#region nine_slice
# A frame from an asset pack: the Panel's Base Appearance becomes a nine-slice.
# Scale keeps pixel art crisp; padding follows the frame's margins unless set
# to Custom.
func use_a_pixel_frame(template: TalkBoxTemplate, frame: Texture2D) -> void:
template.base.visual_source = TalkBoxLayer.Source.NINE_SLICE
var panel := template.base.style as StyleBoxTexture
panel.texture = frame
panel.set_texture_margin_all(8)
template.base.scale = 3
template.base.filter = CanvasItem.TEXTURE_FILTER_NEAREST
#endregion
#region layers
# Layers decorate the Panel, the Speaker Name and the Dialogue Visual. Each
# draws a native resource: here an AtlasTexture region in a corner, and a
# StyleBoxFlat name box behind the name.
func add_an_ornament_and_a_name_box(template: TalkBoxTemplate, ornaments: Texture2D) -> void:
var gem := AtlasTexture.new()
gem.atlas = ornaments
gem.region = Rect2(0, 0, 16, 16)
var corner := TalkBoxLayer.new()
corner.name = "Top Left Ornament"
corner.position = TalkBoxLayer.Position.TOP_LEFT
corner.depth = TalkBoxLayer.Depth.ABOVE
corner.visual_source = TalkBoxLayer.Source.IMAGE
corner.texture = gem
corner.scale = 2
var name_box := TalkBoxLayer.new()
name_box.name = "Name Box"
name_box.slot = TalkBoxLayer.Slot.SPEAKER_NAME
name_box.depth = TalkBoxLayer.Depth.BEHIND
name_box.visual_source = TalkBoxLayer.Source.FLAT_STYLE
(name_box.style as StyleBoxFlat).bg_color = Color(0.48, 0.18, 0.11)
name_box.padding = Vector4(8, 2, 8, 2)
template.name_placement = TalkBoxTemplate.NamePlacement.TOP_EDGE
template.layers.append(corner)
template.layers.append(name_box)
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
use_a_template(talk)
if talk.dialogue_boxes.is_empty():
failures.append("dialogue box: the preset did not load")
elif talk.dialogue_boxes[0].font != null or talk.dialogue_boxes[0].base.style is not StyleBoxFlat:
failures.append("dialogue box: a shipped template must bundle no font and no texture")
var style := _instant_box()
warm_up_the_box(style)
var painted := style.build_panel_style() as StyleBoxFlat
if painted == null or painted.get_corner_radius(CORNER_TOP_LEFT) != 18:
failures.append("dialogue box: the tweaks did not reach the generated StyleBox")
# A theme must actually win, or the escape hatch is decoration.
var label := RichTextLabel.new()
follow_the_project_theme(style, Theme.new())
style.apply_to(PanelContainer.new(), label)
if label.has_theme_font_size_override("normal_font_size"):
failures.append("dialogue box: paint overrides must stand down under a theme")
label.free()
# The resolution chain, end to end.
var conversation := TalkConversation.new()
conversation.conversation_id = &"tale"
var plain := TalkLine.new()
plain.text = "One."
var flashback := TalkLine.new()
flashback.text = "Two."
conversation.lines = [plain, flashback]
conversation.default_dialogue_box = style
talk.conversations = [conversation]
var owned: Array[TalkBoxTemplate] = [style]
talk.dialogue_boxes = owned
var memory := _instant_box()
use_another_box_for_one_line(flashback, memory)
talk.play(&"tale")
if talk._box.effective_template() != style:
failures.append("dialogue box: the conversation default should paint the first line")
talk.advance()
if talk._box.effective_template() != memory:
failures.append("dialogue box: a line override should paint its own line")
talk.stop()
# And the line after an override goes back to the default on its own.
var third := TalkLine.new()
third.text = "Three."
conversation.lines = [plain, flashback, third]
talk.play(&"tale")
talk.advance()
talk.advance()
if talk._box.effective_template() != style:
failures.append("dialogue box: the line after an override must return to the default")
talk.stop()
var warning := _instant_box()
use_another_box_for_a_conversation(conversation, warning)
talk.play(&"tale")
if talk._box.effective_template() != warning:
failures.append("dialogue box: changing the conversation default should repaint it")
talk.stop()
var framed := _instant_box()
var art := ImageTexture.create_from_image(Image.create_empty(24, 24, false, Image.FORMAT_RGBA8))
use_a_pixel_frame(framed, art)
if not framed.drawn_panel_style() is StyleBoxTexture or framed.effective_padding() != Vector4(24, 24, 24, 24):
failures.append("dialogue box: a nine-slice frame should pad by its margins at ×3")
add_an_ornament_and_a_name_box(framed, art)
if framed.layers_for(TalkBoxLayer.Slot.PANEL).size() != 1 or framed.layers_for(TalkBoxLayer.Slot.SPEAKER_NAME).size() != 1:
failures.append("dialogue box: the ornament and the name box should land on their slots")
var one := NPCTalkKit.new()
var two := NPCTalkKit.new()
share_one_template([one, two], style)
if one.dialogue_boxes != two.dialogue_boxes:
failures.append("dialogue box: sharing must hand both NPCs the same resource")
one.free()
two.free()
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 templateAlready have a StyleBox, such as your theme's panel? Choose Existing Godot Resource and assign it. TalkKit draws it as it is.
Name boxes, portrait frames and corner ornaments are layers on top of the frame; see Dialogue Box Anatomy.
4. Your project's theme
If you already have a Theme for the rest of your UI:
extends Node
## Docs: /guide/dialogue-box — what the box is and how it looks.
func _ready() -> void:
_build()
#region template
# Three templates ship with the addon. None bundle a font or a texture, so
# they inherit whatever font your project already uses.
func use_a_template(talk: NPCTalkKit) -> void:
# A typed array, because that is what the node carries. Assigning a bare
# `[...]` through an untyped `$NPCTalkKit` fails at run time, not compile.
var boxes: Array[TalkBoxTemplate] = [
preload("res://addons/npc_talkkit/templates/terminal.tres")]
talk.dialogue_boxes = boxes
#endregion
#region share
# A style is an ordinary Resource. Save one as a .tres, assign it to twenty
# NPCs, edit it once, and all twenty change. Godot's Make Unique lets one of
# them diverge. There is no TalkKit-specific sharing mechanism to learn.
func share_one_template(npcs: Array[NPCTalkKit], template: TalkBoxTemplate) -> void:
var boxes: Array[TalkBoxTemplate] = [template]
for npc in npcs:
npc.dialogue_boxes = boxes
#endregion
#region tweak
# Styles hold appearance only — no target, no NodePath, no per-character data.
# That is what makes sharing safe.
func warm_up_the_box(template: TalkBoxTemplate) -> void:
# The Panel's Base Appearance is a plain Godot StyleBoxFlat.
var panel := template.base.style as StyleBoxFlat
panel.bg_color = Color(0.14, 0.09, 0.06, 0.96)
panel.border_color = Color(0.82, 0.58, 0.28)
panel.set_corner_radius_all(18)
template.text_size = 21
template.open_transition = "slide"
template.transition_duration = 0.22
#endregion
#region chain
# Two levels, and only two:
#
# line.dialogue_box → conversation.default_dialogue_box
#
# A conversation says once which box paints it. A line says so only when it is
# the exception — a flashback, an interruption — and the line after it goes
# back to the default without being told to.
func use_another_box_for_one_line(line: TalkLine, flashback: TalkBoxTemplate) -> void:
line.dialogue_box = flashback
func use_another_box_for_a_conversation(conversation: TalkConversation, warning: TalkBoxTemplate) -> void:
conversation.default_dialogue_box = warning
#endregion
#region theme
# Already have a UI theme? Assign it and the box stops overriding fonts, sizes
# and text colours, so your theme wins. The Panel, padding and layers always
# apply: a Theme styles the text, never removes the frame.
func follow_the_project_theme(template: TalkBoxTemplate, theme: Theme) -> void:
template.theme = theme
#endregion
#region nine_slice
# A frame from an asset pack: the Panel's Base Appearance becomes a nine-slice.
# Scale keeps pixel art crisp; padding follows the frame's margins unless set
# to Custom.
func use_a_pixel_frame(template: TalkBoxTemplate, frame: Texture2D) -> void:
template.base.visual_source = TalkBoxLayer.Source.NINE_SLICE
var panel := template.base.style as StyleBoxTexture
panel.texture = frame
panel.set_texture_margin_all(8)
template.base.scale = 3
template.base.filter = CanvasItem.TEXTURE_FILTER_NEAREST
#endregion
#region layers
# Layers decorate the Panel, the Speaker Name and the Dialogue Visual. Each
# draws a native resource: here an AtlasTexture region in a corner, and a
# StyleBoxFlat name box behind the name.
func add_an_ornament_and_a_name_box(template: TalkBoxTemplate, ornaments: Texture2D) -> void:
var gem := AtlasTexture.new()
gem.atlas = ornaments
gem.region = Rect2(0, 0, 16, 16)
var corner := TalkBoxLayer.new()
corner.name = "Top Left Ornament"
corner.position = TalkBoxLayer.Position.TOP_LEFT
corner.depth = TalkBoxLayer.Depth.ABOVE
corner.visual_source = TalkBoxLayer.Source.IMAGE
corner.texture = gem
corner.scale = 2
var name_box := TalkBoxLayer.new()
name_box.name = "Name Box"
name_box.slot = TalkBoxLayer.Slot.SPEAKER_NAME
name_box.depth = TalkBoxLayer.Depth.BEHIND
name_box.visual_source = TalkBoxLayer.Source.FLAT_STYLE
(name_box.style as StyleBoxFlat).bg_color = Color(0.48, 0.18, 0.11)
name_box.padding = Vector4(8, 2, 8, 2)
template.name_placement = TalkBoxTemplate.NamePlacement.TOP_EDGE
template.layers.append(corner)
template.layers.append(name_box)
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
use_a_template(talk)
if talk.dialogue_boxes.is_empty():
failures.append("dialogue box: the preset did not load")
elif talk.dialogue_boxes[0].font != null or talk.dialogue_boxes[0].base.style is not StyleBoxFlat:
failures.append("dialogue box: a shipped template must bundle no font and no texture")
var style := _instant_box()
warm_up_the_box(style)
var painted := style.build_panel_style() as StyleBoxFlat
if painted == null or painted.get_corner_radius(CORNER_TOP_LEFT) != 18:
failures.append("dialogue box: the tweaks did not reach the generated StyleBox")
# A theme must actually win, or the escape hatch is decoration.
var label := RichTextLabel.new()
follow_the_project_theme(style, Theme.new())
style.apply_to(PanelContainer.new(), label)
if label.has_theme_font_size_override("normal_font_size"):
failures.append("dialogue box: paint overrides must stand down under a theme")
label.free()
# The resolution chain, end to end.
var conversation := TalkConversation.new()
conversation.conversation_id = &"tale"
var plain := TalkLine.new()
plain.text = "One."
var flashback := TalkLine.new()
flashback.text = "Two."
conversation.lines = [plain, flashback]
conversation.default_dialogue_box = style
talk.conversations = [conversation]
var owned: Array[TalkBoxTemplate] = [style]
talk.dialogue_boxes = owned
var memory := _instant_box()
use_another_box_for_one_line(flashback, memory)
talk.play(&"tale")
if talk._box.effective_template() != style:
failures.append("dialogue box: the conversation default should paint the first line")
talk.advance()
if talk._box.effective_template() != memory:
failures.append("dialogue box: a line override should paint its own line")
talk.stop()
# And the line after an override goes back to the default on its own.
var third := TalkLine.new()
third.text = "Three."
conversation.lines = [plain, flashback, third]
talk.play(&"tale")
talk.advance()
talk.advance()
if talk._box.effective_template() != style:
failures.append("dialogue box: the line after an override must return to the default")
talk.stop()
var warning := _instant_box()
use_another_box_for_a_conversation(conversation, warning)
talk.play(&"tale")
if talk._box.effective_template() != warning:
failures.append("dialogue box: changing the conversation default should repaint it")
talk.stop()
var framed := _instant_box()
var art := ImageTexture.create_from_image(Image.create_empty(24, 24, false, Image.FORMAT_RGBA8))
use_a_pixel_frame(framed, art)
if not framed.drawn_panel_style() is StyleBoxTexture or framed.effective_padding() != Vector4(24, 24, 24, 24):
failures.append("dialogue box: a nine-slice frame should pad by its margins at ×3")
add_an_ornament_and_a_name_box(framed, art)
if framed.layers_for(TalkBoxLayer.Slot.PANEL).size() != 1 or framed.layers_for(TalkBoxLayer.Slot.SPEAKER_NAME).size() != 1:
failures.append("dialogue box: the ornament and the name box should land on their slots")
var one := NPCTalkKit.new()
var two := NPCTalkKit.new()
share_one_template([one, two], style)
if one.dialogue_boxes != two.dialogue_boxes:
failures.append("dialogue box: sharing must hand both NPCs the same resource")
one.free()
two.free()
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 templateWhen theme is set, the style stops overriding fonts, sizes and text colours, so your theme wins. The Panel, padding, layers and layout still apply: a Theme styles the text and never removes the frame.
If a theme seems ignored
Check for theme_override_* on the control in question. Overrides beat a theme, always. The shipped box has none, specifically so this route works.
Transitions
Animation is part of the template, so it travels with the box:
| Setting | Values |
|---|---|
open_transition, close_transition, line_transition | none, fade, slide, scale_pop |
transition_duration, transition_curve, transition_easing | Godot Tween settings |
For anything else, subclass TalkTransition and assign it to custom_transition.
5. Build the whole box yourself
When none of the above is enough, the renderer itself is replaceable with four methods. See a renderer of your own.