viniciusgerevini / godot-clyde-dialogue

Clyde Dialogue Language Importer and interpreter for Godot.
MIT License
91 stars 11 forks source link

Best way of handling big amounts of flags? #47

Open ThePat02 opened 3 weeks ago

ThePat02 commented 3 weeks ago

What is the best way to handle a big amount of flags in a game that has a similar structure as DISCO ELYSIUM? Is there a way to set variables without loading them into the Resource first?

viniciusgerevini commented 3 weeks ago

Hey @ThePat02 . Not sure what's your game's structure, but here is how I went about loading game data in my game.

I haven't played Disco Elysium yet so I'm not sure exactly how the flow goes. In my game I initialize the dialogue in two steps: first I load the persisted internal dialogue data, and then I load all relevant global variables.

I'd start like this for the internal data:

var dialogue = ClydeDialogue.new()
dialogue.load_dialogue('my_dialogue')
dialogue.load_data(persisted_dialogue_data)

Then load the globals like this:


# game data is a dictionary that has the variables/flags

for v in game_data:
   dialogue.set_external_variable(v, game_data[v])

After that I started the dialogue normally dialogue.start(). However, set_external_variable can be called at any time, so in some cases I'd set variables based on events. For example, inside my dialogue I could have an event like: { trigger job_accepted }. And then in my event callback I'd do:

func _on_dialogue_event_triggered(event_name: String):
  if event_name == "job_accepted":
    # here I load any other information I need. For example, maybe accepting
    # the job generates some random prize and you want to share that:
    var reward = randi_range(10, 100) # this would be persisted somewhere
    dialogue.set_external_variable("reward", reward)

Again, this will vary depending on how you structure your game, but if you always want your dialogues to start with all the global variables available in your game, your best bet is creating a wrapper around it that initializes like I did in the first two blocks.

ThePat02 commented 3 weeks ago

So you use the same persisted_dialogue_data resource for all your dialogues?

I haven't played Disco Elysium yet

In Disco Elyisum there are a few different type of dialogue lines that can appear:

For example this line

Kim Kitsuragi - Please don't kick this trash can again.

will appear if you have kicked the trash can before. This is probably done by checking a flag trash_can_kicked or something similar. This other line can appear depending on how you built your character as

Logic [Hard: Failure] - You cannot determine any structure behind the action of the murderer.

or

Logic [Hard: Success] - The murderer is operating with a pattern. Logic - He uses the git reset command as a weapon to kill programmers. Logic - You should watch out too!

What would be the best approach to built similar dynamic systems with Clyde?

viniciusgerevini commented 3 weeks ago

You can do this in few different ways. Not sure what's the best for your game as I don't know how you are structuring stuff, but here is how I think I'd go about it.

Internal data

So you use the same persisted_dialogue_data resource for all your dialogues?

No, I keep one per .clyde file. This data contains internal information like options accessed and variations so these features remember previous choices. They are dialogue file specific.

For the internal dialogue data, I'd keep a dictionary that is updated when the dialogue ends. This is what I do in my helpers example (https://github.com/viniciusgerevini/godot-clyde-dialogue/blob/godot_4/addons/clyde/examples/examples_with_helpers/fixed_bubble/clyde_dialogue_config.gd), but using the original modules, here is an example:

Wherever you start you dialoguer you do this:

var dialogue = ClydeDialogue.new()
dialogue.load_dialogue(dialogue_name)
dialogue.load_data(persistence.dialogue_data[dialogue_name])

When the dialogue ends you do this:

var content = dialogue.get_content()

if content.type == "end":
     persistence.dialogue_data[dialogue_name] = dialogue.get_data()

In this example I'm assuming you have a global module (persistence) where you store game data, which are persisted when you save the game. That should be enough to guarantee that variations and options will work correctly in all your dialogues regardless how you organise them.

External data

For the flags and game data you described, I believe the best thing is to keep your game data as the source of truth, storing it outside clyde. You can keep that data in sync by using the external_variable_changed signal from clyde in case you change them inside the dialogue.

The things you know before starting the dialogue can be loaded like I showed you before:

for v in game_data.skills:
   dialogue.set_external_variable(v, game_data[v])

If something changes externally during the dialogue you can also use dialogue.set_external_variable to update the information.

That's how you would let the dialogue know that trash_can_kicked happened or that the character has a given skill.

Hopefully this doesn't sound too vague. I need to play the game to understand this mechanics as this is not the first time people ask me something about it :D

ThePat02 commented 3 weeks ago

If I understand correctly now, there is no way to query outside data directly. You need to load everything inside the dialogue before starting?

viniciusgerevini commented 3 weeks ago

there is no way to query outside data directly

That is correct. Not as of now. I was playing with the idea of providing a data fetching callback for external variables, but I didn't go ahead with it. I'll revisit the idea

You need to load everything inside the dialogue before starting?

No necessarily before starting, but before you reach the content that uses it.

Your question made me see that this can be improved indeed. When I implemented external variables it was mostly to solve the issue of duplicated data being stored, but the data fetching aspect is lacking. I'll look into it next. But as of today things need to be loaded prior to the usage.

ThePat02 commented 3 weeks ago

Can you point me into direction of what would need to be changed in order to access data without preloading first. Maybe can modify Clyde for my personal use.

viniciusgerevini commented 3 weeks ago

I've already started working on it. This branch has an working implementation: https://github.com/viniciusgerevini/godot-clyde-dialogue/tree/external_variable_fetching

You can use it or fork from it.

I removed the methods to deal with external variables and now they are just a proxy for external data.

You can find usage examples here: https://github.com/viniciusgerevini/godot-clyde-dialogue/blob/external_variable_fetching/addons/clyde/examples/simple_example/example.gd#L21-L22

Or in the usage docs: https://github.com/viniciusgerevini/godot-clyde-dialogue/blob/external_variable_fetching/USAGE.md#external-variables

The idea is that when setting up the dialogue you can set two methods to proxy external data requests. Like this:

_dialogue.on_external_variable_fetch(func (variable_name: String):
  return persistence.get(variable_name)
)

_dialogue.on_external_variable_update(func (variable_name: String, value):
  persistence.set(variable_name, value)
)

With this, if you do something like:

Kim Kitsuragi: Please don't kick this trash can again. { @trash_can_kicked }

The value from persistence["trash_can_kicked"] will be used.

My examples always use dictionaries returning the actual value, but it doesn't have to be like this. Using a "skills" scenario for instance, instead of exposing skills as a string it's possible to implement checks like this:

_dialogue.on_external_variable_fetch(func (variable_name: String):
  if variable_name.begins_with("has_skill_"):
      return _check_if_player_has_skill(variable_name)
  return persistence.get(variable_name)
)

And in your dialogue you do something like:

Can you fix this for me? { @has_skill_hacker }

I think this provides the foundation blocks for runtime data retrieval. I still need to make it work in the editor player, but this implementation should work already in-game.

ThePat02 commented 2 weeks ago

This looks like a great addition! It is a really easy and customizable solution! I like it!