Người nói
Bốn từ là đủ cho toàn bộ addon:
| Quyết định | |
|---|---|
| Conversation | nói gì |
| Speakers | ai nói |
| Dialogue Box | trình bày thế nào |
| Placement | hiện ở đâu |
Trang này là từ thứ hai.
Speaker là một danh tính hội thoại
Không phải nhân vật trong game. TalkSpeaker giữ một cái tên, một gương mặt cho hộp, và một giọng nói — hết. Không NodePath, không AnimationPlayer, không sprite thế giới.
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateNhờ vậy người kể chuyện, giọng bộ đàm, tấm biển, hay một nhân vật đang đứng ở màn còn chưa load đều là speaker bình thường. Chẳng có gì để trỏ vào cả.
TalkKit không điều khiển nhân vật của bạn
Nó không bao giờ ánh xạ speaker sang một node, không phát animation talk hay idle, không đọc trạng thái gameplay. Muốn nhân vật cử động lúc nói thì game của bạn nói ra điều đó:
$NPCTalkKit.play(&"greeting")
$AnimationPlayer.play("talk")Một dòng đó rõ ràng hơn mọi cách cấu hình, và nó để code animation của bạn ở đúng chỗ bạn nhìn thấy.
Hình nhân vật trong hộp
Dialogue visual là speaker trông thế nào bên trong hộp: một chân dung, một nửa người, hoặc SpriteFrames động.
Tách khỏi node không đồng nghĩa với tách khỏi asset. Trỏ speaker vào đúng bộ SpriteFrames mà người chơi đang dùng chính là cách để biến người chơi thành speaker:
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateTalkKit không bao giờ đi tìm $Player/AnimatedSprite2D và không sao chép trạng thái chạy của nó. Nó đọc đúng cái resource bạn đưa — nên cách này vẫn chạy với một nhân vật không hề có mặt trong scene.
Gương mặt có tên
Một speaker có thể mang nhiều gương mặt đặt tên, và câu thoại chọn một:
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateTên mà speaker không có sẽ lùi về gương mặt mặc định chứ không vẽ rỗng, nên câu viết cho một Haru đang giận vẫn chạy được với một Haru chỉ có một gương mặt.
Sao không gọi là "emotion"?
Vì một chuỗi cảm xúc tự do mà chẳng phân giải ra cái gì là một trường trông như đang sống nhưng thực ra thì không — addon này từng có đúng một trường như vậy. Tên variant hoặc phân giải ra một visual thật, hoặc lùi về mặc định. Không có khả năng thứ ba.
Nhiều người nói trong một hội thoại
Conversation khai một lần ai là người nói thường trực. 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.
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateKhông có dàn nhân vật nào phải khai báo, không có danh sách nào phải dựng trước. Nhiều người nói không phải một chế độ — nó chỉ là chuyện xảy ra khi một câu gọi tên người khác.
Hai tầng, và chỉ hai
ghi đè ở line → mặc định của conversationĐó là toàn bộ mô hình phân giải, cho speaker, cho hộp và cho vị trí như nhau. Node bên dưới giữ một collection để chọn ra; nó không phải tầng thứ ba quyết hộ những gì conversation chưa nói.
Trong editor
Workspace TalkKit hiện mặc định của conversation đúng một lần trên danh sách. Mỗi row ưu tiên chữ và chỉ hiện danh tính speaker đã phân giải thật gọn:
Defaults Haru Classic Panel Screen · Bottom
1 H Haru Defaults This road is dangerous.
2 H Haru Defaults Stay close.
3 R Ren 2 overrides Wait!
4 H Haru Defaults What is it?Bạn đọc ra ngoại lệ trong một lượt mà không lặp field kế thừa trên mọi row. Chọn câu 3 để xem Speaker, Dialogue Box và các ngoại lệ khác trong detail pane; mọi lựa chọn kế thừa đều ghi giá trị hiệu lực.
Mỗi dropdown liệt kê những gì node đang mang, cộng thêm những câu trả lời không phải resource:
| Default (Haru) | kế thừa từ conversation |
| None · narrator | không ai nói câu này |
| Haru, Ren, Player… | speaker của node |
Đây vẫn không phải rẽ nhánh
Một hội thoại là một chuỗi thẳng. Nhiều speaker làm đổi cái tên trên hộp; chúng không làm đổi cái gì chạy tiếp theo. Rẽ nhánh vẫn nằm đúng chỗ cũ: game của bạn trả lời một event chặn rồi gọi play() lần nữa.
Chọn, chứ không sửa
Conversation và line chỉ chọn speaker hoặc hộp. Không cái nào sửa chúng. Tạo, nhân bản, chia sẻ và xoá diễn ra trong section Speakers và Dialogue Boxes của workspace. Mỗi manager có list, vùng sửa tập trung, runtime preview, scope và usage. Inspector chỉ tóm tắt collection và mở workspace.
Vì vậy phần Overrides của một line chỉ để chọn, không hơn. Trong pane detail của line, Speaker là dropdown gồm Inherit · Haru (default của conversation), None · narrator và chính các speaker mà node đang mang; Dialogue Box là Inherit cùng các hộp của node. Muốn dùng thứ node chưa mang thì thêm nó trong manager trước. Hàng Defaults của conversation là nơi duy nhất có Browse…, để chọn speaker hoặc hộp lưu ở bất kỳ đâu trong project.
Chọn Ren cho một câu là việc cục bộ; còn Ren thì không. Sửa Ren trong manager Speakers sẽ đổi mọi line và mọi NPC trỏ vào resource đó — danh sách usage của manager cho biết là những chỗ nào.
Ranh giới này là quy tắc chứ không phải tiện nghi: nếu cùng một hộp có thể sửa đầy đủ từ manager và từ một line, thì màn hình thứ hai chỉ là bản sao của màn hình thứ nhất và sẽ lặng lẽ lệch khỏi nó. Muốn một biến thể đỏ của Speech Bubble? Nhân bản nó trong manager, đổi tên, rồi chọn nó ở line.
Speaker tạo trong manager không cần speaker_id: thứ nào trỏ vào nó là trỏ thẳng vào resource. Id là khoá lưu trữ, chỉ bắt buộc khi speaker được đưa vào kho dùng chung, nơi nội dung khác gọi nó bằng tên.
Không ai nói
Không có speaker nào cũng là một cấu hình hoạt động được, không phải thiếu sót:
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateChữ vẫn gõ, event vẫn bắn. Dòng tên và hình nhân vật chỉ để trống — đúng như một tấm biển, một người kể chuyện, hay một thông báo hệ thống nên trông như vậy.
Thoại không cần soạn trước
say() là con đường còn lại để nội dung đến được hộp. Nó nhận một chuỗi, một TalkLine, hoặc một dãy những thứ đó:
extends Node
## Docs: /guide/speakers — who is talking, how they look, how they sound.
func _ready() -> void:
_build()
#region speaker
# A speaker is a dialogue identity. No NodePath, no AnimationPlayer, no world
# sprite — so a narrator, a radio voice, or someone standing in a level that is
# not even loaded are all ordinary speakers.
func make_a_speaker() -> TalkSpeaker:
var haru := TalkSpeaker.new()
haru.speaker_id = &"haru"
haru.display_name = "Haru"
haru.visual = TalkDialogueVisual.new()
haru.visual.texture = preload("res://addons/npc_talkkit/icons/talk_speaker.svg")
haru.voice_blip = AudioStreamWAV.new()
return haru
#endregion
#region reuse
# Separate from the node is not the same as separate from the asset. Pointing a
# speaker at the very SpriteFrames the player already uses is the intended way
# to make the player a speaker — TalkKit never looks for the player node.
func reuse_the_game_art(frames: SpriteFrames) -> TalkSpeaker:
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
player.visual = TalkDialogueVisual.new()
player.visual.sprite_frames = frames
player.visual.animation = &"idle"
return player
#endregion
#region variants
# Named faces, not a free-text mood. A variant the speaker does not have falls
# back to their default face rather than drawing nothing.
func add_an_angry_face(haru: TalkSpeaker, angry: Texture2D) -> void:
var variant := TalkDialogueVisual.new()
variant.texture = angry
haru.variants[&"angry"] = variant
func shout_this_line(line: TalkLine) -> void:
line.visual_variant = &"angry"
#endregion
#region multi
# One conversation, several speakers, one straight sequence. The conversation
# says once who normally speaks; a line says so only when it is the exception,
# and the line after it goes back to the default without being told to.
func a_conversation_with_three_people(
haru: TalkSpeaker, guard: TalkSpeaker, player: TalkSpeaker, box: TalkBoxTemplate,
) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"gate"
conversation.default_speaker = haru
conversation.default_dialogue_box = box
for pair in [[null, "Who are you?"], [guard, "Step away."],
[player, "I am only passing through."], [null, "Let him go."]]:
var line := TalkLine.new()
if pair[0] != null:
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = pair[0]
line.text = pair[1]
conversation.lines.append(line)
return conversation
#endregion
#region nobody
# No speaker at all is a working configuration, not a gap. The text still
# types, events still fire; the name row and the visual just stay empty.
func a_sign_nobody_speaks(box: TalkBoxTemplate) -> TalkConversation:
var conversation := TalkConversation.new()
conversation.conversation_id = &"sign"
conversation.default_speaker = null # nobody, and that is the point
conversation.default_dialogue_box = box
var line := TalkLine.new()
line.text = "[i]The gate is barred.[/i]"
conversation.lines = [line]
return conversation
#endregion
#region say
# Nothing has to be authored anywhere. say() takes a string, a TalkLine, or a
# sequence of them, and runs them through the pipeline play() uses — same box,
# same placement, same events.
func bark(text: String) -> void:
$NPCTalkKit.say(text)
func interrupt(guard: TalkSpeaker) -> void:
var line := TalkLine.new()
line.text = "[color=#ff6b6b]Stop right there![/color]"
line.speaker_mode = TalkLine.SpeakerMode.SPEAKER
line.speaker = guard
$NPCTalkKit.say(line)
# Or name them by id, from the collections this node carries. An option is a
# selection, never a description: say() cannot build a speaker or restyle a box.
func interrupt_by_name() -> void:
$NPCTalkKit.say("[color=#ff6b6b]Stop right there![/color]",
{}, {"speaker": &"guard", "dialogue_box": &"warning"})
# The key is what counts, not the value. Leaving "speaker" out falls through to
# the Quick Say default; passing it as null says there is no speaker at all.
func narrate(text: String) -> void:
$NPCTalkKit.say(text, {}, {"speaker": null})
#endregion
func _build() -> void:
var talk := NPCTalkKit.new()
talk.name = "NPCTalkKit"
talk.dialogue_boxes = [_instant_box()]
add_child(talk)
func _verify() -> Array[String]:
var failures: Array[String] = []
var talk: NPCTalkKit = $NPCTalkKit
var haru := make_a_speaker()
var guard := TalkSpeaker.new()
guard.speaker_id = &"guard"
guard.display_name = "Guard"
var player := TalkSpeaker.new()
player.speaker_id = &"player"
player.display_name = "Player"
var box: TalkBoxTemplate = talk.dialogue_boxes[0]
talk.speakers = [haru, guard, player]
talk.quick_say_speaker = haru
talk.quick_say_dialogue_box = box
var gate := a_conversation_with_three_people(haru, guard, player, box)
talk.conversations = [gate, a_sign_nobody_speaks(box)]
talk.play(&"gate")
for expected in ["Haru", "Guard", "Player", "Haru"]:
if talk._box.get_name_label().text != expected:
failures.append("speakers: expected %s, got '%s'"
% [expected, talk._box.get_name_label().text])
talk.advance()
if talk.is_running():
failures.append("speakers: the sequence should have finished")
# A named face, and a name that does not exist.
add_an_angry_face(haru, PlaceholderTexture2D.new())
shout_this_line(gate.lines[0])
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.variants[&"angry"]:
failures.append("speakers: a named variant should be chosen")
gate.lines[0].visual_variant = &"nonexistent"
if talk.resolve_dialogue_visual(haru, gate.lines[0]) != haru.visual:
failures.append("speakers: an unknown variant should fall back, not blank out")
gate.lines[0].visual_variant = &""
# Nobody speaking.
talk.play(&"sign")
if talk.resolve_speaker(talk.conversations[1].lines[0]) != null:
failures.append("speakers: a sign must resolve to no speaker")
if talk._box.get_name_label().visible:
failures.append("speakers: and show no name row")
talk.stop()
# Reusing game art, with no reference to any node.
var frames := SpriteFrames.new()
frames.add_animation(&"idle")
frames.add_frame(&"idle", PlaceholderTexture2D.new())
var reused := reuse_the_game_art(frames)
if reused.visual.frame_texture(0.0) == null:
failures.append("speakers: an animated visual must resolve a frame")
# say(), with nothing authored, falling through to the Quick Say defaults.
var blank := NPCTalkKit.new()
var quick := _instant_box()
blank.dialogue_boxes = [quick]
blank.speakers = [haru]
blank.quick_say_speaker = haru
blank.quick_say_dialogue_box = quick
add_child(blank)
if not blank.say("Just this."):
failures.append("speakers: say() must work on a node with no conversations")
if blank._box.get_name_label().text != "Haru":
failures.append("speakers: say() should fall through to the Quick Say speaker")
# ...and an explicit option beating it, including "explicitly nobody".
blank.stop()
blank.say("Three days later…", {}, {"speaker": null})
if not blank._box.get_name_label().text.is_empty():
failures.append("speakers: an explicit null speaker must mean narrator")
blank.stop()
blank.queue_free()
return failures
## Typing speed lives on the dialogue box, so a scripted run hands the node a
## box that types instantly.
func _instant_box() -> TalkBoxTemplate:
var template := TalkBoxTemplate.new()
template.typewriter_speed = 0.0
return templateMột node không có hội thoại nào được lưu vẫn là node hoạt động được. say() cũng không phải engine phát thứ hai: nội dung trở thành một hội thoại không được lưu và chạy qua đúng đường ống mà play() dùng, nên hộp, vị trí, event và quy tắc blocking đều y hệt.
Rich text
Một câu thoại là rich text, trong hội thoại soạn sẵn lẫn trong say():
$NPCTalkKit.say("[color=red][b]Careful![/b][/color] Turn back.")Máy chữ hiện ra chữ, không bao giờ hiện thẻ — một câu đang gõ dở không bao giờ để lộ [colo. Toàn bộ từ vựng là BBCode của chính Godot; TalkKit không thêm ngôn ngữ đánh dấu nào của riêng nó, vì lệnh kể chuyện nội tuyến thuộc về một hệ thống kể chuyện, không thuộc về một cái hộp vẽ chữ.
Cái gì thuộc về đâu
| Speaker | Dialogue Box | |
|---|---|---|
| Tên hiển thị | ✅ cung cấp | ✅ quyết định có hiện không |
| Gương mặt | ✅ cung cấp | ✅ quyết định kích thước, bên nào, có hiện không |
| Tiếng blip | ✅ âm thanh | — |
| Tốc độ gõ | — | ✅ |
| Màu, font, padding | — | ✅ |
Speaker cung cấp dữ liệu; hộp cung cấp chỗ chứa và quy tắc vẽ. Cả hai không cần biết nhiều về nhau — nhờ vậy một speaker dùng được trong mọi hộp bạn có.