Skip to content

Sự kiện và rẽ nhánh ​

TalkKit không có ngôn ngữ điều kiện, không có kho biến, không có trình tính biểu thức — vì game của bạn đã có đủ cả ba. Thứ nó có là một điểm nhường.

Sự kiện không chặn ​

Gán event_id cho một câu và game sẽ được báo khi câu đó kết thúc. Hội thoại chạy tiếp bình thường.

gd
extends Node

## Docs: /guide/quick-start, /guides/use-with-existing-interaction-system
##
## TalkKit asks; it never takes. Every hook below is opt-in — ignore them all
## and the addon still works, it just will not know about your player.

func _ready() -> void:
	_build()


#region signals
func _connect_talk() -> void:
	var talk: NPCTalkKit = $NPCTalkKit
	talk.request_player_lock.connect(_on_player_lock)
	talk.event_triggered.connect(_on_talk_event)
	talk.conversation_finished.connect(_on_finished)
#endregion


#region lock
# TalkKit does not own your player controller. It asks, you decide.
func _on_player_lock(locked: bool) -> void:
	_player_accepts_input = not locked
#endregion


#region event
# A non-blocking event: fire and forget, playback carries on.
func _on_talk_event(event_id: StringName, payload: Variant) -> void:
	match event_id:
		&"give_quest":
			_quests.append(payload)
		&"play_sound":
			pass
#endregion


#region existing-interaction
# Already have an interaction system? Then skip TalkInteractionArea2D entirely
# and call play() from the code you already have. That is the preferred path.
func _on_my_own_interact_pressed(npc: Node) -> void:
	var talk := npc.get_node_or_null("NPCTalkKit") as NPCTalkKit
	if talk != null and not talk.is_running():
		talk.play(&"greeting", {"player_name": _player_name})
#endregion


var _player_accepts_input := true
var _quests: Array = []
var _player_name := "Alex"


func _on_finished(_id: StringName) -> void:
	pass


func _build() -> void:
	var box := _instant_box()
	var talk := NPCTalkKit.new()
	talk.name = "NPCTalkKit"
	talk.dialogue_boxes = [box]

	var line := TalkLine.new()
	line.text = "Good to see you, {{player_name}}."
	line.event_id = &"give_quest"
	line.event_payload = "deliver_the_letter"
	var conversation := TalkConversation.new()
	conversation.conversation_id = &"greeting"
	conversation.default_dialogue_box = box
	conversation.lines = [line]
	talk.conversations = [conversation]
	add_child(talk)
	_connect_talk()


func _verify() -> Array[String]:
	var failures: Array[String] = []
	var talk: NPCTalkKit = $NPCTalkKit

	_on_my_own_interact_pressed(self)
	if not talk.is_running():
		failures.append("integration: play() from host code did not start")
	if _player_accepts_input:
		failures.append("integration: the lock request never reached the host")

	# A second press must not restart what the player is reading.
	_on_my_own_interact_pressed(self)

	talk.advance()
	if not _quests.has("deliver_the_letter"):
		failures.append("integration: the non-blocking event never arrived")
	if not _player_accepts_input:
		failures.append("integration: the lock was never released at the end")
	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

Dùng cho hiệu ứng âm thanh, rung camera, trao nhiệm vụ — bất cứ thứ gì không cần câu trả lời.

Blocking event: chỗ rẽ nhánh ​

Tích event_blocking cho một câu thì hội thoại dừng lại cho tới khi game trả lời. Dùng signal riêng, cố ý như vậy, để không thể bỏ sót việc cần phản hồi.

gd
extends Node

## Docs: /guide/events — player choice without a branching engine.

func _ready() -> void:
	_build()


#region listen
func _connect_talk() -> void:
	$NPCTalkKit.event_blocked.connect(_on_blocked)
#endregion


#region respond
# Playback is paused. The game decides what happens next, reading its own quest
# and inventory state — TalkKit never learns what a quest is.
func _on_blocked(event_id: StringName, payload: Variant) -> void:
	if event_id != &"offer_job":
		return
	var accepted: bool = _player_has_room_for(payload)
	$NPCTalkKit.play(&"accepted" if accepted else &"declined")
#endregion


#region resume
# The other two answers: carry on where it paused, or end the conversation.
func _keep_going() -> void:
	$NPCTalkKit.resume()


func _walk_away() -> void:
	$NPCTalkKit.stop()
#endregion


func _player_has_room_for(_payload: Variant) -> bool:
	return true


