How to store and then pull correct dialogue?

Godot Version

4.3

Question

` I’m looking to make a farming sim, and I am totally lost on how to make a Stardew-like dialogue system. I’m using Dialogue Manager, but my core problem is finding a sane way of storing the dialogue I’ve written and then finding an equally sane way of figuring out in code how to pull from this well of dialogue nodes when I interact with an NPC.

I understand that at the end of the day it’s a conditional, but somehting like Stardew would have a huge web of conditionals that breaks my brain.

Things like… current location, current season, time of day, current affection, specific festivals, weather, whether or not you’re married, if certain events have been seen, etc. And some of these I’d want to have the possibility of overlapping (say, “six hearts of affection on a rainy summer night” having a unique dialogue).

And maybe not every character has all of the exact same dialogue opportunities, so there’d have to be fallbacks.

I just have no idea how to even begin to tackle this. Something with arrays or dictionaries, I feel like, but I’m at a loss. `

As your requirements for your dialogue system are vast and complex I am afraid there is no clear and easy solution for your problem.

The more complex the requirement the more complex the solution, but I can try to give you a few tips and directions for how I might approach a similar system.

Note that I am also unfamiliar with Dialogue Manager so I will ignore that part, my answer is however pretty generic and can probably coexist with it.

First, try to keep your code as clear as you possibly can when doing this. In complex systems it is easy to mix stuff up and have code for the same system be in a bunch of different places. I would suggest you make a function that takes 3 inputs, lets call them WorldState, CharacterState and DialogueOptions. WorldState being all the information about the current state of the world and CharacterState being all the information about the character invoking the dialogue. Then DialogueOptions are all possible dialogues along with their metadata for the invoking character. This way you can always just call this function whenever you need and pass it the relevant information, then get a clean result back. Lets call this function selectDialogue.

How you manage WorldState, CharacterState and DialogueOptions and how you pass them to this function I will leave up to you. I am just trying to paint a high level picture of a possible implementation.

Then it is clear to me that you need some sort of priority system. For example you might have 2 dialogue options, one normal one and one you want when a character is married to you. The married one should have higher priority and when its conditions are met should overrule the normal option. How you store this priority is again up to you, it could be a number stored next to the dialogue or it could be the order of dialogue options in an Array (probably better to avoid unnecessary calculations).

There are some advantages to determining the priority at runtime (with numbers or some other metadata). If you do this just try to only calculate the order once and not every time dialogue is invoked.

When called selectDialogue can go through all the options from highest to lowest in priority, checking conditions and choose the first dialogue of which conditions are met. Of course always have an option that requires no conditions as a fallback if the list is exhausted. Here I suggest you store commonly used checks (like weather, marriage status, time of day, …) as properties that are attached to a dialogue and selectDialogue knows how to check for itself.

If you require more specific logic that is maybe used for only one specific dialogue option in one character this becomes a bit more trickier as you would probably require attaching custom logic to that specific dialogue in order to decide if it is relevant. Godot does not support attaching Callables in the editor so this special options would have to all be created within code. While it is not impossible it is a bit tedious.

Same goes for some of the other requirements you might have. For example you may want to have an option of having multiple dialogues having the same priority and then choose between them at random or with some other arbitrary logic. For this you would have to modify the above solution and create a data structure with a bit more complexity, allowing a “dialogue” to store multiple actual dialogues and a way to then select one of them.

Now you can create a separate DialogueOptions for every character and when dialogue with that character is invoked you call selectDialogue passing DialogueOptions along and get the dialogue you have to show. It allows you to clearly store each dialogue tree next to the character owning it and do not multiply dialogue selection logic across your project.

I created a very basic example of this (without custom callables and multiple dialogues at the same priority level) as I know examples are the best when trying to grasp an idea (and the salad I wrote above is probably a bit hard to digest).

Fair warning, this code is not very good and is here just to illustrate an idea. I suggest you implement this yourself in a better way.

character.gd

class_name Character
extends Node

# You can safely export this and Godot editor will
# give you a nice way of editing it.
@export var options: Array[Dialogue]

# Manage this however you like
var character_state: CharacterState

func start_dialogue():
	# Here it is on you to acquire a way
	# to get a reference to world handler
	var dialogue = (get_node("WorldHandler") as WorldHandler).select_dialogue(character_state, options)
	print(dialogue.dialogue)

class CharacterState:
	var is_married: bool
	var affection_level: int

dialogue.gd

class_name Dialogue
extends Resource

# All of these should be edited inside the Godot inspector
@export_multiline var dialogue = "EMPTY"

@export var marriage_required: bool
@export var time_of_day: WorldHandler.TimeOfDay
@export var affection_threshold = 0

world_handler.gd

class_name WorldHandler
extends Node

var world_state: WorldState

enum TimeOfDay {
	Any,
	Morning,
	Day,
	Evening,
}

func select_dialogue(character_state: Character.CharacterState, dialogue_options: Array[Dialogue]) -> Dialogue:
	# Here we go through every dialogue, in my implementation lower array index
	# means higher priority. (As I am checking in order)
	for d in dialogue_options:
		# A single unmet condition means we discard
		# this option and go check the next one.
		if d.marriage_required and !character_state.is_married:
			continue
		if d.time_of_day != TimeOfDay.Any and d.time_of_day != world_state.time_of_day:
			continue
		if d.affection_threshold > character_state.affection_level:
			continue
		# If all check passed we chose this dialogue option
		return d
	push_error("No dialogue option met requirements, consider adding a fallback.")
	return null

class WorldState:
	var time_of_day: TimeOfDay

I hope I helped you at least somewhat. Good luck in your future game dev journey :).

1 Like

thank you for the very in depth response! I really do appreciate seeing how other people tackle my problems.

That said, I did miraculously figure something out myself. I’m sure it’s inefficient. I’m using loads of strings and concatenation shenanigans to check a dictionary (which is created by the DialogueManager plugin, but referenced by the NPC in its _ready function)

All of the current_x variables are strings that are controlled by other managers, and are only updated when I talk to an NPC.

With this system, I have to set the priority pretty much manually by organizing the if statements, but I don’t have to update anything on the character script if I add to the dialogue resource, provided I name the dialogues correctly. That said, the tree of if statements I know is liable to get crazy quickly, depending on the amount of permutations I’d like to have available to me.

func dialogueCheck():
	getTime()
	if talked_to:
		return current_season + "Bye"
	if dialogue_dict.has(current_season + current_location):
		return current_season + current_location
	if dialogue_dict.has(current_season + current_day):
		return current_season + current_day
	if dialogue_dict.has(current_season + current_weather):
		return current_season + current_weather

	else:
		return "backup"
func getTime():
	current_season = DayAndNightCycleManager.current_season
	current_day = DayAndNightCycleManager.day_string
	current_weather = DayAndNightCycleManager.current_weather
	current_location = parent.current_location