derkork / godot-statecharts

A state charts extension for Godot 4
MIT License
797 stars 43 forks source link

State's metadata in state change function as parameter #115

Closed Meldanor closed 5 months ago

Meldanor commented 5 months ago

TL;DR

Add a getter to the state_entered and state_exited signals to easily access the meta data of State nodes.

Problem

I want to add data to states, The following is an example of an entity with a three states. Think of it as an enemy or boss with multiple stages. The state machine has three states first, second, third. Every state changes the attributes dealing_damage and damage_resistance. Imagine that state a state change could also change the animation, the texture or other nodes of the entity itself.

Disclaimer: The example is a simplified version of a problem I've encountered and I think other devs will encounter. Such a simple version does not need the power of state machines. My suggestion targets more complex Nodes but I want to keep this issue simple and clear to understand.

The current implementation of this addon leads to this code:

extends Node2D

var dealing_damage := 10.0
var damage_resistance := 1

# The entity has the signals connected via the UI instead in the _ready function

func _on_first_state_entered() -> void:
    dealing_damage = 20.0
    damage_resistance = 2

func _on_second_state_entered() -> void:
    dealing_damage = 30.0
    damage_resistance = 2

func _on_third_state_entered() -> void:
    dealing_damage = 40.0
    damage_resistance = 2

This is a bit static and inflexible to use. If I want a new state, I have to add it and then change the code to support it. The different function only differ in state dependent attributes.

Based on the discussion in https://github.com/derkork/godot-statecharts/issues/85 I've found a solution to use the nodes metadata.

But the function itself does not know which states is changed to or exists from. We have to use to Callable.bind(...) method in the _ready() method of the node. While this helps me in certain cases, it is not much more flexible because of the initial connection. Adding a new state also changes my code which I want to avoid.

Suggestion

Make the State node or its meta data available in the signal itself:

extends Node2D

var dealing_damage := 10.0
var damage_resistance := 1

# The entity has the signals connected via the UI instead in the _ready function
# Each state node has Metadata set in the editor

# Version 1 with metadata as a dictionary
func _on_state_entered_1(metadata: Dictionary) -> void:
    dealing_damage = metadata.get("Damage")
    damage_resistance = metadata.get("Resistance")

# Version 2 with metadata as a dictionary
func _on_state_entered_2(state: State)-> void:
    dealing_damage = state.get_meta("Damage")
    damage_resistance = state.get_meta("Resistance")

Version 1 is a bit more complicated because we would have to construct the dictionary itself before providing it in the method. Object.get_meta_list() only returns a list of keys for the metadata, but does not provide a Dictionary. This could lead to an unnecessary performance impact - especially when the user does not need this feature.

Version 2 is a bit easier but has one advantage and one disadvantage. It enables us to access childs of the state. The user could add nodes as childs to the node like Sprites. The disadvantage is that is contradicts the approach of a state machine to not expose the states itself. The user could abuse the tool and try to access the state methods.

What to you think?

derkork commented 5 months ago

I think in this case it's probably better to connect the signals via code. I'd put all states that have interesting meta in a group (e.g. "states_with_meta") and then run something like this on _ready:

func _ready():
   for state_with_meta in get_tree().get_nodes_in_group("state_with_meta"):
       state_with_meta.state_entered.connect(_on_state_entered_2.bind(state_with_meta))

func _on_state_entered_2(state: State)-> void:
     ...        

Then you can add arbitrary many states with meta information to your state chart, just put them in the group and they will be connected automatically to your function which is arguably less clicking than in the editor as well. As an additional plus this does also not require any breaking changes to the library 😄

Meldanor commented 5 months ago

Thank you for the answer and an alternative solution to my problem. I forgot that my proposal would introduce a breaking change - which should be avoided. This issue can be closed :)

derkork commented 5 months ago

Awesome, thanks for letting me know and good luck with your project!