Hộp thoại
Một template là toàn bộ những gì làm nên hộp thoại: nó có những bộ phận nào, được tô ra sao, và chuyển động thế nào. Bạn chọn một thứ, không phải hai.
Trước đây là hai
Presentation chọn scene còn style tô màu cho nó, mỗi cái một chuỗi phân giải làm cùng một việc. Chúng chưa bao giờ là hai kiến trúc — hai scene có sẵn dùng chung một class cha và chỉ khác nhau ở chỗ có ô hình nhân vật hay có đuôi nhọn. Nên giờ các bộ phận đó là thiết lập.
Quy trình
Chọn preset → Sửa cùng live preview → Save as Shared…Không phải dựng scene, không phải viết script, không phải hiện thực interface.
Ba cái có sẵn
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 template| Template | Vẻ ngoài |
|---|---|
classic_panel | Mặc định. Panel tối trải ngang đáy, có hình nhân vật, tên vàng. |
speech_bubble | Bong bóng kem có đuôi nhọn, không hình nhân vật, bật ra. |
terminal | Góc vuông, chữ xanh lá trên nền gần đen, không hiệu ứng. |

Không cái nào kèm font hay texture, nên chúng thừa hưởng font sẵn có của project.
Panel và bong bóng dùng chung một renderer, chỉ khác thiết lập — một bên có hình nhân vật và dấu tiếp tục, bên kia có đuôi nhọn.
Không cấu hình gì vẫn đẹp
Để trống Dialogue Box thì Classic Panel được dùng. Bạn không bắt buộc phải mở trang này mới ra được sản phẩm coi được.
Biến nó thành của bạn
Mở section Dialogue Boxes trong workspace TalkKit. + New bắt đầu từ preset trực quan—Classic Panel, Speech Bubble, Terminal hoặc Blank Custom—thay vì một resource trắng đầy field. Chọn hộp để sửa các nhóm theo tác vụ trong khi pane bên phải vẽ sample trực tiếp.
Vì sao preset xuất hiện trước
Phần lớn tùy chỉnh nên bắt đầu từ một look đang chạy rồi đổi dần. Blank Custom vẫn có sẵn khi bạn thật sự muốn bắt đầu từ mặc định trần.
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 templateChọn Actions → Duplicate trước khi tạo biến thể local. Save as Shared… ghi .tres rồi thay entry trong collection của node bằng resource dùng chung; Make Unique đưa một hộp shared trở lại scene. Scope và usage luôn hiện cạnh editor để bạn biết một thay đổi sẽ lan tới đâu.
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 template
Preset của riêng bạn
Actions → Save as Preset… lưu một bản sao của hộp đang chọn vào thư mục preset của project, mặc định là res://talkkit_presets, đổi được ở Project Settings › npc_talkkit › Dialogue Boxes › Preset Folder. Từ đó + New liệt kê nó trong nhóm Your presets, cạnh các preset có sẵn.
Hộp bạn lưu từ đó vẫn giữ nguyên: preset là điểm xuất phát mà mỗi hộp mới sao chép, không phải resource dùng chung. Lưu trùng tên thì preset cũ bị thay.
Preset chỉ là một file .tres bình thường, nên template tải từ nơi khác sẽ xuất hiện trong danh sách ngay khi được đặt vào thư mục đó. + New → Show Presets Folder mở thư mục trong FileSystem dock, nơi bạn đổi tên hoặc xoá preset.
Xoá một hộp
Chuột phải vào hộp trong danh sách, nhấn Delete, hoặc chọn Actions → Remove. Hộp chưa ai dùng thì bị xoá ngay. Nếu conversation hay line vẫn đang dùng nó, hộp thoại sẽ liệt kê những chỗ đó và hỏi dùng hộp nào thay thế; tất cả được chuyển trong cùng một bước undo. File .tres dùng chung vẫn còn trên đĩa.
Chỉnh được những gì
Thiết lập được nhóm theo phần của hộp mà nó ảnh hưởng, và skin của phần nào nằm trong nhóm của phần đó. Giải phẫu hộp thoại chỉ từng phần trên một hộp thật.
| Nhóm | Thiết lập |
|---|---|
| Panel | Appearance (Flat Style, Nine-slice Texture, Image hoặc một resource có sẵn) cùng các control của nó, khoảng đệm, Decorations |
| Text | font, cỡ, màu, giãn dòng, kích thước tối thiểu, nhịp gõ chữ |
| Speaker Name | hiện/ẩn, vị trí (trong hộp hoặc trên mép trên), font, cỡ, màu, Name Box |
| Dialogue Visual | hiện/ẩn, kích thước, bên nào, khoảng cách, bộ lọc, Frame |
| Tail | hiện/ẩn, kiểu (màu hoặc ảnh khi panel dùng texture), kích thước, độ lấn |
| Continue Indicator | hiện/ẩn, ký tự hoặc ảnh, chuyển động khi chờ |
| Transitions | mở, đóng, đổi dòng, thời lượng, đường cong, easing |
| Advanced | Theme, transition tuỳ biến, renderer |
Decorations, Name Box và Frame là Visual Layer: ảnh hoặc style vẽ thêm lên Panel, tên hoặc portrait. Layers (n) → phía trên setting liệt kê tất cả.

