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".
Defining the Base Implementation
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 _remove()
function.
Scripts/Orbital.gd
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()
The _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!
Scripts/Orbital.gd
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 :)
Defining the Ship Specific Logic
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 Ship.gd
script.
Ship.gd
extends "res://Scripts/Orbital.gd"
In this example, when the hp
reaches 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.
Ship.gd
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.
Ship.gd
func _destroyed():
sprite.visible = false
Engine.time_scale = 1
The Result
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.
Base Implementation
Base Implementation + Delegate
What about Signals?
I am aware the Godot supports a feature called Signals to handle communication between objects. The reason I decided against using this feature is because it didn't feel very object oriented. Signals is a great way for two unrelated objects to talk to one another, sort of like observables in JavaScript, however I really wanted a parent/child relationship to exist between the base class and its child.
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
Discussion