Inefficient dialog system

Godot Version

4.3

Question

edit: I’ve solved the problem. I can’t delete the post, I guess, but I no longer need help.

I’m still very new to Godot (and programming in general), so please forgive any misuse of terminology or sloppy code.
I have a dialog system–the kind with a text box at the bottom of the screen. It technically works in action, but I think the way I programmed it is very inefficient for something that I would have to make hundreds of times, and I’m struggling to make it easier to ‘mass-produce’.
Each instance of dialog takes two scripts attached to two different objects-- one for the hurtbox of the object the player interacts with, and another that spawns in the dialog box and loads the text.

I implement each ‘conversation’ (from interaction to the end of speaking) by copying a script I pre-wrote for both the hurtbox and dialog-box, then changing certain parts to fit the node-path.

Below is the script for the hurtbox:

extends Area2D
# The hurtbox that will load the dialog when the player interacts with it.
@onready var dialogCHANGETHIS2 = preload('res://dialogs/CHANGETHIS').instantiate()
# Load the dialog you want to play. Change the quotations. Change the name of the variable and dialog path in quotations.
@onready var camera = get_tree().get_root().get_node("world/CanvasLayer")
@onready var player = get_tree().get_root().get_node("world/player")
# The path will vary depending on the name of the greater scene. Do '[main scene]/player'.
@onready var animations = get_tree().get_root().get_node("world/player/AnimationPlayer")
# Gets the animation player for player stoppage. Make sure the path is correct.
var speaking = false
#  Determines whether the player is currently reading the dialog.

func _on_area_entered(area):
	if speaking == false:
		load_dialog()
#  Loads the dialog when the player's interaction hitbox collides with this object's hurtbox.

func load_dialog():
# Please remember to attach the node function.
	camera.add_child(dialogCHANGETHIS2)
	speaking = true
# Change the name of the variable. Spawns in dialog.
	animations.play(player.DIRECTION)
	animations.advance(0)
	animations.active = false
	global.canmove = false
# Stops the player and animations

func done_dialog():
	camera.remove_child(dialogCHANGETHIS2)
	speaking = false
	animations.active = true
	global.canmove = true
# This removes the dialog from the screen and allows the player to re-interact with it. Also allows the player to move.

Below is the script for the dialog box:

extends Control
# The y value for box position is 91. This puts it bottom center of the screen.

@onready var CHANGETHIS1 = get_tree().get_root().get_node('/root/world/CHANGETHIS')
# This is changed to whatever the node path for the hurtbox is.

var dialog = [
	'I\'m talking right now.',
	'As it turns out, I am still talking.',
	'Here is a third line. I will make this one longer, to test dialog scroll speed.'
	]
	# This dialog is to be changed to whatever the desired dialog is. The page corresponds to the instance of dialog.

var portrait = [
	'0',
	'2', 
	'3', 
]
#Each portrait is matched with the same page number of dialog. Please continue to mark the hashtags with descriptions of...
#...the portrait to avoid confusion.
# 0: Neutral
# 1: Confounded
# 2: Neutral, eyes averted
# 3: Shine, curious
# 4: Weak smile
# 5: shocked/interested, 'wow'

var page = 0
# the page variable corresponds to the instance of dialog. Dialog and portrait both advance with the page.
var finished = false
# finished corresponds to whether the text for the page is done displaying. It is only whether the PAGE is finished.
var fullyfinished = false
# fullyfinished is the actual "finished" for dialog. When this is true, dialog will disappear.
@onready var text = $RichTextLabel
# Please keep this variable named 'text' for conciseness and simplicity.

func _ready():
	load_dialog()
	text.visible_ratio = 0
# Runs the load_dialog function and begins the first page at no completion when this is added as the player's child.

func _process(delta):
	if Input.is_action_just_pressed('interact') and finished == true:
		text.visible_ratio = 0
		load_dialog()
# Advances the page if the player clicks interact and the text is done scrolling.
	if finished == false and Input.is_action_just_pressed('interact'):
		text.visible_ratio += 1.0
# Instantly completes the text if the interact button is pressed while the text is not done scrolling.
	if text.visible_ratio == 1.0:
		finished = true
	else: finished = false
# Determines whether the text for the page is done scrolling.


func load_dialog():
	if page < dialog.size():
		text.parse_bbcode(dialog[page])
# The 'meat' of the text, runs the text if dialog is not run on the last page.
		if finished == false: $Timer.paused == false
# Runs the text scrolling function if it is not done scrolling.
		if finished == true: $Timer.paused == true
# Stops scrolling if text is done scrolling. Unsure if this part is necessary, but whatever
		$Portraits.frame = float(portrait[page])
# Updates portrait number as the page updates. Make sure the correct portrait set is a child, named 'Portraits'.
	else: 
		CHANGETHIS1.done_dialog()
		fullyfinished = true
# Closes dialog if the dialog is finished.
	page += 1 


func _on_timer_timeout():
	if text.visible_ratio < 1.0: text.visible_characters = text.visible_characters + 1
# Text scroll function. Reveals one character per timer tick if text is not done scrolling. Does not need to be an if.
	#elif page == x and text.visible_ratio < 1.0: text.visible_ratio = text.visible_ratio + 0.05
	# ^^ Unimportant potential line to use for "..." dialog to make the periods appear slower. 
	if fullyfinished == true:
		page = 0
		load_dialog()
		fullyfinished = false
# Resets dialog when finished so it can be played once more upon re-interaction. Not always necessary depending on dialog.
# Not using this function will play the last line of the dialog when re-interacting. This can be helpful for...
#... 'I said what I said' kinds of dialog where it needs not to be replayed.

I’d like to know if it’s possible to simplify the process of creating dialog. There are a few issues that slow down the process:

  • There are two different scripts that both need to reference each other; it would be ideal if one ‘conversation’ could be condensed into only one script/object, but I don’t know if that’s possible.
  • Working with the Canvas Layer is a bit annoying, as I’ve needed to manually set it to the right position with no visual indicator every time I make a new main scene.

If there are any possible improvements, I would love to know; I’ve been throwing myself at simplifying the process to little success.

A couple of things that might help:

  • The hurtbox script probably does not need to be on the actual hurtbox. If you change extends Area2D at the top to extends Node, then it can be attached to any kind of node. Then, you can give it a reference to the hurtbox in the form of an export variable, so you can set it via the inspector, and you can connect the signals through code. You can also reference the dialogue box node as an export variable.

It could look something like:

extends Node

@export var hurtbox : Area2D
@export var dialogue_box : Control

# ... other variables ...

func _ready():
  hurtbox.area_entered.connect(_on_area_entered)

# ... other functions ...
  • The dialog script can use an export variable for the actual dialogue lines, as well as for the hurtbox node - I think you can figure out how to this based on the example above.

Together, these changes mean that you won’t have to copy the scripts. They can be two components that you add to a character, and then you configure - in the inspector UI - their connection to each other, as well as the dialogue text.

It should even be possible to move things around so all of the “outward-facing” properties - the ones that don’t have to do with getting these two scripts specifically connected - are in one script, and then you can set it up as a single scene you can drag in as a child of any character, and configure.