Closed uzkbwza closed 11 months ago
So this is essentially the same as connecting the state_entered/state_exited signals of all child states of a compound state to the same method, just with less clicking - right? Could you maybe give an example for a case where this would be useful. This would be nice to have for the documentation of this.
I would also like to see an example on this, but the idea overall sounds good. Also I want to ask, will this be merged and added to the doc if the example proves this have a common use case?
Well yes I will merge it and update the manual when it is merged. Right now I don't understand the use case though so that makes it difficult to write a proper documentation for it.
So this is essentially the same as connecting the state_entered/state_exited signals of all child states of a compound state to the same method, just with less clicking - right? Could you maybe give an example for a case where this would be useful. This would be nice to have for the documentation of this.
Ok. For example, in my game Your Only Move Is HUSTLE (which uses a traditional state machine implementation), characters and objects have dozens of states. This is typical for a fighting game. Character states have a lot of shared logic on enter: the animation is changed, sound effects are played, the "action frame counter" is refreshed, and so on. When characters exit states, it is ensured that any state-related invulnerability is ended, hitboxes are terminated, and so on.
Here are the actual enter and exit methods that are shared for all character states.
func _enter_shared():
if force_same_direction_as_previous_state:
host.reverse_state = false
._enter_shared()
started_during_combo = false
if dynamic_iasa:
interruptible_on_opponent_turn = start_interruptible_on_opponent_turn
hit_yet = false
hit_anything = false
was_blocked = false
started_in_air = false
host.update_grounded()
if change_stance_to:
host.change_stance_to(change_stance_to)
if !host.is_grounded() or air_type == AirType.Aerial:
started_in_air = true
if uses_air_movement:
if !host.infinite_resources and host.gravity_enabled:
host.air_movements_left -= 1
if host.hitlag_ticks == 0 and host.blockstun_ticks == 0:
call_deferred("update_sprite_frame")
if has_hitboxes:
var dir = host.get_move_dir()
if dir == 0 or dir == host.get_opponent_dir():
host.add_penalty(-8)
host.gain_super_meter(fixed.round(fixed.mul(str(WHIFF_SUPER_GAIN), whiff_meter_gain_multiplier)))
func _exit_shared():
beats_backdash = false
if feinting and end_feint:
host.update_facing()
host.feinting = false
feinting = false
host.melee_attack_combo_scaling_applied = false
._exit_shared()
if update_facing_on_exit:
host.update_facing()
else:
host.reverse_state = false
host.set_facing(host.get_facing_int())
terminate_hitboxes()
host.end_invulnerability()
host.end_projectile_invulnerability()
host.end_throw_invulnerability()
if release_opponent_on_exit:
host.release_opponent()
host.got_parried = false
host.colliding_with_opponent = true
host.state_interruptable = false
host.projectile_hit_cancelling = false
host.has_hyper_armor = false
host.has_projectile_armor = false
host.state_hit_cancellable = false
host.clipping_wall = false
emit_signal("state_ended")
host.z_index = 0
and the base object methods:
func _enter_shared():
if force_same_direction_as_previous_state:
var prev = _previous_state()
if prev:
host.set_facing(prev.last_facing)
if reset_momentum:
host.reset_momentum()
if reset_x_momentum:
var vel = host.get_vel()
host.set_vel("0", vel.y)
if reset_y_momentum:
var vel = host.get_vel()
host.set_vel(vel.x, "0")
current_tick = -1
current_real_tick = -1
start_tick = host.current_tick
if enter_sfx_player and !ReplayManager.resimulating:
enter_sfx_player.play()
emit_signal("state_started")
func _exit_shared():
if current_hurtbox:
current_hurtbox.end(host)
last_facing = host.get_facing_int()
host.reset_hurtbox()
Here is how many states one character may have.
https://github.com/derkork/godot-statecharts/assets/43023911/815a941e-4971-4b51-90c2-a29ec2ff2cb0
As you can tell it would become pretty tedious to connect these all by hand, and I feel that programmatically doing so is a less elegant solution. I think it simply makes sense to include a way to tell if a compound states' internal state has changed, for these reasons, and there is not an exposed way to straightforwardly do so currently.
I see, thanks a lot for the explanation. I'll update the manual and then merge this.
Rebased & Merged, thanks a lot!
Often when dealing with state transitions, I've found that it's convenient to have common logic for all states of a particular category. Currently, you can accomplish this for state logic that runs every frame by using state_processing and state_physics_processing signals emitted by the CompoundState node. But there is no simple way to define behavior that is triggered by any transition between child states of a CompundState. I feel that this is an essential behavior that is trivial to implement, easy to understand and has little to no impact on performance.