Need help with NPC's and the game world's environment's dialogue boxes. Not entirely sure how to fix.

Godot Version

Godot 4.4

Question

Struggling with how to come up with my own dialogue box system, in similar vein to Undertale and Deltarune, I watched this YouTube tutorial:

While it seems to be for an older version of Godot (Godot 3, to be specific), I followed along and managed to get it all working exactly how the video showed me to - Just a simple textbox displaying some scrolling dialogue in a _ready() function.

extends CanvasLayer

const CHAR_READ_RATE = 0.0210

@onready var textbox = $"."
@onready var textbox_container = $TextboxContainer
@onready var start_symbol = $TextboxContainer/MarginContainer/HBoxContainer/Start
@onready var end_symbol = $TextboxContainer/MarginContainer/HBoxContainer/End
@onready var label = $TextboxContainer/MarginContainer/HBoxContainer/Textbox
@onready var boxspr = $TextboxContainer/MarginContainer/HBoxContainer/TextureRect

var tween: Tween

# state machine for dialogue
enum State {
	READY,
	READING,
	FINISHED
}

var current_state = State.READY
var text_queue = []
var dialog_spr_queue = []

func _ready():
	_hide_textbox()
	
	
	
	
	

	
func _process(delta):
	match current_state:
		State.READY:
			if !text_queue.is_empty():
				display_text()
				display_sprite()
		State.READING:
			if Input.is_action_just_pressed("skip"):
				label.visible_ratio = 1
				end_symbol.text = ">"
				tween.kill()
				change_state(State.FINISHED)
				typing_dialog_sfx.stop()
		State.FINISHED:
			if Input.is_action_just_pressed("interact"):
				change_state(State.READY)
				_hide_textbox()
				
func queue_text(next_text):
	text_queue.push_front(next_text)
	
func display_sprite():
	var next_face = dialog_spr_queue.pop_back()
	boxspr.texture = next_face
	
func queue_face(next_face):
	dialog_spr_queue.push_front(next_face)


func _hide_textbox():
	start_symbol.text = ""
	end_symbol.text = ""
	label.text = ""
	textbox_container.hide()
	
func show_textbox():
	start_symbol.text = "
	
	>"
	textbox_container.show()
	
func display_text():
	tween = get_tree().create_tween()
	var next_text = text_queue.pop_back()
	label.text = next_text
	label.visible_ratio = 0.0
	change_state(State.READING)
	show_textbox()
	tween.tween_property(label, "visible_ratio", 1.0, len(next_text) * CHAR_READ_RATE)
	tween.connect("finished", on_tween_finished)
	
	
func on_tween_finished():
	end_symbol.text = ">"
	change_state(State.FINISHED)
	
func change_state(next_state):
	current_state = next_state
	match current_state:
		State.READY:
			print("Changing to be ready...")
		State.READING:
			print("Changing to be reading...")
		State.FINISHED:
			print("Changing to be finished...")
	

When I had it working, I got to implementing how it is in games usually - Talk to an NPC, object, whatever, and it’ll show some dialogue, and the conversation will end, the box disappearing.

I did manage to sort of do that. I put an Area2D on a node, and when my player character enters that node, they can press an interact button and the box and text will appear. However, I noticed the dialogue was looping, so I simply added a signal that checks if you are in the NPC’s area or not.

@onready var textbox = $"../CharacterBody2D/Camera2D2/Textbox"

var area_on = false

func _process(delta):
	pass

func _physics_process(delta):
	pass

	
func _unhandled_input(event):
	if Input.is_action_just_pressed("interact") && area_on == true:
		textbox.queue_text("Dialogue stuff.")
		textbox.queue_text("Dialogue stuff and more.")
		textbox.queue_text("And that's all I have to say.")
		area_on = false
			

func _on_area_entered(area):
	area_on = true
	print("Inside area.")
	
	

func _on_area_exited(area):
	area_on = false
	print("Out of area.")

if ’ area_on = false’ is at the end of that if statement, then the text doesn’t loop, and it simply ends. But that’s not the end of any issues – I can’t speak to the NPC again if I’m in the same spot, I’d have to move out of the NPC’s area zone and walk back in again to speak to them again. I’m aware it’s because I’m turning off the area. But I can’t help but scratch my head to try and figure out a solution to not use that ā€˜area_on’ variable.

I had tried using the _hide_textbox() function from the tutorial code to hide the textbox after the conversation would normally end. But that hides the textbox from the start of the first line the NPC would speak. So I’m not seeing any lines at all. That goes for any value – So, If I wanted to, say, after a certain line passes in the character’s conversation: Give a player a health addition, make a character dig around in their nose, do anything - It would all happen on the first line, from what I’m gathering.

I also am curious if I should be using any _input() functions for the dialogue, or even if I should be using an ā€˜if’ statement for that matter. If I were, for the sake of this question, put the dialogue all in an ā€˜_on_area_entered(area)’ signal, the dialogue does work smoothly, no looping or anything. But I don’t want that signal to make the dialogue occur.

I should also probably mention that I’m new to Godot and programming in general. I’d say I’m getting a grip on the engine as a whole, but with this specific case, I think there’s just something very crucial I’m missing. Any help I would highly appreciate.

First of all, i’d swap the name of the area check to something like is_in_area.

To get the functionality you want, you can add another boolean ā€œcan_start_dialogueā€, that you set to true when area entered, to false when dialogue starts, AND true again when your state changes from finished to ready. This way you still check if you’re in range to start dialogue, but you also allow for a restart without having to exit and enter again.

1 Like

Got it working - thank you!