I am no expert, but I recently had a similar situation.
In the end, the use of a composition based state machine, which made maintainability so much easier, but I had to go with a few death states like ‘exploding’ and ‘dead’. In the exploding state my ships are blowing up, with animations, particles and sounds etc. That state then lead to the death state, where the body was cleaned up out of dictionaries and various trackers and then freed. In other actors I used three states, disabled, exploding and dead.
I have learned to keep all my state changes in one place, usually the enemy_manager or the ally_manager or the player_manager. That keeps everything clean. I used signals to indicate if a state was completed, or external events for continuous states like ‘attacking’.
All my states contol other managers, like the life_manager, visuals_manager, score_manager etc. This is how I waited for the explosion node in my visuals_manager to finish the explosion cycle.
func _process(_delta):
check_if_explosion_completed()
if explosion_completed:
SignalManager.emit_signal("explosion_completed")
func _on_animation_finished():
flame_animation_finished = true
func _on_explosion_particles_finished():
flame_particles_finished = true
func _on_explosion_sound_finished():
flame_sound_finished = true
func check_if_explosion_completed():
if flame_animation_finished and flame_particles_finished and flame_sound_finished:
explosion_completed = true
The state_manager picks up the signal and changes state to Dead, where cleanups happen and queue freeing.
I get to re-use the same state_manager in all my actors, and can nearly always re-use different states by dragging the state node into the state manager. (It picks up the children automatically adding them to the dictionary of available states etc)
The eploding state also preps everything for the death cycle when it is entered:
func enter_state():
set_process(true)
Host.EngineManager.set_engines_enabled(false)
Host.WeaponsManager.set_weapons_enabled(false)
Host.MovementManager.set_speed_to_zero()
Host.VisualsManager.handle_death_cycle()
Since implementing this, adding and controlling behaviour of new actors (allys or enemies) has become a relatively simple task, but before it was shorter code (as you pointed out) but very quickly became unmaintanable. Now I can track bugs down very easily and quickly.
Is there a better way to do this? I don’t know. Would love to hear of one if there is.
As for the correctness of your code, if it works, then it works. There is no ‘correct’ way. However technical debt is a real thing, and only once you have produced spaghetti code, do you realise how awful it is when an inevitable change, bug fix or complication is introduced.
For me, separation of concerns and single responsibility are the most important aspects of maintainable code, not the length of the code.
Best wishes,
Paul
PS When I am making a prototype I hammer out all the code as fast as possible. But when I start again to make the production version, that is when I implement all the structures and architecture to keep my code as clean as possible. So your code, depending on what you were coding it for, could be perfectly acceptable and in no way needs to be clubbed to death
IMHO.