I've started cleaning up some of my Godot code recently and found myself in need of an abstract function (aka delegate).
For those of you who you don't know, an abstract function is a function that you delegate to the child class, however the parent class can assume it exists and call it within it's own code.
In my case my base class
Orbital.gd had logic to deal with destroying the object. The logic was common among all types of objects: 1) run an animation and 2) mark the object as destroyed. Rather than having this logic repeated for my asteroids, health canisters and space ships, it made more sense to create a base class to deal with this.
That being said, I needed my spaceship to behave slightly differently when it is destroyed. You see when the ship begins to destroy it puts the game engine in slow-mo mode, and when the animation ends it needs to be put the engine back in normal mode. Here is how I achieved this using a pattern similar to a "delegate".
My base implementation of destroy is pretty simply. It includes playing the
"Destroy" animation, setting the
destroyed variable to 'true' and then setting a delay of
.25 seconds to allow the animation to complete before finally calling the
func destroy(): destroyed = true animation.play("Destroy") destroyTimer = Timer.new() destroyTimer.set_wait_time(.25) destroyTimer.connect("timeout", self, "_remove") add_child(destroyTimer) destroyTimer.start()
_remove() function handles the logic after the animation finishes. The secret to getting this to work is assigning
self to a local variable. This allows us to cheat the compiler so that it doesn't check to see if the
_destoryed delegate has been defined. Don't worry though, we do the check ourselves using
obj.has_method('_destroyed'), and if we have the function defined we call it!
func _remove(): var obj = self if obj.has_method('_destroyed'): obj._destroyed() if removeOnDestroy: queue_free()
Note: The reason I chose not to have a base implementation of
_destroyed() and simply override it is to have more control over when the child's
_destoryed() function is called. In the example above I have the ability to add logic before and after the event handler is invoked. Ultimately it boils down to preference though :)
Now that we have our delegate implemented and available, we can define our space ship specific death sequence.
Before we can start using the base class we need to extend the
Orbital.gd script in our
In this example, when the
0 we enable slow motion by setting
Engine.time_scale = .125 then proceed by calling the
destory() function defined in the
Orbital.gd base class.
if hp == 0: laser.enabled = false collider.disabled = true Engine.time_scale = .125 destroy()
Because we are inheriting
Orbital.gd we can assume that by defining the
_destroyed() delegate the code in this function will fire once the base class (aka
Orbital.gd) has completed its destroy sequence.
Our specific logic here is setting
Engine.time_scale = 1 so we can get back to normal-mode and hiding the sprite.
func _destroyed(): sprite.visible = false Engine.time_scale = 1
Now that we have a base implementation of destroy with an optional delegate to handle the spaceship specific death sequence lets take a look at the result.
At the end of the day I just didn't like using this pattern to achieve my desired result, but you might disagree (that's okay!). To learn more about Signals see https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html