derkork / godot-statecharts

A state charts extension for Godot 4
MIT License
761 stars 39 forks source link

Events with additional payload data #18

Closed DavisGoglin closed 1 year ago

DavisGoglin commented 1 year ago

I'm trying out this library for enemy behavior. The problem I ran into is that it's hard to keep track of data attached to my incoming events. I have a damage event handler with the signature:

func _on_takes_damage(hurtbox:Area3D, damage:HitDamage):

My impulse is to pass it into the state chart, and then use event handlers to change behavior based on the state, but this currently isn't possible due to send_event only taking a single argument.

Would it make sense to add a payload argument?

I'm imagining:

# Signal from the hurtbox
func _on_gets_hit(damage:HitDamage, player):
    state_chart.send_event('take_damage', {
        'damage': damage,
        'player': player,
    })

# From the statechart:
func _on_calm_event_received(event, payload):
    match event:
        'take_damage':
            apply_damage(payload['damage'].raw_damage)
            print('Player %s is a jerk' % payload['player'] )

func _on_enraged_event_received(event, payload):
    match event:
        'take_damage':
            var reduced_damage = payload['damage'].raw_damage * 0.5
            apply_damage(reduced_damage)

This would also allow processing of events like Area2D body_entered(body) through the statechart while retaining information about the triggering body.

Dangomation commented 1 year ago

bloop

statechat.sent_event() as I understand it is only used if you want to transition states through code as opposed to "event" under the "inspector" editor tab. I myself am using the statecharts state system to accompany the signals they send to a "major" or "main" script of a object/character. Somthing that would hosue character information and actions.

I.e: have a single script that manages all the signals presuming its only for a single thing.

Kinda sounds like you are doing a thing that needs something along the lines of:


extends (some node)

var who_hit_me : Collision_body2D

func _on_area_2d_body_entered(body):
    # Put your code determining if you got hit from a valid source
        if body is monster:
              who_hit_me = body
              state_chart.send_event("take_damage")

 # Below would be "take damage" state you'd put yourself in through the signal "state_entered()"
 # I wouldn't treat these "states" as a way to manage data. 
 # more-so for the ability to have a "rigid" and constant results in a flow graph kinda "pathing".

func _on_state_entered():
      # calc damage here too and call what struck you
      # do damage, activate "i frames", etc.
      entity_event_function_thing(event, value)

# heck, if you wanted to its setup to keep this "train going"
func _on_state_processing(delta)
      do_some_animation(delta, "animation name")

image

I hope I am straightforward enough to be helpful. The last note I have to say here is you could also use signals on the "monster" or whatever party that give you damage.

aka:

Just in case you missed this like I did: https://github.com/derkork/godot-statecharts/blob/main/manual/manual.md

DavisGoglin commented 1 year ago

That who_hit_me would be a valid workaround.

I was thinking of storing last_damage info, using the states to update a damage modifier, or even modifying a callable that is run on the damage DataClass before damage is applied. I'll implement one of those meanwhile since the statechart system is working so well, but keeping it along with the event seems like a more straightforward solution.

I got the impression from the demos and from issue 4, that one way to use this is to use the state's signals like state_input to run different code when in different states.

For example the jump input on the frog in the platformer demo is only run when it's in a state that can jump, or in the same demo the breaking_box only handles custom events like player_entered when the box is idle.

I don't necessarily need to change states for my example. I was thinking of a scenario where a monster takes half damage if it's enraged, or less water damage if it's covered in lava, etc., keeping the logic that depends on those states all in one place.

I did read the documentation, and it's very high quality and thorough especially when complemented with the demos.

Dangomation commented 1 year ago

Seems like you've got a good head on things. Only need that check for the transition to "enraged" for half damage and keeping track of last damage.

I guess you could just have only a function or a very simple state system. either way.

derkork commented 1 year ago

I don't necessarily need to change states for my example. I was thinking of a scenario where a monster takes half damage if it's enraged, or less water damage if it's covered in lava, etc., keeping the logic that depends on those states all in one place.

I'd think that modeling this with states is not really a good fit, especially since there is no notion of substates which are not mutually exclusive but are not truly parallel either. E.g. you can be in state "enraged" or "covered in lava" at the same time, or you could be in either one or even none of them and they are fully independent of each other. This can neither be expressed with a parallel state nor a compound state.

Also the pattern of "i execute code when i am in this state" does not really work here, even if the library had a thing to model this. Just think of a situation where you are both enraged and covered in lava and receive water damage. Your damage handling code would now be called twice (assuming we model this as a sort of parallel states) because now two event handlers would be called:

image

Or it would not handle half of the logic (if we model it as a sort of compound state) because only one of the event handlers would be called:

image

In this case your monster should probably only take 1/4 damage, which neither of these can properly calculate. I think you will need to model this with something else but states, e.g. some kind of modifier pipeline:

image

DavisGoglin commented 1 year ago

You're right, that would be an issue. I'll look at other options for that particular mechanic.

As for the original change, I'll go back to getting more used to state charts in their current form and put the change request aside for now.