func _build() -> void:
	var box := _instant_box()
	var talk := NPCTalkKit.new()
	talk.name = "NPCTalkKit"
	talk.dialogue_boxes = [box]

	var ask := TalkLine.new()
	ask.text = "Carrying a message to the mill pays three silver. Take it?"
	ask.event_id = &"offer_job"
	ask.event_blocking = true
	var offer := TalkConversation.new()
	offer.conversation_id = &"offer"
	offer.default_dialogue_box = box
	offer.lines = [ask]

	var yes := TalkLine.new()
	yes.text = "Good. The mill before dusk."
	var accepted := TalkConversation.new()
	accepted.conversation_id = &"accepted"
	accepted.default_dialogue_box = box
	accepted.lines = [yes]

	var no := TalkLine.new()
	no.text = "Suit yourself."
	var declined := TalkConversation.new()
	declined.conversation_id = &"declined"
	declined.default_dialogue_box = box
	declined.lines = [no]

	talk.conversations = [offer, accepted, declined]
	add_child(talk)
	_connect_talk()


func _verify() -> Array[String]:
	var failures: Array[String] = []
	var talk: NPCTalkKit = $NPCTalkKit
	var spoken: Array[String] = []
	talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))

	talk.play(&"offer")
	talk.advance()
	if spoken.size() != 2 or not spoken[1].begins_with("Good."):
		failures.append("blocking: the branch did not play, got %s" % str(spoken))
	if talk.is_blocked():
		failures.append("blocking: still blocked after branching")
	talk.stop()

	# resume() continues the conversation that paused.
	var second := TalkLine.new()
	second.text = "Dusk, remember."
	talk.conversations[0].lines.append(second)
	talk.event_blocked.disconnect(_on_blocked)
	spoken.clear()
	talk.play(&"offer")
	talk.advance()
	if not talk.is_blocked():
		failures.append("blocking: advancing past a blocking line must pause")
	talk.resume()
	if spoken.size() != 2:
		failures.append("blocking: resume() did not continue, got %s" % str(spoken))
	talk.stop()
	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
gd
extends Node

## Docs: /guide/events — player choice without a branching engine.

func _ready() -> void:
	_build()


#region listen
func _connect_talk() -> void:
	$NPCTalkKit.event_blocked.connect(_on_blocked)
#endregion


#region respond
# Playback is paused. The game decides what happens next, reading its own quest
# and inventory state — TalkKit never learns what a quest is.
func _on_blocked(event_id: StringName, payload: Variant) -> void:
	if event_id != &"offer_job":
		return
	var accepted: bool = _player_has_room_for(payload)
	$NPCTalkKit.play(&"accepted" if accepted else &"declined")
#endregion


#region resume
# The other two answers: carry on where it paused, or end the conversation.
func _keep_going() -> void:
	$NPCTalkKit.resume()


func _walk_away() -> void:
	$NPCTalkKit.stop()
#endregion


func _player_has_room_for(_payload: Variant) -> bool:
	return true


func _build() -> void:
	var box := _instant_box()
	var talk := NPCTalkKit.new()
	talk.name = "NPCTalkKit"
	talk.dialogue_boxes = [box]

	var ask := TalkLine.new()
	ask.text = "Carrying a message to the mill pays three silver. Take it?"
	ask.event_id = &"offer_job"
	ask.event_blocking = true
	var offer := TalkConversation.new()
	offer.conversation_id = &"offer"
	offer.default_dialogue_box = box
	offer.lines = [ask]

	var yes := TalkLine.new()
	yes.text = "Good. The mill before dusk."
	var accepted := TalkConversation.new()
	accepted.conversation_id = &"accepted"
	accepted.default_dialogue_box = box
	accepted.lines = [yes]

	var no := TalkLine.new()
	no.text = "Suit yourself."
	var declined := TalkConversation.new()
	declined.conversation_id = &"declined"
	declined.default_dialogue_box = box
	declined.lines = [no]

	talk.conversations = [offer, accepted, declined]
	add_child(talk)
	_connect_talk()


func _verify() -> Array[String]:
	var failures: Array[String] = []
	var talk: NPCTalkKit = $NPCTalkKit
	var spoken: Array[String] = []
	talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))

	talk.play(&"offer")
	talk.advance()
	if spoken.size() != 2 or not spoken[1].begins_with("Good."):
		failures.append("blocking: the branch did not play, got %s" % str(spoken))
	if talk.is_blocked():
		failures.append("blocking: still blocked after branching")
	talk.stop()

	# resume() continues the conversation that paused.
	var second := TalkLine.new()
	second.text = "Dusk, remember."
	talk.conversations[0].lines.append(second)
	talk.event_blocked.disconnect(_on_blocked)
	spoken.clear()
	talk.play(&"offer")
	talk.advance()
	if not talk.is_blocked():
		failures.append("blocking: advancing past a blocking line must pause")
	talk.resume()
	if spoken.size() != 2:
		failures.append("blocking: resume() did not continue, got %s" % str(spoken))
	talk.stop()
	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

