Use your own interaction system
Most games that need dialogue already have a way to press a key on things: a raycast, an Area2D on the player, an interaction manager, a focus system for gamepads. Adding a dialogue addon should not mean adding a second one.
The short version
Call play() from wherever your interaction already resolves.
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 templateThat is the whole integration. TalkInteractionArea2D and TalkInteractionArea3D exist for projects with nothing of their own — they are a convenience, not the API.
Finding the node
Two reasonable patterns:
# The NPC exposes it
var talk: NPCTalkKit = npc.get_node_or_null("NPCTalkKit")
# Or your interactable interface returns it
var talk: NPCTalkKit = interactable.get_dialogue()Either works. TalkKit does not care how it was found, and it does not register itself anywhere you have to query.
Letting your system know it is busy
if talk.is_running():
return # do not re-trigger, do not show the "press E" promptplay() already refuses while a conversation is being read, so this is a safeguard for your own prompt UI rather than for TalkKit.
The exception is a blocking event: play() is accepted while blocked, because that is how branching works. If your interaction layer might fire during a pause, check is_blocked() too.
Player control
TalkKit will not touch your controller. It asks:
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 templateIf you already have a global "input locked" stack, push and pop it here instead of setting a boolean.
Set request_lock_on_start to false on the node if you do not want dialogue to lock the player at all — for ambient barks, say.
Ambient lines with no interaction
Nothing requires a trigger:
func _on_player_entered_market() -> void:
$Crier/NPCTalkKit.say("Fresh bread! Two coppers!")say() builds a throwaway conversation, so there is nothing to author for a one-off line.
What to connect, and what to ignore
| Signal | Connect it when |
|---|---|
request_player_lock | you want movement to stop during dialogue |
conversation_finished | something should happen afterwards |
event_triggered | lines hand things to your game |
event_blocked | a line asks a question |
request_face_target | your NPCs turn to face the player |
request_camera_focus | your camera frames the speaker |
speech_progress | you drive mouth-flap or character animation |
Connect none of them and the addon still works. That is the design.