How can I improve my dialog system?

Godot Version

Godot 4.2

Question

Hi,

I am currently trying to build my first ever game. I had some great progress in the first few weeks, but now I want to improve some things that are annoying me. My major pain point right now is the dialog system.

In my project I have NPCs that the player can interact with if they come close. To achieve this I use the body_entered and body_exited signals in the NPC’s script like this:

func _on_worker_talking_area_body_entered(body):
  if body.name == "Player":
	$BubbleAnimation.show()
	$BubbleAnimation.play("on")
	$DialogLayer/Dialog/Panel/DialogText.text = approach_text		
	$DialogLayer.show()
	
	
func _on_worker_talking_area_body_exited(body):
  if body.name == "Player":
	$BubbleAnimation.stop()
	$BubbleAnimation.hide()
	$DialogLayer.hide()`

This works nicely, so that when the player comes near an NPC a little bubble appears. Now the player can interact using different buttons (“space”, “y” and “n”) to trigger a dialog depending on the state of the NPC. I get these inputs like this:

func _input(event: InputEvent):
  if event.is_action_pressed("interaction") and $BubbleAnimation.visible:
	if worker_name == "welcome_worker" and state == 0:
		await $DialogLayer.split_text(get_current_text())
		state = 1
		state_changed.emit()
	
	if (worker_name == "crane_worker" or worker_name == "milling_machine_worker") \
	and state == 1:
		await $DialogLayer.split_text(get_current_text())
		state = 2 
	
	if (worker_name == "crane_worker" or worker_name == "milling_machine_worker") \
	and state == 2:
		await $DialogLayer.split_text(get_current_text())		
	
  if event.is_action_pressed("no") and state == 2 and $BubbleAnimation.visible:
	state = 3
	await $DialogLayer.split_text(get_current_text())
	state_changed.emit()

  if event.is_action_pressed("yes") and state == 2 and $BubbleAnimation.visible:
	state = 4
	await $DialogLayer.split_text(get_current_text())
	state_changed.emit()`

The text is then displayed in the DialogLayer which displays it by scrolling letter by letter:

func split_text(input_text: String) -> void:
   $Dialog.show()
   var text_array = input_text.split("/")
   for text_piece in text_array:
	  scroll_text(text_piece)
	  await get_tree().create_timer(5.5).timeout
   $Dialog.hide()

func scroll_text(input_text: String) -> void:
   var text = input_text
   var visible_characters = 0
   for i in text.length():
	  visible_characters += 1
	  await get_tree().create_timer(0.1).timeout
	  $Dialog/Panel/DialogText.text = text.left(visible_characters)`

Now there are several things that don’t work properly here:

  • The player can leave the “talking area” while the text is scrolling, which will stop displaying the text, but the scrolling continues without the player seeing it.
  • It is super annoying to wait for 5.5 seconds for each line of dialog, if that line of dialog is super short. So it would be nice to give the player the ability to skip to the end using space (“interaction”).
  • Sometimes this whole set-up seems to fail completely for reasons I just can’t figure out. In this case the dialog just disappears suddenly.

So I was wondering how could I set up such a dialog system properly? What are some guidelines or tips how to get this working? I would really apreciate every input, as I am quite stuck right now.

My 2 cents? Don’t do scrolling text. Ever. It’s extremely annoying, especially for the modern ADHD audience. Auto-resizing dialogue bubble is pretty easy to do and more than worth the effort, so you can display entire paragraph at a time, instantly.

And yes, skip button is a must.

In general - think of a game whose dialogue system you enjoyed the most and just copy those principles.

As far as your code (if you do stick with scrolling): a timer of 0.1 between each character makes sense in general, but hard-coding 5.5 for each line makes no sense, as text length can vary. Instead, have a class-wide bool like is_dialog_active, store the full dialog to show in (in your case, the “text_array”) in another class-wide variable, index of current dialogue line in yet another class-wide variable, and then have scroll_text emit a signal when it’s done scrolling. The func that reacts to this signal just takes the dialogue text stored in the class-wide variable, pulls the next line out of it, and feeds that to scroll_text.

You need to be VERY careful when using await in loops. The state of your game could change for many reasons while this loop is ongoing. So after each await, you need to re-check if the conditions for the action that’s supposed to occur after the wait are still valid. For example, in your case, after await get_tree().create_timer(0.1).timeout you want to check if is_dialogue_active is still true. If not, exit the loop.

Ofc same check should be after await get_tree().create_timer(5.5).timeout IF you keep that line (which I recommend that you do not).

Lastly, on an unrelated note, I’d very, very strongly advise against using scene tree paths inside methods (i…e anything that starts with $). A) it’s not efficient for performance, but most importantly, B) if you move any nodes, you’ll have to change things in many places. Instead, ctrl (or cmd on mac) + drag a node from scene tree to top of your class file, and it will auto-create ‘@onready var blah_variable = $path/nice_node’ line for you. Now you have it in one place and the paths are processed only once on load (and not every time you refer to them).

Hope this helps, cheers!

1 Like

Thanks for your reply. I ended up using Dialogic. But I guess there is a lot of other stuff to improve as well :smiley:

1 Like