Trong lúc dừng, advance() không làm gì — hộp vẫn trên màn hình, đứng yên. Game của bạn phải chọn một trong ba:

GọiKết quả
play("other")Bỏ phần còn lại, bắt đầu hội thoại khác. Hộp được tái dùng nếu cùng renderer nên không bị nháy.
resume()Đi tiếp từ câu kế của hội thoại đang dừng.
stop()Huỷ. Phát conversation_cancelled và mở khoá người chơi.
gd
extends Node

## Docs: /guide/events — player choice without a branching engine.

func _ready() -> void:
	_build()


#region listen
func _connect_talk() -> void:
	$NPCTalkKit.event_blocked.connect(_on_blocked)
#endregion


#region respond
# Playback is paused. The game decides what happens next, reading its own quest
# and inventory state — TalkKit never learns what a quest is.
func _on_blocked(event_id: StringName, payload: Variant) -> void:
	if event_id != &"offer_job":
		return
	var accepted: bool = _player_has_room_for(payload)
	$NPCTalkKit.play(&"accepted" if accepted else &"declined")
#endregion


#region resume
# The other two answers: carry on where it paused, or end the conversation.
func _keep_going() -> void:
	$NPCTalkKit.resume()


func _walk_away() -> void:
	$NPCTalkKit.stop()
#endregion


func _player_has_room_for(_payload: Variant) -> bool:
	return true


func _build() -> void:
	var box := _instant_box()
	var talk := NPCTalkKit.new()
	talk.name = "NPCTalkKit"
	talk.dialogue_boxes = [box]

	var ask := TalkLine.new()
	ask.text = "Carrying a message to the mill pays three silver. Take it?"
	ask.event_id = &"offer_job"
	ask.event_blocking = true
	var offer := TalkConversation.new()
	offer.conversation_id = &"offer"
	offer.default_dialogue_box = box
	offer.lines = [ask]

	var yes := TalkLine.new()
	yes.text = "Good. The mill before dusk."
	var accepted := TalkConversation.new()
	accepted.conversation_id = &"accepted"
	accepted.default_dialogue_box = box
	accepted.lines = [yes]

	var no := TalkLine.new()
	no.text = "Suit yourself."
	var declined := TalkConversation.new()
	declined.conversation_id = &"declined"
	declined.default_dialogue_box = box
	declined.lines = [no]

	talk.conversations = [offer, accepted, declined]
	add_child(talk)
	_connect_talk()


func _verify() -> Array[String]:
	var failures: Array[String] = []
	var talk: NPCTalkKit = $NPCTalkKit
	var spoken: Array[String] = []
	talk.line_started.connect(func(line: TalkLine) -> void: spoken.append(line.text))

	talk.play(&"offer")
	talk.advance()
	if spoken.size() != 2 or not spoken[1].begins_with("Good."):
		failures.append("blocking: the branch did not play, got %s" % str(spoken))
	if talk.is_blocked():
		failures.append("blocking: still blocked after branching")
	talk.stop()

	# resume() continues the conversation that paused.
	var second := TalkLine.new()
	second.text = "Dusk, remember."
	talk.conversations[0].lines.append(second)
	talk.event_blocked.disconnect(_on_blocked)
	spoken.clear()
	talk.play(&"offer")
	talk.advance()
	if not talk.is_blocked():
		failures.append("blocking: advancing past a blocking line must pause")
	talk.resume()
	if spoken.size() != 2:
		failures.append("blocking: resume() did not continue, got %s" % str(spoken))
	talk.stop()
	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

Logic rẽ nhánh là GDScript bình thường trong game của bạn, đọc trạng thái nhiệm vụ và túi đồ của chính bạn. TalkKit không bao giờ biết nhiệm vụ là gì.

Giao diện lựa chọn không có sẵn

TalkKit dừng lại và báo cho bạn. Vẽ nút là việc của game — game đã biết menu của nó trông ra sao.

Sự kiện cấp hội thoại ​

Một TalkConversation có thể mang event_id riêng, phát khi hội thoại kết thúc, ngay trước conversation_finished.

Chỉ không chặn. Tới lúc đó không còn gì để dừng; một blocking event ở cuối hội thoại chính là blocking event trên câu cuối.

play() làm gì khi đang bận ​

Gọi lúcKết quả
rảnhbắt đầu
đang mở hoặc đang đọctừ chối, trả false — lần bấm tương tác thứ hai không thể khởi động lại câu người chơi đang đọc dở
đang bị chặncho phép — chính là chỗ rẽ nhánh ở trên

Nó trả về bool và không bao giờ ném lỗi, nên một lời gọi bị từ chối là thứ bạn kiểm tra được chứ không phải thứ làm game sập.

Ư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.