Skip to content

Giải phẫu hộp thoại ​

Anatomy Map: mọi phần của hộp đều có nhãn, cùng tám vị trí có thể đặt trang trí

Đây là Anatomy Map trong workspace Dialogue Boxes. Muốn xem trên hộp của mình, bấm ◎ Anatomy Map phía trên preview:

  • Rê chuột lên một phần để xem đó là gì.
  • Bấm vào một phần để mở setting của nó.
  • Bấm vào một ô vuông để thêm trang trí ở vị trí đó.

⛶ Expand mở bản đồ lớn như ảnh trên.

Content Slot ​

Content Slot là một phần của hộp: cái này là gì?

SlotLà gìNhóm setting
PanelVùng nền chính của hộp.Panel
Speaker NameTên người đang nói.Speaker Name
Dialogue VisualChân dung, avatar hoặc bán thân của người nói.Dialogue Visual
TextCâu thoại.Text
Continue IndicatorHiện khi người chơi có thể đi tiếp.Continue Indicator
TailChỉ hộp về phía người nói khi hộp đi theo nhân vật.Tail

Mỗi slot có đúng một nhóm trong setting của hộp, và skin của slot nằm luôn trong nhóm đó. Không có màn "skin" riêng nào phải đi tìm.

Visual Layer, Depth và Position ​

Visual Layer là phần trang trí thêm cho một slot, ngoài setting cơ bản của nó: name box, khung portrait, hoa văn góc, dải viền, nền giấy. "Layer" ở đây không liên quan gì đến CanvasLayer của Godot.

  • Layer gắn được vào Panel, Speaker Name và Dialogue Visual. Text, Tail và Continue Indicator có setting riêng, không dùng layer.
  • Mỗi layer có Depth: Behind content (nền giấy dưới chữ) hoặc Above content (khung vàng đè lên). Trong cùng một depth, layer đứng sau trong danh sách sẽ nằm trên.
  • Mỗi layer có Position trên slot của nó: Fill, một cạnh (Top, Bottom, Left, Right) hoặc một góc (Top Left, Top Right, Bottom Left, Bottom Right).

Position không phải Placement

Position là chỗ một món trang trí nằm trên hộp. Placement là chỗ cả hộp nằm trên màn hình: đi theo nhân vật, hoặc ghim vào một góc màn hình. Hai khái niệm này không bao giờ lẫn vào nhau.

Panel, name box, frame và mọi layer đều là "box", và dùng chung một bộ chỉnh Appearance: cùng bốn lựa chọn, và với cùng một lựa chọn thì cùng một bộ control. Mỗi cái vẽ một resource Godot bình thường, và editor ghi rõ kiểu của nó:

Visual SourceLưu dưới dạngHợp với
Flat StyleStyleBoxFlatnền màu, viền, bo góc
Nine-slice TextureStyleBoxTexturekhung co giãn, panel pixel
ImageTexture2D hoặc AtlasTexturehoa văn, cạnh, huy hiệu, lớp phủ, panel chỉ là một ảnh
Existing Godot Resourcebất kỳ StyleBox hay Texture2D nàothứ dự án bạn đã có sẵn

Flat Style, rồi khung nine-slice, rồi thêm layer

Hộp thoại flat ​

Trước → cấu hình → kết quả: tạo hộp mới → Panel › Appearance: Flat Style, chọn màu nền, viền, bo góc → ra một panel gọn gàng. Đây là mặc định, và không cần gì khác trên trang này.

gd
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

Hộp thoại nine-slice ​

Dùng khung mua từ asset pack: Panel › Appearance: Nine-slice Texture, chọn ảnh, rồi đặt Nine Slice Margins. Các margin được vẽ thành đường kẻ ngay trên ảnh, nên bạn thấy rõ góc nào được giữ nguyên không bị kéo giãn.

  • Nine Slice Stretch lấp cạnh và phần giữa bằng Stretch, Tile hoặc Tile Fit.
  • Nine Slice Scale vẽ pixel art ở ×2, ×3 hoặc ×4. Đặt Image Filter là Pixel (Nearest) thì pixel giữ cạnh sắc ở mọi tỉ lệ.
gd
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

Padding ​

Chỉ có một padding, và editor ghi rõ con số nào đang áp dụng:

  • Flat Style và Image không có margin riêng, nên bạn đặt padding trực tiếp.
  • Nine-slice Texture và Existing Godot Resource dạng StyleBox mặc định Use style margins, hiển thị chỉ đọc, ví dụ 24 · 24 · 24 · 24 px với margin 8 px ở ×3. Chọn Custom để ghi đè.

Dòng Padding y hệt cũng có ở name box và frame, nơi nó là khoảng trống quanh tên hoặc quanh portrait. Padding không bao giờ được ghi vào StyleBox của bạn, nên một resource dùng chung vẫn giữ nguyên.

