Skip to content

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.

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

That 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:

gdscript
# 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 ​

gdscript
if talk.is_running():
    return    # do not re-trigger, do not show the "press E" prompt

play() 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:

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

If 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:

gdscript
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 ​

SignalConnect it when
request_player_lockyou want movement to stop during dialogue
conversation_finishedsomething should happen afterwards
event_triggeredlines hand things to your game
event_blockeda line asks a question
request_face_targetyour NPCs turn to face the player
request_camera_focusyour camera frames the speaker
speech_progressyou drive mouth-flap or character animation

Connect none of them and the addon still works. That is the design.

Reference ​

Quick start · API reference

GDScript-first. No telemetry, no network requests, no AI service dependency in the shipped addon.