Một hội thoại, một hộp khác
Chỉ còn một chuỗi phân giải, cụ thể nhất thắng:
line.dialogue_box → conversation.default_dialogue_boxextends 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 templateHai tầng, và không có gì bên dưới. Conversation khai một lần hộp nào vẽ nó; line chỉ lên tiếng khi nó là ngoại lệ, và câu ngay sau đó tự quay về mặc định mà không cần ai bảo.
Không có hộp nào ở cấp node
Node mang một collection hộp để chọn ra — không phải một giá trị nằm dưới mọi conversation để quyết hộ những gì conversation chưa nói. Chính cái tầng thừa đó là cách một ô trông như đang sống trong khi một thứ cách đó ba bước lặng lẽ thắng nó.
Conversation lưu trước khi kịp chọn hộp sẽ lùi về Classic Panel có sẵn để không crash, và validator sẽ báo. Đó là chốt chặn crash, không phải một tầng để bạn soạn dựa vào.
Lối thoát
Existing Godot Resource — đặt Panel › Appearance sang lựa chọn này rồi gán bất cứ StyleBox hay Texture2D nào, ví dụ panel trong theme UI của bạn. TalkKit vẽ nó nguyên trạng và không bao giờ sửa nó: padding được giữ ở hộp, không ghi vào resource của bạn.
theme — đã có theme UI sẵn? Gán vào và template sẽ ngừng ghi đè font, cỡ chữ và màu chữ, để theme của bạn thắng. Panel, padding và layer vẫn áp dụng: Theme tạo kiểu cho chữ, không bao giờ xoá khung.
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 templaterenderer — cho hộp mà thiết lập và layer không mô tả nổi: luồng tin nhắn, phụ đề điện ảnh, bố cục truyện tranh. Xem renderer của riêng bạn.
Vị trí là câu hỏi khác
Template không quyết định vị trí. Classic Panel bám được đỉnh đầu NPC, còn Speech Bubble ghim được vào góc màn hình — xem Vị trí.
Đuôi nhọn là bộ phận duy nhất để ý tới điều đó: ghim vào màn hình thì nó không có gì để chỉ vào nên tự ẩn đi.
Renderer của riêng bạn
Khi thiết lập không mô tả nổi thứ bạn muốn, trỏ renderer vào một scene của mình. Kế thừa TalkBoxRenderer là có sẵn typewriter, tiếng blip, xử lý input, styling, layer và anchoring; còn lại là ba accessor. Layer gắn vào phần nào mà renderer của bạn không trả về sẽ được bỏ qua.
extends TalkBoxRenderer
## Docs: /guide/dialogue-box — a renderer of your own, for the rare box the
## template settings cannot describe.
##
## Extending [TalkBoxRenderer] gives you the typewriter, voice blips, input
## handling, styling and anchoring. What is left is telling it which controls to
## drive. The built-in box is never the ceiling.
##
## This file builds its controls in code so it can stand alone in the test
## suite; in a real project they would be nodes in your own .tscn.
var _panel: PanelContainer
var _text: RichTextLabel
var _name: Label
func _ready() -> void:
_build_controls()
super._ready()
#region contract
# Four accessors, and the base class does the rest.
func get_text_label() -> RichTextLabel:
return _text
func get_name_label() -> Label:
return _name
# Required, or anchoring and transitions have nothing to move and go quietly
# inert. Return the control that represents the box itself.
func get_box_control() -> Control:
return _panel
#endregion
func _build_controls() -> void:
_panel = PanelContainer.new()
add_child(_panel)
var column := VBoxContainer.new()
_panel.add_child(column)
_name = Label.new()
column.add_child(_name)
# A RichTextLabel, because a line is rich text and the base class reveals
# it by parsed character — a plain Label would type out the BBCode.
_text = RichTextLabel.new()
_text.bbcode_enabled = true
_text.fit_content = true
_text.scroll_active = false
column.add_child(_text)get_box_control() không phải tuỳ chọn
Anchoring và transition đều tác động lên control mà nó trả về. Renderer không có nó trông vẫn bình thường mà chẳng làm gì.
Đây là đường nâng cao. Dùng bình thường không bao giờ phải tới đây.