Speaker Name: nằm trong hộp hoặc trên mép trên ​

Speaker Name › Placement đặt tên Inline, phía trên chữ thoại, hoặc On top edge, vắt ngang viền trên của Panel, có căn lề và offset.

Muốn có một hộp nền sau tên, chọn Speaker Name › Name Box › + Add Name Box. Hộp dài ra theo tên, hộp nine-slice co giãn gọn, và tự ẩn khi tên bị ẩn.

Khi chỉ có một name box, bạn chỉnh nó ngay trong nhóm Speaker Name, bằng đúng bộ chỉnh Appearance mà Panel dùng. Có từ hai cái trở lên thì nhóm hiện số lượng, và Open in Layers mở chúng ra.

Khung portrait ​

Dialogue Visual › Frame › + Add Frame thêm một layer lên portrait. Chọn Above content để vẽ viền đè lên portrait, hoặc Behind content để làm tấm nền phía sau. Portrait vẫn là visual của người nói; khung chỉ trang trí, và tự ẩn khi câu thoại không có visual. Giống name box, khi chỉ có một khung thì chỉnh nó ngay trong nhóm Dialogue Visual.

Tail và Continue Indicator ​

Tail:

  • Trên panel flat, tail tự lấy màu của panel.
  • Trên panel dùng texture, chọn Colour (nút Sample panel colour lấy màu của khung) hoặc Image. Tail dạng ảnh tự xoay về phía người nói.
  • Tail Overlap cho tail lấn nhẹ dưới viền, để hai phần liền thành một khối.

Continue Indicator:

  • Chọn Glyph như ▼, hoặc Image.
  • Motion thêm chuyển động khi chờ: Bounce hoặc Blink, tuỳ chọn.
  • Indicator chỉ hiện khi người chơi đã có thể đi tiếp.

Hoa văn góc và từng cạnh riêng ​

Panel › Decorations › + Add Decoration, hoặc bấm ô vuông ở góc trên Anatomy Map: Add Layer mở ra với Panel và góc đó đã chọn sẵn.

  • Góc dùng kích thước gốc của ảnh và đặt tâm ngay tại góc, nên tự lòi ra ngoài. Offset dời vị trí, Scale giữ pixel art sắc nét.
  • Cạnh có Fit (Stretch, Tile, Tile Fit, Keep size) và Edge Inset chừa khoảng ở hai đầu, để cạnh chạy giữa hai hoa văn góc.

Phần lòi ra ngoài không bao giờ bị cắt. Khi đặt hộp lên màn hình, phần lòi ra cũng được tính, nên hoa văn không bị đẩy ra khỏi màn hình.

gd
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

Nhiều layer và thứ tự ​

Mở Layers (n) → phía trên setting để xem mọi layer, nhóm theo slot và depth. Trong danh sách, bạn có thể:

  • tick để bật/tắt layer;
  • kéo để đổi thứ tự, hoặc thả vào Behind content / Above content để đổi depth;
  • nhân bản hoặc xoá layer.

Chọn một layer thì vùng nó ảnh hưởng sáng lên trong preview. Vài quy tắc:

  • ← All settings để quay lại.
  • Layer luôn ở trên slot của nó; muốn chuyển thì đổi Slot.
  • Base Appearance nằm đầu danh sách, kèm link quay về nhóm Panel.
  • Mọi thay đổi đều là một bước undo bình thường của editor.

Nền thứ hai chỉ đơn giản là thêm một layer Fill; không có hệ thống riêng nào cho nó.

Asset dạng atlas ​

Ở bất cứ chỗ nào nhận ảnh, AtlasTexture đều dùng được và chỉ vẽ vùng của nó. Điều này áp dụng cho hoa văn, khung, cạnh, lớp phủ, tail và indicator. Khi tile, phần lặp lại là vùng đó, không phải cả atlas, nên một tấm ảnh có thể chứa cả viên ngọc, tail và mũi tên.

Thứ tự ưu tiên của Theme ​

Theme trong Advanced › Theme quyết định font, cỡ chữ và màu chữ. Khi đã gán Theme, các setting đó chuyển sang chỉ đọc, kèm ghi chú rằng Theme đang điều khiển chúng. Panel, padding và layer luôn áp dụng: Theme không bao giờ xoá khung của bạn.

gd
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

Khi nào dùng custom renderer ​

Layer dành cho skin tĩnh. Hãy dùng renderer của riêng bạn khi chính cấu trúc thay đổi, ví dụ:

  • khung chat dạng tin nhắn;
  • bố cục động kiểu Persona;
  • UI chạy bằng shader.

Custom renderer vẫn nhận layer cho những slot nó có. Màn Layers đánh dấu layer nó không vẽ được là not shown by this renderer.

Ưu tiên GDScript. Addon phát hành không có telemetry, không gọi mạng, không phụ thuộc dịch vụ AI.